Zero-knowledge, serverless, headless passkey authentication and relationship-based authorization. Gratos stores only public key material and permission tuples: no passwords, no usernames, no email on the server. User identity lives in the consuming app; Gratos handles WebAuthn/account-key credential storage, session management, and Zanzibar-style permission checks.
Currently powering authgravity.org.
Inspired by Ory Kratos and SpiceDB, built on WebAuthn, hat tip to Let's Encrypt, but not affiliated. Licensed AGPLv3.
Every tenant is a domain. Claim yours at authgravity.org — via Domain Connect (approve one screen at your DNS provider) or a manual CNAME — and authgravity.<yourdomain> becomes your auth endpoint:
authgravity.myapp.com ──CNAME──► cname.authgravity.net
(tenant = myapp.com)
Users, credentials, sessions, and authz data are isolated per tenant. A user who registers on authgravity.foo.com has no relationship to a user on authgravity.bar.com. Because the endpoint lives on your registrable domain, the session_id cookie is first-party — no iframes, no popups, no cross-domain redirects.
For development there are instant sandboxes: POST /sandbox mints an isolated throwaway pool at https://sandbox.authgravity.org/<id> with no domain and no DNS, and npx @authgravity/cli listen proxies it on localhost:8787 with production-identical cookie auth.
Browser
├─ your app (@authgravity/browser, @simplewebauthn/browser, or hosted surfaces)
│
└─ authgravity.myapp.com (CNAME → gratos-multi Worker)
├─ Hono + @simplewebauthn/server on Cloudflare Workers
├─ D1 (users, public keys — scoped by tenant)
├─ KV (sessions, challenges)
└─ AUTHZ service binding → gratos-authz Worker (internal-only)
└─ D1 (schemas, relationship tuples, service tokens)
The provisioner Worker handles domain claims (DNS verification, Domain Connect, Cloudflare Custom Hostnames) and a reconcile cron. Deploy order matters for service bindings: authz → multi → provisioner → dash (bun run deploy does the whole chain).
Three ways in, all ending with the same first-party httpOnly session cookie:
- Hosted surfaces (zero UI to build) — send users to
https://authgravity.myapp.com/login?return_to=<url>(also/register,/recover,/logout). Passkey-first UI with account creation on the login page; after the ceremony the cookie is set and the user is redirected back. - Passkeys (build your own) — spec-shaped WebAuthn JSON: options endpoints return standard
PublicKeyCredential*OptionsJSONunmodified, verify endpoints take the bare credential response as the whole body. Any conforming client works. - Account keys (no-passkey fallback + recovery) — a client-generated 128-bit secret rendered as
agak1_…or 12 BIP39 words, HKDF-derived into a per-tenant P-256 keypair. The server stores only the public key, exactly like a passkey. Silent device keys (non-extractable WebCrypto keys) handle daily logins so the words are only typed at setup and recovery. Bring-your-own BIP39 phrases work with no server support: decode → register (claim) → on 409, login (recover).
The WebAuthn RP ID is the registrable domain (authgravity.myapp.com → RP ID myapp.com), so passkeys work across all its subdomains. Sessions record how they authenticated (amr: webauthn | device | key), so apps can require passkey-strength sessions for sensitive actions.
Optional Zanzibar/SpiceDB-style permissions, served on the same host and session as auth:
- Schema: JSON object definitions with relations and computed permissions (union / intersection / exclusion / arrows like
parent->view), validated onPUT /v1/authz/schema. An AI draft can be generated by crawling the tenant's site. - Tuples:
POST /v1/authz/relationshipswrites facts likedocument:readme#viewer@user:<uuid>. - Checks:
POST /v1/authz/check {object, permission}— omit the subject to use the session user; one round trip authenticates and authorizes. Batch up to 50.min_amrenforces step-up strength. - Service tokens: owner-minted
agk_…bearer credentials let the app backend write tuples at runtime; schema changes stay owner-only. - Console: a self-contained editor at
/authzon every tenant host.
Domains are owner-managed from the dashboard (via on-behalf routes gated by a root-space control plane); anonymous sandboxes are fully open for development.
- The server generates a UUID per user and never stores usernames — the
userstable contains onlyid; no email, no name, no PII - Usernames exist only client-side as the authenticator display name
- Only public key material is stored, never private keys
- Your app keys its own users table by the UUID and collects profile data when it needs it
packages/
gratos-multi/ Auth API Worker — WebAuthn, account keys, sessions, sandboxes,
hosted surfaces, multi-tenant by Host
gratos-authz/ Authorization Worker — schemas, tuples, checks, service tokens,
control plane (internal-only, reached via service binding)
provisioner/ Domain claims — DNS verify, Domain Connect, custom hostnames, cron
gratos-dash/ authgravity.org — dashboard, docs, signup/domains UI (Astro)
browser/ @authgravity/browser — account/device keys + client helpers (npm)
server/ @authgravity/server — server-side authz client + tooling (npm)
cli/ @authgravity/cli — `authgravity listen` local dev proxy (npm)
example/ Reference multi-user notes app built on the hosted surfaces
npx @authgravity/cli listen # mints a sandbox, proxies it on http://localhost:8787Point your app at PUBLIC_AUTH_ENDPOINT=http://localhost:8787 and use the exact same code you'll ship to production.
Claim your domain at authgravity.org/signup (Domain Connect or a manual authgravity CNAME cname.authgravity.net record), then set PUBLIC_AUTH_ENDPOINT=https://authgravity.myapp.com.
Zero-UI: link to https://authgravity.myapp.com/login?return_to=https://myapp.com/. Or build your own:
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
const API = 'https://authgravity.myapp.com';
const api = (path: string, init: RequestInit = {}) =>
fetch(API + path, { ...init, credentials: 'include' });
export async function register() {
const opts = await (await api('/v1/register/options')).json();
const cred = await startRegistration({ optionsJSON: opts });
return (await api('/v1/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cred), // bare response — no wrapper, no correlation id
})).json(); // { verified, user: { id } } — session_id cookie is now set
}
export async function login() {
const opts = await (await api('/v1/login/options')).json();
const cred = await startAuthentication({ optionsJSON: opts });
return (await api('/v1/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cred),
})).json();
}const res = await fetch(API + '/v1/whoami', { credentials: 'include' });
const session = res.ok ? await res.json() : null; // { user_id, amr } or nullServer-side, forward the incoming cookie header to /v1/whoami.
bun install
bun --cwd packages/gratos-multi dev # Auth API on :8789
bun --cwd packages/gratos-authz dev # Authz on :8790 (run alongside gratos-multi)
bun --cwd packages/provisioner dev # Provisioner on :8788
bun --cwd packages/gratos-dash dev # Dash on :4322
bun run deploy # Deploy all workers in binding orderServed on every tenant host (authgravity.<domain> or sandbox.authgravity.org/<id>):
| Method | Path | Description |
|---|---|---|
| GET | /v1/register/options, /v1/login/options |
Standard WebAuthn options JSON, unmodified |
| POST | /v1/register/verify, /v1/login/verify |
Verify bare credential response, create session |
| GET/POST | /v1/key/(register|login)/(options|verify) |
Account-key / device-key credentials |
| GET | /v1/credentials · DELETE /v1/credentials/:id |
Credential management (rank-guarded) |
| GET | /v1/whoami · POST /v1/logout |
Session (cookie or Authorization: Bearer) |
| GET/PUT/POST | /v1/authz/(status|schema|relationships|check) |
Authorization API |
| GET | /login, /register, /recover, /logout |
Hosted auth surfaces |
| GET | /authz |
Authz console |
| GET | /llms.txt · /.well-known/authgravity |
Per-host agent guide + discovery JSON |
| POST | /sandbox · GET/DELETE /sandboxes[/:id] |
Mint / manage instant sandboxes |
Human docs live at authgravity.org/docs. The machine-readable guide is per-host: every tenant endpoint serves its own /llms.txt with the live authz schema and pre-filled endpoint — point a coding agent at it and it has the whole integration. authgravity.org/llms.txt is the onboarding framing of the same generator.
- @simplewebauthn/browser / @simplewebauthn/server — WebAuthn
- Hono — Worker HTTP framework
- Bun — package manager, runtime, tests
- Cloudflare Workers, D1, KV, Custom Hostnames, Workers AI