Form Access Control
A form is a public route by default. That is right for a contact form and wrong for an internal expense claim, a partner-only order sheet, or a member survey — each of which needs the URL to answer differently depending on who asked.
access states the requirement, using the same permission model as tables, pages, buckets, automations and agents:
forms:
- id: 2
name: member-survey
title: Member Survey
submitTo: { table: survey_responses }
access:
require: authenticated
fields:
- { kind: standalone, name: rating, inputType: rating, required: true }
Access Properties
| Property | Description | Default |
|---|---|---|
require |
all, authenticated, or an array of role names — at least one when an array. |
all when access is omitted. |
redirectTo |
Path to send a denied submitter to instead of the denial response. Must start with /. |
No redirect. |
A role array may also name a group, with a group: prefix — ['admin', 'group:partners'] admits anyone whose role is admin or who belongs to the partners group.
redirectTo is accepted and validated today; the canonical /forms/{name} route currently answers a denial with the response below rather than a redirect.
What a Denied Submitter Gets
The gate applies to both opening the form and posting to it. The denial is deliberately not uniform.
require |
Anonymous visitor | Signed in, wrong role | Signed in, right role |
|---|---|---|---|
omitted / all |
Allowed | Allowed | Allowed |
authenticated |
401 — sign-in required |
Allowed | Allowed |
['partner'] |
404 |
404 |
Allowed |
The 401 names the form and the level it requires, because "log in to use this" is a useful thing to tell a visitor.
A role gate answers 404, not 403 — and that is the feature. 403 would confirm the form exists to anyone who guessed its name, letting an outsider enumerate your internal forms by watching which URLs answer differently. A role-restricted form is simply not there for anyone who lacks the role. When you hit an unexpected 404 on a form you know you configured, check the signed-in user's role before you go looking for a routing bug.
Access and $user References
Requiring authentication is what makes the session available to the renderer, and therefore what makes $user.<prop> resolvable. On a public form there is no session to read: a $user.email in the prefill map silently drops and the field renders empty.
The inline form of the same reference is stricter. A per-field defaultValue: $user.email on a public form is rejected when the config is decoded, with an error naming the form and the field — a value that can only ever resolve empty is a mistake worth catching before boot rather than a blank input worth shipping.
Embedded Forms
A form rendered inside a page through formRef keeps its own gate, and the page inherits the strictest answer: if any embedded form denies the session, the whole page returns 404. The alternative — rendering the page with a hole where the form should be — would leak the form's existence through the shape of the page, which is exactly what the role gate exists to prevent.
Who Submitted
When a session is present, the submission ledger records submitter_user_id alongside the answers, and a bound table's created-by column is populated with the same id. Public submissions leave both null. Access control is therefore also what makes a submission attributable.
Related Pages
- Roles & RBAC — the role names
requirematches against. - Auth Groups — the groups a
group:entry refers to. - Prefill —
$userreferences, which depend on this gate. - Availability — the other reason a form may refuse a submission.
Last updated July 27, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta — contributions and corrections are welcome.