Back to path
LargePortfolio centerpiece ~34h· 8 milestones

Build a production SaaS slice (multi-tenant + billing)

You build the part of a SaaS that separates hobby projects from real products: multiple tenants isolated from each other, roles and permissions, a billing flow with plan limits, and the CI/CD plus observability glue to actually operate it. This is the realistic core of a B2B product.

Multi-tenancyRBACBilling integrationCI/CDTestingObservabilitySecurityCost modelingChaos & failover testingIncident postmortems

What you'll build

A multi-tenant application with role-based access, a test-mode billing flow with plan enforcement, CI/CD, observability, and tests, built and documented the way a team would maintain it.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

src/db/schema.tsts
import { pgTable, text, timestamp, uuid, pgEnum } from 'drizzle-orm/pg-core';

export const roleEnum = pgEnum('role', ['owner', 'admin', 'member']);
export const planEnum = pgEnum('plan', ['free', 'pro', 'enterprise']);

export const tenants = pgTable('tenants', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: text('name').notNull(),
});

export const memberships = pgTable('memberships', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
  userId: text('user_id').notNull(),
  role: roleEnum('role').notNull().default('member'),
});

export const subscriptions = pgTable('subscriptions', {
  tenantId: uuid('tenant_id').primaryKey().references(() => tenants.id),
  plan: planEnum('plan').notNull().default('free'),
  stripeCustomerId: text('stripe_customer_id'),
  seatLimit: text('seat_limit').notNull().default('3'),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

export const projects = pgTable('projects', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
  name: text('name').notNull(),
});

Reading this file

  • roleEnum = pgEnum('role',Restricting roles to a fixed set at the database level stops a typo or bad value from sneaking in as a 'role'.
  • tenantId: uuid('tenant_id').notNull()Every tenant-scoped table carries this column, the anchor that keeps one customer's data separate from another's.
  • .references(() => tenants.id)A foreign key ties rows to a real tenant, so you cannot orphan data under a tenant that does not exist.
  • subscriptions = pgTable('subscriptions'Keying billing state by tenant means a whole organization shares one plan, the unit B2B SaaS actually charges.

tenant_id on every tenant-scoped table; roles on memberships; subscriptions keyed by tenant.

That's 1 of 12 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Isolate tenants

    5 guided steps

    A single cross-tenant leak is a company-ending incident in B2B SaaS. Tenant isolation is the foundation every other feature sits on, so it must be provably correct.

  2. 2

    Roles & permissions

    5 guided steps

    Tenants need owners, admins, and members with different powers. Authorization done only in the UI is no authorization at all, the API is the real gate.

  3. 3

    Billing flow

    5 guided steps

    Billing is where SaaS makes money, and where state gets subtle. Webhooks, not the checkout redirect, are the source of truth for what a customer actually has.

  4. 4

    Ship through CI

    5 guided steps

    A SaaS evolves continuously. Without automated tests, safe migrations, and a repeatable deploy, every release is a gamble against tenant data.

  5. 5

    Observe & secure

    5 guided steps

    When a tenant reports a bug or a charge looks wrong, you need to answer "what happened?" in minutes. And a multi-tenant app is a high-value target, a security pass is non-negotiable.

  6. 6

    Model the unit economics

    5 guided steps

    In B2B SaaS, a feature that costs more to serve than the plan charges is a slow bankruptcy. Knowing cost-per-tenant is what lets you price plans and spot the tenant that’s losing you money.

  7. 7

    Break it on purpose

    5 guided steps

    Production fails, the DB hiccups, the payment provider times out, a region blips. A SaaS that corrupts data or leaks across tenants under failure is worse than one that’s simply down. You have to prove it fails safely.

  8. 8

    Write the postmortem

    5 guided steps

    Mature teams turn incidents into systemic fixes, not blame. A blameless postmortem is the artifact that proves you can learn from failure operationally, not just patch it.

What's inside when you start

4 starter files, ready to clone
8 guided milestones
8 full reference solutions
12 code blocks explained line-by-line
8 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

A deployed multi-tenant app with RBAC and test-mode billing
A CI/CD pipeline and test suite, including cross-tenant isolation tests
A security + multi-tenancy design note covering your isolation model and threat model
A cost model with cost-per-tenant, gross margin per plan, and the first scaling cliff
A chaos experiment report and a blameless postmortem with landed action items

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building