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>
This commit is contained in:
kevin-asprec
2026-03-04 18:13:27 +08:00
parent 2506a6745c
commit 7e6d286fca
6 changed files with 1100 additions and 7 deletions

View File

@@ -0,0 +1,206 @@
---
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"
---
<objective>
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.
</objective>
<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>
<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
</context>
<tasks>
<task type="auto">
<name>Task 1: Tenant signup API, tenant service, and signup UI</name>
<files>
src/lib/tenant.ts
src/app/api/tenants/signup/route.ts
src/app/(auth)/signup/page.tsx
prisma/schema.prisma
</files>
<action>
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
</action>
<verify>
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.
</verify>
<done>New ISP can sign up via /signup form, which creates tenant + admin user in a transaction. User can immediately log in after signup.</done>
</task>
<task type="auto">
<name>Task 2: Prisma tenant middleware, PostgreSQL RLS, and isolation tests</name>
<files>
src/lib/prisma-tenant.ts
prisma/migrations/
src/lib/__tests__/tenant-isolation.test.ts
</files>
<action>
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.
</action>
<verify>
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).
</verify>
<done>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).</done>
</task>
</tasks>
<verification>
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
</verification>
<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>
<output>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
</output>