Radon

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 changes

Radon 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 user

You 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 id

The 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.com

Unverified 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.

Next steps

On this page