--- phase: 01-foundation plan: 03 type: execute wave: 2 depends_on: ["01-01"] files_modified: - 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 autonomous: true must_haves: truths: - "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" artifacts: - path: "src/app/api/tenants/signup/route.ts" provides: "Tenant signup API endpoint" exports: ["POST"] - path: "src/app/(auth)/signup/page.tsx" provides: "Tenant signup form UI" min_lines: 50 - path: "src/lib/prisma-tenant.ts" provides: "Tenant-scoped Prisma client with automatic tenant filtering" exports: ["createTenantPrisma", "withTenantContext"] - path: "src/lib/tenant.ts" provides: "Tenant creation and management service" exports: ["createTenant"] - path: "src/lib/__tests__/tenant-isolation.test.ts" provides: "Cross-tenant data leakage tests" min_lines: 30 key_links: - from: "src/app/api/tenants/signup/route.ts" to: "src/lib/tenant.ts" via: "createTenant function call" pattern: "createTenant" - from: "src/lib/prisma-tenant.ts" to: "prisma/schema.prisma" via: "Prisma middleware injects tenantId filter" pattern: "tenantId" - from: "src/lib/__tests__/tenant-isolation.test.ts" to: "src/lib/prisma-tenant.ts" via: "Tests verify tenant scoping" pattern: "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. @C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md @.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 - 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 After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`