Bablu Kumar Singh
Back to Blog
Backend Development
4 min read
May 19, 2026

Building SaaS Platforms with Node.js: Architecture to Payment Integration

Building SaaS Platforms with Node.js: Architecture to Payment Integration

Building a SaaS platform means building a platform that hosts many smaller applications inside it — each isolated, billed, and governed on its own. This is the architecture behind ROI Spectrum, a multi-tenant SaaS platform I built and shipped, and what held up in production.

Choosing a Tenant Isolation Strategy

The first decision: how do you separate tenant data?

Strategy Isolation Level Cost Complexity
Database-per-tenant Complete High High
Schema-per-tenant Strong Medium Medium
Row-level (shared tables) Moderate Low Low

For most B2B SaaS products handling fewer than 10,000 tenants, row-level isolation with a tenantId column on every table provides the best balance of cost and simplicity. I use this strategy with strict middleware enforcement.

Tenant Context Middleware

Every request must carry a tenant identifier. I extract it from a JWT claim or a custom header and attach it to the request context:

import { Request, Response, NextFunction } from 'express';

export function tenantContext(req: Request, res: Response, next: NextFunction) {
  const tenantId = req.user?.tenantId || req.headers['x-tenant-id'];
  if (!tenantId) {
    return res.status(400).json({ error: 'Tenant context is required' });
  }
  req.tenantId = tenantId as string;
  next();
}

Every database query then scopes to the tenant:

const orders = await Order.find({ tenantId: req.tenantId, status: 'active' });

A Mongoose middleware can enforce this automatically to prevent accidental cross-tenant data leaks:

OrderSchema.pre(/^find/, function (next) {
  const tenantId = this.getOptions().tenantId;
  if (tenantId) this.where({ tenantId });
  next();
});

Subscription Billing with Stripe

Stripe is the industry standard for SaaS billing. Here is the integration flow:

1. Creating a Checkout Session

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

async function createCheckoutSession(tenantId: string, priceId: string) {
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    metadata: { tenantId },
    success_url: `${process.env.APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/billing/cancel`,
  });
  return session.url;
}

2. Handling Webhooks

Stripe sends events asynchronously. You must verify the webhook signature and handle critical events:

app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'] as string;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return res.status(400).send('Webhook signature verification failed');
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await activateTenant(session.metadata!.tenantId!, session.subscription as string);
      break;
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await suspendTenant(invoice.metadata!.tenantId!);
      break;
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      await deactivateTenant(sub.metadata!.tenantId!);
      break;
    }
  }

  res.json({ received: true });
});

3. Subscription Lifecycle Management

async function activateTenant(tenantId: string, subscriptionId: string) {
  await Tenant.findByIdAndUpdate(tenantId, {
    subscriptionId,
    subscriptionStatus: 'active',
    plan: 'pro',
  });
  await redis.del(`tenant:settings:${tenantId}`); // invalidate cache
}

Feature Gating by Plan

Different plans unlock different features. I store plan capabilities in a config and check at the middleware level:

const PLAN_LIMITS = {
  free: { maxUsers: 3, maxProjects: 5, apiCalls: 1000 },
  pro: { maxUsers: 25, maxProjects: 50, apiCalls: 50000 },
  enterprise: { maxUsers: Infinity, maxProjects: Infinity, apiCalls: Infinity },
};

export function requirePlan(minPlan: 'free' | 'pro' | 'enterprise') {
  return async (req: Request, res: Response, next: NextFunction) => {
    const tenant = await getTenantSettings(req.tenantId);
    const planOrder = ['free', 'pro', 'enterprise'];
    if (planOrder.indexOf(tenant.plan) < planOrder.indexOf(minPlan)) {
      return res.status(403).json({ error: 'Upgrade your plan to access this feature' });
    }
    next();
  };
}

Audit Logging

Every mutation should be logged for compliance and debugging:

async function logAudit(tenantId: string, action: string, userId: string, details: object) {
  await AuditLog.create({ tenantId, action, userId, details, timestamp: new Date() });
}

Wrapping Up

Row-level isolation plus middleware that enforces it on every query gets you further than it sounds like it should. The part that actually took the longest to get right wasn't the isolation — it was the Stripe webhook handling, and making sure a failed payment couldn't leave a tenant half-active.

#SaaS#Node.js#Multi-Tenant#Stripe
Bablu Kumar Singh
Written by

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Writes about backend systems and databases based on what comes up while building with Node.js, MongoDB, PostgreSQL, and Redis.

You May Also Like

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security
Backend Development
4 min read

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security

Build a production-grade authentication system in Node.js — covering JWT access tokens, refresh token rotation, password hashing with bcrypt, and common security pitfalls to avoid.

May 28, 2026Read
Designing a Role-Based Access Control (RBAC) System from Scratch
Backend Development
2 min read

Designing a Role-Based Access Control (RBAC) System from Scratch

A blueprint on implementing hierarchical permission matrices in Express.js middleware using bitwise operations and MongoDB caching.

May 24, 2026Read
Redis Caching Strategies for Backend Developers
Backend Development
4 min read

Redis Caching Strategies for Backend Developers

Learn when and how to use Redis as a cache layer in Node.js applications — covering cache-aside, write-through, TTL policies, cache invalidation, and distributed locking.

May 10, 2026Read