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 read
user.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 carries
app_idand 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.