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.
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:
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
Isolate tenants
5 guided stepsA 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
Roles & permissions
5 guided stepsTenants 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
Billing flow
5 guided stepsBilling 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
Ship through CI
5 guided stepsA SaaS evolves continuously. Without automated tests, safe migrations, and a repeatable deploy, every release is a gamble against tenant data.
- 5
Observe & secure
5 guided stepsWhen 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
Model the unit economics
5 guided stepsIn 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
Break it on purpose
5 guided stepsProduction 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
Write the postmortem
5 guided stepsMature 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
You'll walk away with
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