The project tree shows where your code runs. Every group has a server project. Only a Widget group also has a custom client project. Knowing that boundary prevents two common mistakes: putting a secret into browser code and calling a browser-only API from server code.
Create a test app using Create your first developer app and add a Widget group to inspect both sides.
What you’ll have at the end
A traced widget call from the React client to a named server action and back.
- React input
- action(name, arguments)
- Server function
- Small response
Before you start
- A Widget group in an unpublished app
- The generated client and server projects
On this page
The widget's server entry is index.ts directly under the group. Its Widget subtree contains index.tsx and index.css for the client. They are distinct projects even though they appear under one group.
| File or setting | Executes or applies where | Use it for |
|---|---|---|
Group index.ts |
campai's server app runtime | Validation, business logic, permitted API access, named widget actions |
Widget index.tsx |
The visitor's browser | React state, controls, rendering, calls to server actions |
Widget index.css |
The widget interface | Styling; the generated file imports Tailwind |
| App server settings | Server execution policy | Organization permissions, database access, outbound hosts, secrets, runtime limits |
| Form/widget Scope | Client access policy | Who may load and use that group |
Do not assume arbitrary npm packages, Node built-ins, filesystem APIs, or environment variables are available. The editor supplies a controlled runtime and supported imports. Resolve validation errors against those declarations instead of copying a full Node.js service into the entry file.
The review dashboard’s server entry exports a function that reads app storage and returns only counts:
Example
export async function getReviewSummary() {
let contacts = 0
let missingEmail = 0
let missingAddress = 0
const entries = storage.list<{ issues: string[] }>({ prefix: ['contact-review'] }, { limit: 501, batchSize: 100 })
for await (const entry of entries) {
contacts += 1
if (contacts > 500) throw new Error('Review list exceeds the 500-contact dashboard limit')
if (entry.value.issues.includes('Add an email address')) missingEmail += 1
if (entry.value.issues.includes('Add a postal address')) missingAddress += 1
}
return { contacts, missingEmail, missingAddress, checkedAt: new Date().toISOString() }
}The client calls action('getReviewSummary'), awaits the result, and updates React state. The first argument names the server export; any remaining arguments become that function's parameters. A missing export name or mismatched arguments is an application error, not a REST authentication problem.
This is an app action bridge. It is not a request to a public REST endpoint for each export, and you should not invent such an API reference link. Use Create a custom widget for the complete interface and error handling.
This action takes no inputs. Validate any arguments you add in the server function. Types help during editing; they do not prevent a caller from submitting unexpected values at runtime.
The server runtime supplies the api client and an api module. It builds REST requests from an operation registry and uses the app-managed credential. Configure the relevant app permissions in Settings → Server access rather than copying a separate key into source code.
For example, an app that reads members ultimately uses List contacts, with the same organization, mandate, field-access, and filtering rules as an external integration. The editor's generated declarations describe the app client's method signatures; the linked reference describes the underlying HTTP operation.
The runtime can default the organization to the app's organization and the mandate parameter to its all-mandates value. For mandate-specific workflows, choose the mandate deliberately. Do not assume the currently selected mandate in your browser is automatically the mandate of every server call.
Keep client-side action calls and server-side api calls distinct. A browser widget does not need to know X-API-Key to invoke its permitted server actions.
A server result returned to a widget becomes browser-visible data. Project records into the fields the interface requires. Do not return complete contact objects, credentials, internal diagnostics, or raw third-party responses merely because the server can read them.
Handle loading and failures in the client, and validate important conditions again on the server immediately before a write. A disabled button is useful feedback but is not an authorization check.
Save changes saves draft source. Simulate validates and builds the saved project for a test run. Publish creates or replaces the published app version. These are separate stages.
For widgets, test both the rendered client and at least one server action. A client preview that renders correctly does not establish that its server exports can execute. A server action that returns the expected value does not establish that the public or contact access policy is correct.
Continue with Simulate an app before publishing, then verify the intended audience through Set access for forms and widgets.
Check your result
- Client renders
- Rendering alone does not prove the action ran.
- Action called
- The log and interface show the same result.
- Secret or full record
- It never appears in the action response.