Five objects, one mental model.
GATillAuth 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 per-app data encryption key that protects the app's stored secrets — OAuth refresh tokens, OAuth secrets, TOTP secrets, and webhook secrets.
- A per-app signing keypair; verifiers fetch the public half from the app's 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.
Sign-in allowlist. Each app can gate who may sign in or register through settings.allowlist. When enabled, only listed email addresses or bare domains (e.g. example.com) are admitted; everyone else is rejected before an account is created. Left off — the default — sign-up stays open. Enabling it with both lists empty locks the app: nobody can sign in. Manage it on the app's Policies page, alongside MFA enforcement.
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, or OAuth / 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 identity records (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 record. TillAuth never stores tokens in plaintext: refresh tokens are stored hashed, in rotation families for theft detection. On rotation the old token is marked replaced and revoked. If a token that 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 device signature derived from request attributes. New signatures trigger a “new device” notification email; users can trust a device, which cascades into the sessions tied to it.
Device recognition 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.