Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Clerk (with Next.js)

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.

Create the app:

title="bun"
bunx create-next-app@latest clerk-nextjs-prisma
pnpm
pnpm dlx create-next-app@latest clerk-nextjs-prisma
yarn
yarn dlx create-next-app@latest clerk-nextjs-prisma
npm
npx create-next-app@latest clerk-nextjs-prisma

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

Navigate to the project directory:

cd clerk-nextjs-prisma

Sign in to Clerk and navigate to the home page. From there, press the Create Application button to create a new application. Enter a title, select your sign-in options, and click Create Application.

Install the Clerk Next.js SDK:

title="bun"
bun add @clerk/nextjs
pnpm
pnpm add @clerk/nextjs
yarn
yarn add @clerk/nextjs
npm
npm install @clerk/nextjs

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

title=".env"
# Clerk 

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>

CLERK_SECRET_KEY=<your-secret-key>

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

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

title="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)(.*)", 

  ], 

}; 

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:

title="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:

title="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>   ); }; 

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

title="bun"
bun add prisma@prev tsx @types/pg --dev
pnpm
pnpm add prisma@prev tsx @types/pg --save-dev
yarn
yarn add prisma@prev tsx @types/pg --dev
npm
npm install prisma@prev tsx @types/pg --save-dev
bun add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
pnpm add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
yarn add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
npm install @prisma/client@7 @prisma/adapter-pg dotenv pg

[!NOTE] If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of @prisma/adapter-pg. For more information, see Database drivers.

Once installed, initialize Prisma in your project:

Once installed, initialize Prisma in your project:

bunx --bun prisma init --output ../app/generated/prisma
Bash
pnpm prisma init --output ../app/generated/prisma
Bash
yarn prisma init --output ../app/generated/prisma
Bash
npx prisma init --output ../app/generated/prisma

[!NOTE] prisma init creates the Prisma scaffolding and a local DATABASE_URL. In the next step, you will create a Prisma Postgres database and replace that value with a direct postgres://... connection string.

This will create:

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

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

This will create:

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

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

title="bun"
bunx create-db
pnpm
pnpm dlx create-db
yarn
yarn dlx create-db
npm
npx create-db

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

title="prisma/schema.prisma"
generator client {

  provider = "prisma-client"

  output   = "../app/generated/prisma"

}

datasource db {

  provider = "postgresql"

}

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.

Create a prisma.config.ts file to configure Prisma:

title="prisma.config.ts"
import "dotenv/config"; 

import { defineConfig, env } from "prisma/config"; 

export default defineConfig({

  schema: "prisma/schema.prisma", 

  migrations: {

    path: "prisma/migrations", 

  }, 

  datasource: {

    url: env("DATABASE_URL"), 

  }, 

}); 

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

bunx prisma migrate dev --name init
Bash
pnpm prisma generate
Bash
yarn prisma generate
Bash
npx prisma generate

[!WARNING] It is recommended that you add /app/generated/prisma to your .gitignore file.

bunx prisma generate
Bash
pnpm add --global ngrok
ngrok http 3000
Bash
yarn global add ngrok
ngrok http 3000
Bash
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:

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

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

.env
# PrismaDATABASE_URL=<your-database-url># ClerkNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>CLERK_SECRET_KEY=<your-secret-key>CLERK_WEBHOOK_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.

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

title="lib/prisma.ts"
import { PrismaClient } from "../app/generated/prisma/client"; 

import { PrismaPg } from "@prisma/adapter-pg"; 

const adapter = new PrismaPg({

  connectionString: process.env.DATABASE_URL!, 

}); 

const globalForPrisma = global as unknown as {

  prisma: PrismaClient; 

}; 

const prisma =

  globalForPrisma.prisma ||

  new PrismaClient({

    adapter, 

  }); 

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

export default prisma; 

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

Import the necessary dependencies:

title="app/api/webhooks/clerk/route.ts"
import { verifyWebhook } from "@clerk/nextjs/webhooks"; 

import { NextRequest } from "next/server"; 

import prisma from "@/lib/prisma"; 

Create the POST method that Clerk will call and verify the webhook:

title="app/api/webhooks/clerk/route.ts"
import { verifyWebhook } from "@clerk/nextjs/webhooks";

import { NextRequest } from "next/server";

import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {

  try {

    const evt = await verifyWebhook(req); 

    const { id } = evt.data; 

    const eventType = evt.type; 

    console.log(

      `Received webhook with ID ${id} and event type of ${eventType}`, 

    ); 

  } catch (err) {

    console.error("Error verifying webhook:", err); 

    return new Response("Error verifying webhook", { status: 400 }); 

  } 

} 

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:

title="app/api/webhooks/clerk/route.ts"
import { verifyWebhook } from "@clerk/nextjs/webhooks";

import { NextRequest } from "next/server";

import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {

  try {

    const evt = await verifyWebhook(req);

    const { id } = evt.data;

    const eventType = evt.type;

    console.log(`Received webhook with ID ${id} and event type of ${eventType}`);

    if (eventType === "user.created") {

      const { id, email_addresses, first_name, last_name } = evt.data; 

      await prisma.user.upsert({

        where: { clerkId: id }, 

        update: {}, 

        create: {

          clerkId: id, 

          email: email_addresses[0].email_address, 

          name: `${first_name} ${last_name}`, 

        }, 

      }); 

    } 

  } catch (err) {

    console.error("Error verifying webhook:", err);

    return new Response("Error verifying webhook", { status: 400 });

  }

}

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

title="app/api/webhooks/clerk/route.ts"
import { verifyWebhook } from "@clerk/nextjs/webhooks";

import { NextRequest } from "next/server";

import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {

  try {

    const evt = await verifyWebhook(req);

    const { id } = evt.data;

    const eventType = evt.type;

    console.log(`Received webhook with ID ${id} and event type of ${eventType}`);

    if (eventType === "user.created") {

      const { id, email_addresses, first_name, last_name } = evt.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("Webhook received", { status: 200 }); 

  } catch (err) {

    console.error("Error verifying webhook:", err);

    return new Response("Error verifying webhook", { status: 400 });

  }

}

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:

bun add --global ngrok

ngrok http 3000
Bash
pnpm prisma studio
Bash
yarn prisma studio
Bash
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.

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:

title=".env"
# Prisma

DATABASE_URL=<your-database-url>

# Clerk

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>

CLERK_SECRET_KEY=<your-secret-key>

CLERK_WEBHOOK_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.

bunx prisma studio

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

Start by importing the necessary dependencies:

title="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:

title="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:

title="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:

title="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 }); 

}

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

title="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:

title="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:

title="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();

  }

  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">

        {" "}

        // [!code ++] 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

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:

title="app/page.tsx"
export default function Home() {

  return ()

}

Import the necessary dependencies:

title="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:

title="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:

title="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:

title="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 now have a Next.js application where Clerk handles authentication, new sign-ups are synced to your Prisma Postgres database through the webhook, and signed-in users can create and list their own posts.

  • 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.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu