Security & privacy

Authentication and sessions

How we issue, store and revoke the tokens that keep you signed in.

Every sign-in issues a revocable AuthSession bound to one client. Tokens are hashed before they hit disk, rotated on password change, and cross-verified against each identity provider for social sign-in. You can see every active session and revoke them individually — there is no hidden permanent token anywhere.

Tokens hashed on the fly

Authentication tokens are never stored in clear. Only a SHA-256 digest is persisted; the token itself is returned to the client once at issuance.

Revocable sessions

Each session (mobile, web, watch, Twoody) is its own record, revocable at any time — by support or by you when your password changes.

Automatic rotation

All active sessions are invalidated when your password changes. No token has unlimited lifetime.

Trusted-identity sign-in

Sign in with Apple and Google Sign-In are supported. JWT credentials are verified against each provider's public keys (JWKS), the audience is checked against our allow-list, the nonce is validated when supplied, and any replayed token is rejected.

Client-side storage

On iOS and Android, tokens are stored in the system Keychain / Keystore, protected by the device passcode or biometrics. On macOS and Apple Watch, same practice, with an optional "high security" mode triggering FaceID/TouchID on each sensitive request.

Password hashing with bcrypt

Passwords are hashed with bcrypt at a work factor that takes hundreds of milliseconds per attempt. Even a full database leak would require enormous compute to brute-force a single strong password.

Short-lived pairing codes

Device pairing uses a 6-digit code that expires after 10 minutes and can be validated only a handful of times before being invalidated. A bystander can't observe a brief setup and later hijack your device.

Session list and revocation

The "Sessions" screen in your profile lists every active bearer (device, last-seen timestamp, client kind). Revoking one is immediate — the next request carrying that token is rejected server-side. Logging out only revokes the current session.

Deep dive

How a login works end-to-end

On POST /session, we look up the user by email (NULL-safe against soft-deleted accounts), constant-time compare the bcrypt hash, and on success issue a new AuthSession row tied to the client fingerprint (user agent, client kind). The row stores a SHA-256 digest of the bearer; the plaintext token is returned once, inside the JSON response. The client writes it to the OS keystore. We never see or store the plaintext again.

On every subsequent request, the client sends the bearer in X-User-Token. We hash it, look up the digest, check that the session is unrevoked and not expired, and bump last_seen_at (coalesced to once per minute to keep writes cheap). If any of those checks fail, the request gets a 401 — no amount of retries changes that.

Session scope and revocation

Each AuthSession maps to one client. A fresh sign-in on a new device does not invalidate existing sessions on other devices — you are always free to see the list at /sessions and revoke each individually. Server-side, revocation is immediate: the next request with that token is rejected.

Specific events trigger fleet-wide revocation: a password change invalidates every active session (the rationale is that you may be rotating passwords in response to a suspected breach), and an admin-initiated eviction can revoke all bearers for a given user in one call.

Social sign-in path

When you sign in with Apple or Google, your device negotiates a JWT ID token with the provider. We fetch the provider's public JWKS (cached 10 minutes), verify the token signature, the issuer, the audience (against an ENV allow-list) and the nonce if supplied. The email from the token is then matched against an existing undeleted account, or a new one is created.

The bearer we return looks identical to the one from POST /session — the client code is unified. Under the hood, the same AuthSession row gets created, with an extra provider tag for auditability.

If you lose a device, sign in from another one, open the Sessions screen, and revoke the lost client. The lost device becomes a permanent 401 from that moment on, even if someone later unlocks the screen.

Related topics