Next.js App Router, Auth.js, Drizzle ORM, PostgreSQL, Authentication Tutorial

Learn how to implement authentication in a Next.js app using Auth.js (formerly NextAuth) with Drizzle ORM and PostgreSQL.

Step 1: Initialize Next.js project

npx create-next-app@latest

Step 2: Create a Postgres database

create database nextjs_authjs_tutorial;

Step 3: Install dependencies

npm install next-auth@beta drizzle-orm @auth/drizzle-adapter pg
npm install drizzle-kit @types/pg --save-dev

Step 4: Load environment variables

/lib/config.ts

import { loadEnvConfig } from "@next/env";

const projectDir = process.cwd();
loadEnvConfig(projectDir);

const config = {
  POSTGRES_URL: process.env.POSTGRES_URL!,
  APP_ENV: process.env.APP_ENV!,
};

export default config;

Next.js automatically loads environment variables from .env.local, however, Drizzle Kit does not. So, we'll need to call loadEnvConfig manually.

Step 5: Define schema

/lib/schema.ts

import {
  timestamp,
  pgTable,
  text,
  primaryKey,
  integer,
} from "drizzle-orm/pg-core";
import type { AdapterAccount } from "@auth/core/adapters";

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

export const accounts = pgTable(
  "account",
  {
    userId: text("userId")
      .notNull()
      .references(() => users.id, { onDelete: "cascade" }),
    type: text("type").$type<AdapterAccount["type"]>().notNull(),
    provider: text("provider").notNull(),
    providerAccountId: text("providerAccountId").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) => ({
    compoundKey: primaryKey({
      columns: [account.provider, account.providerAccountId],
    }),
  })
);

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

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

The schema was copied and pasted from the Auth.js docs.

Step 6: Create the Drizzle DB instance

/lib/db.ts

import config from "@/lib/config";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

let sslmode = "";
if (config.APP_ENV === "prod") {
  sslmode = "?sslmode=require";
}

export const pool = new Pool({
  connectionString: config.POSTGRES_URL + sslmode,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export const db = drizzle(pool, { schema, logger: true });

We are preemptively fixing the sslmode require problem that occurs after deploying to Vercel.

Step 7: Configure Auth.js

/lib/auth.ts

import { DrizzleAdapter } from "@auth/drizzle-adapter";
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import { db } from "./db";
export const {
  handlers: { GET, POST },
  auth,
} = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [GitHub],
  callbacks: {
    async session({ session, user, token }: any) {
      return session;
    },
  },
});

The session callback is required to add the user id to the user object.

Step 8: Create Auth API route

/app/api/auth/[...nextauth]/route.ts

export { GET, POST } from "@/lib/auth";

Step 9: Extend Next Auth Type With Module Augmentation

/types/next-auth.d.ts

import NextAuth from "next-auth";

declare module "next-auth" {
  /**
   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
   */
  interface Session {
    user: {
      id: string;
      name: string;
    };
  }
}

Step 10: Sign In Button

/components/signin-btn-auto.tsx

"use client";
import { useSession, signIn, signOut } from "next-auth/react";

export default function SignInBtnAuto() {
  const { data: session } = useSession();
  if (session) {
    return (
      <>
        Signed in as {session.user?.name} <br />
        <button onClick={() => signOut()}>Sign out</button>
      </>
    );
  }
  return (
    <>
      Not signed in <br />
      <button onClick={() => signIn()}>Sign in</button>
    </>
  );
}

This button uses the prebuilt sign in page from Auth.js.

Step 11: Session Provider

/components/session-provider.tsx

"use client";
import { SessionProvider } from "next-auth/react";
export default SessionProvider;

Use this session provider on any layout or component that needs to check user session.

Step 12: Use the Session Provider

/app/(dynamic)/layout.tsx

import { ReactNode } from "react";
import SessionProvider from "@/components/session-provider";
import { auth } from "@/lib/auth";

export default async function Layout({ children }: { children: ReactNode }) {
  const session = await auth();
  return (
    <SessionProvider session={session}>
      <div>{children}</div>
    </SessionProvider>
  );
}

We are using a route group to avoid putting the session provider on the root layout so that we don't force static pages to become dynamic. For example, static marketing pages.

Step 13: Sign In Page with Auth.js prebuilt log in buttons

/app/(dynamic)/signin-auto/page.tsx

import SigninBtnAuto from "@/components/signin-btn-auto";

export default function Page() {
  return (
    <div>
      <p>Sign In Auto</p>
      <SigninBtnAuto />
    </div>
  );
}

Step 14: Create GitHub OAuth App

Go to GitHub > Settings > Developer Settings > OAuth Apps.

Click on "New OAuth App".

Create a new client secret.

The home page can be http://localhost:3000 for local development.

The authorization callback should be http://localhost:3000/api/auth/callback/github.

Step 15: Create env local file

/.env.local

POSTGRES_URL=postgresql://postgres:postgres@localhost/nextjs_authjs_tutorial
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
AUTH_SECRET=
APP_ENV=local

Step 16: Configure Drizzle

/drizzle.config.ts

import config from "@/lib/config";
import { defineConfig } from "drizzle-kit";

let sslmode = "";
if (config.APP_ENV === "prod") {
  sslmode = "?sslmode=require";
}

export default defineConfig({
  schema: "./lib/schema.ts",
  out: "./drizzle",
  driver: "pg",
  dbCredentials: {
    connectionString: config.POSTGRES_URL + sslmode,
  },
  verbose: true,
  strict: true,
});

Step 17: Run the push command

npx drizzle-kit push:pg

This command will sync the schema to the database.

Step 18: Test the sign in page

Test the login by going to the sign in page and signing in. http://localhost:3000/signin-auto

Step 19: Create a protected page

/app/(dynamic)/protected/page.tsx

import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";

export default async function Page() {
  const session = await auth();
  if (!session) {
    redirect("/signin");
  }
  return <div>{session.user.name}</div>;
}

Test by visiting http://localhost:3000/protected.

Step 20: Create a protected API Route

/app/api/protected/route.ts

import { auth } from "@/lib/auth";

export async function GET() {
  const session = await auth();
  if (!session) {
    return Response.json({ message: "unauthenticated" }, { status: 401 });
  }
  return Response.json({ name: session?.user.name });
}

Test by sending a GET request to http://localhost:3000/api/protected.

Step 21: Create a custom sign in button

/components/signin-btn-custom.tsx

"use client";

import { OAuthProviderType } from "next-auth/providers";
import { signIn } from "next-auth/react";

export default function SigninBtnCustom({
  provider,
}: {
  provider: { id: OAuthProviderType; name: string };
}) {
  return (
    <button onClick={() => signIn(provider.id)}>
      Sign in with {provider.name}
    </button>
  );
}

In the previous stable version of NextAuth, getProviders was used to get the list of active providers. getProviders function doesn't work in the new beta version of Auth.js. Will need to hardcode the provider type id.

Step 22: Create a custom sign out button

/components/signout-btn-custom.tsx

"use client";

import { signOut } from "next-auth/react";

export default function SignOutBtnCustom() {
  return <button onClick={() => signOut()}>Sign Out</button>;
}

Step 23: Create a custom sign in page

/app/signin-custom/page.tsx

import SigninBtnCustom from "@/components/signin-btn-custom";
import SignOutBtnCustom from "@/components/signout-btn-custom";
import { auth } from "@/lib/auth";

export default async function Page() {
  const session = await auth();

  if (session) {
    return (
      <div>
        <div>Signed in as: {session.user.name}</div>
        <SignOutBtnCustom />
      </div>
    );
  }

  return (
    <div>
      <h1>Sign In Custom</h1>
      <SigninBtnCustom provider={{ id: "github", name: "GitHub" }} />
    </div>
  );
}

References