Skip to content

Repository files navigation

Gratos

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.

How It Works

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.

Architecture

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).

Authentication

Three ways in, all ending with the same first-party httpOnly session cookie:

  1. 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.
  2. Passkeys (build your own) — spec-shaped WebAuthn JSON: options endpoints return standard PublicKeyCredential*OptionsJSON unmodified, verify endpoints take the bare credential response as the whole body. Any conforming client works.
  3. 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.

Authorization

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 on PUT /v1/authz/schema. An AI draft can be generated by crawling the tenant's site.
  • Tuples: POST /v1/authz/relationships writes facts like document: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_amr enforces 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 /authz on 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.

Privacy Model

  • The server generates a UUID per user and never stores usernames — the users table contains only id; 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

Monorepo Structure

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

Getting Started

Local dev in 60 seconds (no domain, no DNS)

npx @authgravity/cli listen    # mints a sandbox, proxies it on http://localhost:8787

Point your app at PUBLIC_AUTH_ENDPOINT=http://localhost:8787 and use the exact same code you'll ship to production.

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.

Wire up auth

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();
}

Check auth state (client or server)

const res = await fetch(API + '/v1/whoami', { credentials: 'include' });
const session = res.ok ? await res.json() : null; // { user_id, amr } or null

Server-side, forward the incoming cookie header to /v1/whoami.

Development

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 order

API Endpoints

Served 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

Documentation

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.

Key Dependencies

About

Gratos is an open-source, serverless, headless, first-party authentication service for the post-password era. It provides modern, privacy-focused user authentication using passkeys, designed to run easily at the edge.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages