Full Stack LinkedIn Prototype With Next.js Course Preview
👉 Full Course
Build And Deploy A Professional Network App With Next.js, Drizzle ORM, PostgreSQL, TailwindCSS, Mantine, NextAuth, And Vercel
Welcome to the first few chapters of the Full Stack LinkedIn Prototype Course.
This article contains the notes for setting up the authentication using NextAuth.
Create a Postgres Database
create database next_auth_drizzle;
Create a Next.js App
npx create-next-app@latest
Clear out the home page and default styles
Remove the content in /app/page.tsx
and classes in /app/globals.css
.
Install Dependencies
npm install next-auth
npm install drizzle-orm @auth/drizzle-adapter
npm install drizzle-kit --save-dev
npm install pg
npm install @types/pg --save-dev
Copy the database schema from Next Auth Docs
/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] }),
})
);
Note: This can be found in the Next-Auth docs: https://authjs.dev/reference/adapter/drizzle
Create a config file for loading environment variables
/lib/config.ts
import { loadEnvConfig } from "@next/env";
const projectDir = process.cwd();
loadEnvConfig(projectDir);
const config = {
POSTGRES_URL: process.env.POSTGRES_URL!,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET!,
GITHUB_ID: process.env.GITHUB_ID!,
GITHUB_SECRET: process.env.GITHUB_SECRET!,
APP_ENV: process.env.APP_ENV!,
};
export default config;
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 });
Note: We are preemptively dealing with the sslmode require problem in Vercel production.
Create an auth options file for Next-Auth
/lib/auth.ts
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import GithubProvider from "next-auth/providers/github";
import { db } from "./db";
import config from "./config";
import { AuthOptions } from "next-auth";
export const authOptions: AuthOptions = {
adapter: DrizzleAdapter(db) as any,
secret: config.NEXTAUTH_SECRET,
providers: [
GithubProvider({
clientId: config.GITHUB_ID,
clientSecret: config.GITHUB_SECRET,
}),
],
callbacks: {
async session({ session, user, token }) {
session.user.id = token.sub;
return session;
},
},
session: {
strategy: "jwt",
},
};
Note:
- The
as any
is used for fixing a typescript error. secret
must be defined.
Monkey patch the typescript error
/lib/next-auth.d.ts
import NextAuth from "next-auth/next";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}
Create the Next-Auth route
/app/api/auth/[...nextauth]/route.ts
import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
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
.
Copy environment variables into .env.local
Copy the client id and client secret into .env.local
.
GITHUB_ID="copy client id here"
GITHUB_SECRET="copy client secret here"
NEXTAUTH_SECRET="a random string"
NEXTAUTH_URL="http://localhost:3000/api/auth"
POSTGRES_URL="a postgres url"
Note: NEXTAUTH_URL does not need to be defined if deploying to Vercel.
Create a Drizzle Config
/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,
});
Update tsconfig.json compiler target
Update the target
from es5
to es6
. If not, there will be an error when you run the push
command. If you're already on es6
or esnext
, then no action is required.
Run the push command
npx drizzle-kit push:pg
This command will sync the schema to the database.
Create SessionProvider component
/components/session-provider.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export default SessionProvider;
Note: Must be a client component or else there will be a React Context error.
Use the SessionProvider component
/app/(dynamic)/layout.tsx
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { ReactNode } from "react";
import SessionProvider from "@/components/session-provider";
export default async function Layout({ children }: { children: ReactNode }) {
const session = await getServerSession(authOptions);
return (
<SessionProvider session={session}>
<div>{children}</div>
</SessionProvider>
);
}
Note: authOptions must be passed into getServerSession.
Note: We are not using the session provider in the root layout. Instead, We are using (dynamic)
route group to separate static marketing pages and dynamic pages. The session makes a page dynamic because it is using cookies. This is to prevent losing the benefits of caching for static marketing pages.
Create a Login Button component
/components/login-btn.tsx
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
export default function LoginBtn() {
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>
</>
);
}
Use the Login Button component
/app/(dynamic)/signin/page.tsx
import LoginBtn from "@/components/login-btn";
export default function Page() {
return (
<div>
<p>Sign In</p>
<LoginBtn />
</div>
);
}
Test the login by going to the sign in page and signing in.
Create a restricted route
/app/api/restricted/route.ts
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
import { authOptions } from "@/lib/auth";
export async function GET() {
const session = await getServerSession(authOptions);
return NextResponse.json({ name: session?.user?.name ?? "Not Logged In" });
}
Test by sending a GET request to http://localhost:3000/api/restricted.
Create a restricted page
/app/restricted/page.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function Page() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/");
}
return (
<div>
<h1>Restricted Page</h1>
</div>
);
}
Test by visiting http://localhost:3000/restricted.
Add a logo to the automatically generated signin page
Add an image file to the /public
folder.
Add a theme
option to authOptions.
/lib/auth.ts
theme: {
logo: "/logo.svg",
},
Create a custom signin button and signin page
/components/signin-btn.tsx
"use client";
import { signIn } from "next-auth/react";
export default function SigninBtn({ provider }: { provider: any }) {
return (
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
);
}
/app/signin/page.tsx
import { getProviders } from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import SigninBtn from "@/components/signin-btn";
import { redirect } from "next/navigation";
export default async function SignIn() {
const session = await getServerSession(authOptions);
if (session) {
redirect("/dashboard");
}
const providers: any = await getProviders();
return (
<>
{Object.values(providers).map((provider: any) => (
<div key={provider.name}>
<SigninBtn provider={provider} />
</div>
))}
</>
);
}
References
- https://authjs.dev/reference/adapter/drizzle
- https://github.com/nextauthjs/next-auth/issues/7727#issuecomment-1688714579
- https://stackoverflow.com/questions/71385330/next-auth-jwedecryptionfailed
- https://next-auth.js.org/warnings#nextauth_url
- https://github.com/nextauthjs/next-auth/discussions/4255
- https://authjs.dev/reference/adapter/pg