Back to posts

Setting up PostgreSQL & Clerk with Fastify

Published on March 22, 2025
Blog image

Introduction

Overview of Fastify, PostgreSQL, and Clerk

This guide demonstrates how to integrate three powerful tools: Fastify (a performant Node.js web framework), PostgreSQL with Drizzle ORM (a robust database system), and Clerk (a comprehensive authentication solution). Together, these technologies enable you to build secure, scalable web applications with modern authentication features. We'll walk through the setup process and show how these components work together.

Why use this stack for authentication

This technology stack offers several important advantages:

  • Clerk Integration Benefits
    • Ready-to-use authentication components
    • Built-in user management system
    • Secure session handling out of the box
    • Multiple authentication methods (OAuth, email, etc.)
  • PostgreSQL Advantages
    • Robust data persistence and relationships
    • Advanced querying capabilities with Drizzle ORM
    • Excellent scalability for growing applications
    • Strong data integrity and ACID compliance

Prerequisites

Before starting, ensure you have these requirements:

  • Node.js (version 16 or higher)
  • PostgreSQL database server
  • A Clerk account and API keys
  • Basic knowledge of TypeScript and REST APIs

Using a template

If you'd like to skip the setup, I've created a GitHub template with the completed tutorial. However, I recommend reading through the guide to better understand the implementation and help you develop your API further. Here's the link to the template: template here

Setting up Fastify

Initial project setup

Initialize your Node.js project in a new folder, accepting all defaults:

sh
1npm init

Next, install TypeScript and nodemon:

sh
1npm install --save-dev nodemon ts-node typescript
2npx tsc init

Modify the scripts section in your package.json file:

sh
1"scripts": {
2    "dev": "nodemon src/index.ts"
3 },

Install Fastify:

sh
1npm install fastify

To verify the setup, create a new src folder and add an index.ts file with this content:

typescript
1import Fastify, { FastifyInstance } from 'fastify'
2
3const server: FastifyInstance = Fastify()
4
5server.get('/ping', async (request, reply) => {
6  return { pong: 'it worked!' }
7})
8
9const start = async () => {
10  try {
11    await server.listen({ port: 3000 })
12
13    const addressObject = server.server.address();
14    if (typeof addressObject === 'object' && addressObject !== null) {
15      const address = addressObject.address;
16      const port = addressObject.port;
17      console.log(`Server is running on ${address}:${port}`);
18    } else {
19      throw new Error('Error starting server');
20    }
21  } catch (err) {
22    server.log.error(err)
23  }
24}
25
26start()

Start the development server:

sh
1npm run dev

Visit http://localhost:3000/ping in your browser. You should see the response {"pong":"it worked!"}

If successful, continue to the next step. If not, double-check the previous steps and verify if any recent updates might affect the functionality.

Drizzle ORM Configuration

Installing Drizzle ORM

First, install Drizzle ORM and its development kit:

sh
1npm i drizzle-orm
2npm i -D drizzle-kit

Next, choose and install your database driver. Visit this page to select the appropriate driver. For this tutorial, we'll use Neon:

sh
1npm i @neondatabase/serverless

Then, install the dotenv package for environment variable management:

sh
1npm i dotenv

Create a .env file in your project's root directory and add your database URL:

text
1DATABASE_URL="<DATABASE_CONNECTION_URL>"

Setting up Drizzle ORM

Now, create a folder in src called db and within this folder create a new file called index.ts. While you'll need to adjust the driver based on your choice, here's the configuration for Neon:

typescript
1import { drizzle } from "drizzle-orm/neon-http";
2import { neon } from "@neondatabase/serverless";
3import { config } from "dotenv";
4
5config({ path: ".env" });
6
7const sql = neon(process.env.DATABASE_URL!);
8export const db = drizzle({ client: sql });

Setting up database schemas

Now you can set up your database schema in a file called schema.ts within the db directory. Here's the database schema I've created for stripe subscriptions:

typescript
1import { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core";
2
3export const stripeCustomerTable = pgTable("stripe_customer", {
4    id: uuid("id").primaryKey().defaultRandom(),
5    stripe_customer_id: text("stripe_customer_id").unique().notNull(),
6    user_id: text("user_id").notNull()
7}).enableRLS();
8
9export const customerSubscriptionsTable = pgTable("customer_subscriptions", {
10	id: uuid("id").primaryKey().defaultRandom(),
11	stripe_customer_id: text("stripe_customer_id").unique().notNull(),
12	subscription_status: text("subscription_status").notNull().default("None"),
13	subscription_id: text("subscription_id"),
14	priceId: text("price_id"),
15	current_period_end: timestamp("current_period_end"),
16	current_period_start: timestamp("current_period_start"),
17	cancel_at_period_end: boolean("cancel_at_period_end"),
18	payment_brand: text("payment_brand"),
19	payment_last4: text("payment_last4")
20}).enableRLS();

Create a file called drizzle.config.ts in the root folder with the following configuration:

typescript
1import { config } from 'dotenv';
2import { defineConfig } from "drizzle-kit";
3
4config({ path: '.env' });
5
6export default defineConfig({
7  schema: "./src/db/schema.ts",
8  out: "./migrations",
9  dialect: "postgresql",
10  dbCredentials: {
11    url: process.env.DATABASE_URL!,
12  },
13});

To apply this schema to your database, run:

sh
1npx drizzle-kit push

Testing the database connection

To test our database connection, let's modify our main index.ts file to perform some CRUD operations. I'll demonstrate using my database schema.

typescript
1import Fastify, { FastifyInstance } from 'fastify'
2import { db } from './db'
3import { stripeCustomerTable } from './db/schema'
4import { eq } from 'drizzle-orm'
5
6const server: FastifyInstance = Fastify()
7
8server.post('/create-customer', async (request, reply) => {
9  const { userId } = request.body as { userId: string };
10  if (!userId) {
11    return reply.status(400).send({ error: 'User ID is required' });
12  }
13  const customer = await db.insert(stripeCustomerTable).values({
14    stripe_customer_id: 'sample-customer-id',
15    user_id: userId
16  }).returning().then((res) => res[0]);
17  return reply.status(200).send({ customer });
18})
19
20server.get('/get-customer/:userId', async (request, reply) => {
21  const { userId } = request.params as { userId: string };
22  if (!userId) {
23    return reply.status(400).send({ error: 'User ID is required' });
24  }
25  const customer = await db.select().from(stripeCustomerTable).where(eq(stripeCustomerTable.user_id, userId)).then((res) => res.at(0)??null);
26  if (!customer) {
27    return reply.status(404).send({ error: 'Customer not found' });
28  }
29  return reply.status(200).send({ customer });
30})
31
32const start = async () => {
33  try {
34    await server.listen({ port: 3000 })
35
36    const addressObject = server.server.address();
37    if (typeof addressObject === 'object' && addressObject !== null) {
38      const address = addressObject.address;
39      const port = addressObject.port;
40      console.log(`Server is running on ${address}:${port}`);
41    } else {
42      throw new Error('Error starting server');
43    }
44  } catch (err) {
45    server.log.error(err)
46  }
47}
48
49start()

You can test this using Postman, or alternatively, use this curl command:

sh
1curl --location '<http://localhost:3000/create-customer>' \\
2--header 'Content-Type: application/json' \\
3--data '{
4    "userId": "random-user-id"
5}'

To retrieve the created user, you can use either Postman or the following command:

sh
1curl --location '<http://localhost:3000/get-customer/random-user-id>'

Implementing Clerk Authentication

Now that our database is set up, let's integrate Clerk for authentication. First, install the required Clerk package:

sh
1npm install @clerk/fastify

Add these environment variables to your .env file:

text
1CLERK_PUBLISHABLE_KEY="<CLERK_PUBLISHABLE_KEY>"
2CLERK_SECRET_KEY="<CLERK_SECRET_KEY>"

Then modify your main index.ts file as follows:

typescript
1import Fastify, { FastifyInstance } from 'fastify'
2import { db } from './db'
3import { stripeCustomerTable } from './db/schema'
4import { clerkPlugin, getAuth } from '@clerk/fastify'
5import { eq } from 'drizzle-orm'
6
7const server: FastifyInstance = Fastify()
8
9server.register(clerkPlugin, {
10  publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
11  secretKey: process.env.CLERK_SECRET_KEY,
12})
13
14server.post('/create-customer', async (request, reply) => {
15  const { userId } = getAuth(request);
16  if (!userId) {
17    return reply.status(400).send({ error: 'User ID is required' });
18  }
19  const customer = await db.insert(stripeCustomerTable).values({
20    stripe_customer_id: 'sample-customer-id',
21    user_id: userId
22  }).returning().then((res) => res[0]);
23  return reply.status(200).send({ customer });
24})
25
26server.get('/get-customer', async (request, reply) => {
27  const { userId } = getAuth(request);
28  if (!userId) {
29    return reply.status(400).send({ error: 'User ID is required' });
30  }
31  const customer = await db.select().from(stripeCustomerTable).where(eq(stripeCustomerTable.user_id, userId)).then((res) => res.at(0)??null);
32  if (!customer) {
33    return reply.status(404).send({ error: 'Customer not found' });
34  }
35  return reply.status(200).send({ customer });
36})
37
38const start = async () => {
39  try {
40    await server.listen({ port: 3000 })
41
42    const addressObject = server.server.address();
43    if (typeof addressObject === 'object' && addressObject !== null) {
44      const address = addressObject.address;
45      const port = addressObject.port;
46      console.log(`Server is running on ${address}:${port}`);
47    } else {
48      throw new Error('Error starting server');
49    }
50  } catch (err) {
51    server.log.error(err)
52  }
53}
54
55start()

To test this implementation from your frontend, you'll need to include a Clerk authentication token in the Authorization header. This token can be generated using any of Clerk's SDKs. Add it to your requests like this:

text
1Authorization: Bearer <CLERK_TOKEN>

Next Steps

Now that you have a basic setup with Fastify, PostgreSQL, and Clerk authentication, you can extend your application further:

  • Implement additional security features
    • Role-based access control
    • Comprehensive error handling
  • Enhance database functionality
    • Complex database queries
    • Data validation and sanitization
  • Add supporting features
    • Email notifications
    • Logging system