Magic link
One-click passwordless sign-in — Radon emails a signed, single-use link that logs the user in.
Same idea as email code, but instead of typing a code the user clicks a link. Radon emails a signed, single-use, time-limited URL; clicking it verifies the user and sets a session.
1. Enable the provider
Magic link needs a sender and a baseUrl — the page in your app
the link points at. Radon appends the signed token as a ?token= query param.
import { resendSender } from "@radonsdk/auth/senders/resend";
const sender = resendSender({ apiKey: process.env.RESEND_API_KEY!, from: "Acme <auth@acme.com>" });
export const auth = new Radon({
adapter: postgresAdapter(pool),
session: { secret: process.env.RADON_SECRET! },
appName: "Acme",
providers: {
magicLink: {
sender,
baseUrl: "https://acme.com/api/auth/magic-link/verify",
},
},
});| Option | Type | Description |
|---|---|---|
sender | EmailSender | Required. Delivers the link email. |
baseUrl | string | Required. Where the link points. The token is appended as ?token=. |
ttlMs | number | Link lifetime in ms. Default 900_000 (15 minutes). |
template | LinkTemplate | Custom subject/HTML/text. |
from | string | Overrides the sender's default From. |
What baseUrl should point to
Point baseUrl at your integration's verify route — …/api/auth/magic-link/verify.
That route validates the token, sets the session cookie, and redirects to
successRedirect (default /). See framework integrations.
2. Use it over HTTP
| Method & path | Body / query | Effect |
|---|---|---|
POST /magic-link/send | { email } | Emails a sign-in link |
GET /magic-link/verify | ?token=… | Signs in → sets cookie, then redirects |
// The user submits their email; Radon emails them a link.
await fetch("/api/auth/magic-link/send", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email }),
});
// Show "Check your inbox." The link itself is a GET the browser follows —
// your app doesn't fetch the verify route, the user's click does.When the user clicks the emailed link, their browser hits
GET /api/auth/magic-link/verify?token=…. Radon verifies it, sets the
radon_session cookie, and 302-redirects to your successRedirect (or
failureRedirect if the token is bad or expired).
3. Or call it directly in code
// Issue + email a link. You also get the URL back, e.g. to log or test.
const { url, expiresAt } = await auth.magicLink.sendLink({ email: "sam@acme.com" });
// Later, verify the signed token from the link's `?token=` param:
const { user, created } = await auth.magicLink.verify(tokenFromQuery);
const { token } = auth.createSessionToken(user.id); // issue your own session if neededReturn shapes
sendLink({ email, redirectTo?, metadata? }) → { url: string, expiresAt: Date }.
verify(signedToken) → { user: RadonUser, created: boolean }.
Carrying a post-login redirect
Pass redirectTo to override the link target for one send — handy for sending
the user back to the page they came from:
await auth.magicLink.sendLink({
email,
redirectTo: "https://acme.com/api/auth/magic-link/verify?next=/billing",
});How the link is secured
The token in the URL is the engine's single-use code wrapped in an
HMAC-SHA256 signature bound to the email, using your RADON_SECRET. That
means:
- The link is tamper-evident — changing the email or token invalidates the signature before any database lookup.
- It's single-use — the underlying code is consumed on first verify.
- It expires — 15 minutes by default (
ttlMs).
Errors
| Error / code | HTTP | Meaning |
|---|---|---|
TokenInvalidError / token_invalid | 401 | Bad signature, malformed, or already used. |
CodeExpiredError / code_expired | 401 | Link older than ttlMs. |
RateLimitError / rate_limit_exceeded | 429 | Too many sends. Carries retryAfterMs. |
Over HTTP, a failed verify simply redirects to failureRedirect rather than
showing an error body — so the user never sees a raw error page.