Users, roles, and authentication
How people sign in, what each role can do, and how Cusp guards the front door
Cusp ships its own authentication. It is small, auditable, and lives behind a single Auth interface in src/shared/auth/, so the backing implementation can be swapped without touching the routes that depend on it. Passwords are hashed with Argon2id and nothing is ever stored in plaintext. Every admin route carries an explicit authentication check — there is no silent default-admin middleware, so a missing check is a bug rather than an inheritance.
The four roles
Cusp has four built-in roles. Permissions for each role are fixed in v1; per-role configuration is a later addition. The roles, from most to least access:
- Administrator — full control of the deployment: content, collections and fields, users, themes, plugins, and settings.
- Editor — manages all content: create, edit, publish, schedule, and unpublish any document, plus the media library.
- Author — creates and edits documents and uploads media, saving drafts for review, but cannot publish, schedule, or unpublish; an editor or administrator releases the work. No access to site configuration.
- Subscriber — the lightest-weight authenticated role: a registered account with read access and no editing rights. Cusp has no gated-content feature in v1, so a subscriber account carries no special front-end privileges beyond being signed in.
In v1, authorization is by role: each admin route checks role membership, and access does not vary by collection. The one document-level boundary is author ownership on writes — an author may edit, discard, preview, or open an edit session only on documents they authored, while editors and administrators manage any document. Per-collection and per-document permissions are a v1.x refinement; the seeded (resource, action) permission set is the foundation for that work, but today the role itself is what each route enforces.
Signing in and sessions
You create the first administrator account in the first-run wizard. After that, sign-in verifies the submitted password against the stored Argon2id hash. On success Cusp mints a session and sets it as an httpOnly, secure, sameSite=lax cookie, so JavaScript can never read the token and the cookie is never sent on cross-site form posts.
- Argon2id hashing — OWASP 2024 parameters (19 MiB memory, 2 iterations, 1 parallelism), landing at roughly 150 to 300 ms per verify on a Worker isolate.
- Parameters encoded in the hash — the cost can be raised later and rotates per user on next sign-in via a lazy rehash, with no bulk migration.
- Minimum password length of 12 characters, enforced at the Worker boundary rather than only in the browser.
- Sessions use a sliding 30-day window — every time you're active, the 30-day clock resets, so a session stays alive as long as you keep using it and only lapses after 30 days of inactivity. Changing your password immediately ends every other session, so a lost or shared login can be revoked by resetting it.
The stored session credential is hashed, so a database leak does not yield usable session tokens. Changing a password invalidates every other session for that user. The session that performs the change is included — you re-authenticate afterwards. That is intentional: it is the visible signal the change took effect, and it locks out any session an attacker may have opened on a compromised password.
Password reset
A reset request sends a single-use link by email through Cloudflare's email binding. The link is valid for one hour and can only be used once; completing it invalidates the user's other sessions just like a self-service change. To avoid leaking which addresses have accounts, the request endpoint returns the same response whether or not the email matches a user. An administrator can also reset another user's password directly from the user-management screen, which does not depend on email being configured.
Rate limiting
The sensitive entry points are guarded by the RateLimit Durable Object, which keeps sliding-window counters in transactional storage. Sign-in, password reset, and the editor's collaboration WebSocket are all rate-limited. Email addresses are hashed inside the counter keys so personal data never lands in the limiter's storage. When a limit trips, the response is a 429 carrying a Retry-After header.
- Sign-in by email — 10 attempts per 15-minute sliding window.
- Sign-in by IP address — 50 attempts per 15-minute window.
- Password reset request — 5 per hour by email; 20 per hour by IP address.
- Collaboration WebSocket upgrade — 30 per minute per user, capping how often an editor can reconnect to a document session.
One auth path for admin and API
The admin UI and the headless API share a single authentication code path. An admin session is a long-lived API token carried in the session cookie; a headless client sends the same kind of token as a bearer credential in the Authorization header. Every token carries scopes, and an admin session holds the wildcard scope so it satisfies every check. Each endpoint validates the scope it requires, so a token can only do what it was granted.
Cross-site request forgery is handled without a token. The admin SPA is same-origin with the Worker, sameSite=lax keeps the cookie off cross-site posts, and write requests additionally require the Origin header to match the host. Bearer-token requests carry their credential explicitly in a header, so they are not subject to the cookie-based origin check.
You add users, set their roles, and reset passwords from the Users screen at /admin/users.
Next: Themes and plugins