ObjectStackObjectStack

Collect data from the public

Expose one form to anonymous visitors (web-to-lead, contact-us, intake) without opening the underlying base. Authorization is derived from the form's own declaration; only whitelisted fields are accepted.

Collect data from the public

Scenario

I want anonymous visitors to submit a form — a "Contact us" / "Request a demo" / intake form — that creates a record. But I must not give them access to the base, and they must only be able to set the fields on the form.

Declare a public form view; the submit route derives a narrow authorization from that declaration. No guest profile, no broad grant — and it works even under secure-by-default auth (ADR-0056 Option A).

1. Mark a form view public

formViews: {
  contact: {
    type: 'simple',
    data: { provider: 'object', object: 'showcase_inquiry' },
    sections: [
      { label: 'Tell us about yourself', columns: 1, fields: [
        { field: 'name', required: true },
        { field: 'email', required: true },
        { field: 'company' },
        { field: 'message', required: true },
      ]},
    ],
    sharing: { enabled: true, allowAnonymous: true, publicLink: '/forms/contact-us' },
    submitBehavior: { kind: 'thank-you', title: 'Thanks!', message: 'We will be in touch.' },
  },
}

That sharing block wires the anonymous endpoints automatically:

GET  /api/v1/forms/contact-us          → resolved form + whitelisted schema
POST /api/v1/forms/contact-us/submit   → INSERT one showcase_inquiry

2. The field whitelist is the security boundary

Only the fields listed in sections[].fields are accepted on submit; everything else is stripped server-side. Server-controlled fields (status, source, owner stamps) are set by a defaults hook, never by the submitter. The submit route derives a narrow publicFormGrant: { object: 'showcase_inquiry' } — create + read-back of the row it just made, and nothing else.

System-managed anchors (owner_id, organization_id, audit columns, id) are never accepted from a public submission — not even when a section declares them, and not through the zero-sections all-fields fallback. The route filter and the data layer enforce the same set (PUBLIC_FORM_SERVER_MANAGED_FIELDS from @objectstack/spec/security), so an anonymous visitor cannot forge record ownership or tenant placement regardless of how the form is declared (#3022).

3. Keep the object private once inside

Set sharingModel: 'private' on the object so submissions are staff-scoped after creation. The public path only ever inserts; it never lists.

Why

Authorization is derived from the declaration, not configured separately — so the grant can't drift wider than the form. There is no standing "anonymous can write to this object" rule to misconfigure: the only thing the public can do is create one record through one whitelisted form. This is the difference from Airtable, where interfaces can't be shared publicly at all (only forms can) — here the same FormView metadata renders both internally (authed) and publicly (anonymous) through one renderer.

Runnable example

Anti-patterns

  • Granting a guest profile broad write to the object. Let the form declaration derive a narrow grant instead.
  • Relying on the UI to hide server-controlled fields. The submit whitelist is the boundary; fields not listed are stripped regardless of the client.
  • Leaving the object's sharingModel open so submissions are world-readable. Keep it private; the public path only inserts.

See also

On this page