Bablu Kumar Singh
Back to Blog
Backend Development
8 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 is one of the most rewarding — and complex — challenges for a backend developer. You are not just building an application; you are building a platform that hosts many applications, each isolated, billed, and governed independently. Having built and shipped ROI Spectrum, a production multi-tenant SaaS platform, I want to share the architecture decisions that made it work.

Choosing a Tenant Isolation Strategy

The first decision: how do you separate tenant data?

StrategyIsolation LevelCostComplexity
Database-per-tenantCompleteHighHigh
Schema-per-tenantStrongMediumMedium
Row-level (shared tables)ModerateLowLow

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:

typescript Code Block
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:

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

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

typescript Code Block
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

typescript Code Block
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:

typescript Code Block
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

typescript Code Block
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:

typescript Code Block
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:

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

Key Takeaways

  • Start with row-level tenant isolation — it scales to thousands of tenants without infrastructure overhead.
  • Use middleware to enforce tenant scoping on every database query.
  • Integrate Stripe via webhooks, not polling — it is reliable, real-time, and idempotent.
  • Gate features by plan at the middleware layer so business logic stays clean.
  • Audit everything — future-you will thank past-you during the first compliance review.

Building SaaS is a marathon, not a sprint. Get the architecture right, and scaling becomes an operations problem instead of a rewrite.

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

Bablu Kumar Singh

Backend-Focused Full Stack Developer

Backend-Focused Full Stack Developer specializing in Node.js, MongoDB, PostgreSQL, Redis, RabbitMQ, AWS, Docker, System Design, and React Native.

You May Also Like

Authentication Systems in Node.js: JWT, Refresh Tokens, and Security
Backend Development
7 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
5 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
7 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