How to Build a SaaS Product: The Architectural Blueprint
Building a Software-as-a-Service (SaaS) product involves more than rendering dashboards. It requires designing a scalable multi-tenant architecture, securing tenant data, managing subscription states, and implementing granular access controls. In 2026, launching a SaaS platform requires a modern, secure, and auto-scaling technical foundation.
This guide provides a developer's blueprint for building a SaaS product. We will cover multi-tenant database isolation, role-based access control (RBAC), subscription billing integration, and secure API middleware configurations.
Multi-Tenant Architecture: Single DB vs. Isolated Schemas
The first architectural decision in SaaS engineering is selecting the tenant isolation model. This choice affects query performance, data safety, compliance, and infrastructure costs.
Multi-Tenant Isolation Models:
1. Shared Database, Shared Schema (Low Cost, High Performance)
Table [users] -> Columns: [id, tenant_id, email, role] (Isolated via Row-Level Security)
2. Shared Database, Isolated Schemas (Medium Cost, Safe)
Schema [tenant_A] -> Table [users]
Schema [tenant_B] -> Table [users]
3. Isolated Database per Tenant (High Cost, Maximum Security)
Database [tenant_A_db] on Node A
Database [tenant_B_db] on Node B
1. Shared Database, Shared Schema (Row-Level Security)
All tenants share the same database tables. Every record contains a tenant_id foreign key. Isolation is enforced logically via code queries or database Row-Level Security (RLS).
- Pros: Low infrastructure cost, easy database migrations, and simple global analytics.
- Cons: High risk of data leaks if developer queries omit the
tenant_idfilter (known as the "Noisy Neighbor" problem).
2. Shared Database, Separate Schemas
Tenants share the same database instance, but each tenant has their own logical schema (e.g. tenant_a.users, tenant_b.users).
- Pros: Clearer logical separation of data and custom schema changes per tenant.
- Cons: Running schema updates across hundreds of schemas requires complex migration scripts.
3. Separate Database per Tenant
Each tenant has their own physical database instance.
- Pros: Maximum data security and compliance, zero noisy neighbor performance drops.
- Cons: High hosting costs and complex backup and maintenance workflows.
Recommendation: For standard B2B SaaS platforms, we recommend the Shared Database, Shared Schema model using PostgreSQL Row-Level Security (RLS). It provides a balanced mix of performance, security, and cost-effectiveness.
Designing a Multi-Tenant Database with PostgreSQL RLS
PostgreSQL Row-Level Security (RLS) restricts database queries to return only rows that match specific security policies, preventing data leaks at the database engine level.
Step 1: Enable RLS on Your Tables
-- Enable Row-Level Security on the workspaces table
ALTER TABLE workspaces ENABLE ROW LEVEL SECURITY;
-- Enable RLS on the tasks table
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;Step 2: Create a Security Policy
Create a policy that checks the active tenant ID from the database session configuration:
CREATE POLICY tenant_isolation_policy ON tasks
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), ''));Step 3: Apply the Tenant ID in Your Node.js Middleware
When a request arrives, query the tenant ID from the user's JWT token and set the session parameter before running database queries:
import { Request, Response, NextFunction } from 'express';
import { Pool } from 'pg';
const pool = new Pool();
export async function tenantContextMiddleware(req: Request, res: Response, next: NextFunction) {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = req.user?.id; // Set by auth middleware
if (!tenantId) {
return res.status(400).json({ error: "Missing x-tenant-id header" });
}
// Verify the user is authorized to access this tenant workspace
const isAuthorized = await checkUserTenantAccess(userId, tenantId);
if (!isAuthorized) {
return res.status(403).json({ error: "Access denied to this tenant workspace" });
}
// Get a client from the pool and set the session parameter
const client = await pool.connect();
try {
await client.query(`SET LOCAL app.current_tenant_id = '${tenantId}'`);
req.dbClient = client; // Pass the client with tenant context to the route handler
next();
} catch (err) {
client.release();
res.status(500).json({ error: "Internal server error" });
}
}Implementing Role-Based Access Control (RBAC)
RBAC governs what actions users can perform within their tenant workspace. A standard model includes three primary roles:
Role hierarchy:
[Owner] -> Can manage billing, delete workspace, invite Admins.
|
[Admin] -> Can invite Members, edit workspace configurations.
|
[Member] -> Can read data, write tasks, execute standard workflows.
Define a TypeScript decorator or helper function to authorize API routes:
export function authorizeRole(allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role; // Set by auth middleware
if (!userRole || !allowedRoles.includes(userRole)) {
return res.status(403).json({ error: "Insufficient permissions for this action" });
}
next();
};
}
// Usage in Express routes
app.delete('/api/workspace', tenantContextMiddleware, authorizeRole(['Owner']), async (req, res) => {
// Delete workspace logic
});Subscription Billing and Stripe Webhooks
SaaS platforms rely on subscription billing systems (like Stripe) to manage accounts and process payments. Here is how to configure a Node.js route to handle Stripe webhook updates.
Stripe Webhook Flow:
[Stripe Billing System] -> [Post Request to /api/webhooks] -> [Verify Signature]
|
+-----------------------------------------+------------------+
| (Event: customer.subscription.created) | (Event: customer.subscription.deleted)
v v
[Update DB Workspace Tier to 'active'] [Update DB Workspace Tier to 'suspended']
[Log active billing state] [Revoke API access credentials]
import stripe from 'stripe';
const stripeClient = new stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2022-11-15' });
app.post('/api/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature']!;
let event;
try {
event = stripeClient.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
return res.status(400).send(`Webhook Signature Verification Failed: ${err.message}`);
}
// Process the webhook event
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
const subscription = event.data.object as stripe.Subscription;
const workspaceId = subscription.metadata.workspaceId;
const planTier = subscription.items.data[0].plan.metadata.tier; // e.g. "Growth", "Enterprise"
await updateWorkspaceBillingState(workspaceId, {
stripeSubscriptionId: subscription.id,
planTier,
billingStatus: 'active',
currentPeriodEnd: new Date(subscription.current_period_end * 1000)
});
break;
case 'customer.subscription.deleted':
const deletedSub = event.data.object as stripe.Subscription;
const targetWorkspace = deletedSub.metadata.workspaceId;
await updateWorkspaceBillingState(targetWorkspace, {
billingStatus: 'suspended',
planTier: 'free'
});
break;
}
res.json({ received: true });
});Security Audit & Compliance Checklist
Building a SaaS Product?
Our architects design multi-tenant cloud backends, secure relational databases, and subscription billing engines to help you launch fast and scale.
Before launching your SaaS product, perform a security audit to protect user data and meet compliance standards:
- [ ] Multi-factor Authentication (MFA): Secure administrative accounts and offer MFA options to users.
- [ ] HTTP-only Cookies: Store JWT session tokens in secure, HTTP-only, SameSite cookies to protect against cross-site scripting (XSS) attacks.
- [ ] Data Encryption: Enforce TLS 1.3 for data in transit and AES-256 for databases and file storage at rest.
- [ ] API Rate Limiting: Configure rate limiters (e.g. using Redis) to protect endpoints from brute-force and DDoS attacks.
- [ ] Comprehensive Audit Logs: Write read-only logs capturing critical actions (e.g. billing edits, role updates, data exports) with user details and timestamps.
Summary
Building a SaaS product requires planning for data security, scalability, and clean workflows. By designing a multi-tenant database using PostgreSQL Row-Level Security, implementing role-based access control, and configuring secure billing webhooks, you can build a stable foundation that supports users and scales with your business growth.
