Files
NetForge/.planning/phases/01-foundation/01-03-PLAN.md
kevin-asprec 7e6d286fca docs(01): create phase plan
Phase 01: Foundation
- 5 plan(s) in 4 wave(s)
- 2 parallel (wave 2: auth + tenant provisioning), 3 sequential
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:13:27 +08:00

9.7 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-foundation 03 execute 2
01-01
src/app/(auth)/signup/page.tsx
src/app/api/tenants/signup/route.ts
src/lib/tenant.ts
prisma/schema.prisma
prisma/migrations/
src/lib/prisma-tenant.ts
src/lib/__tests__/tenant-isolation.test.ts
true
truths artifacts key_links
A new ISP can sign up and get their own isolated tenant space
A query by Tenant A returns zero rows from Tenant B's data
Every database query for tenant-scoped data automatically includes the tenant filter
PostgreSQL RLS policies enforce isolation even if application code is bypassed
path provides exports
src/app/api/tenants/signup/route.ts Tenant signup API endpoint
POST
path provides min_lines
src/app/(auth)/signup/page.tsx Tenant signup form UI 50
path provides exports
src/lib/prisma-tenant.ts Tenant-scoped Prisma client with automatic tenant filtering
createTenantPrisma
withTenantContext
path provides exports
src/lib/tenant.ts Tenant creation and management service
createTenant
path provides min_lines
src/lib/__tests__/tenant-isolation.test.ts Cross-tenant data leakage tests 30
from to via pattern
src/app/api/tenants/signup/route.ts src/lib/tenant.ts createTenant function call createTenant
from to via pattern
src/lib/prisma-tenant.ts prisma/schema.prisma Prisma middleware injects tenantId filter tenantId
from to via pattern
src/lib/__tests__/tenant-isolation.test.ts src/lib/prisma-tenant.ts Tests verify tenant scoping tenantId.*Tenant
Implement tenant provisioning: signup flow that creates a new ISP tenant with its admin user, Prisma middleware that automatically injects tenant filtering on all queries, and PostgreSQL Row-Level Security policies as a defense-in-depth layer. Prove zero cross-tenant data leakage with automated tests.

Purpose: Multi-tenancy is the architectural foundation. Every feature built on top must be tenant-isolated. Getting this wrong means data leaks between ISPs. Output: Working tenant signup, automatic tenant scoping on all queries, RLS policies, and isolation tests.

<execution_context> @C:\Users\KevinAsprec.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-01-SUMMARY.md @prisma/schema.prisma @src/lib/prisma.ts Task 1: Tenant signup API, tenant service, and signup UI src/lib/tenant.ts src/app/api/tenants/signup/route.ts src/app/(auth)/signup/page.tsx prisma/schema.prisma Create `src/lib/tenant.ts` with a `createTenant()` function: - Accepts: businessName, ownerFirstName, ownerLastName, ownerEmail, password, businessAddress, contactPhone - Validates: email not already used, businessName not empty, password min 8 chars - In a Prisma transaction ($transaction): 1. Create Tenant record (name=businessName, slug=slugify(businessName), status=ACTIVE, ownerEmail) 2. Create User record (email=ownerEmail, passwordHash=bcrypt(password, 12), firstName, lastName, tenantId=new tenant id, roles=[ADMIN], isActive=true) - Returns: { tenant, user } (without passwordHash) - Throws descriptive errors for validation failures
Add to prisma/schema.prisma if not present:
- businessAddress: String? on Tenant
- contactPhone: String? on Tenant

Run `npx prisma db push` after schema changes.

Create `src/app/api/tenants/signup/route.ts`:
- POST handler accepting JSON body with all signup fields
- Calls createTenant()
- Returns 201 with { tenant: { id, name, slug }, user: { id, email } }
- Returns 400 for validation errors with { error: string }
- Returns 409 if email already exists

Create `src/app/(auth)/signup/page.tsx`:
- Client component ("use client")
- Form fields: Business Name, First Name, Last Name, Email, Password, Confirm Password, Business Address (optional), Contact Phone (optional)
- Client-side validation: passwords match, email format, required fields
- On submit: POST to /api/tenants/signup
- On success: redirect to /login with a success message (use query param ?registered=true, login page shows "Account created, please sign in")
- On error: show error message
- Link to /login: "Already have an account? Sign in"
- Clean Tailwind design matching the login page style
Start the app. Navigate to /signup. Fill out the form and submit. Verify the API returns 201. Check the database has the new tenant and user. Navigate to /login and sign in with the new credentials. New ISP can sign up via /signup form, which creates tenant + admin user in a transaction. User can immediately log in after signup. Task 2: Prisma tenant middleware, PostgreSQL RLS, and isolation tests src/lib/prisma-tenant.ts prisma/migrations/ src/lib/__tests__/tenant-isolation.test.ts Create `src/lib/prisma-tenant.ts`: - Export a `withTenantContext(tenantId: string)` function that returns a Prisma client extended with middleware - The middleware intercepts all find/findMany/create/update/delete operations on tenant-scoped models - For reads (findMany, findFirst, findUnique): automatically inject `where: { tenantId }` filter - For creates: automatically set `tenantId` on the data - For updates/deletes: automatically add `tenantId` to the where clause - Maintain a list of tenant-scoped models (for now: User — more will be added in later phases). Models NOT in this list (like Tenant itself) are not filtered. - Export `TENANT_SCOPED_MODELS` constant array so it can be extended as new models are added
IMPORTANT: Use Prisma's `$extends` with query extensions (not the deprecated middleware API). This is the modern approach:
```typescript
prisma.$extends({
  query: {
    user: {
      async findMany({ args, query }) {
        args.where = { ...args.where, tenantId };
        return query(args);
      },
      // ... same for findFirst, create, update, delete, etc.
    }
  }
})
```

Create PostgreSQL RLS policies as a defense-in-depth layer. Create a raw SQL migration:
- `npx prisma migrate dev --name add-rls-policies --create-only` to create empty migration
- Add SQL to enable RLS on the User table (and any future tenant-scoped tables):
  ```sql
  ALTER TABLE "User" ENABLE ROW LEVEL SECURITY;
  CREATE POLICY tenant_isolation_user ON "User"
    USING ("tenantId" = current_setting('app.current_tenant_id', true)::text)
    WITH CHECK ("tenantId" = current_setting('app.current_tenant_id', true)::text);
  ```
- Note: The Prisma client connects as the DB owner so RLS is bypassed by default. Add a comment explaining that RLS is defense-in-depth — the Prisma middleware is the primary enforcement, RLS catches bugs.
- For RLS to actually enforce, queries would need to SET app.current_tenant_id before each request. Add a helper function `setTenantRLS(prisma, tenantId)` that runs `SET LOCAL app.current_tenant_id = 'tenantId'` inside a transaction.

Create `src/lib/__tests__/tenant-isolation.test.ts`:
- These tests require a real database connection (mark with a describe block or test tag)
- Setup: Create two tenants (Tenant A, Tenant B) with one user each
- Test 1: Query users with Tenant A context — returns only Tenant A's user, zero rows from Tenant B
- Test 2: Query users with Tenant B context — returns only Tenant B's user, zero rows from Tenant A
- Test 3: Creating a user with Tenant A context automatically sets tenantId to Tenant A
- Test 4: Attempting to read Tenant B's user by ID with Tenant A context returns null (not found)
- Teardown: Clean up test data after tests

Add a vitest setup file or test helper that provides database connection for integration tests. Use a test-specific database or transaction rollback pattern to keep tests isolated.
Run `npx prisma migrate dev` — migration applies successfully. Run `npx vitest run` — all tenant isolation tests pass. Specifically verify Test 1 and Test 2 return exactly the right number of rows (1 each, 0 cross-tenant). Prisma tenant middleware automatically filters all queries by tenantId. PostgreSQL RLS policies exist as defense-in-depth. Automated tests prove zero cross-tenant data leakage (TENANT-01). 1. /signup form creates a new tenant and admin user 2. New user can log in immediately after signup 3. Prisma middleware automatically scopes queries to the current tenant 4. Tenant isolation tests pass — zero cross-tenant data leakage 5. RLS policies are applied to the User table in PostgreSQL 6. `npx vitest run` passes all tests including isolation tests

<success_criteria>

  • New ISP can sign up and onboard (TENANT-02)
  • Data is fully isolated per tenant at application layer via Prisma middleware
  • PostgreSQL RLS policies provide defense-in-depth isolation (TENANT-01)
  • Automated test proves cross-tenant query returns zero rows
  • One email per tenant constraint is enforced </success_criteria>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`