Radon
Auth Methods

Email code

Passwordless sign-in with a 6-digit code emailed to the user. The simplest way to authenticate with Radon.

The user types their email, receives a 6-digit code, and types it back. No password to remember, no link to click. It's the fastest passwordless flow to ship and the one used in the Quickstart.

1. Enable the provider

Email code needs a sender to deliver the code.

lib/auth.ts
import { Radon } from "@radonsdk/auth";
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";
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: {
    emailCode: { sender },
  },
});
OptionTypeDescription
senderEmailSenderRequired. Delivers the code email.
fromstringOverrides the sender's default From for these emails.
templateCodeTemplateCustom subject/HTML/text. See below.

Code length, lifetime, and max attempts are configured globally with the code option (default: 6 digits, 10-minute lifetime, 5 attempts).

2. Use it over HTTP

Once you've mounted a framework integration, these routes exist under your base path (e.g. /api/auth):

Method & pathBodyEffect
POST /email-code/send{ email }Emails a 6-digit code
POST /email-code/verify{ email, code }Signs in → sets session cookie
Frontend
// Step 1 — send the code
await fetch("/api/auth/email-code/send", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ email }),
});

// Step 2 — verify what the user typed; on success a session cookie is set
const res = await fetch("/api/auth/email-code/verify", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ email, code }),
});
// res.ok === true → the browser now holds an HttpOnly `radon_session` cookie

3. Or call it directly in code

Every provider is callable without the HTTP layer — useful for server actions, scripts, or custom flows.

// Issue + email a code
const { expiresAt } = await auth.emailCode.sendCode({ email: "sam@acme.com" });

// Verify it → resolves (or creates) the user
const { user, created } = await auth.emailCode.verify({
  email: "sam@acme.com",
  code: "123456",
});
// `created` is true if this was a brand-new signup.
// Issue your own session token if you're not using an HTTP integration:
const { token } = auth.createSessionToken(user.id);

Return shapes

sendCode({ email, metadata? }){ expiresAt: Date }. verify({ email, code }){ user: RadonUser, created: boolean }.

Custom email

Pass a template function to control the subject and body. It receives the code and expiry and returns the email content:

lib/auth.ts
providers: {
  emailCode: {
    sender,
    template: ({ code, expiresInMinutes, appName }) => ({
      subject: `${code} is your ${appName} code`,
      html: `<h1>${code}</h1><p>Expires in ${expiresInMinutes} minutes.</p>`,
      text: `Your ${appName} code is ${code}. Expires in ${expiresInMinutes} minutes.`,
    }),
  },
}

Errors

Verification can fail for a few well-defined reasons. Over HTTP these map to status codes; in code they're typed errors you can catch.

Error / codeHTTPMeaning
CodeInvalidError / code_invalid401Wrong code. Carries attemptsRemaining.
CodeExpiredError / code_expired401Code older than its TTL.
CodeNotFoundError / code_not_found401No active code for that email.
MaxAttemptsError / max_attempts_exceeded401Too many wrong tries — request a new code.
RateLimitError / rate_limit_exceeded429Too many sends. Carries retryAfterMs.
import { CodeInvalidError, MaxAttemptsError } from "@radonsdk/auth";

try {
  await auth.emailCode.verify({ email, code });
} catch (err) {
  if (err instanceof CodeInvalidError) {
    return `Wrong code — ${err.attemptsRemaining ?? 0} tries left`;
  }
  if (err instanceof MaxAttemptsError) {
    return "Too many attempts. Request a new code.";
  }
  throw err;
}

Next steps

On this page