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.

MH
Mahmudul Hassan Mithun
AI SaaS Builder · BSc Data Science & AI, UEL · Building ContentForge AI

Related Posts

Deploying FastAPI to Railway: The Production Checklist
Deploying FastAPI to Railway: The Production Checklist
8 min read →
Building a Full-Stack Booking SaaS with React, Node.js & Prisma
Building a Full-Stack Booking SaaS with React, Node.js & Prisma
13 min read →