Magic links (retired)
This page has been retired. Magic links are now managed inside System settings, in the Magic Links tab.
- Route:
/admin/magic-links - Guard:
/admin/*layout (isAdminTier()); the magic-links API enforcesrequireSuperAdmin— deliberately stricter than the cross-client admin guard, since a magic link is an impersonation credential. - Redirects to:
/admin/settings?tab=magic-links— the screenshot below shows that destination. At capture: 16 tokens listed, metadata only (no plaintext links; the audit run's own tokens appear as "Used").

What this route is now
src/app/admin/magic-links/page.tsx is an unconditional redirect: the
standalone page was folded into System Settings as the Magic Links tab, where
MagicLinksList renders with embedded=true (System supplies the page
header). The old sidebar entry was removed from src/lib/nav.ts.
The Magic Links tab (destination)
- Token list —
GET /api/admin/magic-linksreturns every row in the Supabasemagic_link_tokenstable: user email, label, redirect target, created at/by, expiry, consumed at, last-consumed at + IP, consume count, revoked at/by. Only metadata is shown — the token column stays hashed. - Status derivation — each row renders one of Revoked / Used / Expired /
Active / "Lifetime · Active", computed client-side from the row fields
(revoked wins; a consumed non-lifetime token is Used; past
expiresAtis Expired). - Filters + search — filter modes
all | lifetime | active | used | revoked, plus free-text search over email, label, redirect, and id. - Mint —
POST /api/admin/magic-linkswithtargetEmail,lifetimeflag, optionallabel, andttlMinutes(default 30) for non-lifetime tokens. Lifetime tokens require a label (enforced in the UI for the audit trail). The response is the only time the plaintext URL/token exists client-side — it is shown once with a copy affordance, then never again. - Revoke —
POST /api/admin/magic-links/{id}/revoke; the sole kill switch for a leaked lifetime credential.
Security model (why nothing needed redaction in the screenshot)
src/lib/magic-link.ts persists only sha256(token) — a DB dump cannot be
redeemed, and the admin list can never display a live link after mint time.
The consume flow lives at /api/magic-link/consume: the route hashes the
presented token, matches it against magic_link_tokens, enforces
expiry/revocation, and signs the user in (recording consume count and IP).
Lifetime tokens are multi-use until revoked; standard tokens are single-use.
scripts/mint-magic-link-demo.mjs is the CLI equivalent of the mint form
(used to authenticate the capture run itself, per the manifest).
Behavior summary
| Aspect | Value |
|---|---|
| HTTP behavior | Server redirect to /admin/settings?tab=magic-links |
| Query params | Not preserved |
| Who can use it | List/mint/revoke: super_admin only (API-enforced) |
| Token storage | sha256 hash only; plaintext shown once at mint |
| Rendering mode | force-dynamic |
Key files
| File / endpoint | Role |
|---|---|
src/app/admin/magic-links/page.tsx | The redirect (whole page) |
src/components/admin/MagicLinksList.tsx | Tab UI (list, filters, mint, revoke) |
src/app/admin/settings/SettingsView.tsx | Host tab (?tab=magic-links) |
src/lib/magic-link.ts | Token mint/hash/consume/summary logic |
GET/POST /api/admin/magic-links | List + mint (super_admin) |
POST /api/admin/magic-links/{id}/revoke | Revoke |
/api/magic-link/consume | Public consume/sign-in endpoint |
scripts/mint-magic-link-demo.mjs | CLI mint helper |
Supabase magic_link_tokens | Backing table |