Multi-Tenant SaaS Boilerplate Built by Ahmad Shah β a secure, scalable starting point so you don't rebuild auth, billing, and tenancy from scratch.
TenantKit is for developers and teams who are tired of:
- Rebuilding authentication, roles, and permissions for every new SaaS project
- Worrying about one tenant accidentally seeing another tenant's data
- Wrestling with Stripe webhooks, subscription states, and billing logic
- Spending weeks on DevOps before writing business logic
- Tutorial-grade boilerplates that break the moment you try to deploy them
If you are an indie hacker, agency owner, startup CTO, or full-stack engineer who needs a multi-tenant SaaS foundation you can actually read and extend, this is your starting line.
| Concern | How TenantKit Handles It |
|---|---|
| Tenant Data Isolation | PostgreSQL Row-Level Security (RLS) enforced per request via SET LOCAL ROLE + set_config, on top of AsyncLocalStorage request-scoped tenant context |
| Auth | JWT access/refresh tokens with rotation, refresh-token reuse detection, role-based access control (RBAC), and atomic user+tenant provisioning in one transaction |
| Billing | Stripe Checkout + signed webhook verification, plan tiers, and a mock sandbox mode so you can develop billing without Stripe keys |
| Frontend/Backend Split | Next.js 16 App Router client app, decoupled from the NestJS API over REST, with automatic tenant context propagation and JWT refresh handling |
| Deployment | Terraform AWS infrastructure (ECS Fargate, RDS, ElastiCache, ALB), multi-stage Docker builds, and Docker Compose for local development |
| Security Baseline | Helmet headers, input validation, sanitized error responses, and CloudWatch log aggregation |
Some hardening features (rate limiting, webhook idempotency, billing grace periods, strict CORS) are scaffolded or planned β see Roadmap for the honest status.
βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββββββββββ
β Client ββββββΆβ Next.js 16 ββββββΆβ NestJS API (ECS) β
β (Browser) β β (Frontend) β β βββββββββββββββββββββββ β
βββββββββββββββ βββββββββββββββ β β AsyncLocalStorage β β
β β Tenant Context β β
β β + per-request RLS β β
β βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββ β
β β PostgreSQL (RDS) β β
β β Row-Level Security β β
β βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββ β
β β Redis (ElastiCache) β β
β β Caching β β
β βββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββ
Key Design Decisions:
- Decoupled SPA + API: the Next.js frontend and NestJS backend are separate deploys that communicate only over the REST API β independently buildable, scalable, and replaceable.
- Layered modular monolith backend: feature modules (
auth,billing,tenancy,health) in one NestJS process, one PostgreSQL database β not microservices. - RLS as defense-in-depth: tenant context lives in
AsyncLocalStorage; an interceptor opens a per-request transaction that setsapp.current_tenantand switches to the non-superusertenantkit_approle so RLS policies apply. Services also filter bytenantIdexplicitly. RLS shared-table design avoids schema-per-tenant operational complexity.
See ADR-001 for the full rationale.
- β
Subdomain routing (
tenant.yourapp.com) - β
Custom domain support (
client.com) - β Automatic tenant resolution via middleware (host β tenant)
- β
Request-scoped tenant context via
AsyncLocalStorage - β
Per-request PostgreSQL RLS enforcement (
set_config+SET LOCAL ROLE tenantkit_app) - β Atomic tenant provisioning (user + tenant + owner membership in one transaction)
β οΈ ATenantAwareRepositorybase class is included but not currently wired in β services use the RLS-scoped manager plus explicittenantIdfiltering instead
- β JWT access tokens (15 min) + refresh tokens (7 days, stored as SHA-256 hashes)
- β Refresh token rotation with reuse detection (reusing a spent token revokes all sessions)
- β Role-based access control: Owner, Admin, Member, Viewer
- β Password reset via signed, time-limited (1 h) JWT tokens
β οΈ Soft delete is on the User entity (and the tenant-scoped base entity) only β Tenant, Membership, and RefreshToken are hard-deleted
- β Stripe Checkout integration
- β
Webhook handling with signature verification (raw-body +
stripe-signature) - β Plan tiers: Free (default), Pro, Enterprise
- β Mock sandbox mode (develop billing without Stripe keys)
- β
App Router, client-rendered (
'use client') components - β
Tenant context propagation via middleware (subdomain β
/_tenants/[tenant]rewrite) - β Automatic JWT refresh on 401 (axios interceptor with request queue)
- β Zustand auth state + TanStack Query
- β Responsive dashboard with billing settings (Tailwind CSS)
- βΉοΈ Served as a standalone Node server (
output: 'standalone'); no Server Actions and no static-site generation of dynamic content
- β Terraform AWS infrastructure (VPC, ECS Fargate, RDS, ElastiCache Redis, ALB)
- β Docker multi-stage builds (frontend + backend)
- β Docker Compose for local development
- β GitHub Actions CI (lint β test β e2e β build) for both apps
- β Health checks for app, database, Redis, and Stripe
- β CloudWatch log groups for backend and frontend
- βΉοΈ ECS runs a fixed
desired_countof 2 tasks per service (no auto-scaling policy yet); CI has no deploy stage (deploy is manual via Terraform/Docker)
- β Helmet security headers (HSTS, X-Frame-Options; CSP enabled in production)
- β
Input validation (global
ValidationPipe: whitelist + forbidNonWhitelisted + transform) - β Sanitized error responses (structured JSON, no stack traces in production)
- βΉοΈ CORS is currently permissive (the origin callback allows all origins with credentials) β tighten before production; see Roadmap
| Layer | Technology |
|---|---|
| Backend | NestJS 11, TypeScript, TypeORM |
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS |
| Database | PostgreSQL 16 with RLS |
| Cache | Redis (ioredis) |
| Auth | Passport, JWT, bcrypt |
| Billing | Stripe |
| Infrastructure | AWS (ECS Fargate, RDS, ElastiCache, ALB) |
| IaC | Terraform |
| CI | GitHub Actions |
| Containers | Docker, Docker Compose |
| Package Manager | pnpm |
- Node.js 22+
- pnpm 10+ (
corepack enable) - Docker & Docker Compose
- Git
git clone https://github.com/AhmadS7/TenantKit.git
cd TenantKit
pnpm install
cd frontend && pnpm install && cd ..Create a .env file in the repo root with at least:
NODE_ENV=development
PORT=3000
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=tenantkit
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Auth
JWT_SECRET=change_me
# Stripe (optional β omit/leave as 'change_me' to run in mock sandbox mode)
STRIPE_API_KEY=change_me
STRIPE_WEBHOOK_SECRET=change_me
# STRIPE_PRO_PRICE_ID=price_...
# STRIPE_ENT_PRICE_ID=price_...
# RLS (off in development by default; set true to exercise RLS locally β
# requires the tenantkit_app role + policies from migrations)
# RLS_ENABLED=true
# DB_TENANT_ROLE=tenantkit_appdocker compose up --buildThis starts:
- PostgreSQL on
localhost:5432 - Redis on
localhost:6379 - Backend API on
http://localhost:3000 - Frontend on
http://localhost:3001
Database migrations run automatically on startup when
NODE_ENVis notdevelopment(migrationsRuninapp.module.ts). In development the schema is created via TypeORMsynchronize.
- Frontend: http://localhost:3001
- API Docs (Swagger): http://localhost:3000/api/docs
- Health Check: http://localhost:3000/v1/health
# Unit tests
pnpm test
# E2E tests (requires a running PostgreSQL)
pnpm run test:e2e
# Coverage
pnpm run test:covThe e2e suite contains 21 test cases across 4 spec files (auth, billing, dashboard, tenant middleware) covering authentication, tenant resolution, and billing. test-db-setup.js resets the e2e test database.
cd infrastructure/terraform
terraform init
terraform plan
terraform applyThis provisions:
- VPC with public/private subnets
- ECS Fargate services (fixed 2 tasks each for frontend + backend)
- RDS PostgreSQL (private, encrypted)
- ElastiCache Redis (private)
- Application Load Balancer (path
/v1/*and/api/*β backend, else frontend) - CloudWatch log groups; secrets via AWS Secrets Manager
Before deploying: provision the
tenantkit_appPostgreSQL role with a real password (the RLS migration uses a placeholder), and pointDB_USERNAMEat it so per-requestSET LOCAL ROLEworks. WithNODE_ENV=production, RLS is enforced by default.
- Architecture Decision Records (ADRs)
- API Documentation (Swagger UI, when running)
These are scaffolded, partially implemented, or planned β not yet production-ready:
- Rate limiting β
@nestjs/throttleris a dependency but not wired (noThrottlerModule/guard registered yet) - Webhook idempotency β Stripe webhook handler does not yet deduplicate events by ID; duplicates are reprocessed
- Billing grace period β no
past_duegrace-period logic; subscription status is stored as-is from Stripe - Single-use password reset β reset JWTs are time-limited (1 h) but not invalidated after first use
- Strict CORS β origin callback currently allows all origins; restrict to known hosts before production
- Correlation-ID logging + field redaction β the logging interceptor currently logs method/URL/duration only
- ECS auto-scaling β services run a fixed task count; add
aws_appautoscaling_*policies - CloudWatch alarms β log groups exist; metric alarms are not yet defined
- CD stage β GitHub Actions runs CI only; add a deploy job
- Starter plan tier β only Free/Pro/Enterprise are wired in checkout
- Wire
TenantAwareRepositoryβ or remove it in favor of the RLS-scoped manager
Contributions are welcome. Please open an issue or pull request describing your change.
MIT β use it for personal projects, client work, or commercial products. Attribution appreciated but not required. (A LICENSE file is not yet included in the repo.)
Ahmad Shah and Kazi Efazul Karim built TenantKit to solve a recurring problem: every SaaS project starts with the same months of boilerplate for auth, billing, and tenancy.
"We built TenantKit because I was tired of rebuilding auth, billing, and tenancy for every client project." β Ahmad Shah
- Repository: https://github.com/AhmadS7/TenantKit
- Issues & Roadmap: https://github.com/AhmadS7/TenantKit/issues
Built with discipline. Shipped with confidence.
Β© 2026 Ahmad Shah