Start With the End in Mind (But Build for Today)
Every SaaS founder faces the same tension: you need to ship fast, but the decisions you make now will compound for years. Pick the wrong multi-tenancy model and you'll be rewriting your data layer at 500 customers. Choose an exotic tech stack and you'll struggle to hire your second engineer.
The good news is that most of these decisions have well-trodden paths. You don't need to innovate on infrastructure. You need to innovate on your product. This guide covers the architecture decisions that matter most in the first year of a SaaS product — with specific recommendations, not just hand-waving.
The overarching philosophy here is simple: start with a monolith, use boring technology, and defer complexity until you have evidence you need it.
Multi-Tenancy: How Your Customers Share Infrastructure
Multi-tenancy is the defining architectural question for any SaaS. It determines how customer data is isolated, how you scale, and how much operational overhead you carry. There are three common models, and each has a clear sweet spot.
Shared Database with Tenant ID
Every table includes a tenant_id column. All customers live in the same database, same schema.
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Every query filters by tenant
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
Pros: Simplest to build and operate. One database to back up, monitor, and migrate. Adding a new customer is just inserting a row. Resource-efficient — you're not spinning up infrastructure per tenant.
Cons: A bug in your query logic can leak data between tenants. You need Row Level Security (RLS) or disciplined middleware to enforce isolation. One noisy tenant can degrade performance for everyone.
Best for: Most SaaS products, especially in the first two years. This is the default choice unless you have a specific reason to do otherwise.
Schema-Per-Tenant
Each tenant gets their own database schema (in PostgreSQL terms, a separate namespace within the same database). Tables are identical but physically separated.
Pros: Stronger data isolation without the operational cost of separate databases. Easier to reason about data boundaries. You can drop an entire schema cleanly when a customer churns.
Cons: Schema migrations become painful — you're running ALTER TABLE across potentially thousands of schemas. Connection pooling gets more complicated. Most ORMs don't handle this well out of the box.
Best for: Products with strict data isolation requirements (healthcare, finance) where you're not yet at the scale to justify full database separation.
Database-Per-Tenant
Every customer gets their own database instance.
Pros: Maximum isolation. One tenant's data is physically separate from another's. Easy to comply with data residency requirements. You can scale, back up, and restore individual tenants independently.
Cons: Operational complexity scales linearly with your customer count. Migrations are a deployment event across N databases. Costs multiply quickly. Cross-tenant analytics become a separate engineering project.
Best for: Enterprise SaaS where customers demand dedicated infrastructure, or where regulatory requirements mandate physical data separation.
Our recommendation: Start with shared database + tenant_id. Use PostgreSQL's Row Level Security as a safety net. You can always migrate to schema-per-tenant later if isolation requirements demand it — and by then, you'll understand your access patterns well enough to do it right.
Authentication and Authorization
Auth is the canonical example of "buy, don't build." The surface area for security mistakes is enormous, and the problem is well-solved by existing services.
Authentication: Who Are You?
Use a managed auth provider. Services like Clerk, Auth0, or Supabase Auth handle email/password, social login, MFA, and session management. They've already handled the edge cases you haven't thought of — account enumeration attacks, timing-safe password comparison, token rotation, and email deliverability.
If you insist on building it yourself, use session-based authentication over JWTs for your primary web application. JWTs are useful for service-to-service communication and stateless API access, but they have a critical limitation for user-facing sessions: you can't revoke them instantly. When a user changes their password or an admin deactivates an account, you need that to take effect immediately — not whenever the token happens to expire.
// Session-based: revocation is immediate
DELETE FROM sessions WHERE user_id = $1;
// JWT-based: you need a blocklist (which defeats the purpose)
// or you accept a window of vulnerability until expiry
Authorization: What Can You Do?
Role-Based Access Control (RBAC) is sufficient for 90% of SaaS products. Don't reach for attribute-based access control (ABAC) until you genuinely need it.
Design your permission model around organizations, not individual users, from day one. Even if your first version only supports single-user accounts, structuring it as "user belongs to organization, organization has subscription" saves you from a painful refactor later.
User -> Membership (role) -> Organization -> Subscription
-> Projects
-> Team Members
Common roles to start with: Owner (full access, billing), Admin (manage team, all features), Member (standard access), Viewer (read-only). You can always add granular permissions later.
Database Choice
PostgreSQL as Your Default
PostgreSQL should be your default database for a new SaaS. This is not controversial advice — it's the consensus for good reason. PostgreSQL gives you:
- Robust ACID transactions
- JSON/JSONB columns for semi-structured data (reducing the need for a separate document store)
- Row Level Security for multi-tenant isolation
- Full-text search that's good enough to defer adding Elasticsearch
- Extensions like PostGIS (geospatial), pg_cron (scheduled jobs), and pgvector (AI embeddings)
- A mature ecosystem of tooling, hosting options, and engineers who know it
When to Add Redis
Add Redis when you need caching, rate limiting, or real-time features — not as a primary data store. Common use cases:
- Session storage — fast reads, automatic TTL expiration
- API rate limiting — sliding window counters
- Caching — expensive query results, external API responses
- Job queues — if you're using a Redis-backed queue like BullMQ
- Real-time presence — pub/sub for websocket fan-out
Don't add Redis preemptively. PostgreSQL with proper indexing handles more load than most early-stage SaaS products will ever see.
When You Need a Document Store
Rarely, and later than you think. PostgreSQL's JSONB columns handle most semi-structured data needs. If you find yourself needing MongoDB or DynamoDB, it's usually because:
- You have genuinely schema-less data that changes shape per record (audit logs, event streams)
- You need horizontal write scaling beyond what a single PostgreSQL instance can handle
- You're building a content management system where documents are the primary abstraction
For most SaaS products in the first two years, PostgreSQL alone is the right answer.
API Design
REST vs GraphQL
Start with REST. It's simpler to build, simpler to cache, simpler to debug, and every developer on earth understands it. GraphQL solves real problems — over-fetching, under-fetching, and the need for flexible client-driven queries — but those problems usually don't bite you until you have multiple client applications consuming the same API.
If you're building a SaaS with a single web frontend, REST with well-designed endpoints will serve you well. Use consistent naming (/api/v1/projects/:id/tasks), return proper HTTP status codes, and include pagination from the start.
If you do choose GraphQL (for example, because you're building a platform with both web and mobile clients), use a code-generation workflow. Write your schema, generate typed client code, and never manually construct query strings. Tools like PostGraphile (which generates a GraphQL API directly from your PostgreSQL schema) or Pothos (code-first schema builder) reduce the boilerplate significantly.
Versioning
Use URL-based versioning (/api/v1/, /api/v2/) for external APIs. It's explicit, easy to understand, and lets you run multiple versions in parallel during migration periods.
For internal APIs consumed only by your own frontend, you often don't need versioning at all — you deploy the API and the client together, so they're always in sync.
Background Jobs and Async Processing
Not everything needs to happen in the request-response cycle. Emails, PDF generation, webhook delivery, usage aggregation, and data exports should all happen asynchronously.
Start Simple
A PostgreSQL-backed job queue is perfectly adequate for early-stage SaaS. Libraries like Graphile Worker (Node.js) or pgboss give you durable, transactional job processing without adding new infrastructure.
// Adding a job is just an INSERT, wrapped in your existing transaction
await worker.addJob("send-welcome-email", {
userId: newUser.id,
email: newUser.email,
});
The advantage of a PostgreSQL-backed queue is transactional enqueuing — your job is created in the same transaction as the data it depends on. No messages lost because the database write succeeded but the queue publish failed.
When to Graduate
Move to a dedicated message broker (RabbitMQ, AWS SQS, or Redis-backed BullMQ) when:
- Job throughput exceeds what your PostgreSQL instance comfortably handles alongside your application queries
- You need complex routing, priority queues, or dead-letter handling
- You're breaking into multiple services that need to communicate asynchronously
For most SaaS products, that transition happens somewhere between 10,000 and 100,000 jobs per day, depending on job complexity.
Billing Integration
Billing is another clear "buy, don't build" domain. Use Stripe. The API is excellent, the documentation is best-in-class, and the ecosystem of tooling around it is unmatched. Trying to build billing yourself — with proration, tax calculation, failed payment retry, dunning emails, and compliance — is a multi-month project that adds zero product value.
Pricing Models
Seat-based pricing (per-user, per-month) is the simplest to implement and the easiest for customers to understand. It aligns well with RBAC since you're already tracking users per organization.
Usage-based pricing (metered billing) is more complex but can be more fair and more profitable. If you go this route, track usage events in your own database and report aggregates to Stripe via their Meter Events API. Don't rely on Stripe as your system of record for usage data — you need that data locally for dashboards, alerts, and dispute resolution.
Handling Plan Changes
This is where billing gets tricky. When a customer upgrades mid-cycle, do they pay the difference immediately or at the next billing date? When they downgrade, do they retain access until the current period ends?
Stripe handles proration automatically, but you need to map plan changes to feature access in your application. The simplest approach:
- Store the customer's current plan and feature entitlements in your database
- Listen to Stripe webhooks (
customer.subscription.updated,customer.subscription.deleted) - Update your local entitlements based on webhook events
- Check entitlements at the application layer, not by querying Stripe on every request
// Middleware: check feature entitlement
function requireFeature(feature) {
return (req, res, next) => {
const org = req.organization;
if (!org.entitlements.includes(feature)) {
return res.status(403).json({ error: "Plan upgrade required" });
}
next();
};
}
Deployment and Infrastructure
Monolith First
Deploy a single application. One codebase, one deployment artifact, one set of logs. The monolith gets an unfairly bad reputation. At the scale of a new SaaS (zero to tens of thousands of users), a well-structured monolith on a single server will outperform a poorly structured microservices architecture every time — in performance, developer productivity, and operational simplicity.
Structure your monolith with clear internal boundaries — modules or domains that could become services later if needed. But don't split them prematurely.
Platform Choice
For most SaaS products, a managed platform beats running your own infrastructure. Options in order of increasing complexity:
- Vercel / Railway / Render — deploy from Git, managed scaling, minimal ops. Best for getting to market fast.
- AWS ECS / Google Cloud Run — container-based, more control, still managed. Good when you need specific infrastructure (VPCs, private networking, compliance).
- Kubernetes — only when you have a dedicated platform team. If you're asking whether you need Kubernetes, you don't.
What to Monitor From Day One
Don't wait until something breaks. Set up basic observability from the start:
- Error tracking (Sentry) — know when your users hit errors before they tell you
- Uptime monitoring (Better Uptime, Checkly) — know when your service is down
- Application metrics (response times, error rates) — spot degradation early
- Structured logging — use JSON logs with request IDs so you can trace issues across your stack
Build vs Buy: A Practical Guide
Your time as an early-stage team is your most constrained resource. Spend it on what makes your product unique.
Buy (use a service):
- Authentication (Clerk, Auth0, Supabase Auth)
- Email delivery (Resend, Postmark, SendGrid)
- Payments and billing (Stripe)
- Error tracking (Sentry)
- File storage (S3, Cloudflare R2)
- Search (Algolia, Typesense — when PostgreSQL full-text search is no longer enough)
Build (it's your core product logic):
- Your domain model and business rules
- The workflows that differentiate your product
- Integrations specific to your market
- Your API and data access layer
The boundary is simple: if it's a solved problem that isn't your competitive advantage, use a service. If it's the reason customers pay you, build it yourself.
Wrapping Up
The architecture of a successful SaaS is not about choosing the most sophisticated technology. It's about choosing the simplest thing that works, maintaining the discipline to keep your codebase modular, and deferring complexity until you have the data to justify it.
Start with PostgreSQL, a monolithic application, and managed services for the undifferentiated heavy lifting. Focus your engineering energy on the features that make your product worth paying for.
If you're planning a SaaS product and want to get the architecture right from day one, Citadel can help. We've guided teams from initial architecture through to production launch, building MVPs that are designed to scale without premature complexity. Get in touch to talk about your project.
