TILLAUTH · FLOWS
Magic links — bound, brief, single-use.
Magic links are the most-stolen credential on the internet because they live in inboxes. TillAuth's links are bound to the requesting IP /24, expire in 15 minutes, and are single-use. Forwarding the email gets you nowhere.
What's actually in the link
- Token: 32 random bytes, base64url. Only
sha256(token)is stored. - TTL: 15 minutes — enough for a real user to read the email, short enough that a leaked link is dead before anyone gets to it.
- Single-use: on the first
/v1/magic/consumehit, the row'sused_atis filled inside the same transaction that mints the session. Replay is a 410 Gone. - IP
/24binding: we store the prefix of the requester's IP at/sendtime; consume must come from the same prefix. A forwarded link that opens in a different network refuses to log in. Carrier-NAT roaming inside the same prefix still works. - Redirect allow-list: the optional
redirect_uriis checked against the app'sallowed_redirect_origins. Mismatch = the link refuses to consume.
Send
POST /v1/magic/send with { email, redirect_uri? }:
- Rate limit per IP and per (app, email) — no email-bombing surface.
- Mint the token. Store
sha256(token),ip_prefix = ip/24,redirect_uri, 15-minute TTL. - Send the email via your configured provider (Resend by default), using the per-app branding template.
- Always respond 204 — never reveal whether the email matched a real user. Audit row:
signin.magic.sent.
Consume
POST /v1/magic/consume with { token }:
- Find the row by
sha256(token). - Check expiry — if expired, audit
signin.magic.expired, 410. - Check IP
/24— if mismatch, auditsignin.magic.ip_mismatch, 410. The token is consumed regardless — the original link no longer works either, by design. - Set
used_at, mint the session inside the same transaction, auditsignin.magic.ok. - If
redirect_uriwas bound and it passes the allow-list, return it in the response so your SDK can navigate the user there.
Hosted-login flow
On <slug>.tilldev.app the user clicks the magic-link button, types their email, gets the mail, clicks the link. The hosted-login page handles the consume side and redirects back to the app — your app never has to embed the magic-link UI itself.
SDK
From the React SDK, hosting your own flow is two hooks:
// Send page
const { send, sent, pending } = useMagicLink()
await send({ email, redirect_uri: '/' })
// Callback page — read ?t= from the URL on mount
const { consume } = useMagicLink()
const r = await consume(token)
if (r.ok) router.push(r.redirect ?? '/')When magic links are wrong
- High-value workflows (admin dashboards, treasury, anything regulated) should use passkeys orOAuth. Mailbox access shouldn't grant superuser.
- Shared inboxes (
support@,team@) — the link works for whoever gets it first. Use email + password with MFA.
Next: OAuth · Google + GitHub or OIDC SSO.