Skip to content

Models

Every table below carries tenant_id, has row-level security forced on it, and is scoped by TenantScoped in the application as well.

uuid, subdomain, name, settings, archived_at. The only model not tenant-scoped, because it is the tenant. Generates a signing key on create.

Tenant.resolve(host) finds one by subdomain; Tenant.switch(tenant) { } sets the Postgres session variable RLS reads and restores the previous value afterwards.

kid, algorithm, private_pem (encrypted), public_jwk, activated_at, retired_at.

SigningKey.active what signing uses
SigningKey.published what JWKS lists — includes a key retiring in the future
SigningKey.rotate! mint a replacement, retire the incumbent after an overlap

A person. uuid (the sub claim), nickname, name, email, password_digest, otp_secret (encrypted), scopes, plus the rest of the OIDC standard profile: given_name, family_name, middle_name, profile_url, picture_url, website_url, gender, birthdate, zoneinfo, locale. All nullable, and nothing edits them yet.

Actor.authenticate(identifier, password) accepts a nickname or an email. #claims(scopes, requested:) returns only what the granted scopes permit — profile releases the thirteen standard profile claims, email releases the address and email_verified, and neither is included without them. requested: is the claims request parameter, which releases a claim named directly rather than through its scope.

Unset fields are omitted rather than returned as null, so an actor releases what it has and stays quiet about the rest.

An application. client_id, secret_digest (bcrypt), redirect_uris, grant_types, response_types, scopes, token_endpoint_auth_method, dynamic, registration_token_digest.

#public? is token_endpoint_auth_method == "none", which is what makes PKCE mandatory. Client.register! is the DCR path and is the only place dynamic is set.

Single table, type column, four subclasses.

Stored as
AuthorizationCode SHA-256 of the secret holds the PKCE challenge, redirect_uri, nonce, authenticated_at, requested_claims
AccessToken the jti of the JWT the JWT itself is never stored; parent_id is the code it came from
RefreshToken SHA-256 of the secret parent_id chains rotations

Token.mint! generates a secret and returns the record with #secret readable exactly once. Token.redeem(secret) finds a live one by digest. Token.spent(secret) finds one redeem has already refused, which is how a replay is noticed at all. #consume! burns it; #revoke! burns it and everything below it.

parent_id is one chain doing three jobs: it rotates refresh tokens, it records provenance through an exchange, and — because an access token points back at the code that minted it — it is what lets a replayed code revoke what it already issued.

A browser session. digest, user_agent, ip_address, authenticated_at, expires_at, revoked_at. The cookie holds the secret; the database holds its digest.

What an actor has allowed a client to do: scopes and audience, unioned across grants. Consent.covers? is what decides whether the consent screen is shown again.