Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Let visitors call an operation

Open one operation, such as a contact form or an RSVP, to people who are not members of your App, while the rest of the App stays closed.

Open one operation, not the whole App

A public App lets anyone read it, but only its members can change anything. Some Apps need one exception: a contact form, a booking request, an RSVP, a guestbook entry. Declare that exception on the route itself with callers, and every other operation stays members-only.

callers is part of the 2026-10-16 App contract. Declare $schema: "https://charm.ing/schema/app-manifest/2026-10-16.json" in the manifest. Apps on 2026-07-31 ignore the key, and saving one that declares it returns a warning naming the URL to switch to.

export const manifest = {
  $schema: 'https://charm.ing/schema/app-manifest/2026-10-16.json',
  id: 'enquiries',
  meta: { name: 'Enquiries' },
  capabilities: { imports: ['charming:storage/[email protected]'] },
};

export const routes = [
  {
    op: 'submitEnquiry',
    callers: 'anyone',
    inputSchema: {
      type: 'object',
      required: ['email', 'message'],
      properties: { email: { type: 'string' }, message: { type: 'string' } },
      additionalProperties: false,
    },
    handler: async (input, { env }) => {
      const at = new Date().toISOString();
      await env.storage.put(`enquiry:${at}:${crypto.randomUUID()}`, { ...input, at });
      return { received: true };
    },
  },
  {
    op: 'listEnquiries',
    handler: async (_input, { env }) => {
      const keys = (await env.storage.list()).filter((key) => key.startsWith('enquiry:'));
      return Promise.all(keys.map((key) => env.storage.get(key)));
    },
  },
];

Who can call it

callers Who may call the operation
'members' (default) People who can run the App through its access setting, a share, or team membership
'signed-in' Also anyone signed in to Charming who can view the App
'anyone' Also a visitor with no account

The declaration only widens an operation for someone who can already view the App. Make the App Public, or open it to signed-in visitors, in its Access settings. A private App stays closed to visitors whatever its routes declare.

A route that declares 'signed-in' or 'anyone' must stay public, so public: false together with either value is refused when you save.

What the handler does

The handler decides what a visitor can do, and it runs against the App’s own data, the same data its members see. On a confirmed custom domain, an anonymous visitor’s calls use that domain’s per-visitor storage instead, so what they submit there stays in their own storage and members do not see it. In the example, submitEnquiry writes one new key per enquiry and nothing else, so two visitors who submit at once never overwrite each other, and only members can call listEnquiries, so only they read the enquiries back.

An operation with annotations.readOnlyHint: true is open to everyone who can view the App, whatever callers says, and public: false only hides a route from discovery. Never mark an operation read-only when it returns what visitors submitted.

env.user is the caller’s public identity for a signed-in caller and null for a visitor with no account. Validate visitor input with inputSchema, and keep what the handler stores small and specific to the operation.

Every call the declaration admits counts as a Run against the owner’s plan, like any other call. Callers who are not members are limited per address when signed out and per account when signed in, and together they get only a share of the App’s run bounds, so a flood of visitors never locks members out. A call those limits refuse answers 429 visitor_rate_limited, does not run, and is not a Run. See Limits for the numbers.

Ask for an account only when the operation needs one

When a visitor without an account calls a 'signed-in' operation, Charming answers sign_in_required. Wrap the call in window.charming.withUser(action), and the visitor signs in in a popup and the call runs again. See Gate an action behind sign-in for the full pattern.

window.charming.viewer.can(op) returns true for a declared operation, because Charming decides on the server whether this visitor qualifies. The one exception is a 'signed-in' operation where window.charming.login.available is false, as in a chat host’s embed: it returns false there, because no sign-in can run.

Inside a chat host’s embed, a signed-in visitor’s calls reach the App without their account, so a 'signed-in' operation answers sign_in_required there even when window.charming.user names them. Offer that operation on the App’s own page, at window.charming.login.canonicalUrl, rather than inside the embed.

Was this page helpful?