# AuthGravity > Zero-knowledge, serverless, headless passkey authentication. AuthGravity is a hosted passkey authentication service. It stores only public key material - no passwords, no usernames on the server. User identity lives in your app; AuthGravity handles WebAuthn credential storage and session management. ## How It Works AuthGravity runs on your domain via a CNAME record. This means: - **First-party cookies**: the session cookie is set on your own domain (e.g., `.myapp.com`), not a third-party domain. Your browser sends it automatically on every request to any subdomain. - **Your own Relying Party (RP)**: the WebAuthn RP ID is your registrable domain (e.g., `myapp.com`), so passkeys created through AuthGravity work across all your subdomains. - **Server-side session checks**: because the cookie lives on your domain, your backend can validate sessions by forwarding it to the AuthGravity `/whoami` endpoint - no tokens to manage, no JWTs to verify. ## Setup 1. Sign up at authgravity.org with a passkey and enter your domain (e.g., `myapp.com`). 2. AuthGravity generates a unique CNAME record for your domain: ``` Type: CNAME Name: authgravity Target: .cname.authgravity.net ``` The target is a unique per-claim token (e.g., `ab3kx7.cname.authgravity.net`) that proves you control the domain's DNS. 3. Add this CNAME record in your DNS provider. AuthGravity polls DNS automatically and activates your domain once the record propagates. 4. Once active, your auth endpoint is `https://authgravity.myapp.com`. Each domain gets its own isolated user pool. A user on `authgravity.foo.com` has no relationship to a user on `authgravity.bar.com`. ## HTTP API All endpoints are relative to your auth endpoint (e.g., `https://authgravity.myapp.com`). Include `credentials: 'include'` on every fetch from the browser so the session cookie is sent. ### Registration **Step 1: Get registration options** ``` GET /register/options ``` Returns WebAuthn `PublicKeyCredentialCreationOptions` plus a `userId` field (a server-generated UUID). **Step 2: Verify registration** Pass the WebAuthn attestation response from the browser along with the `userId` from step 1. ``` POST /register/verify Content-Type: application/json { "userId": "", "response": } ``` Returns `{ "verified": true, "user": { "id": "" } }` on success. Sets a `session_id` cookie on your domain. ### Authentication (Login) **Step 1: Get authentication options** ``` GET /login/options ``` Returns WebAuthn `PublicKeyCredentialRequestOptions` plus a `challengeId` field. **Step 2: Verify authentication** Pass the WebAuthn assertion response from the browser along with the `challengeId` from step 1. ``` POST /login/verify Content-Type: application/json { "challengeId": "", "response": } ``` Returns `{ "verified": true, "user": { "id": "" } }` on success. Sets a `session_id` cookie on your domain. ### Session Management **Check current session:** ``` GET /whoami ``` Returns `{ "user_id": "" }` if authenticated, or 401 if not. The session can be identified two ways: - **Cookie** (browser): the `session_id` cookie set automatically by registration/login. - **Bearer token** (server-to-server): pass `Authorization: Bearer ` header. **Logout:** ``` POST /logout ``` Destroys the session and clears the cookie. ## Using @simplewebauthn/browser The `@simplewebauthn/browser` library handles the browser-side WebAuthn ceremony. Install it: ``` npm install @simplewebauthn/browser ``` ### Registration example ```javascript import { startRegistration } from '@simplewebauthn/browser'; const API = 'https://authgravity.myapp.com'; // 1. Fetch options from AuthGravity const optRes = await fetch(API + '/register/options', { credentials: 'include' }); const opts = await optRes.json(); // 2. Browser prompts user for passkey (biometric, security key, etc.) const attResp = await startRegistration({ optionsJSON: opts }); // 3. Send attestation response back for verification const verRes = await fetch(API + '/register/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ response: attResp, userId: opts.userId }), }); const result = await verRes.json(); // result: { verified: true, user: { id: "" } } // A session_id cookie is now set on your domain. ``` ### Login example ```javascript import { startAuthentication } from '@simplewebauthn/browser'; const API = 'https://authgravity.myapp.com'; // 1. Fetch options from AuthGravity const optRes = await fetch(API + '/login/options', { credentials: 'include' }); const opts = await optRes.json(); // 2. Browser prompts user for passkey const assertResp = await startAuthentication({ optionsJSON: opts }); // 3. Send assertion response back for verification const verRes = await fetch(API + '/login/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ response: assertResp, challengeId: opts.challengeId }), }); const result = await verRes.json(); // result: { verified: true, user: { id: "" } } // A session_id cookie is now set on your domain. ``` ### Checking auth state from the browser ```javascript const res = await fetch('https://authgravity.myapp.com/whoami', { credentials: 'include', }); if (res.ok) { const data = await res.json(); console.log('Authenticated user:', data.user_id); } else { console.log('Not authenticated'); } ``` ## Server-Side Session Validation Because the `session_id` cookie is scoped to your registrable domain (e.g., `.myapp.com`), the browser sends it on requests to both `authgravity.myapp.com` and `www.myapp.com`. Your backend can validate the session by forwarding this cookie to the `/whoami` endpoint. ### Astro middleware example ```typescript import { defineMiddleware } from "astro:middleware"; const AUTH_API = "https://authgravity.myapp.com"; export const onRequest = defineMiddleware(async (context, next) => { if (context.url.pathname.startsWith("/protected")) { const cookieHeader = context.request.headers.get("cookie"); const res = await fetch(AUTH_API + "/whoami", { headers: cookieHeader ? { cookie: cookieHeader } : {}, }); if (!res.ok) { return context.redirect("/login"); } // Optionally read the user ID // const { user_id } = await res.json(); } return next(); }); ``` ### Express / Node.js example ```javascript app.use('/protected', async (req, res, next) => { const cookieHeader = req.headers.cookie; const authRes = await fetch('https://authgravity.myapp.com/whoami', { headers: cookieHeader ? { cookie: cookieHeader } : {}, }); if (!authRes.ok) { return res.redirect('/login'); } const { user_id } = await authRes.json(); req.userId = user_id; next(); }); ``` ## Architecting Your App with AuthGravity AuthGravity gives you a stable UUID for each user but stores no personal information. Your app owns the user profile. The pattern is: 1. **User registers or logs in** via AuthGravity. You get back a UUID. 2. **Your app stores profile data** (name, email, subscription, etc.) in your own database, keyed by that UUID. 3. **On each request**, your backend calls `/whoami` to get the UUID, then looks up the user in your database. This is progressive disclosure: users authenticate instantly with a passkey, and only share personal information when they choose to (e.g., at checkout, when subscribing, when filling out a profile). ### Your database schema Your users table uses the AuthGravity UUID as the primary key: ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, -- the UUID from AuthGravity email TEXT, -- collected later, when the user provides it name TEXT, -- collected later stripe_customer_id TEXT, -- set after first purchase created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` When a user first authenticates, you may not have a row for them yet. Create one on first sight: ```javascript // In your backend, after calling /whoami const { user_id } = await authRes.json(); let user = await db.get('SELECT * FROM users WHERE id = ?', user_id); if (!user) { await db.run('INSERT INTO users (id) VALUES (?)', user_id); user = { id: user_id }; } ``` ### Example: Stripe Checkout with AuthGravity Here's a complete flow for a SaaS app where a user signs in with a passkey and buys a subscription via Stripe Checkout. **Step 1: User lands on your site and authenticates** The user clicks "Sign in" and completes a passkey ceremony. Your frontend now has a session cookie. No email was required. **Step 2: User clicks "Subscribe" - your backend creates a Stripe Checkout session** ```javascript // POST /api/subscribe handler app.post('/api/subscribe', async (req, res) => { // Validate the session via AuthGravity const cookieHeader = req.headers.cookie; const authRes = await fetch('https://authgravity.myapp.com/whoami', { headers: cookieHeader ? { cookie: cookieHeader } : {}, }); if (!authRes.ok) { return res.status(401).json({ error: 'Not authenticated' }); } const { user_id } = await authRes.json(); // Ensure user exists in your database let user = await db.get('SELECT * FROM users WHERE id = ?', user_id); if (!user) { await db.run('INSERT INTO users (id) VALUES (?)', user_id); user = { id: user_id }; } // Create or reuse a Stripe customer let stripeCustomerId = user.stripe_customer_id; if (!stripeCustomerId) { const customer = await stripe.customers.create({ metadata: { authgravity_user_id: user_id }, }); stripeCustomerId = customer.id; await db.run( 'UPDATE users SET stripe_customer_id = ? WHERE id = ?', stripeCustomerId, user_id ); } // Create a Checkout session const session = await stripe.checkout.sessions.create({ customer: stripeCustomerId, mode: 'subscription', line_items: [{ price: 'price_xxx', quantity: 1 }], success_url: 'https://myapp.com/dashboard?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://myapp.com/pricing', }); res.json({ url: session.url }); }); ``` **Step 3: Stripe collects payment and email** Stripe Checkout collects the customer's email and payment details. This is where personal information enters your system - from the user's explicit action, not from a forced registration form. **Step 4: Stripe webhook updates your database** ```javascript // POST /webhooks/stripe handler app.post('/webhooks/stripe', async (req, res) => { const event = stripe.webhooks.constructEvent( req.body, req.headers['stripe-signature'], webhookSecret ); if (event.type === 'checkout.session.completed') { const session = event.data.object; const customer = await stripe.customers.retrieve(session.customer); // Update your user record with the email Stripe collected await db.run( 'UPDATE users SET email = ? WHERE stripe_customer_id = ?', customer.email, session.customer ); } res.json({ received: true }); }); ``` **The result**: the user signed in with zero personal information, browsed your app, decided to buy, and only then shared their email - through Stripe, not through your registration form. Your database now links the AuthGravity UUID to a Stripe customer and an email, all collected progressively. ## Privacy - AuthGravity generates a UUID for each user and never stores usernames - Usernames are only used client-side as the WebAuthn authenticator display name - No email, no name, no PII is stored server-side - Only public key material is stored, never private keys ## Source Code AuthGravity is powered by the open source Gratos project, licensed AGPLv3: https://github.com/polvi/gratos