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
authinstance 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.
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.
import { auth } from "../lib/auth";
await auth.init();
console.log("✅ Radon tables ready");npx tsx scripts/init-db.tsYou should see:
✅ Radon tables ready. If you get a connection error, checkDATABASE_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.
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.
"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.
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/mewhile signed in returns{ "user": { "id": "…", "email": "you@email.com", "emailVerified": true, … } }. Signed out, it returns401 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
Add more sign-in methods
Magic links, passwords, Google — each turns on with one more provider entry.
Protect pages & middleware
Guard server components, route handlers, and middleware.
Understand the model
Sessions, merge-by-email, and how the pieces connect.
Full configuration
Every option the Radon constructor accepts.