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.