Skip to content
campaiDeveloper
campai.com

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.

  1. React input
  2. action(name, arguments)
  3. Server function
  4. 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.

The group’s index.ts runs on the server. Widget/index.tsx renders the interface and calls named server actions.

The group’s index.ts runs on the server. Widget/index.tsx renders the interface and calls named server actions.

Project files with the Widget client index.tsx selected and its server index.ts above it
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.
Continue with Create a custom widget