Radon
Auth Methods

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.

lib/auth.ts
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",
    },
  },
});
OptionTypeDescription
senderEmailSenderRequired. Delivers the link email.
baseUrlstringRequired. Where the link points. The token is appended as ?token=.
ttlMsnumberLink lifetime in ms. Default 900_000 (15 minutes).
templateLinkTemplateCustom subject/HTML/text.
fromstringOverrides 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 & pathBody / queryEffect
POST /magic-link/send{ email }Emails a sign-in link
GET /magic-link/verify?token=…Signs in → sets cookie, then redirects
Frontend
// 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 needed

Return 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",
});

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 / codeHTTPMeaning
TokenInvalidError / token_invalid401Bad signature, malformed, or already used.
CodeExpiredError / code_expired401Link older than ttlMs.
RateLimitError / rate_limit_exceeded429Too 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.

Next steps

On this page