Core concepts
The mental model behind Radon — adapters, senders, providers, JWT sessions, and merge-by-email account linking.
Radon is small on purpose. Once you understand these five ideas, every page in these docs will make sense.
The shape of the library
┌─────────────────────────────────────────────┐
│ Radon (the SDK) — the object you call │
│ providers · senders · JWT sessions │
└───────────────┬─────────────────┬─────────────┘
integrations/* │ │ senders/*
(next · express · │ │ (resend · sendgrid ·
fastify · hono) │ │ postmark · ses)
┌───────▼─────────┐ ▼
│ Radon engine │ EmailSender
│ codes · sessions│
│ merge-by-email │
└───────┬─────────┘
adapters/* (mongo · postgres · mysql ·
prisma · supabase · firebase · sqlite)You configure a Radon instance with an adapter and some providers. The
providers use senders to email people. A framework integration exposes it
all over HTTP. Underneath, a shared engine handles codes, sessions, and
account linking.
1. The adapter — your database
An adapter is a small set of methods (createUser, findUserByEmail,
createOneTimeCode, …) that Radon calls to read and write data. Because that
contract is all Radon depends on, any database can back it.
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";
adapter: postgresAdapter(pool) // swap for mongoAdapter(conn), etc. — nothing else changesRadon never sees plaintext secrets in your database. Codes and session tokens are hashed (SHA-256) before they're handed to the adapter; passwords are bcrypt-hashed. A database leak never exposes a live credential. → Adapters
2. The sender — outbound email
A sender is one method — send({ to, subject, html, text }) — that delivers an
email. Providers that email the user (email code, magic link, password reset)
call it. Built-in senders talk to their API over fetch, so most need no extra
dependency.
import { resendSender } from "@radonsdk/auth/senders/resend";
const sender = resendSender({ apiKey: process.env.RESEND_API_KEY!, from: "Acme <auth@acme.com>" });You can chain senders for failover, or write your own in a few lines. → Senders
3. Providers — the sign-in methods
Each entry under providers turns on one way to authenticate. You enable only
what you need, and unused providers never ship in your bundle.
providers: {
emailCode: { sender }, // 6-digit codes
magicLink: { sender, baseUrl: "https://acme.com/verify" },
google: { clientId, clientSecret, redirectUri },
}Every provider is reachable in code as auth.<provider> (e.g.
auth.emailCode.sendCode(...)), and the same flows are exposed as HTTP routes by
the framework integrations. → Auth methods
4. Sessions — stateless JWTs
When a sign-in succeeds, Radon issues a JWT session token and (through the
framework integration) sets it as an HttpOnly, Secure, SameSite=Lax cookie
named radon_session. Verifying a request is just verifying the token's
signature — no database round-trip.
// Under the hood, on a successful verify:
const { token, expiresAt } = auth.createSessionToken(user.id);
// Later, on a protected request:
const user = await auth.getSessionUser(token); // verify + load the userYou can verify a token anywhere with the same secret — no Radon instance
required:
import { verifyToken } from "@radonsdk/auth";
const claims = verifyToken(cookieValue, process.env.RADON_SECRET!); // claims.sub is the user idThe stateless trade-off
Statelessness is what makes JWT sessions fast and horizontally scalable. The
cost: you can't invalidate one token before it expires — rotating RADON_SECRET
logs everyone out. If you need per-device revocation, use the
Pro sessions or refresh-token providers.
5. Merge-by-email — one user, many sign-in methods
This is the idea that keeps you from creating duplicate accounts. When someone authenticates, Radon resolves them to a user by verified email:
- If a user with that verified email already exists, the new sign-in method is
linked to that same user — same
id, same data. - If not, a new user is created.
So a person who signed up with an email code on Monday and clicks "Sign in with Google" (same, Google-verified email) on Tuesday lands on the same account — not a second one. Each linked method is stored as an identity on the user.
user (id: u_123, email: sam@acme.com)
├── identity: email → sam@acme.com
├── identity: google → 118…927 (Google sub)
└── identity: password → sam@acme.comUnverified emails don't merge
Merging only happens on a verified email. A provider that hasn't verified the address (or has none, like phone OTP) links or creates without merging — this is deliberate, so an attacker can't hijack an account by claiming an email they don't control.
Errors are typed
Every failure is a RadonError subclass with a stable code you can branch on,
so you never match on message text. Framework integrations map these to sensible
HTTP statuses automatically.
import { RateLimitError, CodeInvalidError } from "@radonsdk/auth";
try {
await auth.emailCode.verify({ email, code });
} catch (err) {
if (err instanceof CodeInvalidError) { /* wrong code — err.attemptsRemaining */ }
if (err instanceof RateLimitError) { /* too many tries — err.retryAfterMs */ }
}→ Full list in the API reference.