How to Design a Scalable SaaS Architecture
Tenancy, billing, background work, environments and the boundaries that let a SaaS product grow without a rewrite. A practical architecture for a first version that is built to last, with Next.js, Laravel and PostgreSQL as the reference stack.
- Author
- Kamran Khan
- Published
- Reading time
- 7 min read
On this page
"Scalable" is usually said about traffic. For a SaaS product the harder kind of scale is organisational: more customers with different needs, more plans, more integrations, more people working on the codebase. The architecture decisions that matter most are the ones that keep those from turning into a rewrite in year two.
This is the shape I use for a first SaaS version — small enough to ship in months, structured enough to grow.
The decisions that are expensive to change later
Some things can be refactored gradually. These cannot, so decide them first:
- Tenancy model — how customer data is isolated.
- Identity — how users, organisations and roles relate.
- The domain boundary — what the product actually owns versus what it integrates.
- Background work — the queue is a first-class part of the system, not an add-on.
- Environment and deployment shape.
Everything else — UI framework details, which email provider, caching strategy — can evolve.
Tenancy
Almost every B2B SaaS is multi-tenant: many customer organisations share one deployment. The question is how their data is separated.
Shared database, tenant column. Every row carries a tenant_id, and every query filters on it. Simplest to operate, most efficient, and the model I recommend for the first version of nearly every product. The risk is a missed filter leaking data between tenants — mitigate with a global query scope enforced at the ORM level (Laravel's global scopes, or a Prisma/Drizzle middleware), row-level security in PostgreSQL as a second net, and tests that assert isolation.
Schema per tenant or database per tenant. Stronger isolation, much more operational overhead (migrations across hundreds of schemas, connection management, backups). Justified when customers demand isolation contractually or when data volumes per tenant are very large. Rare for a first version.
Whichever you choose, hide it behind a Tenant context that every request resolves once (from the subdomain, the session or an API key) and that all data access reads from. If tenancy is resolved in one place, changing the model later is possible. If it is sprinkled through controllers, it is not.
Identity and organisations
Model three things separately from day one:
- User — a person with credentials.
- Organisation (the tenant) — the customer account, with its plan and settings.
- Membership — a user's role within an organisation.
Users belong to many organisations; consultants, agencies and people who change jobs will thank you. Roles live on the membership, not the user. Invitations, SSO and "switch organisation" all become straightforward with this shape and painful without it.
The domain and its boundaries
Write down what the product owns. A scheduling product owns appointments, availability and reminders; it does not own invoicing, email delivery or identity providers. Everything it does not own is an integration behind an interface:
Product core ──► Billing provider (Stripe or similar)
──► Email/SMS provider
──► Identity providers (Google, Microsoft)
──► Customer's other tools (via API + webhooks)Put an adapter between the core and each provider. The adapter is thin, but it means the provider's data model does not leak into yours — when you change email providers, the core does not notice.
Plans, entitlements and billing
Billing is where first versions get tangled. Separate three concepts:
- Plan — a named commercial package (Starter, Team, Enterprise).
- Entitlements — the concrete capabilities and limits a tenant has right now (seats, projects, API access, feature flags).
- Subscription — the billing state from the provider (active, past due, cancelled, trial).
Code checks entitlements, never plan names. if (tenant.can('api_access')), not if (tenant.plan === 'team'). Entitlements are derived from the plan plus overrides (a custom deal, a trial extension), and updated when the billing provider sends a webhook. This lets sales sell exceptions without engineering changes and lets you repackage plans without touching feature code.
Billing webhooks must be idempotent and verified, and every event stored before it is processed. A missed or double-processed payment event is the kind of bug that costs customers.
Background work as a first-class citizen
A SaaS product does more work outside requests than inside them: emails, exports, imports, webhooks out to customers, scheduled reports, billing sync, search indexing. Design the queue in from the start:
- A job system with retries, backoff and a dead-letter queue (Laravel Horizon on Redis, or a Node worker on a managed queue).
- Idempotent jobs — a retried job must be safe.
- Jobs carry the tenant context explicitly.
- A scheduler for periodic work, with overlap protection.
- Observability: queue depth, failure rate and job duration on a dashboard from the first week.
Long-running or heavy work (large imports, AI processing) runs as jobs that report progress, never as requests that time out.
The API is a product surface
Even if the first version has no public API, build the internal one as though it will be public:
- Versioned routes (
/api/v1). - Consistent resource shapes and error format.
- Authentication with per-tenant API keys and scoped permissions.
- Rate limiting per tenant.
- Outbound webhooks with signatures, retries and a delivery log customers can see.
Customers integrating your product is what makes them stay. Making that possible later is much harder if the internal API grew ad hoc.
Reference stack
The architecture above is framework-agnostic. For a concrete first version I typically build:
- Next.js (App Router, server components) for the product interface and marketing site, or
- Laravel as the API and job system, with a React or Next.js frontend when the interface is interactive enough to justify the split. For simpler products, Laravel with server-rendered views plus targeted interactivity is faster to build and easier to run.
- PostgreSQL with row-level security for the second isolation net; Redis for queues, cache and rate limiting.
- Stripe-style billing provider behind an adapter; entitlements stored locally.
- Docker images, deployed to a container platform or a managed service, with staging and production as separate environments and secrets injected at runtime.
The principle behind each choice: mature, well-understood, boring in the best sense.
Environments and deployment
- Local with the same containers as production.
- Preview environments per pull request where feasible — they change how quickly a team can review.
- Staging that mirrors production, including the billing provider in test mode.
- Production with migrations run as a deploy step, zero-downtime rollouts and a rollback path.
Backups are tested by restoring them, on a schedule, somewhere other than production. Nobody has backups until a restore has been done.
Observability
From launch: structured logs with tenant and user IDs, error tracking, request and job metrics, and a small set of product metrics (sign-ups, activations, active tenants, failed payments). Alerts on error rate, queue backlog and billing webhook failures. This is a day-one requirement because the first month after launch is when you learn the most and have the least data.
Growing without a rewrite
The signs that the first-version architecture is holding:
- New features are added inside existing boundaries rather than around them.
- A tenant's data can be exported or deleted with one command, because tenancy is explicit.
- A plan can be repackaged in configuration.
- The queue absorbs spikes without customer-visible failures.
- A provider can be swapped by changing one adapter.
When one of those stops being true, that is the module to invest in. Everything else can wait.
Summary
A scalable SaaS architecture is mostly a small number of correct boundaries: tenant context resolved once and enforced in the data layer; users, organisations and memberships modelled separately; entitlements decoupled from plan names; integrations behind adapters; a proper job system; an API designed as if public; and environments that let you deploy with confidence. Get those right and the first version is also the foundation.