Fastify Multi-Tenant Auth: A Better Auth Integration Guide

A step‑by‑step guide to integrating Better Auth into your Fastify app for lightweight, scalable authentication—including multi‑tenant support and session management.

sanya shree

2 months ago

fastify-multi-tenant-auth-a-better-auth-integration-guide

Hello Dazzling people on the internet. ✨

If you’ve ever tried to build authentication into your app, you know the struggle—handling sessions, managing tokens, securing user data, and making sure everything scales smoothly. While many solutions exist, they often feel either too restrictive or too complex to integrate.

That’s where Fastify and Better Auth come in. Fastify is known for its blazing‑fast performance and developer‑friendly API, while Better Auth provides a framework‑agnostic way to handle authentication without the usual headaches. Put them together, and you get a lightweight yet robust authentication system (puns intended) that doesn’t slow you down. In this article, I’ll walk you through integrating Better Auth with Fastify, ensuring your app gets secure authentication without the unnecessary bloat. Let’s dive in! 🚀

Note: We won’t focus on setting up a complete Fastify project here—feel free to use a ready‑made template or explore Fastify on your own. I’ll add a GitHub link to a Fastify starter template in the resources section below.

Step 1: Configure Environment Variables
Before you begin, create these environment variables. Auth secret acts as your key for generating hashes or encryptions:
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=http://localhost:5000

Step 2: Create your auth.ts file
Inside your project (for example, in src/libs/auth.ts), import Prisma and Better Auth, then configure the adapter and plugins. Here’s a minimal setup:

import { PrismaClient } from "@prisma/client"; import { betterAuth } from "better-auth"; import { bearer, jwt, organization, openAPI } from "better-auth/plugins"; import { prismaAdapter } from "better-auth/adapters/prisma"; const prisma = new PrismaClient(); export const auth = betterAuth({ secret: process.env.BETTER_AUTH_SECRET, baseURL: process.env.BETTER_AUTH_URL, database: prismaAdapter(prisma, { provider: "postgresql" }), emailAndPassword: { enabled: true }, session: { expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24 }, plugins: [bearer(), organization(), openAPI()], });

This config uses PostgreSQL via Prisma, enables email/password login, sets a one‑week session lifespan, and loads three plugins: bearer (for header‑based auth), organization (for multi‑tenant support), and openAPI (for documentation).

Step 3: Generate your schema
Better Auth’s CLI can scaffold all necessary database tables and migrations for you. Just run:

npx @better-auth/cli generate

Then apply the migrations to your database.

Step 4: Define your Fastify route schemas
Fastify recommends JSON‑schema validation. Using Zod and zod-to-json-schema, you can define your request and response shapes, for example:

import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; const RegisterSchema = z.object({ full_name: z.string(), email: z.string().email(), password: z.string().min(6).max(18), organization_name: z.string(), organization_slug: z.string(), }); const registerJsonSchema = { body: zodToJsonSchema(RegisterSchema), response: { 200: zodToJsonSchema(SignupResponseSchema) }, }; fastify.post("/register", { schema: registerJsonSchema }, authController.register);

Step 5: Implement your register controller
Using Better Auth’s API client, you can sign up a user and then create their organization in one flow:

async function register(payload) { const { user, token } = await auth.api.signUpEmail({ body: { name: payload.full_name, email: payload.email, password: payload.password }, }); if (!user || !token) throw new Error("Registration failed"); const org = await auth.api.createOrganization({ body: { name: payload.organization_name, slug: payload.organization_slug, userId: user.id }, }); if (!org) throw new Error("Organization creation failed"); return { token, organizationId: org.id }; }

Step 6: Protect your routes with a preHandler hook
Install fastify-plugin and then define an authentication plugin that verifies the bearer token and attaches session details to each request:

import fp from "fastify-plugin"; import { auth } from "../libs/auth"; export default fp(async (fastify) => { fastify.decorate("authenticate", async (req, reply) => { const header = req.headers.authorization; if (!header?.startsWith("Bearer ")) return reply.code(401).send(); const sessionData = await auth.api.getSession({ headers: new Headers(req.headers) }); if (!sessionData) return reply.code(401).send(); req.session = sessionData.session; req.user = sessionData.user; }); });

Then apply it to any protected route:

fastify.get("/organizations", { preHandler: [fastify.authenticate] }, orgController.list);

Conclusion
That was a deep dive into integrating Better Auth with Fastify—covering secret management, schema generation, multi‑tenant considerations, route validation, and session protection. With this setup, you get a framework‑agnostic, performant authentication layer that scales and adapts to your needs.

Until next time, share for good karma and enjoy a bowl of ice cream! 🍨