Tenelix
All posts
Engineering

Hybrid multi-tenancy: shared by default, dedicated on graduation

How we let standard tenants share a database while enterprise tenants get their own — without changing application code.

MO
Michael Okonji
Founder & Engineer
March 28, 2026 8 min read

Healthcare SaaS sits on a tenancy spectrum. On one end, you have small clinics that need cost-effective, shared infrastructure. On the other, you have hospital groups whose compliance team won't sign off on anything less than full database isolation. Most platforms pick a side. Tenelix tries to live on both ends — at the same time, on the same code.

The hybrid model

Every tenant in Tenelix has a tenancy mode:

-- in the central database
tenants (
  id uuid primary key,
  name text,
  is_enterprise boolean,        -- if true, dedicated DB
  database_name text nullable   -- populated when is_enterprise = true
)

Standard tenants share tenelix_tenants_shared. Every operational table in that database has a tenant_id column, and queries are automatically scoped to the current tenant via Eloquent global scopes.

Enterprise tenants get their own database — tenant_{uuid} — with the same schema, but no tenant_id columns. The whole database is the isolation boundary.

The tricky part: services

A naive implementation works for application data. What breaks is Laravel's own services — sessions, cache, queue. By default, those write to whichever connection is configured globally. So you'd have sessions for an enterprise tenant ending up in the shared database. Audit fail.

We solved this with a HybridServiceConnectionBootstrapper that runs on tenant initialization. It rebinds the session, cache, and queue connections to point to the current tenant's database — whether that's tenelix_tenants_shared or tenant_{uuid}. Application code keeps using Cache::get() and Auth::user() like nothing changed.

ServiceShared tenantEnterprise tenant
Sessionstenelix_tenants_shared.sessionstenant_{uuid}.sessions
Cachetenelix_tenants_shared.cachetenant_{uuid}.cache
Queuetenelix_tenants_shared.jobstenant_{uuid}.jobs

Migrations

Schema changes work the same way: a new migration file describes the change, and our deploy step runs it against tenelix_tenants_shared and every tenant_{uuid} database. Slow? Surprisingly not — most schema changes are O(seconds) per database, and we run them in parallel.

Why this matters

Customers don't have to pick a tier upfront. They start on Standard for cost efficiency. When they need stronger isolation — for compliance, for performance, for sovereignty — they graduate. We migrate their data, the bootstrapper rebinds, and they're on dedicated infra without changing their application footprint.

The same model lets us ship a self-hosted Docker stack: the customer's single tenant runs on tenant_{their_uuid}, just on their own PostgreSQL. Same image, same code.

That portability — between deployment models, between tenancy modes — is the most important architectural property we have. We optimized hard for it from day one, and it pays off every time a customer's requirements change.

Ready when you are

Want this delivered to your team?

Quarterly engineering letter — architecture decisions, multi-tenancy patterns, and what we're shipping.