diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 8fa6f6a..e871eaf 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -30,14 +30,14 @@ Decimal phases appear between their surrounding integers in numeric order.
3. A user with the Technician role cannot access billing or subscriber management routes — a 403 is returned at the API layer, not just hidden in the UI
4. A new ISP tenant can be created; a query issued by Tenant A returns zero rows from Tenant B's data — verified by automated test
5. Platform super-admin can log in on a separate auth context and view all tenants without being scoped to any single tenant
-**Plans**: TBD
+**Plans**: 5 plans
Plans:
-- [ ] 01-01: Docker environment, PostgreSQL with RLS, Redis, project scaffolding (Next.js 15 + TypeScript + Prisma)
-- [ ] 01-02: Auth module — Auth.js credential login, JWT with tenant_id + role, session persistence, logout
-- [ ] 01-03: Tenant provisioning — signup flow, tenant resolver, Prisma middleware tenant injection, PostgreSQL RLS policies
-- [ ] 01-04: RBAC with CASL — permission matrix for all five roles, API-layer enforcement, unauthorized access tests
-- [ ] 01-05: Super-admin auth context — separate login, tenant management UI (create, suspend, view), unit test harness for core business logic
+- [ ] 01-01-PLAN.md — Docker environment, PostgreSQL with RLS, Redis, project scaffolding (Next.js 15 + TypeScript + Prisma)
+- [ ] 01-02-PLAN.md — Auth module: NextAuth credentials login, JWT with tenant_id + roles, session persistence, logout
+- [ ] 01-03-PLAN.md — Tenant provisioning: signup flow, Prisma tenant middleware, PostgreSQL RLS policies, isolation tests
+- [ ] 01-04-PLAN.md — RBAC with CASL: permission matrix for all five roles, API-layer enforcement, unauthorized access tests
+- [ ] 01-05-PLAN.md — Super-admin panel: tenant management UI (list, suspend, activate), comprehensive test harness
---
@@ -132,7 +132,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Foundation | 0/5 | Not started | - |
+| 1. Foundation | 0/5 | Planned | - |
| 2. Subscriber and Billing Core | 0/5 | Not started | - |
| 3. Operational Modules | 0/5 | Not started | - |
| 4. Inventory, Expenses, and Financial Reports | 0/5 | Not started | - |
diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md
new file mode 100644
index 0000000..10b0092
--- /dev/null
+++ b/.planning/phases/01-foundation/01-01-PLAN.md
@@ -0,0 +1,218 @@
+---
+phase: 01-foundation
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - docker-compose.yml
+ - Dockerfile
+ - .dockerignore
+ - .env.example
+ - .env
+ - package.json
+ - tsconfig.json
+ - next.config.ts
+ - tailwind.config.ts
+ - postcss.config.mjs
+ - prisma/schema.prisma
+ - src/app/layout.tsx
+ - src/app/page.tsx
+ - src/app/globals.css
+ - src/lib/prisma.ts
+ - vitest.config.ts
+ - src/lib/__tests__/setup.test.ts
+autonomous: true
+
+must_haves:
+ truths:
+ - "Running docker compose up starts PostgreSQL 16, Redis 7, and the Next.js dev server with zero manual steps"
+ - "Prisma can connect to the Dockerized PostgreSQL and run migrations"
+ - "vitest runs and passes at least one smoke test"
+ - "The Next.js app renders at localhost:3000"
+ artifacts:
+ - path: "docker-compose.yml"
+ provides: "PostgreSQL, Redis, and app service definitions"
+ contains: "postgres"
+ - path: "prisma/schema.prisma"
+ provides: "Base Tenant and User models with RLS-ready tenant_id"
+ contains: "model Tenant"
+ - path: "src/lib/prisma.ts"
+ provides: "Singleton Prisma client"
+ exports: ["prisma"]
+ - path: "vitest.config.ts"
+ provides: "Test runner configuration"
+ contains: "vitest"
+ key_links:
+ - from: "docker-compose.yml"
+ to: "prisma/schema.prisma"
+ via: "DATABASE_URL env var"
+ pattern: "DATABASE_URL"
+ - from: "src/lib/prisma.ts"
+ to: "prisma/schema.prisma"
+ via: "PrismaClient import"
+ pattern: "PrismaClient"
+---
+
+
+Scaffold the entire NetForge project from scratch: Next.js 15 with App Router, TypeScript, Tailwind CSS, Prisma ORM with PostgreSQL, Redis, Docker Compose for local dev, and Vitest for testing.
+
+Purpose: Every subsequent plan depends on this foundation existing. Nothing can be built without the project scaffold, database connection, and dev environment.
+Output: A running dockerized development environment with base schema and test infrastructure.
+
+
+
+@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/STATE.md
+@.planning/phases/01-foundation/01-CONTEXT.md
+
+
+
+
+
+ Task 1: Create Next.js project with Docker Compose dev environment
+
+ docker-compose.yml
+ Dockerfile
+ .dockerignore
+ .env.example
+ .env
+ package.json
+ tsconfig.json
+ next.config.ts
+ tailwind.config.ts
+ postcss.config.mjs
+ src/app/layout.tsx
+ src/app/page.tsx
+ src/app/globals.css
+
+
+ Initialize a Next.js 15 project with App Router, TypeScript, and Tailwind CSS using `npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"` (answer yes to all prompts or use flags to avoid interactive mode).
+
+ Create `docker-compose.yml` with three services:
+ 1. **db**: PostgreSQL 16 image, port 5432, volume for data persistence, POSTGRES_USER=netforge, POSTGRES_PASSWORD=netforge_dev, POSTGRES_DB=netforge_dev
+ 2. **redis**: Redis 7 image, port 6379
+ 3. **app**: Builds from Dockerfile, port 3000, depends_on db and redis, mounts source code as volume for hot reload, environment variables from .env
+
+ Create a `Dockerfile` for development (Node 20 alpine, installs deps, runs `npm run dev`).
+
+ Create `.env.example` and `.env` with:
+ - DATABASE_URL=postgresql://netforge:netforge_dev@db:5432/netforge_dev
+ - REDIS_URL=redis://redis:6379
+ - NEXTAUTH_SECRET=dev-secret-change-in-production
+ - NEXTAUTH_URL=http://localhost:3000
+
+ Create `.dockerignore` excluding node_modules, .next, .git.
+
+ Update `src/app/page.tsx` to render a simple "NetForge" heading so we can verify the app loads.
+
+ IMPORTANT: Also create a local DATABASE_URL variant in .env for running Prisma commands from the host (outside Docker):
+ - DATABASE_URL_LOCAL=postgresql://netforge:netforge_dev@localhost:5432/netforge_dev
+ Add a note in .env.example explaining when to use each.
+
+
+ Run `docker compose up -d` and verify all three services start. Run `docker compose ps` to confirm all services are healthy/running. Visit http://localhost:3000 and confirm the page loads.
+
+ All three Docker services (db, redis, app) start successfully. Next.js app serves at localhost:3000.
+
+
+
+ Task 2: Prisma schema with base models and Vitest setup
+
+ prisma/schema.prisma
+ src/lib/prisma.ts
+ vitest.config.ts
+ src/lib/__tests__/setup.test.ts
+ package.json
+
+
+ Install Prisma: `npm install prisma @prisma/client` and `npm install -D vitest @vitejs/plugin-react`.
+
+ Create `prisma/schema.prisma` with PostgreSQL provider and the following base models:
+
+ **Tenant** model:
+ - id: String @id @default(uuid())
+ - name: String (business name)
+ - slug: String @unique (URL-friendly identifier, auto-generated from name)
+ - ownerEmail: String
+ - status: TenantStatus enum (ACTIVE, SUSPENDED, PENDING_SUSPENSION)
+ - suspendedAt: DateTime? (when suspension was triggered)
+ - gracePeriodEndsAt: DateTime? (when service actually stops — 7 day grace period)
+ - createdAt: DateTime @default(now())
+ - updatedAt: DateTime @updatedAt
+ - users: User[]
+
+ **User** model:
+ - id: String @id @default(uuid())
+ - email: String
+ - passwordHash: String
+ - firstName: String
+ - lastName: String
+ - tenantId: String? (nullable for super-admins who are not tenant-scoped)
+ - tenant: Tenant? @relation
+ - roles: Role[] (array of enum values — multi-role support)
+ - isActive: Boolean @default(true)
+ - isSuperAdmin: Boolean @default(false)
+ - createdAt: DateTime @default(now())
+ - updatedAt: DateTime @updatedAt
+ - @@unique([email, tenantId]) — email unique within tenant
+ - @@index([tenantId])
+
+ **Role** enum: ADMIN, OFFICE_STAFF, COLLECTOR, TECHNICIAN, CLIENT
+
+ **TenantStatus** enum: ACTIVE, SUSPENDED, PENDING_SUSPENSION
+
+ IMPORTANT: Add a comment block at the top of schema.prisma noting that tenant_id is included on all tenant-scoped models for RLS. Future models (Subscriber, Invoice, etc.) MUST include tenantId.
+
+ Create `src/lib/prisma.ts` — singleton PrismaClient pattern (check for existing instance on globalThis in dev to avoid hot-reload connection exhaustion).
+
+ Create `vitest.config.ts` with:
+ - resolve alias for @/* pointing to src/*
+ - test.globals: true
+ - test.environment: 'node'
+
+ Create `src/lib/__tests__/setup.test.ts` — a smoke test that imports prisma client and asserts it's defined (does NOT need DB connection, just verifies the import chain works).
+
+ Run `npx prisma generate` to generate the Prisma client.
+ Run `npx prisma db push` to sync the schema to the Dockerized PostgreSQL (use DATABASE_URL_LOCAL if running from host, or exec into app container).
+
+ Add to package.json scripts:
+ - "test": "vitest run"
+ - "test:watch": "vitest"
+ - "db:push": "prisma db push"
+ - "db:generate": "prisma generate"
+ - "db:studio": "prisma studio"
+
+
+ Run `npx vitest run` — the smoke test passes. Run `npx prisma db push` — schema syncs without errors. Run `npx prisma studio` briefly to confirm Tenant and User tables exist in the database.
+
+ Prisma schema with Tenant and User models is synced to PostgreSQL. Vitest runs and passes the smoke test. PrismaClient singleton is importable from @/lib/prisma.
+
+
+
+
+
+1. `docker compose up -d` starts all services without errors
+2. `docker compose ps` shows db, redis, and app running
+3. http://localhost:3000 renders the Next.js app
+4. `npx prisma db push` succeeds (schema matches DB)
+5. `npx vitest run` passes all tests
+6. `npx prisma studio` shows Tenant and User tables
+
+
+
+- Docker Compose starts PostgreSQL 16, Redis 7, and Next.js 15 dev server with one command
+- Prisma schema has Tenant and User models with RLS-ready tenantId field
+- Vitest is configured and runs at least one passing test
+- All environment variables are documented in .env.example
+
+
+
diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md
new file mode 100644
index 0000000..e8c6c04
--- /dev/null
+++ b/.planning/phases/01-foundation/01-02-PLAN.md
@@ -0,0 +1,197 @@
+---
+phase: 01-foundation
+plan: 02
+type: execute
+wave: 2
+depends_on: ["01-01"]
+files_modified:
+ - package.json
+ - src/lib/auth.ts
+ - src/lib/auth-options.ts
+ - src/app/api/auth/[...nextauth]/route.ts
+ - src/app/(auth)/login/page.tsx
+ - src/app/(auth)/layout.tsx
+ - src/middleware.ts
+ - src/types/next-auth.d.ts
+ - src/lib/__tests__/auth.test.ts
+autonomous: true
+
+must_haves:
+ truths:
+ - "A user can log in with email and password and receive a valid session"
+ - "A logged-in user stays logged in after browser refresh"
+ - "Logging out ends the session and redirects to login"
+ - "The JWT contains tenantId and roles for downstream use"
+ - "Unauthenticated users are redirected to the login page"
+ artifacts:
+ - path: "src/lib/auth-options.ts"
+ provides: "NextAuth configuration with credentials provider"
+ exports: ["authOptions"]
+ - path: "src/app/api/auth/[...nextauth]/route.ts"
+ provides: "NextAuth API route handler"
+ exports: ["GET", "POST"]
+ - path: "src/app/(auth)/login/page.tsx"
+ provides: "Login form UI"
+ min_lines: 40
+ - path: "src/middleware.ts"
+ provides: "Route protection middleware"
+ contains: "middleware"
+ - path: "src/types/next-auth.d.ts"
+ provides: "Extended session/JWT types with tenantId and roles"
+ contains: "tenantId"
+ key_links:
+ - from: "src/lib/auth-options.ts"
+ to: "prisma.user"
+ via: "credential validation query"
+ pattern: "prisma\\.user\\.findFirst"
+ - from: "src/lib/auth-options.ts"
+ to: "src/types/next-auth.d.ts"
+ via: "JWT callback populates tenantId and roles"
+ pattern: "token\\.tenantId"
+ - from: "src/middleware.ts"
+ to: "src/lib/auth-options.ts"
+ via: "getToken checks authentication"
+ pattern: "getToken|withAuth"
+---
+
+
+Implement email/password authentication using NextAuth.js (Auth.js) with the credentials provider, JWT sessions carrying tenantId and roles, login/logout UI, and route protection middleware.
+
+Purpose: Every protected feature in the app depends on knowing WHO the user is and WHICH tenant they belong to. Auth is the gateway.
+Output: Working login/logout flow with JWT-based sessions that persist across refresh.
+
+
+
+@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
+
+
+
+
+
+ Task 1: NextAuth.js configuration with credentials provider and JWT
+
+ package.json
+ src/lib/auth-options.ts
+ src/lib/auth.ts
+ src/app/api/auth/[...nextauth]/route.ts
+ src/types/next-auth.d.ts
+ src/middleware.ts
+
+
+ Install dependencies: `npm install next-auth@4 bcryptjs` and `npm install -D @types/bcryptjs`.
+
+ Use NextAuth.js v4 (not v5 — v5/Auth.js is still beta-ish for credentials provider with custom JWT). This is a deliberate choice for stability.
+
+ Create `src/types/next-auth.d.ts` to extend NextAuth types:
+ - Session.user includes: id, email, tenantId (string | null), roles (Role[]), isSuperAdmin (boolean), firstName, lastName
+ - JWT includes same fields
+
+ Create `src/lib/auth-options.ts` exporting `authOptions: NextAuthOptions`:
+ - CredentialsProvider with email and password fields
+ - In authorize(): look up user by email using prisma.user.findFirst where email matches AND (tenantId matches an ACTIVE tenant OR user.isSuperAdmin is true). Use bcryptjs.compare for password verification.
+ - If user found and password valid, return user object with id, email, tenantId, roles, isSuperAdmin, firstName, lastName
+ - If not found or password invalid, return null (NextAuth shows error)
+ - JWT callback: persist tenantId, roles, isSuperAdmin, firstName, lastName into token
+ - Session callback: expose tenantId, roles, isSuperAdmin, firstName, lastName on session.user
+ - Session strategy: "jwt"
+ - JWT maxAge: 24 hours (session duration)
+ - Pages: signIn: "/login"
+
+ Create `src/lib/auth.ts` exporting a helper `getServerSession()` wrapper and a `getCurrentUser()` helper that returns the typed session user or null.
+
+ Create `src/app/api/auth/[...nextauth]/route.ts` — standard NextAuth route handler exporting GET and POST.
+
+ Create `src/middleware.ts`:
+ - Use NextAuth's `withAuth` or `getToken` to check for valid JWT
+ - Protect all routes EXCEPT: /login, /signup, /api/auth/*, /_next/*, /favicon.ico, static assets
+ - If no valid token, redirect to /login
+ - If valid token, allow request to continue
+
+
+ Build check: `npx next build` completes without type errors. The auth route is accessible at /api/auth/providers (returns JSON with credentials provider listed).
+
+ NextAuth configured with credentials provider. JWT contains tenantId and roles. Middleware redirects unauthenticated users to /login. TypeScript types are extended for session.
+
+
+
+ Task 2: Login page UI, logout flow, and seed script for testing
+
+ src/app/(auth)/login/page.tsx
+ src/app/(auth)/layout.tsx
+ prisma/seed.ts
+ package.json
+ src/lib/__tests__/auth.test.ts
+
+
+ Create `src/app/(auth)/layout.tsx` — a centered layout for auth pages (no sidebar/nav, just centered card on neutral background). Use Tailwind.
+
+ Create `src/app/(auth)/login/page.tsx`:
+ - Client component ("use client")
+ - Form with email and password fields, submit button
+ - Uses `signIn("credentials", { email, password, redirect: false })` from next-auth/react
+ - On success: redirect to /dashboard (or / for now)
+ - On error: show error message below form (red text, "Invalid email or password")
+ - Loading state on submit button
+ - Clean, minimal design with Tailwind — no heavy UI library needed
+ - Page title: "Sign in to NetForge"
+ - Include a "Sign up" link pointing to /signup (page will be created in plan 01-03)
+
+ Add logout functionality:
+ - Create a simple header component at `src/components/layout/header.tsx` that shows the user's name and a "Sign out" button
+ - Sign out button calls `signOut({ callbackUrl: "/login" })` from next-auth/react
+ - Include this header in the main app layout (not the auth layout)
+ - Create `src/app/(dashboard)/layout.tsx` as the authenticated layout that includes the header
+
+ Create `prisma/seed.ts`:
+ - Creates a test tenant: "Demo ISP", status ACTIVE
+ - Creates an admin user: admin@demo.com / password "admin123" (bcrypt hashed), role ADMIN, linked to demo tenant
+ - Creates a super-admin user: superadmin@netforge.com / password "super123" (bcrypt hashed), isSuperAdmin true, no tenantId
+ - Uses upsert to be idempotent
+ - Add "prisma.seed" config to package.json pointing to "ts-node prisma/seed.ts" (or use tsx: `npx tsx prisma/seed.ts`)
+ - Add script: "db:seed": "npx tsx prisma/seed.ts"
+
+ Create `src/lib/__tests__/auth.test.ts`:
+ - Test that authOptions has credentials provider configured
+ - Test that JWT callback adds tenantId and roles to token (mock the callback)
+ - Test that session callback exposes tenantId and roles on session
+ - These are unit tests of the auth config, NOT integration tests requiring a running server
+
+ Run the seed script to populate test data.
+
+
+ Run `npm run db:seed` — creates test users without errors. Run `npx vitest run` — auth tests pass. Start the app, navigate to /login, log in with admin@demo.com / admin123 — redirects to dashboard. Click sign out — redirects to /login. Refresh after login — session persists.
+
+ Login page renders with email/password form. Seed data creates demo tenant + admin + super-admin. Login with valid credentials works and session persists across refresh. Logout redirects to login. Auth unit tests pass.
+
+
+
+
+
+1. /login page renders a clean login form
+2. Login with admin@demo.com / admin123 succeeds and redirects to authenticated area
+3. Session persists after browser refresh (no re-login required)
+4. Sign out button ends session and redirects to /login
+5. Visiting a protected route while logged out redirects to /login
+6. `npx vitest run` passes all auth tests
+
+
+
+- Email/password authentication works end-to-end (AUTH-01)
+- JWT contains tenantId and roles for downstream RBAC and tenant scoping
+- Session persists across browser refresh (AUTH-04)
+- Unauthenticated users cannot access protected routes
+- Seed data provides test accounts for development
+
+
+
diff --git a/.planning/phases/01-foundation/01-03-PLAN.md b/.planning/phases/01-foundation/01-03-PLAN.md
new file mode 100644
index 0000000..4931a7f
--- /dev/null
+++ b/.planning/phases/01-foundation/01-03-PLAN.md
@@ -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"
+---
+
+
+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
+
+
+
diff --git a/.planning/phases/01-foundation/01-04-PLAN.md b/.planning/phases/01-foundation/01-04-PLAN.md
new file mode 100644
index 0000000..5c8f232
--- /dev/null
+++ b/.planning/phases/01-foundation/01-04-PLAN.md
@@ -0,0 +1,250 @@
+---
+phase: 01-foundation
+plan: 04
+type: execute
+wave: 3
+depends_on: ["01-02", "01-03"]
+files_modified:
+ - package.json
+ - src/lib/casl/ability.ts
+ - src/lib/casl/permissions.ts
+ - src/lib/casl/types.ts
+ - src/lib/middleware/authorize.ts
+ - src/app/api/test-rbac/route.ts
+ - src/lib/__tests__/rbac.test.ts
+autonomous: true
+
+must_haves:
+ truths:
+ - "A Technician cannot access billing or subscriber management routes and receives a 403"
+ - "A Collector can view subscribers but cannot modify subscribers outside their zone"
+ - "A Client can only see their own account data"
+ - "An Admin has full access within their tenant"
+ - "A user with multiple roles gets the union of all permissions"
+ artifacts:
+ - path: "src/lib/casl/ability.ts"
+ provides: "CASL ability factory that builds permissions from user roles"
+ exports: ["defineAbilityFor"]
+ - path: "src/lib/casl/permissions.ts"
+ provides: "Permission matrix for all five roles"
+ contains: "ADMIN"
+ - path: "src/lib/middleware/authorize.ts"
+ provides: "API route authorization wrapper"
+ exports: ["authorize", "withPermission"]
+ - path: "src/lib/__tests__/rbac.test.ts"
+ provides: "RBAC permission tests for all roles"
+ min_lines: 80
+ key_links:
+ - from: "src/lib/casl/ability.ts"
+ to: "src/lib/casl/permissions.ts"
+ via: "Reads role permission definitions"
+ pattern: "defineAbilityFor"
+ - from: "src/lib/middleware/authorize.ts"
+ to: "src/lib/casl/ability.ts"
+ via: "Builds ability from session user and checks permission"
+ pattern: "defineAbilityFor.*can\\("
+ - from: "src/lib/middleware/authorize.ts"
+ to: "src/lib/auth.ts"
+ via: "Gets current user session for authorization"
+ pattern: "getCurrentUser|getServerSession"
+---
+
+
+Implement role-based access control using CASL.js with a permission matrix for all five roles (Admin, Office Staff, Collector, Technician, Client), an API-layer authorization middleware that returns 403 for unauthorized access, and comprehensive tests proving each role's boundaries.
+
+Purpose: Without RBAC, any authenticated user can access everything. This plan enforces the principle of least privilege — a Technician sees only their jobs, a Client sees only their account.
+Output: Working CASL permission system, API authorization middleware, and tests for all role boundaries.
+
+
+
+@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-02-SUMMARY.md
+@.planning/phases/01-foundation/01-03-SUMMARY.md
+@src/lib/auth-options.ts
+@src/types/next-auth.d.ts
+@prisma/schema.prisma
+
+
+
+
+
+ Task 1: CASL permission definitions and ability factory
+
+ package.json
+ src/lib/casl/types.ts
+ src/lib/casl/permissions.ts
+ src/lib/casl/ability.ts
+
+
+ Install CASL: `npm install @casl/ability`.
+
+ Create `src/lib/casl/types.ts`:
+ - Define AppAbility type using CASL's PureAbility
+ - Define subjects: "Tenant", "User", "Subscriber", "Invoice", "Payment", "Ticket", "JobOrder", "Inventory", "Expense", "Account", "Report", "all"
+ - Define actions: "create", "read", "update", "delete", "manage" (manage = all actions)
+ - Export AppAbility and AppSubjects types
+ - Note: Most subjects (Subscriber, Invoice, etc.) don't have models yet — we're defining the permission structure now so it's ready when those models are created in later phases
+
+ Create `src/lib/casl/permissions.ts`:
+ - Export a function `definePermissionsFor(role: Role, userId: string, tenantId: string)` that returns an array of CASL permission rules
+ - Permission matrix based on CONTEXT.md decisions:
+
+ **ADMIN**: can("manage", "all") — full access within tenant
+
+ **OFFICE_STAFF**:
+ - can("manage", "User") — create/manage users, assign roles
+ - can("manage", "Subscriber") — full subscriber management
+ - can("manage", "Invoice") — billing management
+ - can("manage", "Payment") — record payments
+ - can("manage", "Ticket") — ticketing
+ - can("manage", "JobOrder") — job order management
+ - can("read", "Report") — view financial reports
+ - can("read", "Account") — view accounting (not modify)
+ - cannot("create", "Account") — cannot modify Chart of Accounts
+ - cannot("update", "Account")
+ - cannot("delete", "Account")
+
+ **COLLECTOR**:
+ - can("read", "Subscriber") — can view all subscribers (context for work)
+ - can("create", "Payment") — can record payments (zone filtering done at data layer, not CASL)
+ - can("read", "Payment") — view payment history
+ - cannot("manage", "Invoice") — no billing access
+ - cannot("manage", "User") — no user management
+ - cannot("manage", "Report") — no report access
+
+ **TECHNICIAN**:
+ - can("read", "JobOrder", { assignedToId: userId }) — only their assigned jobs
+ - can("update", "JobOrder", { assignedToId: userId }) — update their own jobs
+ - can("read", "Subscriber") — limited: only contact info for assigned job subscribers (enforced at data layer)
+ - can("read", "Inventory", { assignedToId: userId }) — only inventory checked out to them
+ - cannot("manage", "Invoice")
+ - cannot("manage", "Payment")
+ - cannot("manage", "Subscriber") — no subscriber management
+ - cannot("manage", "Report")
+
+ **CLIENT**:
+ - can("read", "Invoice", { subscriberId: userId }) — only their own invoices
+ - can("read", "Payment", { subscriberId: userId }) — only their own payments
+ - can("read", "Subscriber", { id: userId }) — only their own profile
+ - can("create", "Ticket") — submit tickets
+ - can("read", "Ticket", { submittedById: userId }) — only their own tickets
+ - cannot("manage", "User")
+ - cannot("manage", "Report")
+
+ Create `src/lib/casl/ability.ts`:
+ - Export `defineAbilityFor(user: { id: string, roles: Role[], tenantId: string | null, isSuperAdmin: boolean })`
+ - If isSuperAdmin: can("manage", "all") — unrestricted
+ - Otherwise: iterate over user.roles, call definePermissionsFor for each role, merge all rules (union of permissions for multi-role users)
+ - Return the built CASL Ability instance
+ - Export the Ability type for use in components/routes
+
+
+ TypeScript compiles without errors. Import defineAbilityFor and verify it returns an Ability instance for each role.
+
+ CASL permission matrix defined for all 5 roles plus super-admin. Multi-role users get union of permissions. Ability factory builds correct permissions from user session data.
+
+
+
+ Task 2: API authorization middleware and RBAC tests
+
+ src/lib/middleware/authorize.ts
+ src/lib/__tests__/rbac.test.ts
+
+
+ Create `src/lib/middleware/authorize.ts`:
+ - Export `withPermission(action: string, subject: string)` — a higher-order function that wraps a Next.js API route handler
+ - Flow:
+ 1. Get current user session via getCurrentUser() from auth.ts
+ 2. If no session: return 401 { error: "Unauthorized" }
+ 3. Build CASL ability using defineAbilityFor(session.user)
+ 4. Check ability.can(action, subject)
+ 5. If cannot: return 403 { error: "Forbidden" }
+ 6. If can: call the wrapped handler, passing the ability and user in context
+ - Export `authorize(handler, action, subject)` as an alternative API for convenience
+ - The wrapped handler receives (req, { user, ability }) so handlers can do fine-grained checks internally
+
+ Example usage (add as JSDoc comment):
+ ```typescript
+ // In route.ts:
+ export const GET = withPermission("read", "Subscriber")(async (req, { user, ability }) => {
+ // ability is available for fine-grained checks within the handler
+ const subscribers = await getSubscribers(user.tenantId);
+ return NextResponse.json(subscribers);
+ });
+ ```
+
+ Create `src/lib/__tests__/rbac.test.ts` — comprehensive unit tests:
+
+ **Admin tests:**
+ - Admin can manage Subscribers (returns true)
+ - Admin can manage Users (returns true)
+ - Admin can manage Reports (returns true)
+
+ **Office Staff tests:**
+ - Office Staff can manage Subscribers (true)
+ - Office Staff can read Reports (true)
+ - Office Staff cannot create Account (false — cannot modify COA)
+ - Office Staff cannot delete Account (false)
+
+ **Collector tests:**
+ - Collector can read Subscribers (true)
+ - Collector can create Payment (true)
+ - Collector cannot manage Invoice (false)
+ - Collector cannot manage User (false)
+ - Collector cannot read Reports (false)
+
+ **Technician tests:**
+ - Technician cannot manage Invoice (false)
+ - Technician cannot manage Payment (false)
+ - Technician cannot manage Subscriber (false)
+ - Technician cannot read Reports (false)
+
+ **Client tests:**
+ - Client can create Ticket (true)
+ - Client cannot manage User (false)
+ - Client cannot manage Subscriber (false — not even their own, they can only read)
+ - Client cannot read Reports (false)
+
+ **Multi-role tests:**
+ - User with [TECHNICIAN, COLLECTOR] roles can read Subscribers (from Collector) AND update JobOrder (from Technician)
+ - Union of permissions is additive
+
+ **Super-admin tests:**
+ - Super-admin can manage all (true for any subject)
+
+ All tests use defineAbilityFor directly — no HTTP requests needed. These are pure unit tests.
+
+
+ Run `npx vitest run` — all RBAC tests pass. Verify that at minimum: Technician cannot access billing (critical requirement from phase success criteria).
+
+ API authorization middleware returns 403 for unauthorized access. All 5 roles have correct permission boundaries verified by unit tests. Multi-role union works. Super-admin has full access (AUTH-02, AUTH-03).
+
+
+
+
+
+1. `npx vitest run` passes all RBAC tests
+2. Admin has full access, Office Staff can't modify COA, Collector can't manage billing, Technician can't see billing/subscriber management, Client can only see own data
+3. Multi-role user gets union of permissions
+4. Super-admin bypasses all permission checks
+5. withPermission middleware returns 401 for unauthenticated, 403 for unauthorized
+
+
+
+- All 5 roles have correctly scoped permissions (AUTH-02, AUTH-03)
+- API-layer enforcement returns 403 (not just UI hiding) for unauthorized access
+- Technician cannot access billing or subscriber management routes
+- Multi-role users get union of all assigned role permissions
+- Comprehensive unit tests validate every role boundary
+
+
+
diff --git a/.planning/phases/01-foundation/01-05-PLAN.md b/.planning/phases/01-foundation/01-05-PLAN.md
new file mode 100644
index 0000000..7c703f5
--- /dev/null
+++ b/.planning/phases/01-foundation/01-05-PLAN.md
@@ -0,0 +1,222 @@
+---
+phase: 01-foundation
+plan: 05
+type: execute
+wave: 4
+depends_on: ["01-04"]
+files_modified:
+ - src/app/(super-admin)/layout.tsx
+ - src/app/(super-admin)/admin/page.tsx
+ - src/app/(super-admin)/admin/tenants/page.tsx
+ - src/app/api/admin/tenants/route.ts
+ - src/app/api/admin/tenants/[id]/route.ts
+ - src/app/api/admin/tenants/[id]/suspend/route.ts
+ - src/lib/middleware/super-admin.ts
+ - prisma/seed.ts
+ - src/lib/__tests__/super-admin.test.ts
+ - src/lib/__tests__/tenant-isolation.test.ts
+autonomous: true
+
+must_haves:
+ truths:
+ - "Super-admin can log in and view all tenants without being scoped to any single tenant"
+ - "Super-admin can see tenant status, creation date, and subscriber count for each tenant"
+ - "Super-admin can suspend a tenant with a grace period"
+ - "Super-admin cannot access tenant-scoped data (no impersonation)"
+ - "Non-super-admin users cannot access the admin panel"
+ - "Test harness validates core isolation and auth behaviors"
+ artifacts:
+ - path: "src/app/(super-admin)/admin/tenants/page.tsx"
+ provides: "Tenant management list UI"
+ min_lines: 40
+ - path: "src/app/api/admin/tenants/route.ts"
+ provides: "List all tenants API"
+ exports: ["GET"]
+ - path: "src/app/api/admin/tenants/[id]/suspend/route.ts"
+ provides: "Tenant suspension API"
+ exports: ["POST"]
+ - path: "src/lib/middleware/super-admin.ts"
+ provides: "Super-admin route guard"
+ exports: ["withSuperAdmin"]
+ - path: "src/lib/__tests__/super-admin.test.ts"
+ provides: "Super-admin access and tenant management tests"
+ min_lines: 40
+ key_links:
+ - from: "src/app/api/admin/tenants/route.ts"
+ to: "src/lib/middleware/super-admin.ts"
+ via: "withSuperAdmin guard on route"
+ pattern: "withSuperAdmin"
+ - from: "src/app/(super-admin)/admin/tenants/page.tsx"
+ to: "src/app/api/admin/tenants/route.ts"
+ via: "fetch tenant list"
+ pattern: "fetch.*api/admin/tenants"
+ - from: "src/lib/middleware/super-admin.ts"
+ to: "src/lib/auth.ts"
+ via: "Checks isSuperAdmin from session"
+ pattern: "isSuperAdmin"
+---
+
+
+Implement the super-admin panel: a separate auth context for platform-level management, tenant listing with status/metrics, tenant suspension with grace period, and the consolidated test harness proving all Phase 1 behaviors work together.
+
+Purpose: The platform owner needs to manage ISP tenants (view, suspend, monitor) without being scoped to any single tenant. This closes the super-admin requirement and validates the entire foundation with comprehensive tests.
+Output: Working super-admin panel with tenant management, plus full test suite covering auth, isolation, and RBAC.
+
+
+
+@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-02-SUMMARY.md
+@.planning/phases/01-foundation/01-03-SUMMARY.md
+@.planning/phases/01-foundation/01-04-SUMMARY.md
+@src/lib/auth-options.ts
+@src/lib/casl/ability.ts
+@src/lib/prisma-tenant.ts
+@prisma/schema.prisma
+
+
+
+
+
+ Task 1: Super-admin API routes and middleware guard
+
+ src/lib/middleware/super-admin.ts
+ src/app/api/admin/tenants/route.ts
+ src/app/api/admin/tenants/[id]/route.ts
+ src/app/api/admin/tenants/[id]/suspend/route.ts
+ prisma/seed.ts
+
+
+ Create `src/lib/middleware/super-admin.ts`:
+ - Export `withSuperAdmin(handler)` — wraps an API route handler
+ - Gets current user session via getCurrentUser()
+ - If no session: return 401
+ - If session.user.isSuperAdmin is not true: return 403 { error: "Super-admin access required" }
+ - If authorized: call handler with user context
+
+ Create `src/app/api/admin/tenants/route.ts`:
+ - GET handler wrapped with withSuperAdmin
+ - Queries ALL tenants (no tenant scoping — super-admin sees everything)
+ - Returns array of: { id, name, slug, status, ownerEmail, createdAt, userCount, subscriberCount (0 for now — no Subscriber model yet) }
+ - userCount: count of users per tenant via Prisma _count
+ - Sort by createdAt descending
+
+ Create `src/app/api/admin/tenants/[id]/route.ts`:
+ - GET handler wrapped with withSuperAdmin
+ - Returns single tenant detail: all fields plus users list (id, email, firstName, lastName, roles, isActive)
+ - Return 404 if tenant not found
+
+ Create `src/app/api/admin/tenants/[id]/suspend/route.ts`:
+ - POST handler wrapped with withSuperAdmin
+ - Accepts JSON body: { action: "suspend" | "activate" }
+ - For suspend:
+ - Set status to PENDING_SUSPENSION
+ - Set suspendedAt to now()
+ - Set gracePeriodEndsAt to 7 days from now
+ - Return { message: "Tenant suspension initiated. Grace period ends on {date}." }
+ - For activate:
+ - Set status to ACTIVE
+ - Clear suspendedAt and gracePeriodEndsAt
+ - Return { message: "Tenant activated." }
+ - Return 404 if tenant not found
+
+ Update `prisma/seed.ts`:
+ - Add a second demo tenant: "Test ISP 2" with a different admin user (admin2@demo.com) for testing cross-tenant isolation
+ - Keep existing demo tenant and super-admin user
+
+
+ Run `npm run db:seed` — seed runs cleanly with both tenants. Start the app. Use curl or an API client to:
+ 1. GET /api/admin/tenants without auth — expect 401
+ 2. Log in as super-admin, GET /api/admin/tenants — expect 200 with both tenants
+ 3. POST /api/admin/tenants/{id}/suspend with { action: "suspend" } — expect tenant status changes to PENDING_SUSPENSION
+
+ Super-admin API routes list all tenants and can suspend/activate them. withSuperAdmin middleware blocks non-super-admin access with 403. Grace period suspension works.
+
+
+
+ Task 2: Super-admin UI panel and comprehensive test harness
+
+ src/app/(super-admin)/layout.tsx
+ src/app/(super-admin)/admin/page.tsx
+ src/app/(super-admin)/admin/tenants/page.tsx
+ src/middleware.ts
+ src/lib/__tests__/super-admin.test.ts
+ src/lib/__tests__/tenant-isolation.test.ts
+
+
+ Create `src/app/(super-admin)/layout.tsx`:
+ - Server component that checks session
+ - If user is not super-admin, redirect to /login or show 403 page
+ - Simple layout with sidebar navigation: "Dashboard", "Tenants"
+ - Header shows "NetForge Admin" and the super-admin's name
+ - Sign out button
+
+ Create `src/app/(super-admin)/admin/page.tsx`:
+ - Simple admin dashboard showing: total tenants, active tenants, suspended tenants
+ - Fetches stats from /api/admin/tenants
+
+ Create `src/app/(super-admin)/admin/tenants/page.tsx`:
+ - Table listing all tenants: Name, Status (badge with color: green=Active, yellow=Pending Suspension, red=Suspended), Owner Email, Users, Created Date
+ - Each row has actions: View details, Suspend/Activate toggle button
+ - Suspend button triggers confirmation dialog then calls the suspend API
+ - Activate button calls the suspend API with action: "activate"
+ - Clicking tenant name navigates to a detail view (or opens a modal showing users)
+ - Use Tailwind for styling — clean data table with hover states
+
+ Update `src/middleware.ts`:
+ - Add /admin/* routes to be protected
+ - These routes should check for super-admin status specifically (or rely on the layout redirect + API guards)
+ - Public routes remain: /login, /signup, /api/auth/*
+
+ Create `src/lib/__tests__/super-admin.test.ts`:
+ - Test: super-admin user can access admin routes (withSuperAdmin allows)
+ - Test: regular admin user cannot access admin routes (withSuperAdmin returns 403)
+ - Test: tenant suspension sets correct status and grace period dates
+ - Test: tenant activation clears suspension fields
+
+ Enhance `src/lib/__tests__/tenant-isolation.test.ts` (if needed):
+ - Ensure the existing cross-tenant isolation test still passes with the two-tenant seed data
+ - Add test: super-admin query for tenants returns ALL tenants (not scoped)
+ - Add test: regular user query does NOT return other tenants' data
+
+ Run full test suite: `npx vitest run` — all tests must pass.
+
+
+ Run `npx vitest run` — all tests pass (auth, RBAC, tenant isolation, super-admin). Start the app, log in as superadmin@netforge.com, navigate to /admin/tenants — see both demo tenants listed. Suspend a tenant — status badge updates. Activate it — status reverts. Log in as admin@demo.com — navigating to /admin returns forbidden.
+
+ Super-admin can log in and manage all tenants via /admin panel (TENANT-03). Non-super-admin users are blocked from admin routes. Full test harness covers auth, RBAC, tenant isolation, and super-admin access. Phase 1 success criteria are met.
+
+
+
+
+
+1. Super-admin logs in and sees tenant list at /admin/tenants
+2. Tenant table shows status, owner, user count, creation date
+3. Suspend action sets PENDING_SUSPENSION with 7-day grace period
+4. Activate action restores ACTIVE status
+5. Regular admin user gets 403 when accessing /admin routes
+6. `npx vitest run` passes ALL tests across the entire phase:
+ - Auth config tests
+ - RBAC permission tests for all 5 roles
+ - Tenant isolation tests (zero cross-tenant leakage)
+ - Super-admin access tests
+
+
+
+- Super-admin can log in and view all tenants (TENANT-03)
+- Super-admin sees tenant status, user count, creation date per tenant
+- Tenant suspension with grace period works (not immediate lockout)
+- Non-super-admin blocked from admin panel
+- Full test suite validates Phase 1 foundation (INFRA-02 partial — unit tests for auth/RBAC/isolation)
+- All Phase 1 requirements met: TENANT-01, TENANT-02, TENANT-03, AUTH-01, AUTH-02, AUTH-03, AUTH-04, INFRA-01, INFRA-02
+
+
+