diff --git a/content/800-guides/420-clerk-astro.mdx b/content/800-guides/420-clerk-astro.mdx new file mode 100644 index 0000000000..c626259313 --- /dev/null +++ b/content/800-guides/420-clerk-astro.mdx @@ -0,0 +1,534 @@ +--- +title: 'How to use Prisma ORM with Clerk Auth and Astro' +metaTitle: 'How to use Prisma ORM and Prisma Postgres with Clerk Auth and Astro' +description: 'Learn how to use Prisma ORM in an Astro app with Clerk Auth' +sidebar_label: 'Clerk (with Astro)' +image: '/img/guides/prisma-clerk-astro-cover.png' +completion_time: '25 min' +community_section: true +--- + +## Introduction + +[Clerk](https://clerk.com/) 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 [Astro](https://astro.build/) app and persist users in a [Prisma Postgres](https://prisma.io/postgres) database. You can find a complete example of this guide on [GitHub](https://github.com/prisma/prisma-examples/tree/latest/orm/clerk-astro). + +## Prerequisites + +- [Node.js 20+](https://nodejs.org) +- [Clerk account](https://clerk.com) +- [ngrok account](https://ngrok.com) + +## 1. Set up your project + +Create a new Astro project: + +```terminal +npm create astro@latest +``` + +It will prompt you to customize your setup. Choose the defaults: + +:::info + +- *How would you like to start your new project?* `Empty` +- *Install dependencies?* `Yes` +- *Initialize a new git repository?* `Yes` + +::: + +Navigate into the newly created project directory: + +```terminal +cd +``` + +## 2. Set up Clerk + +### 2.1. Create a new Clerk application + +[Sign in](https://dashboard.clerk.com/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`. + +:::info + +For this guide, the Google, Github, and Email sign in options will be used. + +::: + +Install the Clerk Astro SDK and Node adapter: + +```terminal +npm install @clerk/astro @astrojs/node +``` + +In the Clerk Dashboard, navigate to the **API keys** page. In the **Quick Copy** section, copy your Clerk Publishable and Secret Keys. Paste your keys into `.env` in the root of your project: + +```dotenv file=.env +PUBLIC_CLERK_PUBLISHABLE_KEY= +CLERK_SECRET_KEY= +``` + +### 2.2. Configure Astro with Clerk + +Astro needs to be configured for server-side rendering (SSR) with the Node adapter to work with Clerk. Update your `astro.config.mjs` file to include the Clerk integration and enable SSR: + +```javascript file=astro.config.mjs +import { defineConfig } from 'astro/config' +//add-start +import node from '@astrojs/node' +import clerk from '@clerk/astro' +//add-end + +export default defineConfig({ + //add-start + integrations: [clerk()], + adapter: node({ mode: 'standalone' }), + output: 'server', + //add-end +}) +``` + +### 2.3. Set up Clerk middleware + +The `clerkMiddleware` helper enables authentication across your entire application. Create a `middleware.ts` file in the `src` directory: + +```typescript file=src/middleware.ts +import { clerkMiddleware } from '@clerk/astro/server' + +export const onRequest = clerkMiddleware() +``` + +### 2.4. Add Clerk UI to your page + +Update your `src/pages/index.astro` file to import the Clerk authentication components: + +```html file=src/pages/index.astro +--- +//add-start +import { + SignedIn, + SignedOut, + UserButton, + SignInButton, +} from "@clerk/astro/components"; +//add-end +--- + + + + + + + + Astro + + + + +``` + +Now add a header with conditional rendering to show sign-in buttons for unauthenticated users and a user button for authenticated users: + +```html file=src/pages/index.astro +--- +import { + SignedIn, + SignedOut, + UserButton, + SignInButton, +} from "@clerk/astro/components"; +--- + + + + + + + + Astro + + + //add-start +
+ + + + + + +
+ //add-end + + +``` + +## 3. Install and configure Prisma + +### 3.1. Install dependencies + +To get started with Prisma, you'll need to install a few dependencies: + +```terminal +npm install prisma tsx @types/pg --save-dev +npm install @prisma/client @prisma/adapter-pg dotenv pg +``` + +:::info + +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](/orm/overview/databases/database-drivers). + +::: + +Once installed, initialize Prisma in your project: + +```terminal +npx prisma init --db +``` + +:::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 Astro Project" +::: + +This will create: + +- A `prisma/` directory with a `schema.prisma` file +- A `prisma.config.ts` file with your Prisma configuration +- A `.env` file with a `DATABASE_URL` already set + +### 3.2. Define your Prisma Schema + +Add a `User` model that will store authenticated user information from Clerk. The `clerkId` field uniquely links each database user to their Clerk account: + +```prisma file=prisma/schema.prisma +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +//add-start +model User { + id Int @id @default(autoincrement()) + clerkId String @unique + email String @unique + name String? +} +//add-end +``` + +Run the following command to create the database tables: + +```terminal +npx prisma migrate dev --name init +``` + +After the migration is complete, generate the Prisma Client: + +```terminal +npx prisma generate +``` + +This generates the Prisma Client in the `src/generated/prisma` directory. + +### 3.3. Create TypeScript environment definitions + +Create an `env.d.ts` file in your `src` directory to provide TypeScript definitions for your environment variables: + +```terminal +touch src/env.d.ts +``` + +Add type definitions for all the environment variables your application uses: + +```typescript file=src/env.d.ts +interface ImportMetaEnv { + readonly DATABASE_URL: string; + readonly CLERK_WEBHOOK_SIGNING_SECRET: string; + readonly CLERK_SECRET_KEY: string; + readonly PUBLIC_CLERK_PUBLISHABLE_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} +``` + +### 3.4. Create a reusable Prisma Client + +In the `src` directory, create a `lib` directory and a `prisma.ts` file inside it: + +```terminal +mkdir src/lib +touch src/lib/prisma.ts +``` + +Initialize the Prisma Client with the PostgreSQL adapter: + +```typescript file=src/lib/prisma.ts +import { PrismaClient } from "../generated/prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; + +const adapter = new PrismaPg({ + connectionString: import.meta.env.DATABASE_URL, +}); + +const prisma = new PrismaClient({ + adapter, +}); + +export default prisma; +``` + +## 4. Wire Clerk to the database + +### 4.1. Create a Clerk webhook endpoint + +Webhooks allow Clerk to notify your application when events occur, such as when a user signs up. You'll create an API route to handle these webhooks and sync user data to your database. + +Create the directory structure and file for the webhook endpoint: + +```terminal +mkdir -p src/pages/api/webhooks +touch src/pages/api/webhooks/clerk.ts +``` + +Import the necessary dependencies: + +```typescript file=src/pages/api/webhooks/clerk.ts +import { verifyWebhook } from "@clerk/astro/webhooks"; +import type { APIRoute } from "astro"; +import prisma from "../../../lib/prisma"; +``` + +Create the `POST` handler that Clerk will call. The `verifyWebhook` function validates that the request actually comes from Clerk using the signing secret: + +```typescript file=src/pages/api/webhooks/clerk.ts +import { verifyWebhook } from "@clerk/astro/webhooks"; +import type { APIRoute } from "astro"; +import prisma from "../../../lib/prisma"; + +//add-start +export const POST: APIRoute = async ({ request }) => { + try { + const evt = await verifyWebhook(request, { + signingSecret: import.meta.env.CLERK_WEBHOOK_SIGNING_SECRET, + }); + 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 }); + } +}; +//add-end +``` + +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: + +```typescript file=src/pages/api/webhooks/clerk.ts +import { verifyWebhook } from "@clerk/astro/webhooks"; +import type { APIRoute } from "astro"; +import prisma from "../../../lib/prisma"; + +export const POST: APIRoute = async ({ request }) => { + try { + const evt = await verifyWebhook(request, { + signingSecret: import.meta.env.CLERK_WEBHOOK_SIGNING_SECRET, + }); + const { id } = evt.data; + const eventType = evt.type; + console.log( + `Received webhook with ID ${id} and event type of ${eventType}`, + ); + + //add-start + 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}`, + }, + }); + } + //add-end + } 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: + +```typescript file=src/pages/api/webhooks/clerk.ts +import { verifyWebhook } from "@clerk/astro/webhooks"; +import type { APIRoute } from "astro"; +import prisma from "../../../lib/prisma"; + +export const POST: APIRoute = async ({ request }) => { + try { + const evt = await verifyWebhook(request, { + signingSecret: import.meta.env.CLERK_WEBHOOK_SIGNING_SECRET, + }); + 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}`, + }, + }); + } + + //add-next-line + return new Response("Webhook received", { status: 200 }); + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response("Error verifying webhook", { status: 400 }); + } +}; +``` + +### 4.2. Expose your local app for webhooks + +You'll need to expose your local app for webhooks with [ngrok](https://ngrok.com/). This will allow Clerk to reach your `/api/webhooks/clerk` route to push events like `user.created`. + +Start your development server: + +```terminal +npm run dev +``` + +In a separate terminal window, install ngrok globally and expose your local app: + +```terminal +npm install --global ngrok +ngrok http 4321 +``` + +Copy the ngrok `Forwarding URL` (e.g., `https://a65a60261342.ngrok-free.app`). This will be used to configure the webhook URL in Clerk. + +### 4.3. Configure Astro to allow ngrok connections + +Astro needs to be configured to accept connections from the ngrok domain. Update your `astro.config.mjs` to include the ngrok host in the allowed hosts list: + +```javascript file=astro.config.mjs +import { defineConfig } from "astro/config"; +import node from "@astrojs/node"; +import clerk from "@clerk/astro"; + +export default defineConfig({ + integrations: [clerk()], + adapter: node({ mode: "standalone" }), + output: "server", + //add-start + server: { + allowedHosts: ["localhost", ".ngrok-free.app"], + }, + //add-end +}); +``` + +:::note + +Replace `` with the subdomain from your ngrok URL. For example, if your ngrok URL is `https://a65a60261342.ngrok-free.app`, use `a65a60261342.ngrok-free.app`. + +::: + +### 4.4. Register the webhook 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. It should look similar to this: + +```text +https://a65a60261342.ngrok-free.app/api/webhooks/clerk +``` + +Subscribe to the **user.created** event by checking the box next to it under ***Message Filtering***. + +Click ***Create*** to save the webhook endpoint. + +Copy the ***Signing Secret*** and add it to your `.env` file: + +```dotenv file=.env +# Prisma +DATABASE_URL= + +# Clerk +PUBLIC_CLERK_PUBLISHABLE_KEY= +CLERK_SECRET_KEY= +//add-next-line +CLERK_WEBHOOK_SIGNING_SECRET= +``` + +Restart your dev server to pick up the new environment variable: + +```terminal +npm run dev +``` + +### 4.5. Test the integration + +Navigate to `http://localhost:4321` in your browser and sign in using any of the sign-up options you configured in Clerk. + +Open Prisma Studio to verify that the user was created in your database: + +```terminal +npx prisma studio +``` + +You should see a new user record with the Clerk ID, email, and name from your sign-up. + +:::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 signing up again. +- Check your ngrok URL and ensure it's correct *(it will change every time you restart ngrok)*. +- Verify your Clerk webhook is pointing to the correct ngrok URL. +- Make sure you've added `/api/webhooks/clerk` to the end of the webhook URL. +- Ensure you've subscribed to the **user.created** event in Clerk. +- Confirm you've added the ngrok host to `allowedHosts` in `astro.config.mjs` and removed `https://`. +- Check the terminal running `npm run dev` for any error messages. + +::: + +You've successfully built an Astro 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. + +## Next steps + +Now that you have a working Astro app with Clerk authentication and Prisma connected to a Prisma Postgres database, you can: + +- Add user profile management and update functionality +- Build protected API routes that require authentication +- Extend your schema with additional models related to users +- Deploy to your preferred hosting platform and set your production webhook URL in Clerk +- Enable query caching with [Prisma Postgres](/postgres/database/caching) for better performance + +### More info + +- [Prisma Documentation](/orm/overview/introduction) +- [Astro Documentation](https://docs.astro.build) +- [Clerk Documentation](https://clerk.com/docs) diff --git a/sidebars.ts b/sidebars.ts index 0cbaa59b9f..b5c4a99034 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -391,6 +391,7 @@ const sidebars: SidebarsConfig = { collapsible: false, items: [ "guides/clerk-nextjs", + "guides/clerk-astro", "guides/shopify", "guides/permit-io-access-control", "guides/betterauth-astro", diff --git a/static/img/guides/prisma-clerk-astro-cover.png b/static/img/guides/prisma-clerk-astro-cover.png new file mode 100644 index 0000000000..2ecc738d5c Binary files /dev/null and b/static/img/guides/prisma-clerk-astro-cover.png differ