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:
206
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
206
.planning/phases/01-foundation/01-03-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user