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.
Recommended solution
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_inquiry2. 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
- Object:
examples/app-showcase/src/objects/inquiry.object.ts. - Public form:
examples/app-showcase/src/views/inquiry.view.ts.
Anti-patterns
- Granting a
guestprofile 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
sharingModelopen so submissions are world-readable. Keep itprivate; the public path only inserts.
See also
- Decision record: ADR-0056 (public forms — Option A).
- Guide: Forms (Public + Internal).
- Reference: Sharing, View / FormView schema.