Skip to main content

How to use Prisma ORM with Clerk Auth and Next.js

25 min

Introduction

Clerk is a drop-in auth provider that handles sign-up, sign-in, user management, and webhooks so you don't have to.

In this guide you'll wire Clerk into a brand-new Next.js app, persist users in a Prisma Postgres database, and expose a tiny posts API. You can find a complete example of this guide on GitHub.

Prerequisites

1. Set up your project

Create the app:

npx create-next-app@latest clerk-nextjs-prisma

It will prompt you to customize your setup. Choose the defaults:

info
  • Would you like to use TypeScript? Yes
  • Would you like to use ESLint? Yes
  • Would you like to use Tailwind CSS? Yes
  • Would you like your code inside a src/ directory? Yes
  • Would you like to use App Router? (recommended) Yes
  • Would you like to use Turbopack for next dev? Yes
  • Would you like to customize the import alias (@/* by default)? No

Navigate to the project directory:

cd clerk-nextjs-prisma

2. Set up Clerk

2.1. Create a new Clerk application

Navigate to Clerk's New App page and create a new application. Enter a title, select your sign-in options, and click Create Application.

info

For this guide, the Google, Github, and Email sign in options will be used.

Install the Clerk Next.js SDK:

npm install @clerk/nextjs

Copy your Clerk keys and paste them into .env in the root of your project:

.env
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>

2.2. Protect routes with Clerk middleware

The clerkMiddleware helper enables authentication and is where you'll configure your protected routes.

Create a middleware.ts file in the /src directory of your project:

src/middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
};

2.3. Add Clerk UI to your layout

Next, you'll need to wrap your app in the ClerkProvider component to make authentication globally available.

In your layout.tsx file, add the ClerkProvider component:

src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
</body>
</html>
</ClerkProvider>
);
}

Create a Navbar component which will be used to display the Sign In and Sign Up buttons as well as the User Button once a user is signed in:

src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import {
ClerkProvider,
UserButton,
SignInButton,
SignUpButton,
SignedOut,
SignedIn,
} from "@clerk/nextjs";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Navbar />
{children}
</body>
</html>
</ClerkProvider>
);
}

const Navbar = () => {
return (
<header className="flex justify-end items-center p-4 gap-4 h-16">
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
);
};

3. Install and configure Prisma

3.1. Install dependencies

To get started with Prisma, you'll need to install a few dependencies:

npm install prisma --save-dev
npm install tsx --save-dev
npm install @prisma/extension-accelerate

Once installed, initialize Prisma in your project:

npx prisma init --db --output ../src/app/generated/prisma
info

You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for the database like "My Clerk NextJS Project"

This will create:

  • A prisma/ directory with a schema.prisma file
  • A DATABASE_URL in .env

3.2. Define your Prisma Schema

In the prisma/schema.prisma file, add the following models:

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/app/generated/prisma"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
id Int @id @default(autoincrement())
clerkId String @unique
email String @unique
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}

This will create two models: User and Post, with a one-to-many relationship between them.

Now, run the following command to create the database tables and generate the Prisma Client:

npx prisma migrate dev --name init
warning

It is recommended that you add /src/app/generated/prisma to your .gitignore file.

3.3. Create a reusable Prisma Client

In the src/ directory, create /lib and a prisma.ts file inside it:

src/lib/prisma.ts
import { PrismaClient } from "@/app/generated/prisma";
import { withAccelerate } from "@prisma/extension-accelerate";

const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};

const prisma =
globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate());

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;

4. Wire Clerk to the database

4.1. Create a Clerk webhook endpoint

You'll use Svix to ensure the request is secure. Svix signs each webhook request so you can verify that it's legitimate and hasn't been tampered with during delivery.

Install the svix package:

npm install svix

Create a new API route at src/app/api/webhooks/clerk/route.ts:

Import the necessary dependencies:

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

Create the POST method that Clerk will call and verify the payload.

First, it will check that the Signing Secret is set:

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });
}
note

The Signing Secret is available in the Webhooks section of your Clerk application. You are not expected to have this yet, you'll set it in the next few steps.

Now, create the POST method that Clerk will call and verify the payload:

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });

const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();

const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;
}

When a new user is created, they need to be stored in the database.

You'll do that by checking if the event type is user.created and then using Prisma's upsert method to create a new user if they don't exist:

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });

const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();

const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;

if (event.type === "user.created") {
const { id, email_addresses, first_name, last_name } = event.data;
await prisma.user.upsert({
where: { clerkId: id },
update: {},
create: {
clerkId: id,
email: email_addresses[0].email_address,
name: `${first_name} ${last_name}`,
},
});
}
}

Finally, return a response to Clerk to confirm the webhook was received:

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });

const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();

const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;

if (event.type === "user.created") {
const { id, email_addresses, first_name, last_name } = event.data;
await prisma.user.upsert({
where: { clerkId: id },
update: {},
create: {
clerkId: id,
email: email_addresses[0].email_address,
name: `${first_name} ${last_name}`,
},
});
}

return new Response("OK");
}

4.2. Expose your local app for webhooks

You'll need to expose your local app for webhooks with ngrok. This will allow Clerk to reach your /api/webhooks/clerk route to push events like user.created.

Install ngrok and expose your local app:

npm install --global ngrok
ngrok http 3000

Copy the ngrok Forwarding URL. This will be used to set the webhook URL in Clerk.

Navigate to the Webhooks section of your Clerk application located near the bottom of the Configure tab under Developers.

Click Add Endpoint and paste the ngrok URL into the Endpoint URL field and add /api/webhooks/clerk to the end of the URL. It should look similar to this:

https://a60b-99-42-62-240.ngrok-free.app/api/webhooks/clerk

Copy the Signing Secret and add it to your .env file:

.env
# Prisma
DATABASE_URL=<your-database-url>

# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>
SIGNING_SECRET=<your-signing-secret>

On the home page, press Sign Up and create an account using any of the sign-up options

Open Prisma Studio and you should see a user record.

npx prisma studio
note

If you don't see a user record, there are a few things to check:

  • Delete your user from the Users tab in Clerk and try again.
  • Check your ngrok URL and ensure it's correct (it will change everytime you restart ngrok).
  • Check your Clerk webhook is pointing to the correct ngrok URL.
  • Make sure you've added /api/webhooks/clerk to the end of the URL.

5. Build a posts API

To create posts under a user, you'll need to create a new API route at src/app/api/posts/route.ts:

Start by importing the necessary dependencies:

src/app/api/posts/route.ts
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";

Get the clerkId of the authenticated user. If there's no user, return a 401 Unauthorized response:

src/app/api/posts/route.ts
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });
}

Match the Clerk user to a user in the database. If none is found, return a 404 Not Found response:

src/app/api/posts/route.ts
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });

const user = await prisma.user.findUnique({
where: { clerkId },
});

if (!user) return new Response("User not found", { status: 404 });
}

Destructure the title and content from the incoming request and create a post. Once done, return a 201 Created response:

src/app/api/posts/route.ts
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";

export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });

const { title, content } = await req.json();

const user = await prisma.user.findUnique({
where: { clerkId },
});

if (!user) return new Response("User not found", { status: 404 });

const post = await prisma.post.create({
data: {
title,
content,
authorId: user.id,
},
});

return new Response(JSON.stringify(post), { status: 201 });
}

6. Add a Post creation UI

In /app, create a /components directory and a PostInputs.tsx file inside it:

src/app/components/PostInputs.tsx
"use client";

import { useState } from "react";

export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
}

This component uses "use client" to ensure the component is rendered on the client. The title and content are stored in their own useState hooks.

Create a function that will be called when a form is submitted:

src/app/components/PostInputs.tsx
"use client";

import { useState } from "react";

export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");

async function createPost(e: React.FormEvent) {
e.preventDefault();
if (!title || !content) return;

await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});

setTitle("");
setContent("");
location.reload();
}
}

You'll be using a form to create a post and call the POST route you created earlier:

src/app/page.tsx
"use client";

import { useState } from "react";

export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");

async function createPost(e: React.FormEvent) {
e.preventDefault();
if (!title || !content) return;

await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});

setTitle("");
setContent("");
location.reload();
}

return (
<form onSubmit={createPost} className="space-y-2">
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-2 border border-zinc-800 rounded"
/>
<textarea
placeholder="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full p-2 border border-zinc-800 rounded"
/>
<button className="w-full p-2 border border-zinc-800 rounded">
Post
</button>
</form>
);
}

On submit:

  • It sends a POST request to the /api/posts route
  • Clears the input fields
  • Reloads the page to show the new post

7. Set up page.tsx

Now, update the page.tsx file to fetch posts, show the form, and render the list.

Delete everything within page.tsx, leaving only the following:

src/app/page.tsx
export default function Home() {
return ()
}

Import the necessary dependencies:

src/app/page.tsx
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";

export default function Home() {
return ()
}

To ensure only signed-in users can access the post functionality, update the Home component to check for a user:

src/app/page.tsx
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";

export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;

return ()
}

Once a user is found, fetch that user's posts from the database:

src/app/page.tsx
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";

export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;

const posts = await prisma.post.findMany({
where: { author: { clerkId: user.id } },
orderBy: { createdAt: "desc" },
});

return ()
}

Finally, render the form and post list:

src/app/page.tsx
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";

export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;

const posts = await prisma.post.findMany({
where: { author: { clerkId: user.id } },
orderBy: { createdAt: "desc" },
});

return (
<main className="max-w-2xl mx-auto p-4">
<PostInputs />
<div className="mt-8">
{posts.map((post) => (
<div
key={post.id}
className="p-4 border border-zinc-800 rounded mt-4">
<h2 className="font-bold">{post.title}</h2>
<p className="mt-2">{post.content}</p>
</div>
))}
</div>
</main>
);
}

You've successfully built a Next.js application with Clerk authentication and Prisma, creating a foundation for a secure and scalable full-stack application that handles user management and data persistence with ease.

Below are some next steps to explore, as well as some more resources to help you get started expanding your project.

Next Steps

  • Add delete functionality to posts and users.
  • Add a search bar to filter posts.
  • Deploy to Vercel and set your production webhook URL in Clerk.
  • Enable query caching with Prisma Postgres for better performance

More Info


Stay connected with Prisma

Continue your Prisma journey by connecting with our active community. Stay informed, get involved, and collaborate with other developers:

We genuinely value your involvement and look forward to having you as part of our community!