VOOZH about

URL: https://dev.to/mt211211/authjs-v5-with-nextjs-and-drizzle-orm-the-complete-setup-guide-274i

⇱ Auth.js v5 with Next.js and Drizzle ORM: the complete setup guide - DEV Community


Auth.js v5 with Next.js and Drizzle ORM: the complete setup guide

Auth.js v5 (formerly NextAuth.js) is a significant rewrite. The API is cleaner, but most tutorials still show v4 patterns — which won't work. This guide covers everything from OAuth setup to a Drizzle Postgres adapter, in a way that actually compiles.

What changed in v5

The big shifts:

  • Single auth.ts config file replaces the scattered [...nextauth] options
  • auth() is universal — use it in Server Components, Route Handlers, and Proxy (formerly Middleware)
  • handlers replaces GET/POST export ceremony
  • The middleware.ts convention is now proxy.ts in Next.js 16+

1. Install

npm install next-auth@beta @auth/drizzle-adapter drizzle-orm @neondatabase/serverless
npm install --save-dev drizzle-kit

2. Drizzle schema for Auth.js

Auth.js needs four tables: users, accounts, sessions, verification_tokens. Define them with Drizzle's pg-core:

// src/lib/db/schema.ts
import {
 boolean, integer, pgTable, primaryKey, text, timestamp,
} from "drizzle-orm/pg-core";
import type { AdapterAccount } from "@auth/core/adapters";

export const users = pgTable("users", {
 id: text("id").primaryKey(),
 name: text("name"),
 email: text("email").unique(),
 emailVerified: timestamp("email_verified", { mode: "date" }),
 image: text("image"),
});

export const accounts = pgTable(
 "accounts",
 {
 userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
 type: text("type").$type<AdapterAccount["type"]>().notNull(),
 provider: text("provider").notNull(),
 providerAccountId: text("provider_account_id").notNull(),
 refresh_token: text("refresh_token"),
 access_token: text("access_token"),
 expires_at: integer("expires_at"),
 token_type: text("token_type"),
 scope: text("scope"),
 id_token: text("id_token"),
 session_state: text("session_state"),
 },
 (account) => [primaryKey({ columns: [account.provider, account.providerAccountId] })]
);

export const sessions = pgTable("sessions", {
 sessionToken: text("session_token").primaryKey(),
 userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
 expires: timestamp("expires", { mode: "date" }).notNull(),
});

export const verificationTokens = pgTable(
 "verification_tokens",
 {
 identifier: text("identifier").notNull(),
 token: text("token").notNull(),
 expires: timestamp("expires", { mode: "date" }).notNull(),
 },
 (vt) => [primaryKey({ columns: [vt.identifier, vt.token] })]
);

3. Drizzle client (Neon serverless)

// src/lib/db/index.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

4. Auth config

// src/auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";
import { accounts, sessions, users, verificationTokens } from "@/lib/db/schema";

export const { handlers, auth, signIn, signOut } = NextAuth({
 adapter: DrizzleAdapter(db, {
 usersTable: users,
 accountsTable: accounts,
 sessionsTable: sessions,
 verificationTokensTable: verificationTokens,
 }),
 providers: [
 GitHub,
 Google,
 ],
 callbacks: {
 // Expose user.id in the session — not available by default
 session({ session, user }) {
 session.user.id = user.id;
 return session;
 },
 },
 pages: {
 signIn: "/sign-in",
 },
});

// Augment the Session type to include id
declare module "next-auth" {
 interface Session {
 user: { id: string; name?: string | null; email?: string | null; image?: string | null; };
 }
}

5. Route handler

// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

One line. That's it.

6. Sign-in page with Server Actions

Auth.js v5 uses Server Actions for sign-in — no client-side JS required:

// src/app/(auth)/sign-in/page.tsx
import { signIn } from "@/auth";

export default function SignInPage({
 searchParams,
}: {
 searchParams: { callbackUrl?: string };
}) {
 return (
 <div>
 <form
 action={async () => {
 "use server";
 await signIn("github", {
 redirectTo: searchParams.callbackUrl ?? "/dashboard",
 });
 }}
 >
 <button type="submit">Continue with GitHub</button>
 </form>

 <form
 action={async () => {
 "use server";
 await signIn("google", {
 redirectTo: searchParams.callbackUrl ?? "/dashboard",
 });
 }}
 >
 <button type="submit">Continue with Google</button>
 </form>
 </div>
 );
}

7. Protecting routes with Proxy (Next.js 16)

In Next.js 16, middleware.ts was renamed to proxy.ts. The export is proxy instead of middleware:

// src/proxy.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export const proxy = auth((req) => {
 const isLoggedIn = !!req.auth;
 const isProtected =
 req.nextUrl.pathname.startsWith("/dashboard") ||
 req.nextUrl.pathname.startsWith("/chat");

 if (isProtected && !isLoggedIn) {
 const signIn = new URL("/sign-in", req.nextUrl.origin);
 signIn.searchParams.set("callbackUrl", req.nextUrl.pathname);
 return NextResponse.redirect(signIn);
 }
});

export const config = {
 matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

On Next.js 15 and earlier, name this file middleware.ts and export as middleware instead.

8. Reading the session

In any Server Component or Route Handler:

import { auth } from "@/auth";

// Server Component
export default async function DashboardPage() {
 const session = await auth();
 // session.user.id is available (from our callback above)
 return <div>Hello {session?.user?.name}</div>;
}

// Route Handler
export async function GET() {
 const session = await auth();
 if (!session?.user?.id) {
 return Response.json({ error: "Unauthorized" }, { status: 401 });
 }
 // ...
}

9. Run migrations

npx drizzle-kit generate
npx drizzle-kit migrate

Environment variables

# .env.local
AUTH_SECRET= # openssl rand -hex 32
AUTH_GITHUB_ID= # github.com/settings/applications
AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID= # console.cloud.google.com
AUTH_GOOGLE_SECRET=
DATABASE_URL= # postgresql://...

Auth.js v5 auto-infers AUTH_GITHUB_ID / AUTH_GITHUB_SECRET for the GitHub provider — you don't need to pass them to GitHub({...}).

That's the complete setup

Everything above is production-ready code. The full stack (auth + Stripe billing + Claude AI + MCP server) is in AgentShip, an open-source AI-native SaaS boilerplate — the private repo has the complete implementation if you want to skip the plumbing entirely.