Setting up PostgreSQL & Clerk with Fastify

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:
1npm init
Next, install TypeScript and nodemon:
1npm install --save-dev nodemon ts-node typescript
2npx tsc init
Modify the scripts section in your package.json
file:
1"scripts": {
2 "dev": "nodemon src/index.ts"
3 },
Install Fastify:
1npm install fastify
To verify the setup, create a new src
folder and add an index.ts
file with this content:
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:
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:
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:
1npm i @neondatabase/serverless
Then, install the dotenv package for environment variable management:
1npm i dotenv
Create a .env file in your project's root directory and add your database URL:
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:
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:
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:
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:
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.
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:
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:
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:
1npm install @clerk/fastify
Add these environment variables to your .env file:
1CLERK_PUBLISHABLE_KEY="<CLERK_PUBLISHABLE_KEY>"
2CLERK_SECRET_KEY="<CLERK_SECRET_KEY>"
Then modify your main index.ts
file as follows:
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:
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