Access & permissions
The security model describes authorization as a sandwich: authentication establishes who you are, permissions decide what you may do, and property protection governs which fields you may touch. This page is the first two layers up close — how you get in, and what you can do once you’re in. The third layer stays with the security model.
Getting in: no passwords
People sign in with a six-digit code sent to their registered email. The code is single-use and short-lived; after verification the person holds a session bearer token. External systems authenticate with a cryptographically random WebAPI token, shown once at creation. There are no passwords anywhere to store, hash, rotate or leak — and because the code arrives on a separate channel, the people-flow is already two-factor in spirit.
That last point cuts both ways: if the inbox is the credential, the mailbox is the thing to protect. Hardening editor email is the single most impactful control you own, and it’s a controller duty — see Privacy and GDPR.
The nine atomic permissions
Access control uses nine granular rights that cover every operation the platform supports:
| Permission | Scope |
|---|---|
view | Read nodes, query the graph |
write | Create and edit drafts |
publish | Approve drafts to live |
delete | Remove nodes |
create.route | Create routable pages (with a URL path) |
create.collection | Create embedded content (blocks) |
submit | Public form submissions |
manage.actors | Create and edit persons and API keys |
manage.settings | Space configuration and worker operations |
Presets, not arrays by hand
Presets bundle the rights into familiar roles so you rarely assemble an array yourself:
| Preset | Holds |
|---|---|
subscriber | view |
author | view, write, create.collection |
editor | view, write, publish, delete, create.route, create.collection |
admin | everything except submit |
submit sits outside the human roles on purpose — it’s the public-submission right, granted to a WebAPI key rather than a person. Presets are a convenience; when none fits, you grant a custom combination. Either way it is the resulting array that gets checked.
One actor model
A person, a bot, and an API token are the same kind of thing: an actor with a permissions array. A person’s rights live on their own actor record, a key’s on its own. A bot gets no back door and no softer rule set — exactly the rights you grant it, and a richer audit trail, because automated activity is easy to attribute. One authorization story, not one for humans and a quieter one for machines.
The same model reaches across the whole platform. An account — your identity above any single space — signs in the same passwordless way, and a platform administrator is simply a Person holding manage.settings for cross-space operations, not a superuser with a separate key to the kingdom. Whether the tenant is a space, the account app, or a management module, it’s the same actors, the same nine rights, the same checks at the same gates.
Where the check happens
The array — preset or custom — is checked in more than one place, on purpose:
service layer → at every mutation point, before the change is applied
API edge → again, as the request enters
template → once more, where editing controls render only if the
actor's authenticated state (never a URL parameter) allows
Defense in depth: a gap in one layer is caught by the next, and a key with only view can read the whole graph and change nothing. This is the same posture the depth-gate takes on the data side — enforce the rule in several places rather than trusting one.