TILLAUTH · CONCEPTS

Five objects, one mental model.

TillAuth has more primitives than most auth APIs expose, but they compose from a small set of concepts. Read this once and the rest of the docs are about using the model, not learning it.

The shape

organization (your TillDev account)
  └─ app                 (one per customer-facing application)
        ├─ users         (end-users of THAT app)
        │     ├─ identities    (Google/GitHub/OIDC links)
        │     ├─ totp          (TOTP secret + backup codes)
        │     ├─ passkeys      (WebAuthn credentials)
        │     ├─ devices       (trusted-device list)
        │     └─ sessions      (refresh tokens, with families)
        ├─ audit_log      (everything that happened, append-only)
        ├─ webhooks       (your endpoints + delivery log)
        └─ custom_domains (auth.your-domain.com, customer-DNS)

App

An app is a TillAuth customer-application — for Acme Inc., the Acme Mobile app is one, the Acme Admin dashboard is another. Each app has:

  • A slug (your hosted-login URL becomes <slug>.tilldev.app).
  • A public ID — the publishable identifier your SDK uses at runtime (tau_pub_…). Safe to embed in client bundles.
  • A DEK — per-app data encryption key, AES-256-GCM-wrapped with the org KEK. All sensitive blobs (OAuth refresh tokens, OAuth secrets, TOTP secrets, webhook secrets) are encrypted with the app's DEK.
  • A signing keypair — Ed25519. Verifiers fetch the public half from the per-app JWKS endpoint.
  • App-scoped settings, branding, OAuth provider configs, webhook endpoints, and audit retention.

Cross-app reads are impossible by API. There is no token that grants you access to some users across apps — tokens are minted per-app. The admin dashboard shows org-wide views by querying each app it owns.

User

A user is an end-user of one of your apps. Email is unique within an app — the same email can exist across many apps without collision (different keys, different doors).

A user can sign in by any of the configured methods (password,passkey,magic link, orOAuth / OIDC); the user record stays the same regardless of which method was used. The link between a user and a third-party login is stored as an identity.

Identity

An identity is a binding between a user and a third-party login provider. Each row stores (app, user, provider, provider_subject). Subject is the immutable identifier the IdP gives us (Google's sub, GitHub's numeric user ID). A user can have many identities — sign-in with Google and GitHub both linked to the same email.

Session

A session is a refresh-token row. TillAuth never stores tokens in plaintext: the row holds sha256(refresh_token) and a family_id. On rotation, the row's replaced_by_id fills in and revoked_at is set. If a token whose row is already revoked is ever presented, the entire family is killed (theft detection).

Access tokens — the JWTs your server-side verifier reads — are issued from these sessions and live 10 minutes. Refresh tokens live 30 days and are rotated on every use.

Device

A device is a stable fingerprint derived fromsha256(user_agent + ip_/24 + accept_language). New fingerprints trigger a "new device" notification email; users can trust a device, which cascades into the sessions tied to it.

Device fingerprinting is heuristic, not deterministic — its job is to catch obvious theft, not to be a privacy invasion. Revoking a device revokes every session bound to it.

Audit log

Every signin attempt, password change, MFA challenge, OAuth flow, magic link send, session revoke, and admin action writes a row to the per-app audit log. Append-only, 365-day default retention.

Actions follow a stable dotted vocabulary —signin.password.ok,mfa.totp.bad_code,session.theft_detected. See the audit log doc for the full list.

How they compose

A typical signin reads: a user presents a credential (password, passkey, magic-link token, OAuth callback, OIDC id_token). If MFA is enrolled, a second factor is challenged. On success, a session is created on a device, and the audit log records the action. The client gets back a JWT (10-min TTL) plus an opaque refresh token. The JWT is what your backend verifies; the refresh token only ever speaks to TillAuth.

What this means in practice

  • On the browser side you only handle JWTs +user.id. The SDK manages refresh, rotation, and storage. You never touch the underlying refresh token.
  • On the server side you verify the JWT and readuser.id. To act on a user (impersonate, revoke, change role) you call the admin API at /api/auth/admin/apps/<appId>/users/…with your admin credential.
  • For data residency, every row in the model carriesapp_id and the app inherits its org's region. Moving a customer between regions is an explicit (and rare) operation.

Now you know the shape — head to the quickstart if you want to ship a working signin, or pick a flow:passwords ·passkeys ·OAuth ·OIDC SSO.