Every verified stack secured with Auth.js (NextAuth).
Auth.js (NextAuth v5): self-hosted OAuth running inside your app against your database (Drizzle adapter).
80 verified stacks
Showing 12 across 12 app types. Every stack page links its neighbours along each axis, so any combination is a click or two from here.
What Auth.js (NextAuth) gives you
Auth.js v5 — still shipped as the next-auth package — runs inside your app and stores identity in your own database, but the shape it assumes is OAuth-first, and that decides most of what this cell looks like. No password is stored anywhere in the emitted schema. Sign-up and sign-in are the same act: the vendored screen is one "Continue with GitHub" button calling signIn("github", { redirectTo: "/dashboard" }), and the first successful callback is what creates the row. The identity tables are the canonical @auth/drizzle-adapter set, authored into db/auth-schema.ts as user, account, session and verificationToken. Their keys are worth reading before building on them.
account has no id of its own; its primary key is the composite (provider, providerAccountId), so one user row can hold several provider accounts, one per pair, each cascading from user.id. session is keyed by sessionToken and carries an expires timestamp — this is a database-session setup, not a JWT one, so signing out deletes a row and revocation is a DELETE you can run yourself. user.id is a text (varchar(255) on MySQL) primary key filled by $defaultFn(() => crypto.randomUUID()), and app-type schemas foreign-key it directly. src/lib/auth.ts hands all four tables to DrizzleAdapter explicitly through usersTable / accountsTable / sessionsTable / verificationTokensTable, so the adapter writes this schema rather than looking for its own default table names.
NextAuth's factory returns handlers, auth, signIn and signOut in a single destructure, and each of those lands somewhere specific in the emitted files. handlers is re-exported as GET and POST from src/app/api/auth/[...nextauth]/route.ts, which is where sign-in, callback, session and CSRF requests all terminate. auth is used twice at different strengths: wrapped as the proxy in src/proxy.ts, where a null req.auth redirects traffic off /dashboard and /settings, and called directly inside requireUser() in src/lib/session.ts for the real per-render check. <SessionProvider> in the root layout is what makes useSession() and signOut() work in the sidebar's nav-user. Two constraints follow. This adapter is pinned to Next through supportedFrameworks, because next-auth is Next-specific — React Router and Nuxt would need @auth/core instead.
And because GitHub already verified the address, there is no verify-email or reset-password flow here to wire: composing an email fragment gets you events.createUser calling sendWelcome once the adapter has persisted the user, and nothing more. Adding email or magic-link sign-in means adding a provider yourself, which changes the sign-in surface.
Built for
The (auth)/sign-in screen calls signIn("github", { redirectTo: "/dashboard" }) from next-auth/react; the callback terminates in src/app/api/auth/[...nextauth]/route.ts, which re-exports NextAuth's handlers as GET and POST. The sign-up screen is the same button with different copy.
DrizzleAdapter(db, { usersTable: user, accountsTable: account, sessionsTable: session, verificationTokensTable: verificationToken }) in src/lib/auth.ts writes a session row keyed by sessionToken with an expires timestamp, over the same Drizzle client src/lib/db.ts exports.
src/proxy.ts wraps the exported auth as middleware and redirects when req.auth is null for /dashboard and /settings; requireUser() in src/lib/session.ts awaits auth() again inside the page, so a stale cookie cannot render protected content.
src/app/layout.tsx mounts <SessionProvider> inside <body>, which is what lets nav-user call useSession() for the avatar and email and signOut() for the menu action without threading the session down through props.
providers: [GitHub] in src/lib/auth.ts is the whole provider list. account's composite (provider, providerAccountId) primary key and cascading user_id mean one user row can carry an account row per provider — the adapter stores the tokens, scope and expires_at per pair.
Why Auth.js (NextAuth), specifically
Self-hosted + OAuth-first: NextAuth owns the user/account/session/verificationToken tables (Auth.js Drizzle schema). This stack emits them (db/auth-schema.ts) and hands them to the Drizzle adapter, so app-type schemas can foreign-key `user` directly.
OAuth (GitHub) is wired by default — sign-in creates the account, so there is no separate email-signup step. Swap or add providers in src/lib/auth.ts.
What Auth.js (NextAuth) pulls in
bun add next-auth @auth/drizzle-adapterAUTH_SECRETgenerate with `npx auth secret` (or `openssl rand -base64 32`)AUTH_GITHUB_IDGitHub OAuth app client idAUTH_GITHUB_SECRETGitHub OAuth app client secret