SaaS Dev15 min read · 28 February 2026
Multi-Tenant SaaS Architecture with Next.js 15, Prisma & Auth0
A deep dive into how I architected the Enterprise Headless SaaS platform — handling 4 vertical packages, tenant isolation, Stripe billing, and Cloudflare R2 storage in a single Next.js 15 codebase.
Next.jsPrismaAuth0StripeArchitecture
What Is Multi-Tenancy?
A multi-tenant SaaS serves multiple customers (tenants) from a single deployment. Each tenant sees only their data — even though they share the same database and application code.
Tenant Isolation with Prisma
Every model in the schema has a tenantId:
prisma
model Project {
id String @id @default(cuid())
tenantId String
name String
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id])
@@index([tenantId])
}
Then a middleware automatically injects the tenant filter:
typescript
// middleware/tenant.ts
export function withTenant(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const session = await getSession(req, res)
if (!session?.user?.tenantId) {
return res.status(401).json({ error: 'Unauthorized' })
}
req.tenantId = session.user.tenantId
return handler(req, res)
}
}
// Usage in API route
export default withTenant(async (req, res) => {
const projects = await prisma.project.findMany({
where: { tenantId: req.tenantId } // Always filtered
})
res.json(projects)
})
The 4 Vertical Packages
Each vertical (Agency, Commerce, Enterprise, Franchise) has different feature flags:
typescript
export const PACKAGE_FEATURES = {
agency: {
maxProjects: 10,
customDomain: true,
whiteLabel: true,
apiAccess: false,
},
enterprise: {
maxProjects: Infinity,
customDomain: true,
whiteLabel: true,
apiAccess: true,
sso: true,
},
}
// Check feature in component
const { tenant } = useTenant()
const features = PACKAGE_FEATURES[tenant.plan]
if (!features.apiAccess) {
return <UpgradePrompt feature="API Access" />
}
Stripe Webhook Pipeline
typescript
// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
const sig = req.headers.get('stripe-signature')!
const body = await req.text()
const event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
)
switch (event.type) {
case 'customer.subscription.updated':
await prisma.tenant.update({
where: { stripeCustomerId: event.data.object.customer as string },
data: { plan: getPlanFromPriceId(event.data.object.items.data[0].price.id) }
})
break
case 'invoice.payment_failed':
await sendPaymentFailedEmail(event.data.object)
break
}
return Response.json({ received: true })
}
The platform is live at platform.contentforge.net handling all 4 verticals from a single Railway deployment.