Blog
Software2024-09-1511 min

Real multi-tenancy in Next.js + Prisma: what we learned with OmniTrip

Building a multi-tenant SaaS with Next.js 14 App Router and Prisma involves architectural decisions that impact all subsequent development.

What multi-tenant means in practice

Multi-tenancy means multiple adventure tourism operators use the same application, each with their own data, users and configurations, with no possibility of data mixing.

There are three main models: database per tenant, schema per tenant, and shared database with a tenant_id column. We chose the third, which has the most traps.

The forgotten tenant_id problem

With the shared database model, the main risk is forgetting to filter by tenant_id in some query. In an application with dozens of models and hundreds of queries, this happens. And when it happens in production, one operator sees another's data.

The solution was a Prisma helper that wraps the client and automatically adds the tenant filter to all operations.

Row Level Security as a second defense layer

In addition to application-level filtering, we implemented Row Level Security (RLS) in PostgreSQL. Even if a bug in the application code omits the tenant_id filter, the database rejects access to other tenants' rows.

Coolify as SaaS deployment platform

OmniTrip runs on Coolify on a Hetzner VPS. The deployment workflow: push to main triggers a webhook, Coolify pulls the repository, runs docker build, runs prisma migrate deploy as part of the entrypoint, and does a rolling restart.

What I'd change today

Today I'd use Prisma Client Extensions for tenant isolation instead of the manual helper. Extensions let you extend the client with custom logic in a more type-safe, error-resistant way.