Radon

Quickstart

Go from zero to a working email sign-in flow in about five minutes, using Next.js, Postgres, and Resend.

By the end of this page you'll have a real, working sign-in flow: a user enters their email, receives a 6-digit code, types it back, and gets a session cookie. We'll use Next.js (App Router), Postgres, and Resend — but the same steps map onto any framework, any database, and any email provider.

Prerequisites

Node 18+, a Postgres database you can connect to, and a Resend API key with a verified sending domain. Haven't installed yet? Start with Installation.

What you'll build

  • A shared auth instance your whole app imports.
  • An auth API mounted at /api/auth/* (all the routes, from one handler).
  • A sign-in page that sends a code and verifies it.
  • A protected route that reads the logged-in user.

1. Create the auth instance

This is the object your app talks to. Create it once and export it.

lib/auth.ts
import { Radon } from "@radonsdk/auth";
import { postgresAdapter } from "@radonsdk/auth/adapters/postgres";
import { resendSender } from "@radonsdk/auth/senders/resend";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

export const auth = new Radon({
  adapter: postgresAdapter(pool),
  session: { secret: process.env.RADON_SECRET! },
  appName: "Acme",
  providers: {
    emailCode: {
      sender: resendSender({ apiKey: process.env.RESEND_API_KEY!, from: "Acme <auth@acme.com>" }),
    },
  },
});

2. Create the database tables

Run this once against your database. It creates Radon's tables and is safe to re-run — it only adds what's missing.

scripts/init-db.ts
import { auth } from "../lib/auth";

await auth.init();
console.log("✅ Radon tables ready");
npx tsx scripts/init-db.ts

You should see: ✅ Radon tables ready. If you get a connection error, check DATABASE_URL.

3. Mount the auth routes

One catch-all route file exposes every auth endpoint (/email-code/send, /email-code/verify, /logout, /session, …) under /api/auth.

app/api/auth/[...radon]/route.ts
import { radonNextHandler } from "@radonsdk/auth/integrations/next";
import { auth } from "@/lib/auth";

const handler = radonNextHandler(auth, { basePath: "/api/auth" });

export const GET = handler;
export const POST = handler;

That's the entire backend. No per-route wiring — the handler routes internally.

4. Send and verify a code from the frontend

The flow is two fetch calls. The first emails a code; the second verifies it and — on success — sets an HttpOnly session cookie automatically.

app/signin/page.tsx
"use client";
import { useState } from "react";

export default function SignIn() {
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [sent, setSent] = useState(false);

  async function sendCode() {
    await fetch("/api/auth/email-code/send", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email }),
    });
    setSent(true); // now show the code input
  }

  async function verify() {
    const res = await fetch("/api/auth/email-code/verify", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email, code }),
    });
    if (res.ok) {
      window.location.href = "/dashboard"; // session cookie is now set
    } else {
      alert("Invalid or expired code");
    }
  }

  return !sent ? (
    <div>
      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@email.com" />
      <button onClick={sendCode}>Send code</button>
    </div>
  ) : (
    <div>
      <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="6-digit code" />
      <button onClick={verify}>Verify</button>
    </div>
  );
}

5. Read the logged-in user on a protected route

Anywhere you have the request, getUserFromRequest reads and verifies the session cookie for you. It returns the user, or null if there's no valid session.

app/api/me/route.ts
import { getUserFromRequest } from "@radonsdk/auth/integrations/next";
import { auth } from "@/lib/auth";

export async function GET(req: Request) {
  const user = await getUserFromRequest(auth, req);
  if (!user) return new Response("Unauthorized", { status: 401 });
  return Response.json({ user });
}

You should see: hitting /api/me while signed in returns { "user": { "id": "…", "email": "you@email.com", "emailVerified": true, … } }. Signed out, it returns 401 Unauthorized.

🎉 That's it

You have working authentication: passwordless email sign-in, a secure session cookie, and a way to read the current user. No hosted service, no per-user billing — it's all running in your own app.

Next steps

On this page