
# 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:

```yaml
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.

:::callout
**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](/en/docs/auth-roles-rbac) — the role names `require` matches against.
- [Auth Groups](/en/docs/auth-groups) — the groups a `group:` entry refers to.
- [Prefill](/en/docs/form-prefill) — `$user` references, which depend on this gate.
- [Availability](/en/docs/form-availability) — the other reason a form may refuse a submission.
