Voxarel serves multiple freight forwarding companies from one codebase, one database, and one deployment. These companies are direct competitors. They serve overlapping routes, fight for the same customers, and guard their pricing like state secrets. A single data leak between tenants would be a competitive intelligence disaster.

The multi-tenant architecture makes this safe. Every request passes through a middleware chain that establishes tenant context before any application code runs. Every database query is filtered by organization ID. Every API response is scoped to the requesting tenant.

Subdomain Routing

Each tenant gets its own subdomain: stcourier.voxarel.com, acmeship.voxarel.com, globalfwd.voxarel.com. The middleware extracts the subdomain from the hostname, looks up the organization in the database, and injects the organization context into request headers. Every downstream handler receives the tenant context without needing to parse it from the URL.

New tenants are provisioned by creating an organization record and a DNS entry. No code changes. No new deployments.

Data Isolation with Row-Level Security

PostgreSQL Row-Level Security (RLS) enforces data isolation at the database level. Every table with tenant-specific data has an organizationId column and an RLS policy that filters rows based on the current tenant context. A developer cannot accidentally write a query that returns another tenant's data. The database rejects it.

This differs from application-level filtering, where a WHERE clause in the API layer handles isolation. Application-level filtering is one missed WHERE clause away from a data leak. RLS makes isolation structural. The security boundary lives in the database, not in the application code.

Within each organization, data is further scoped by branch. A freight company with offices in Dubai, Abu Dhabi, and Sharjah has branch-level access controls. A branch manager in Sharjah sees only Sharjah operations. The operations director sees all branches.

353 Indexes for Performance

Multi-tenant queries are inherently more expensive than single-tenant queries because every table scan includes an organizationId filter. Without proper indexing, this filter adds latency to every read.

Voxarel has 353 indexes across 100+ tables. Composite indexes on (organizationId, status), (organizationId, createdAt), and similar patterns ensure the tenant filter uses an index scan, not a sequential scan. The difference: an unindexed tenant filter on a 500,000-row table takes 200+ milliseconds. With a composite index, the same query takes 2 to 5 milliseconds.

Tenant-Specific Configuration

Each organization has its own configuration: branding (logo, colors, email templates), VAT rates (domestic versus international differ by tenant), AWB format strings, notification provider preferences, and feature toggles. Some tenants have access to the AI container optimizer; others do not.

Configuration loads once per request during the middleware chain and caches for the request lifecycle. No configuration crosses tenant boundaries.

What We Learned

Multi-tenancy is an architecture decision, not a feature. Retrofitting it onto a single-tenant codebase is one of the most expensive engineering projects a SaaS company can undertake. The middleware chain, the RLS policies, the index strategy, and the RBAC model all need to exist from the first commit.

The alternative (running separate instances per customer) avoids the multi-tenant complexity but creates operational complexity: dozens of deployments, each with their own migration state, their own monitoring, their own backup schedule. We chose multi-tenant from day one. A new tenant is a database record and a DNS entry, not a new deployment pipeline.