Files
NetForge/.planning/phases/01-foundation/01-03-SUMMARY.md
kevin-asprec dc85d348ef docs(01-03): complete tenant provisioning and isolation plan
Tasks completed: 2/2
- Task 1: Tenant signup API, tenant service, and signup UI
- Task 2: Prisma tenant middleware, PostgreSQL RLS, and isolation tests

SUMMARY: .planning/phases/01-foundation/01-03-SUMMARY.md
2026-03-04 18:47:01 +08:00

8.7 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established duration completed
01-foundation 03 database
prisma
postgresql
multi-tenancy
rls
bcrypt
next.js
vitest
phase provides
01-01 Prisma schema with Tenant/User models, singleton PrismaClient, Docker Compose dev environment
Tenant provisioning via POST /api/tenants/signup (creates Tenant + admin User in a transaction)
Signup UI at /signup with full form validation and redirect to /login?registered=true
createTenant() service with bcrypt password hashing and unique slug generation
withTenantContext(tenantId) / createTenantPrisma() — Prisma $extends client for automatic tenant filtering
setTenantRLS() helper for explicit PostgreSQL RLS enforcement in transactions
TENANT_SCOPED_MODELS constant for future extensibility
PostgreSQL RLS policies on User table (defense-in-depth isolation)
Baseline Prisma migration + RLS migration applied to dev database
6 tenant isolation tests proving zero cross-tenant data leakage
01-04 (plan templates / service plans) — all new models must use withTenantContext()
01-05 (dashboard) — tenant context must be threaded from session into prisma-tenant
All future phases — TENANT_SCOPED_MODELS must be extended as new models are added
added patterns
Prisma $extends query extensions for automatic tenantId injection (not deprecated middleware API)
withTenantContext(tenantId) wraps prisma singleton — one per request, not one per app
RLS as defense-in-depth: Prisma middleware is primary, RLS catches application bugs
Prisma migrate baseline approach for DB first created via db push
EmailAlreadyExistsError custom class for typed 409 vs 400 discrimination in API routes
created modified
src/lib/tenant.ts
src/app/api/tenants/signup/route.ts
src/app/(auth)/signup/page.tsx
src/lib/prisma-tenant.ts
src/lib/__tests__/tenant-isolation.test.ts
prisma/migrations/20260304104214_initial_schema/migration.sql
prisma/migrations/20260304104245_add_rls_policies/migration.sql
prisma/schema.prisma (added businessAddress, contactPhone to Tenant)
src/app/(auth)/login/page.tsx (added ?registered=true success banner)
withTenantContext creates a new $extends instance per call — this is intentional; Prisma $extends is lightweight and request-scoped context is correct
findUnique cross-tenant protection routes through findFirst internally — Prisma unique key requires exact match so tenantId cannot be appended naively
RLS USING clause allows null app.current_tenant_id (no context = super-admin mode) — prevents lockout during migrations or admin queries
Initial schema migration baselined (marked applied without running) because schema was created via db push in 01-01
emailInUse check uses findFirst across all non-super-admin users — prevents same email signing up as owner of multiple tenants
Tenant filtering pattern: import withTenantContext, call with session.user.tenantId, use returned client for all scoped queries
New tenant-scoped models: add model name to TENANT_SCOPED_MODELS + add model block in $extends in prisma-tenant.ts
Signup → login flow: POST /api/tenants/signup returns 201, client redirects to /login?registered=true
9min 2026-03-04

Phase 1 Plan 3: Tenant Provisioning and Isolation Summary

Tenant signup flow (createTenant + /api/tenants/signup + /signup UI) with Prisma $extends tenant middleware auto-filtering all queries and PostgreSQL RLS policies as defense-in-depth, proven by 6 passing isolation tests.

Performance

  • Duration: 9 min
  • Started: 2026-03-04T10:36:27Z
  • Completed: 2026-03-04T10:45:22Z
  • Tasks: 2 completed
  • Files modified: 9

Accomplishments

  • New ISP tenant signup: POST /api/tenants/signup creates Tenant + admin User atomically via Prisma transaction with bcrypt(12) password hashing and unique slug generation
  • Prisma middleware via $extends intercepts all 10 query operations (findMany, findFirst, findUnique, create, createMany, update, updateMany, delete, deleteMany, upsert, count, aggregate, groupBy) on tenant-scoped models and automatically injects tenantId
  • PostgreSQL RLS policies on User table enabled and applied via migration; setTenantRLS() helper for explicit session-level enforcement
  • 6 isolation tests prove zero cross-tenant data leakage: Tenant A queries return zero rows from Tenant B and vice versa; findUnique with wrong tenant returns null

Task Commits

Each task was committed atomically:

  1. Task 1: Tenant signup API, tenant service, and signup UI - 43761d9 (feat)
  2. Task 2: Prisma tenant middleware, PostgreSQL RLS, and isolation tests - 69eac9f (feat)

Files Created/Modified

  • src/lib/tenant.ts - createTenant() service: validation, slug generation, bcrypt, Prisma transaction
  • src/app/api/tenants/signup/route.ts - POST /api/tenants/signup: 201/400/409/500 responses
  • src/app/(auth)/signup/page.tsx - Signup form UI with client-side validation, Tailwind styling
  • src/lib/prisma-tenant.ts - withTenantContext(), createTenantPrisma(), setTenantRLS(), TENANT_SCOPED_MODELS
  • src/lib/__tests__/tenant-isolation.test.ts - 6 integration tests proving isolation
  • prisma/migrations/20260304104214_initial_schema/migration.sql - Baseline migration
  • prisma/migrations/20260304104245_add_rls_policies/migration.sql - RLS policies on User table
  • prisma/schema.prisma - Added businessAddress, contactPhone to Tenant model
  • src/app/(auth)/login/page.tsx - Added registered=true success banner

Decisions Made

  • Used Prisma $extends with query extensions (not deprecated $use middleware API) for forward compatibility with Prisma 6+
  • findUnique cross-tenant protection implemented by routing through findFirst internally, since Prisma requires exact unique key match which cannot have tenantId appended without changing the where shape
  • RLS USING clause permits null app.current_tenant_id (no context set = super-admin mode) to prevent migration lockout
  • Initial migration baselined with prisma migrate resolve --applied since 01-01 used db push to initialize schema
  • Email uniqueness check before tenant creation queries all non-super-admin users to prevent same email owning multiple tenants

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Fixed TypeScript error in Prisma $extends create/upsert handlers

  • Found during: Task 2 (TypeScript compile check after creating prisma-tenant.ts)
  • Issue: Spreading args.data with { ...args.data, tenantId } included the nested tenant relation object, which is mutually exclusive with tenantId in Prisma's UncheckedCreateInput type
  • Fix: Destructure out the tenant key before spreading: const { tenant: _t, ...rest } = args.data then { ...rest, tenantId }
  • Files modified: src/lib/prisma-tenant.ts
  • Verification: npx tsc --noEmit exits clean, all 6 isolation tests still pass
  • Committed in: 69eac9f (Task 2 commit)

2. [Rule 3 - Blocking] Baselined initial migration before creating RLS migration

  • Found during: Task 2 (running prisma migrate dev --name add-rls-policies --create-only)
  • Issue: Prisma detected schema drift — database was created via db push (01-01) so migration history was empty, causing Prisma to require a reset
  • Fix: Manually created the initial schema migration SQL file and used prisma migrate resolve --applied to baseline it, then ran the RLS migration creation cleanly
  • Files modified: prisma/migrations/20260304104214_initial_schema/migration.sql, prisma/migrations/migration_lock.toml
  • Verification: prisma migrate deploy applied only the RLS migration successfully
  • Committed in: 69eac9f (Task 2 commit)

Total deviations: 2 auto-fixed (1 bug, 1 blocking) Impact on plan: Both auto-fixes essential for type safety and migration system correctness. No scope creep.

Issues Encountered

None beyond the auto-fixed deviations above.

User Setup Required

None - no external service configuration required. Database runs in Docker Compose.

Next Phase Readiness

  • Tenant provisioning is complete and tested; 01-04 and beyond can assume tenants exist
  • All new tenant-scoped Prisma models must be added to TENANT_SCOPED_MODELS in src/lib/prisma-tenant.ts AND a query block added to the $extends in withTenantContext()
  • Thread tenant context through session: withTenantContext(session.user.tenantId) in Server Components and route handlers
  • Plan 01-02 (auth module) runs in parallel — integration between the two is session.user.tenantId flowing into withTenantContext()

Phase: 01-foundation Completed: 2026-03-04