Documentation

AuthGravity provides passwordless authentication for your domain using passkeys. Integrate by calling the HTTP API directly from any language or framework.

llms.txt - machine-readable version of these docs

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 with a passkey and enter your domain (e.g., example.com).
  2. AuthGravity will generate a unique CNAME record for your domain:
Type:   CNAME
Name:   authgravity
Target: <token>.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.

  1. Add this CNAME record in your DNS provider. AuthGravity polls DNS automatically and activates your domain once the record propagates.
  2. Once active, your auth endpoint is https://authgravity.example.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: Pass the WebAuthn attestation response along with the userId from step 1.

POST /register/verify
Content-Type: application/json

{
  "userId": "<userId from step 1>",
  "response": <attestation response from navigator.credentials.create()>
}

Returns { "verified": true, "user": { "id": "<uuid>" } } and 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: Pass the WebAuthn assertion response along with the challengeId from step 1.

POST /login/verify
Content-Type: application/json

{
  "challengeId": "<challengeId from step 1>",
  "response": <assertion response from navigator.credentials.get()>
}

Returns { "verified": true, "user": { "id": "<uuid>" } } and sets a session_id cookie on your domain.

Session Management

GET /whoami

Returns { "user_id": "<uuid>" } 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 <session_id> header.
POST /logout

Destroys the session and clears the cookie.

Using @simplewebauthn/browser

The @simplewebauthn/browser library handles the browser-side WebAuthn ceremony.

npm install @simplewebauthn/browser

Registration example

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: "<uuid>" } }
// A session_id cookie is now set on your domain.

Login example

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: "<uuid>" } }
// A session_id cookie is now set on your domain.

Checking auth state from the browser

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

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

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:

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:

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

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

// 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 under the AGPLv3.