Short answer: build a modular monolith in a language your team knows, on PostgreSQL, with a shared schema and a tenant ID on every row. Buy authentication, billing, email delivery and monitoring. Run background work on a simple queue. Host it in containers or on a managed platform, in your own account. Depart from that only for a reason you can state in one sentence.
If you are choosing a stack for a first release of any kind, our MVP tech stack guide covers the general principles. This article deals with what is particular to SaaS: many customers sharing one system and paying for it monthly.
Principles before products
- Familiar beats fashionable. A team ships fastest in the tools it knows. Mainstream tools also mean a larger hiring pool later.
- Spend your architectural effort on what is hard to reverse: tenancy, identity and billing. Most other choices can be revisited.
- Buy what is not your product. Nobody subscribes because of your password-reset flow.
- Fewer moving parts. Every additional service is something to deploy, monitor, secure and pay for.
- Own the accounts. Cloud, domain, billing, email and source control registered to your company from the first day.
Multi-tenancy: the defining decision
A tenant is a customer account: a company, workspace or organisation. Multi-tenancy is how one running system serves many of them while keeping their data apart. There are three established models.
| Shared schema | Schema per tenant | Database per tenant | |
|---|---|---|---|
| How it works | One database, one set of tables; every row carries a tenant ID | One database; each tenant has its own copy of the tables in a separate schema | Each tenant has its own database, sometimes its own server |
| Isolation | Logical, enforced by application code and optionally by row-level security | Stronger; a missing filter cannot cross schemas | Strongest; physical separation |
| Operational effort | Lowest: one migration, one backup, one connection pool | Rising: every migration runs once per tenant; tooling needed past a few hundred tenants | Highest: provisioning, migrations, monitoring and backups per tenant |
| Cost per tenant | Negligible | Low | Significant; small or free plans become uneconomic |
| Cross-tenant analytics | Simple queries | Awkward | Needs a separate data pipeline |
| Per-tenant backup, restore, export, deletion | Needs tooling you write | Easier | Trivial |
| Per-tenant data residency | Only by running a separate regional deployment | Same | Natural |
| “Noisy neighbour” risk | Highest; needs rate limits and query discipline | Moderate | Lowest |
| Suits | Most B2B and prosumer SaaS, many small to mid-sized tenants | Tens to low hundreds of tenants wanting firmer separation | Few, large, high-value tenants with contractual or regulatory isolation needs |
The default: shared schema, done carefully
For most new products the shared schema is correct, and the risk it carries, a query that forgets its tenant filter, is manageable with discipline:
- A
tenant_idcolumn on every tenant-owned table, not null, indexed, and part of composite unique constraints - Tenant scoping applied in one place (a data-access layer or query helper), so that individual features cannot forget it
- PostgreSQL row-level security as a second line of defence where the framework allows it
- Automated tests that create two tenants and assert that one can never read or change the other’s data, for every endpoint
- The same scoping applied to file storage paths, cache keys, search indexes, background jobs and exports
- Per-tenant rate limits so that one customer’s import cannot slow everyone down
Hybrid is normal
Mature products often run the shared model for most customers and a dedicated database or deployment for a few large ones who pay for it. If the application resolves “which database does this tenant live in?” through one lookup, that option stays open without building it on day one.
Model identity separately from tenancy. Keep users and organisations as separate entities joined by a membership that carries the role. People belong to several workspaces, change employers and act as outside consultants. A user table with a single tenant_id column is one of the most common early mistakes and one of the most tedious to undo.
Application framework and database
Any mature web framework can carry a SaaS product: Next.js with Node.js, Ruby on Rails, Django, Laravel, ASP.NET. They differ less than their advocates suggest. Choose on team experience and hiring market.
BBR’s own default is TypeScript throughout, with React and Next.js on the front end, Node.js behind it and PostgreSQL for data, because one language and shared types across the stack reduce both defects and the number of specialists a small company needs. That is a statement about what we know well, not a claim that other stacks are worse.
On the database there is less room for taste. SaaS business data (accounts, users, subscriptions, invoices, the objects in your workflow) is relational, and a relational database with transactions and constraints prevents whole classes of bugs. PostgreSQL also handles JSON columns, full-text search and vector search well enough to postpone adding separate systems for those.
Authentication and authorization
Authentication (who are you?) should be bought or taken from a well-maintained library. The realistic options:
| Option | Strengths | Watch for |
|---|---|---|
| Managed identity service (for example Auth0, Clerk, Amazon Cognito, or the auth built into Supabase or Firebase) | Fastest route to secure login, MFA, social login and enterprise SSO; security updates are someone else’s job | Per-user pricing as you grow; SSO often on higher tiers; migrating away takes planning |
| Framework library (the auth packages of your web framework) | No per-user fees; users live in your database; full control | You own session security, email flows and upgrades; SSO is additional work |
| Hand-written | None worth the risk | Password storage, token handling and reset flows are easy to get subtly wrong |
If you expect to sell to larger companies, make sure the chosen route can support SAML or OIDC single sign-on later, even if it is not switched on at launch.
Authorization (what may you do?) is yours to build, because it encodes your product’s rules. Start with a few fixed roles per membership. Check permissions on the server for every request, in one central policy layer, never only by hiding buttons in the interface. Add custom roles when paying customers need them.
Subscription billing
Use a hosted billing provider. Stripe Billing is the most widely used; Paddle, Lemon Squeezy and Chargebee are established alternatives with different trade-offs. The first decision is structural:
| Payment processor with subscription tooling | Merchant of record | |
|---|---|---|
| Who is the seller | You | The provider, reselling your product |
| Sales tax and VAT | The provider can calculate it; registration, filing and remittance remain your responsibility | Handled by the provider |
| Fees | Lower | Higher |
| Flexibility | Greatest: custom invoicing, usage billing, marketplaces | More constrained |
| Suits | Companies with an accountant and B2B customers in a small number of jurisdictions | Small teams selling worldwide to many small customers |
Integration rules that prevent pain
- The provider is the source of truth for subscription state. Your database keeps a synced copy of plan, status and period end for fast checks.
- Sync by webhook, verify signatures, handle events idempotently, and expect them to arrive late or out of order.
- Use the provider’s hosted checkout and customer portal at first. They handle card updates, invoices and strong customer authentication, and they keep card data off your servers.
- Define plans and limits in one configuration, and have features ask “is this allowed for this tenant?” of one entitlement function. Pricing will change; this makes it a configuration change.
- Handle failed payments deliberately: retries, reminder emails, a grace period, then a restricted state, never silent data deletion.
- For usage-based pricing, record usage events in your own database first, aggregate, then report to the provider. You will need your own record when a customer questions an invoice.
Background jobs and scheduled work
Anything slow or unreliable belongs off the web request: sending email, processing webhooks, imports and exports, report generation, calls to third-party APIs and language models, nightly maintenance. You need a queue with retries, backoff, a dead-letter list for jobs that keep failing, and scheduled jobs.
A queue backed by the PostgreSQL database you already run is enough for many products and adds no infrastructure. A Redis-backed queue is the usual next step when volume grows. Make jobs idempotent, since they will sometimes run twice, and carry the tenant ID in every job so that scoping and per-tenant fairness survive the move off the request path.
Transactional email
Send through a transactional email provider (Postmark, Amazon SES, SendGrid, Resend and Mailgun are common choices), not from your own server. The engineering work is mostly in deliverability and hygiene:
- Authenticate your sending domain with SPF, DKIM and DMARC
- Keep transactional mail separate from marketing mail, ideally on different subdomains
- Process bounces and complaints and stop sending to those addresses
- Keep templates in code, with a plain-text version
- Log what was sent to whom, so that support can answer “I never got the invitation”
Observability
Customers on a subscription notice downtime and tell you by cancelling. A small product needs four things from the start:
- Error tracking (Sentry or similar) on both front end and backend, with the tenant and user attached to each event
- Structured logs with request and tenant identifiers, searchable for at least a few weeks
- Uptime checks and alerts that reach a person by phone or chat, not only a dashboard
- Basic metrics: response times, queue depth, database load, error rate
Add two SaaS-specific items: an audit log of significant actions per tenant (larger customers will ask for it, and it settles support disputes), and product analytics that record the activation and usage events you will steer the business by. Full distributed tracing can wait until there is something distributed to trace.
Hosting and infrastructure
| Option | Good for | Trade-off |
|---|---|---|
| Managed application platform (Render, Railway, Fly.io, Heroku-style) with managed PostgreSQL | Small teams who want deployments, TLS and scaling handled | Higher unit cost as you grow; some platform constraints |
| Docker containers on a cloud VM or VPS, with a managed or self-run database | Low, predictable cost and full portability | You own patching, backups and failover, so they must be set up properly |
| Major cloud (AWS, Google Cloud, Azure) with managed container and database services | Enterprise customers who expect it; compliance programmes; broad service catalogue | More configuration and more ways to overspend |
| Serverless and edge platforms | Spiky traffic, front-end hosting | Connection limits to relational databases, time limits on long jobs, harder local debugging |
Whatever you choose: containerise the application so that you can move, keep staging as close to production as you can afford, automate deployments, take database backups with point-in-time recovery, and rehearse a restore. An untested backup is a hope, not a backup. Choose the hosting region with your customers’ data-protection expectations in mind, since moving it later is disruptive.
Monolith vs microservices
Start with a modular monolith: one deployable application, with clear internal boundaries between areas such as billing, identity, notifications and the core domain. It gives you one deployment, one place to debug, transactions that span features, and refactoring with the help of the compiler.
Microservices solve organisational problems, chiefly many teams needing to release independently, and they charge for it in network failure modes, distributed data consistency, duplicated tooling and operational load. A team of two to six engineers pays the full price and gets little of the benefit.
Reasonable grounds for extracting a service later:
- A component with a very different resource profile, such as video processing or heavy document parsing
- A component that needs a different runtime, such as a Python library with no equivalent in your main language
- A separate team that must deploy on its own schedule
- A security boundary, such as isolating a component that handles especially sensitive data
A background worker process that runs the same codebase as the web application is not a microservice. It is the monolith doing its slow work elsewhere, and you will want one early.
What to buy and what to build
| Capability | Recommendation | Reasoning |
|---|---|---|
| Card payments, subscriptions, invoices | Buy | Regulated, intricate, and solved |
| Authentication | Buy, or use a maintained library | High security risk, no differentiation |
| Email delivery | Buy | Deliverability is a specialism |
| Error tracking, logs, uptime monitoring | Buy | Inexpensive; building them is a distraction |
| File storage and CDN | Buy (object storage) | Commodity |
| Product analytics, support inbox, status page | Buy | Commodity |
| Feature flags | Build simply at first | A table and a helper function cover early needs |
| Search | Start with PostgreSQL full-text search | Move to a dedicated search service when relevance or scale demands it |
| Tenancy, roles and permissions | Build | They encode your product’s rules |
| Entitlements and plan limits | Build | The link between your pricing and your product |
| Internal admin / back office | Build plainly, or use an internal-tool builder over a read replica | Needs knowledge of your domain; must respect audit and access rules |
| The core workflow | Build | This is what customers pay for |
When you buy, check three things: how the price behaves at ten times your current usage, how you would get your data out, and whether the vendor’s data-processing terms fit what you have promised your own customers.
A reference architecture for a first release
- One TypeScript application (web front end and API), plus a worker process from the same codebase
- PostgreSQL with a shared schema, tenant ID on every tenant-owned row, row-level security where practical
- Users, organisations and memberships as separate entities; three fixed roles
- Managed or library-based authentication with a path to SSO
- Hosted billing with checkout, customer portal and webhooks; one entitlement function
- Database-backed job queue with retries and scheduled jobs
- Transactional email provider with an authenticated domain
- Object storage for files, with tenant-prefixed paths and signed URLs
- Error tracking, structured logs, uptime alerts, audit log, product analytics
- Containers on a managed platform or VM, staging and production, automated deploys, tested backups
That setup will carry most B2B products well past their first few hundred customers, at a running cost that stays small next to development. What each part costs to build is set out in our SaaS development cost breakdown, the order to build it in is covered in how to build a SaaS product, and if you would like a team to build it, see BBR’s SaaS development service.
