Ever wondered how apps like Slack manage to serve thousands of different organizations from the same codebase? Or how Shopify hosts millions of stores while keeping each one's data separate and secure? Welcome to the world of multi-tenancy! In this guide, we'll break down everything you need to know about building multi-tenant applications with Next.js, sprinkled with real-world examples and practical code snippets.
Imagine you're building an apartment complex. You could either build separate houses for each tenant (single-tenant) or create one building with multiple apartments (multi-tenant). In software terms, multi-tenancy means running one instance of your application that serves multiple customers (tenants), each with their own private space.
Let's say you're building a project management tool like Asana. Companies A and B both use your app, but:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
export function getTenantDB(tenantId: string) {
return new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}/${tenantId}`
}
}
})
}
Think of this as giving each tenant their own private database. It's like Netflix - each user has their own watchlist, preferences, and billing info in a completely separate space.
This approach offers a great balance between isolation and resource efficiency. Perfect for mid-sized SaaS applications.
// Real-world example: A scheduling application
async function createAppointment(tenantId: string, data: AppointmentData) {
const db = getTenantDB(tenantId) // Connects to tenant's schema
const appointment = await db.appointments.create({
data: {
...data,
organizationId: tenantId
}
})
// Each tenant's calendar sync runs in their own schema
await syncWithCalendar(appointment, tenantId)
return appointment
}
While this is the simplest approach to implement, ensure your tenant isolation logic is rock solid to prevent data leaks.
// Example: A team collaboration tool
async function getTeamDocuments(teamId: string, tenantId: string) {
return await prisma.documents.findMany({
where: {
teamId,
tenantId, // 👈 The magic filter
},
include: {
comments: true,
collaborators: true,
}
})
}
Let's build something real! We'll create a simple analytics dashboard that different companies can use to track their metrics.
// middleware.ts - Your building's reception desk 🏢
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// company-name.your-saas.com
const hostname = request.headers.get('host')
const tenantId = hostname?.split('.')[0]
if (!tenantId) {
return new NextResponse('Welcome to the lobby! Please use your custom domain.',
{ status: 404 })
}
// Check if tenant exists (like verifying a tenant's lease)
const tenant = await getTenant(tenantId)
if (!tenant) {
return new NextResponse('Hmm, we don\'t have a record of this company.',
{ status: 404 })
}
return NextResponse.next()
}
// contexts/TenantContext.tsx
import { createContext, useContext } from 'react'
interface Tenant {
id: string
name: string
theme: {
primaryColor: string
logo: string
}
settings: Record<string, any>
}
const TenantContext = createContext<Tenant | null>(null)
export function TenantProvider({ tenant, children }) {
return (
<TenantContext.Provider value={tenant}>
{children}
</TenantContext.Provider>
)
}
// components/Dashboard.tsx - A tenant's personal space
'use client'
import { useTenant } from '@/contexts/TenantContext'
export function Dashboard() {
const tenant = useTenant()
return (
<div style={{ backgroundColor: tenant.theme.primaryColor }}>
<img src={tenant.theme.logo} alt={`${tenant.name}'s logo`} />
<h1>Welcome to {tenant.name}'s Dashboard</h1>
{/* More tenant-specific goodness */}
</div>
)
}
// Don't let one tenant's heavy usage affect others
export function getCacheKey(tenantId: string, key: string) {
return `tenant:${tenantId}:${key}` // Each tenant gets their own cache space
}
// Keep tenant operations isolated
export async function processDataForTenant(tenantId: string) {
console.log(`🏃♂️ Starting job for ${tenantId}`)
setTenantContext(tenantId)
try {
await processData()
} finally {
clearTenantContext()
console.log(`✅ Finished job for ${tenantId}`)
}
}
// Track tenant-specific metrics
export async function logMetric(metric: string, value: number, tenantId: string) {
await prometheus.gauge({
name: `app_${metric}`,
help: `Tracking ${metric}`,
labels: { tenant: tenantId }
}).set(value)
}
The "Oops, Wrong Tenant" Bug
The Performance Nightmare
The Maintenance Headache
// tests/tenant.test.ts
describe('Tenant Isolation', () => {
it('should not leak data between tenants', async () => {
// Set up test data for two tenants
const tenant1 = await createTestTenant('org1')
const tenant2 = await createTestTenant('org2')
// Create data for tenant1
const document = await createDocument(tenant1.id, { title: 'Secret Plans' })
// Try to access from tenant2
const result = await getDocument(document.id, tenant2.id)
expect(result).toBeNull() // Should not find tenant1's document
})
})
Building multi-tenant applications isn't just about keeping data separate - it's about creating scalable, maintainable systems that can grow with your business. Whether you're building the next Slack, Shopify, or something entirely new, the principles we've covered will help you build robust multi-tenant applications with Next.js.
Remember:
Schedule a free technical consultation to explore how AI and full-stack innovation can drive your business forward.
Join 50+ innovators leveraging our AI-driven solutions for unmatched performance.
Schedule a free technical consultation to explore how AI and full-stack innovation can drive your business forward.
Join 50+ innovators leveraging our AI-driven solutions for unmatched performance.