Monorepo for the multi-tenant SaaS commerce platform: a NestJS API, a tenant (store owner)
dashboard, a super admin console, and a public SEO-first customer storefront. One root git repo,
one pnpm install, Turborepo for task orchestration.
- Architected a schema-per-tenant SaaS commerce platform using NestJS, TypeORM, and PostgreSQL, providing isolated
tenant_<slug>schemas with tenant resolution via headers and subdomains. - Built a tenant-aware DataSource manager with connection pooling, LRU caching, in-flight request deduplication, and automatic connection teardown to support scalable multi-tenant database access.
- Designed a RBAC system with granular permissions, hierarchical roles, super-admin privileges, and direct per-user permissions, with rank-based controls to prevent privilege escalation.
- Implemented stateless JWT authentication with separate access/refresh secrets, refresh-token rotation, JTI revocation, and tenant-bound claims to prevent cross-tenant token reuse.
- Automated the tenant lifecycle, including schema provisioning, RBAC/category seeding, boot-time tenant reseeding, and super-admin activation/deactivation with automatic DataSource and connection cleanup.
- Built platform-level SaaS subscription plans: seeded Free/Starter/Growth/Pro/Enterprise tiers with plan limits (storage, database size, store admins, monthly coupons) and feature entitlements, Super-Admin plan assignment, tenant read-only usage APIs, and server-side enforcement integrated into user/coupon/product/order flows.
- Integrated Stripe payment processing with tenant-scoped payment workflows, payment status tracking, transaction handling, and controlled customer refunds through the Tenant Admin dashboard.
- Implemented tenant-scoped Cloudflare R2 object storage with per-tenant quotas, MIME/size validation, multi-file upload rollback, and PostgreSQL-based storage usage accounting for live capacity monitoring.
- Built a Super Admin console using React, TypeScript, Refine, React Router, shadcn/ui, and Tailwind CSS, supporting tenant provisioning, lifecycle management, RBAC inspection, and storage telemetry.
- Built the Tenant Admin dashboard with server-side pagination, sorting, category management, ordered product images, permission-aware staff administration, and authorized customer refund management.
- Built a public, SEO-first storefront with Next.js App Router server components, subdomain tenant resolution, ISR-cached catalog reads, JSON-LD/sitemap/robots, a cookie-backed customer session proxied through route handlers, and a guest cart that merges into the server cart on sign-in.
| Path | Package | Stack | Dev port |
|---|---|---|---|
apps/api |
saas_store_api |
NestJS 10 + TypeORM 0.3 + Postgres | 4000 |
apps/store_owner_dashboard |
tenant_dash |
Refine v5 + Vite 6 + React 19 | 5174 |
apps/super_admin_dashboard |
super_admin_dash |
Refine v5 + Vite 6 + React 19 | 5173 |
apps/tenant_store |
tenant_store |
Next.js 15 App Router + React 19 | 3000 |
packages/* is declared as a workspace glob but is intentionally empty for now.
- Node.js
>=20(the API image uses Node 26; the dashboards are built on Node 20-compatible Vite; the storefront runs Next.js 15 on Node 20+). - pnpm
12.4.1(corepack enablehonors the rootpackageManagerfield). - Docker + Docker Compose for Postgres (
compose.yamlat the root).
Run from the repository root:
pnpm installA single root pnpm-lock.yaml covers all four apps. Do not run installs from apps/*, and do
not reintroduce a nested pnpm-workspace.yaml or lockfile.
pnpm dev # turbo run dev — API + both SPAs + storefront in parallel
pnpm build # turbo run build
pnpm lint # turbo run lint
pnpm typecheck # turbo run typecheck
pnpm test # turbo run test (super_admin_dash and tenant_store have no test task and are skipped)
pnpm clean # remove dist/, build/, coverage/, .turbo/, node_modules/ from all appsWhen both dashboards run at the same time, Refine Devtools binds port 5001 for whichever starts
first; the second one logs a non-fatal "port 5001 already in use" and keeps serving. Give each its
own port with REFINE_DEVTOOLS_PORT if the devtools UI matters. The SPA servers themselves use
5174 and 5173.
Target one app with a pnpm filter (package names are unchanged from the source repos):
pnpm --filter saas_store_api build
pnpm --filter tenant_dash test
pnpm --filter super_admin_dash build
pnpm --filter tenant_store build
turbo run build --filter saas_store_api...pnpm --filter saas_store_api dev # nest start --watch (alias: start:dev)
pnpm --filter saas_store_api build # nest build
pnpm --filter saas_store_api lint # eslint --fix over src/test
pnpm --filter saas_store_api typecheck # tsc --noEmit
pnpm --filter saas_store_api test # jest unit specs
pnpm --filter saas_store_api test:e2e # needs a live PostgresIntegration verify scripts and the dev-DB reset are manual and need a live Postgres plus a real
apps/api/.env; they are documented in apps/api/AGENTS.md.
pnpm --filter tenant_dash dev # refine dev on http://localhost:5174
pnpm --filter tenant_dash build
pnpm --filter tenant_dash lint
pnpm --filter tenant_dash typecheck
pnpm --filter tenant_dash test # Vitestpnpm --filter super_admin_dash dev # refine dev on http://localhost:5173
pnpm --filter super_admin_dash build
pnpm --filter super_admin_dash lint
pnpm --filter super_admin_dash typecheckThere is no test task for the super admin console yet.
pnpm --filter tenant_store dev # next dev on http://localhost:3000
pnpm --filter tenant_store build # next build --turbopack
pnpm --filter tenant_store start # next start on port 3000
pnpm --filter tenant_store lint
pnpm --filter tenant_store typecheckThe storefront is tenant-scoped by subdomain: browse http://<tenant-slug>.localhost:3000. The
bare http://localhost:3000 renders a neutral "store not found" page. Copy
apps/tenant_store/.env.example to .env and set R2_PUBLIC_URL so product images load.
There is no test task.
compose.yaml keeps the original Compose project name saas_store, so the container
(saas_store_db) and the saas_store_postgres_data volume are the same ones used before the
merge — existing tenant schemas are preserved.
docker compose up -d postgresCompose reads only the DB-related variables from the root .env (see .env.example):
DB_NAME, DB_USERNAME, DB_PASSWORD, POSTGRES_HOST_PORT.
Per-app .env files stay per-app and are gitignored; only .env.example is committed.
apps/api/.env— full API config (Joi-validated at boot).apps/store_owner_dashboard/.env—VITE_API_URL.apps/super_admin_dashboard/.env—VITE_API_URL.apps/tenant_store/.env—API_URL,APP_ROOT_DOMAIN,R2_PUBLIC_URL, optionalNEXT_PUBLIC_*.- root
.env— Compose/Postgres only.
All images build from the repository root. The four Dockerfiles are production images
(Railway-ready); the API's watch-mode image lives in Dockerfile.dev:
docker build -f apps/api/Dockerfile .
docker build -f apps/store_owner_dashboard/Dockerfile .
docker build -f apps/super_admin_dashboard/Dockerfile .
docker build -f apps/tenant_store/Dockerfile .
# Development API image (watch mode, used by the commented compose service):
docker build -f apps/api/Dockerfile.dev .See RAILWAY.md for deploying the four services plus a Postgres plugin to Railway.
The original repositories are frozen archives — do not edit them further:
C:\works\saas_store→apps/api(git history imported;mainand thepre-cleanuptag preserved)C:\works\dash\tenant_dash→apps/store_owner_dashboard(git history imported)C:\works\dash\super_admin_dash→apps/super_admin_dashboard(source repo had zero commits; imported as a plain copy)