SaaS Development
Tenancy, permissions, plans, billing and the realities of running SaaS in production.
- Multi-tenancy
- Permissions
- Billing
- Production delivery
Multi-tenancy affects far more than database structure. It changes authentication, authorization, billing, reporting, background jobs, storage and operational tooling across the entire product.
Data isolation · Tenant context · Membership · Noisy neighbours
How I think about it
The most important question is not “how do we add a tenant_id?” It is where tenant boundaries exist, how they are enforced consistently, and what happens when data flows through APIs, jobs, files, reports and integrations.
Key experience
Scope
Deep dive
The tenant is the unit that owns data and pays — usually an organization or customer account, sometimes a workspace, team or business unit inside one. Getting this wrong is expensive: if the product treats a team as the tenant and a customer later needs several teams under one contract and one invoice, billing, permissions and reporting all need rework.
So the first design question is the hierarchy: who pays, who owns the data, and who can see across which boundaries.
A request can identify its tenant through a subdomain (acme.example.com), a path segment (/orgs/acme/…), an organization chosen after sign-in, or claims in an API token. Whichever is used, the tenant is resolved once, early in the request, verified against the authenticated user's memberships, and passed explicitly to everything downstream.
A tenant ID taken from the request without checking membership is an access-control bug waiting to happen.
User
authenticated
Tenant context
resolved, membership verified
Authorization
role in this tenant
Tenant data
scoped queries, files, jobs
There are three common models. A shared database with a tenant_id on every row is the cheapest to run and simplest to evolve, and it pushes isolation into the code. Schema-per-tenant gives stronger separation inside one database server, at the cost of running every migration once per tenant. Database-per-tenant gives the strongest isolation and easy per-customer backup or data residency, with the highest operational cost.
For most B2B SaaS I start with shared tables and automatic scoping, and keep the option to move a large or regulated customer to dedicated infrastructure later.
// Applied to every tenant-owned model, so queries are scoped by default.
class BelongsToTenantScope implements Scope
{
public function apply(Builder $query, Model $model): void
{
$query->where(
$model->qualifyColumn('tenant_id'),
app(TenantContext::class)->id(),
);
}
} Users and tenants are many-to-many: a consultant may belong to five client organizations with a different role in each. Membership carries the role, status and invitation history; the active tenant is part of the session; and switching tenants is an explicit action that re-checks membership. Invitations are scoped to one tenant, expire, and never grant access to any other.
Subscriptions belong to the tenant, not the user. Features and quotas — seats, storage, projects — are evaluated per tenant, and each tenant needs a clear billing owner who can change the plan.
Downgrades raise the hard questions: what happens to the sixth project when the new plan allows five? Deciding that up front avoids support escalations later.
Jobs run outside the request, so the tenant context that was implicit in the web request has to be passed explicitly: the tenant ID in the job payload, restored before any query runs. Scheduled jobs that work across all tenants process each one in its own context, and failures are logged with the tenant's identity so they can be investigated and retried individually.
A job without tenant context is a job that can process the wrong tenant's data.
Files follow the same boundary as data. Storage paths are prefixed with the tenant, access is checked against the database record — never inferred from the path — and downloads go through short-lived signed URLs issued after authorization. Exports and generated reports are tenant-owned objects with an expiry, not files left in a shared folder.
Tenant-facing analytics query only that tenant's data, through the same scoping as the rest of the application. Cross-tenant reporting for the platform team is a separate, explicitly privileged path — ideally reading from a reporting store — rather than an “all tenants” switch added to customer-facing queries, which is how accidental leakage happens.
Support needs to see a tenant's status, plan, usage and members, apply overrides, and sometimes act on a customer's behalf. “Log in as” features are powerful and dangerous: they need explicit permission, a visible banner while active, and an audit trail of which staff member did what in which tenant.
Every cache key for tenant data includes the tenant ID. A key like dashboard_stats without one will eventually serve one customer's numbers to another — a leak that never shows up when testing with a single tenant. Invalidation is tenant-scoped too, so one tenant's update doesn't flush everyone else's cache.
A search index is a second copy of the data and needs the same boundaries: a tenant filter applied to every query by the search layer itself — not by each caller — or separate indexes for tenants that need stronger isolation. Permissions inside a tenant matter too: search results must not reveal documents a member can't open.
Isolation gets dedicated tests: create two tenants with similar data, act as a member of one, and assert that every endpoint, export, search and job refuses or ignores the other tenant's resources. Tenant switching, removed memberships and role changes get tests too, and background jobs are tested with tenant context set explicitly — the places where scoping is most often lost.
Tenants are never the same size. One large customer's import, report or integration can slow down everyone sharing the same database or queue — the noisy-neighbour problem. Per-tenant rate limits and fair queueing, heavy work moved to background processing, and the ability to move the largest tenants to dedicated resources address it.
Each step adds operational complexity, so it happens when measurements show the need, not on day one.
In practice
Real products
Multi-tenancy was central to the collaboration platform I led, and the same boundaries shape the SaaS products I work on today.
Enterprise SaaS
Led development of a multi-tenant, multi-user platform — tenant boundaries across data, files, APIs and the Vue.js frontend.
Tenancy, permissions, plans, billing and the realities of running SaaS in production.
Maintainable backends: structure, Eloquent, queues, authorization and SaaS patterns.
Predictable API contracts: validation, errors, authorization, idempotency and webhooks.
Workload-first cloud architecture: serverless, storage, data, security and cost.
Design and build SaaS products from MVP to production, including architecture, user management, subscriptions, integrations and cloud infrastructure.
Learn moreBuild reliable web platforms, SaaS products, dashboards, APIs, and internal tools with modern full-stack technologies.
Learn moreWhether it's an architecture decision, a delivery problem or something specific to Multi-Tenant SaaS, I can help you find a practical path forward.
What happens next
Share your goal
A few lines about the product, problem, or audit need.
Get a quick read
A practical reply on the best technical path forward.
Move forward
A clear scope, estimate, or concrete next step.