feat: governance kernel, deployment hardening, local-dev stack

Adds proposals + proposal_votes + policies + incidents + incident_updates
tables with full RLS, async voting flow, dismissal-appeal pathway,
versioned policies with provenance, and an anonymous-incident token mode.
Adds per-space configurable areas, financial visibility settings, member
self-profile + COI disclosure, and a mission statement that renders in
the app footer.

Reorganises lib/actions.ts into per-domain files (tasks, projects,
members, payments, contacts, knowledge-base, secrets, area-leads,
settings, imports, proposals, incidents, policies, areas) plus a barrel.
Adds lib/permissions.ts and lib/auth-helpers.ts so action bodies stop
duplicating the user-to-member-to-role check. Wires every action that has
a Zod schema; introduces a flexibleDateTime preprocessor so HTML date
pickers no longer hit "Invalid datetime".

Hardens RLS: drops NOT NULL on space_members.user_id for offline members,
adds a privilege-escalation trigger that blocks self-edits to role / tier
/ status / approved / has_card_access / space_id, closes four
cross-tenant holes in the governance kernel, and adds a partial unique
index so two admins cannot concurrently activate two drafts of the same
policy slug.

Ships every deployment artifact: Dockerfile, docker-compose,
.do/app.yaml, .env.example, /api/health, plus a managed-Supabase guide
(docs/DEPLOYMENT.md), an end-to-end self-hosted Droplet guide
(docs/DEPLOY_DO_SELFHOSTED.md), and a Supabase-CLI local-dev quickstart
(docs/LOCAL_DEV.md). Replaces middleware.ts with proxy.ts for Next.js 16.
Adds scripts/schema.sql as the canonical idempotent full deploy plus
fourteen numbered incremental migrations.

Adds scripts/seed.sql so a fresh local DB renders something instead of
empty pages: a demo space, five members across all roles, a v1 code of
conduct, three proposals in different states with seven votes already
cast, an open incident, a project, three tasks, a KB entry, and matching
activity-log rows.

Replaces v0-generated test scaffolding with 266 real tests across
__tests__/{actions,auth-helpers,components,governance,permissions,
security,utils,validations}.test.ts. Adds react-markdown + remark-gfm
for proposal / incident / policy body rendering. Deletes four dead v0
fix-* scripts, the unused components/app-shell.tsx, and the duplicate
app/auth/login/page.tsx.

Routes (31 total): /api/health, /api/paypal/sync, /auth/callback, /,
/login, /signup, /signup/confirm, /dashboard, /tasks, /projects,
/members, /payments, /financials, /contacts, /ops, /ops/[id], /ops/new,
/proposals, /proposals/[id], /proposals/new, /incidents, /incidents/[id],
/incidents/new, /policies, /policies/[slug], /policies/new, /profile,
/recruitment, /settings, /comms, /import.

Full pass-by-pass history is in docs/HANDOFF.md; the roadmap is in
docs/GOVERNANCE_FEATURES.md.
This commit is contained in:
Moheeb Zara
2026-05-14 04:52:11 -07:00
parent 8b9128704b
commit ea8d7a5f91
99 changed files with 13823 additions and 1940 deletions
+471
View File
@@ -0,0 +1,471 @@
# Codebase Audit
Date: 2026-05-13 through 2026-05-14 (three passes)
Scope: every file outside `node_modules`, `.next`, and `.git`.
Method: read each source file, cross-check against the live database type definitions in `types/database.ts`, the canonical schema in `scripts/schema.sql`, and the column map in `DB_SCHEMA_MAP.md`.
This document supersedes the older `AUDIT.md`, `IMPLEMENTATION_STATUS.md`, and `docs/PRODUCTION_AUDIT.md` for findings as of this date. Those files remain as historical context.
- **Pass 1** (2026-05-13): fresh-deploy blockers and deployment artifacts.
- **Pass 2** (2026-05-13): validation schema enum drift, RLS privilege escalation, dashboard task filter bug, dead duplicate routes, auth function duplication.
- **Pass 3** (2026-05-14): architecture refactor. Split monolithic `lib/actions.ts` by domain, introduced `lib/permissions.ts` and `lib/auth-helpers.ts`, wired validation into every action with a schema, added 76 real tests, deleted remaining dead code, added `scripts/README.md`.
All three passes are reflected below.
---
## 1. What the system is
A multi-tenant member-management app for hackerspaces. Each tenant is a `spaces` row; every other table is keyed by `space_id`. Authentication uses Supabase Auth (email + password, OAuth wiring is stubbed). All data access goes through Supabase with row-level security enforced at the database. The Next.js app uses server components for reads, server actions for writes, and one client-side Realtime subscription for chat.
Stack:
| Layer | Stack |
|-------|-------|
| Framework | Next.js 16 (App Router), React 19, TypeScript 5.7 |
| Styling | Tailwind v4 (CSS-native config), shadcn/ui, Lucide icons |
| Backend | Next.js server actions, one REST route under `/api/paypal/sync`, one `/api/health` route |
| Data | Supabase Postgres + Auth + Realtime |
| Tests | Vitest (unit), Playwright (E2E) |
| Hosting | Vercel, DigitalOcean App Platform, or self-hosted Droplet (Dockerfile + compose ship with the repo) |
---
## 2. Repo layout
```
/
app/
(app)/ Protected routes. Layout enforces auth + membership.
dashboard/ Server component, parallel counts.
tasks/ Server page + client component.
projects/ Kanban-style board.
members/ Admin/board only mutations.
payments/ Treasurer/admin/board only.
comms/ Realtime chat (Supabase channel subscription).
contacts/ Member-writable CRUD.
ops/ Knowledge base, secrets, area leads.
ops/[id]/ KB entry detail + editor.
ops/new/ KB entry create form.
import/ CSV import UI (partial).
settings/ Space + integrations.
(landing)/ Public landing page.
auth/callback/route.ts OAuth code exchange.
api/
health/route.ts Liveness probe (added this audit).
paypal/sync/route.ts Per-space PayPal pull.
login/, signup/ Public auth pages.
layout.tsx, page.tsx Root layout, root redirect.
components/
ui/ shadcn primitives.
app-sidebar.tsx The actual sidebar (role-gated).
app-shell.tsx DEAD: not imported by any page.
task-claim-button.tsx Used by tasks-client.
theme-provider.tsx next-themes wrapper.
hooks/
use-mobile.ts, use-toast.ts
lib/
supabase/
client.ts Browser client.
server.ts Server component client (cookie-bound).
admin.ts Service-role client. Bypasses RLS.
proxy.ts Middleware session refresher.
actions.ts All mutation server actions.
auth-actions.ts Auth-specific server actions.
validations.ts Zod schemas (most are not wired up, see findings).
security.ts Sanitisation, rate limiting, helpers.
utils.ts Tailwind classname merge.
types.ts Re-exports from generated database types.
types/
database.ts Generated Supabase types. Source of truth for column shape.
middleware.ts Auth gate. Public routes list lives here.
scripts/
schema.sql Canonical, idempotent, full-schema deploy.
014_member_user_id_nullable.sql Most recent incremental migration.
patch-dashboard.mjs, fix-actions.js, fix-sidebar.js, fix-sidebar.mjs DEAD scripts.
__tests__/ Vitest specs.
e2e/ Playwright specs.
docs/ Architecture, deployment, database, API, audit.
Dockerfile, docker-compose.yml, .dockerignore Self-host artifacts.
.do/app.yaml DigitalOcean App Platform spec.
.env.example Env var template.
CLAUDE.md Working agreement.
```
---
## 3. Issues fixed in this audit
### 3.1 `space_members.user_id` was NOT NULL but the app needs it nullable
The schema declared:
```sql
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE
```
But `addMember()` (`lib/actions.ts:302`) and `importMembers()` (`lib/actions.ts:897`) insert rows with no `user_id`. These are "offline" members added by an admin or CSV-imported, who have not signed up yet. On a fresh Supabase project, both code paths fail with a NOT NULL violation.
Fix applied:
- `scripts/schema.sql`: dropped `NOT NULL` on `space_members.user_id`. Added a comment explaining the choice.
- `scripts/014_member_user_id_nullable.sql`: new incremental migration for existing databases.
The `(space_id, user_id)` unique constraint still works because Postgres treats NULL as distinct, so multiple offline members with `user_id IS NULL` coexist. The partial unique index on `(space_id, email)` (`WHERE email IS NOT NULL`) prevents duplicate offline-member emails.
### 3.2 Realtime publication statements were not idempotent
The closing two lines of `schema.sql` were:
```sql
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
```
`ALTER PUBLICATION ... ADD TABLE` errors if the table is already in the publication, which breaks the "idempotent" promise the file makes in its header. Wrapped both in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$` blocks.
### 3.3 RLS policies were not idempotent
Section 6 of `schema.sql` does `CREATE POLICY ...` without `IF NOT EXISTS` (which Postgres 15 does not support). Re-running the file on a database that already has policies errored with `policy ... already exists`. Added a block of `DROP POLICY IF EXISTS` statements before the `CREATE POLICY` block.
Net result of 3.1, 3.2, 3.3: `scripts/schema.sql` now runs cleanly on both a fresh Supabase project and an existing one. The header comment claiming idempotency is now true.
### 3.4 Missing deployment artifacts
The deployment guide instructed users to "create" a Dockerfile, a docker-compose file, and to manually edit `next.config.mjs`. None of these artifacts existed in the repo. App Platform users had no spec to reference.
Added:
- `Dockerfile` (multi-stage, standalone output, runs as non-root, healthcheck).
- `docker-compose.yml` (service definition with healthcheck, env_file).
- `.dockerignore` (excludes git, node_modules, dev artifacts, secrets).
- `.do/app.yaml` (App Platform spec with health check path).
- `.env.example` (every variable the app reads, with comments).
- `app/api/health/route.ts` (liveness probe used by Docker, App Platform, and operators).
- Updated `middleware.ts` to include `/api/health` in the public routes list so the healthcheck does not redirect to `/login`.
- Updated `docs/DEPLOYMENT.md` to reference the bundled artifacts instead of asking users to write their own. Also removed the incorrect note about manually editing `next.config.mjs` (the file already keys `output: 'standalone'` off `DOCKER_BUILD=1`, which the Dockerfile sets).
A fresh clone is now: `pnpm install`, copy `.env.example` to `.env.local`, run `scripts/schema.sql` in Supabase, `pnpm dev`.
### 3.5 Dashboard "Quick Chores" filter was wrong
`app/(app)/dashboard/page.tsx:49` filtered tasks with `.neq('status', 'done')` to find open work. The actual task_status enum has both `done` (legacy) and `completed`, and `completeTask()` writes `'completed'`. Completed tasks therefore leaked into the dashboard's open-tasks list. Replaced with a positive filter on the open statuses: `open, claimed, in_progress, overdue, due_today, blocked`.
### 3.6 Zod validation schemas had wrong enum values
`lib/validations.ts` declared enums that disagreed with the database in six places. The schemas were mostly unused (only `createTask` and `taskId` were wired), so the bugs were latent. Both fixed:
- `createTaskSchema.type`: was free-form string, now `enum(['task','chore'])`.
- `createTaskSchema.recurrence`: missing `biweekly`. Added.
- `updateProjectStatusSchema.status`: had `planning, active, paused, completed, cancelled`. Replaced with the real `backlog, in_progress, review, done, blocked`.
- `addMemberSchema.role` and `updateMemberSchema.role`: missing `associate`. Added.
- `createContactSchema.contact_type`: had `vendor, sponsor, partner, city, media, other`. Replaced with the real `vendor, supplier, partner, landlord, city`.
- `createKbEntrySchema.visibility`: had `public, members, board, admin`. Replaced with the real `all_members, board, admin_only`.
Also tightened: `logCashPaymentSchema` (removed unused `from_identifier`, added `member_id`), `updateSpaceSettingsSchema` (added `slug`), `upsertAreaLeadSchema` (matched the action's parameter shape).
### 3.7 Privilege escalation on `space_members` self-update
The `members_update` RLS policy let a member update their own row including `role`, `tier`, `status`, `approved`, and `has_card_access`. An authenticated member could PATCH their own membership via PostgREST and become an admin.
Fixed with a `BEFORE UPDATE` trigger (`prevent_member_self_role_change`) that rejects any change to protected columns when the row belongs to the current user and the current user is not already admin/board/treasurer in that space. Service-role calls (no `auth.uid()`) and privileged self-edits are unaffected. Applied to `schema.sql` and shipped as incremental migration `scripts/015_prevent_member_self_role_change.sql`.
### 3.8 Validation wired into the highest-risk actions
The Zod schemas were imported by `lib/actions.ts` but never actually invoked except by `createTask` and `claimTask`. Wired the now-correct schemas into:
- `createTask` (already)
- `claimTask` (already)
- `createProject`
- `updateProjectStatus`
- `addMember`
- `logCashPayment`
- `linkPaymentToMember`
Remaining unwired actions are noted in section 4.
### 3.9 Duplicate auth functions in `lib/actions.ts`
Lines 30 to 47 of `lib/actions.ts` redeclared `signIn`, `signOut`, and `getUser`, each a slightly worse copy of the version in `lib/auth-actions.ts`. No call site imported them from `actions.ts`. Removed.
### 3.10 Dead one-off scripts removed
Deleted `scripts/fix-actions.js`, `scripts/fix-sidebar.js`, `scripts/fix-sidebar.mjs`, and `scripts/patch-dashboard.mjs`. All four had hardcoded `/vercel/share/v0-project` paths and were one-time patches that have long since landed in the tree.
### 3.11 `types/database.ts` did not match the schema for `user_id`
After making `space_members.user_id` nullable in `scripts/schema.sql`, the generated types were stale. Updated `Row.user_id` to `string | null` and `Insert.user_id` / `Update.user_id` to optional nullable. Regenerate properly with `supabase gen types typescript` on next schema sync.
### 3.12 Signup page copy referenced channels that are not created
The "Create Space" panel told users "Default channels (general, announcements, random) will be created automatically." The schema's `create_default_channels` trigger creates `general`, `announcements`, `ops`. Updated the copy to match.
### 3.13 Monolithic `lib/actions.ts` split by domain
`lib/actions.ts` was 914 lines and held 29 exported server actions for every entity in the system. Maintenance friction was high. The duplication was severe: 25+ copies of "get user, then look up active member, then check role" and "insert activity_log row" boilerplate.
Refactored into `lib/actions/` containing:
- `tasks.ts`, `projects.ts`, `members.ts`, `contacts.ts`, `payments.ts`, `knowledge-base.ts`, `secrets.ts`, `area-leads.ts`, `settings.ts`, `imports.ts` — one file per entity, each carrying `'use server'`.
- `index.ts` — barrel that re-exports every action so the public import path `@/lib/actions` remains stable.
Every client component that imports from `@/lib/actions` was verified to keep working. The barrel is plain re-exports; Next.js preserves the `'use server'` marking from the source files.
### 3.14 Shared helpers in `lib/auth-helpers.ts` and `lib/permissions.ts`
Two new modules absorb the repeated boilerplate.
`lib/permissions.ts`:
- `ROLES`, `ADMIN_ROLES`, `TREASURER_ROLES`, `ALL_ROLES`, `ACTIVE_STATUSES` as const tuples.
- `Role` and `MemberStatus` type aliases.
- `hasRole(role, allowed)` helper.
`lib/auth-helpers.ts`:
- `getAuthMember(supabase)`: returns the active member or null.
- `requireMember(supabase)`: returns `{ ok: true, member } | { ok: false, error }`.
- `requireMemberWithRole(supabase, allowed, label?)`: same plus role gate.
- `parseInput(schema, input)`: Zod parse with a discriminated result.
- `logActivity(supabase, member, action, entityType, entityId, details?)`: fire-and-forget activity log insert.
The result types use the canonical `ok: boolean` discriminator so TypeScript narrows reliably under `strict: true`.
### 3.15 Validation wired into every action that has a schema
After pass 2, only 7 actions called their schemas. Pass 3 wired the remaining 11:
- `updateMember`, `approveMember`, `removeMember`
- `createContact`, `updateContact`, `deleteContact`
- `createKbEntry`, `updateKbEntry`, `deleteKbEntry`
- `createSecret`, `deleteSecret`
- `upsertAreaLead`
- `updateSpaceSettings`, `saveIntegration`
- `disconnectIntegration` (inline string check, no Zod schema needed)
- `importMembers`, `importPaymentsCsv` (array-level guard + filter)
Bad input now returns a clean error message instead of a database-level failure.
### 3.16 Real tests, not assertion theatre
The existing `__tests__/actions.test.ts` and `__tests__/utils.test.ts` were inline value assertions that never imported project code. Kept for compatibility, but four new files exercise the real units:
| File | What it covers |
|------|----------------|
| `__tests__/validations.test.ts` | Every Zod schema, asserting real DB enum values pass and legacy ones fail. 37 tests. |
| `__tests__/permissions.test.ts` | Role constants, `ACTIVE_STATUSES`, `hasRole`. 10 tests. |
| `__tests__/security.test.ts` | `sanitizeString`, `escapeHtml`, `stripHtml`, `sanitizeEmail`, `sanitizeUrl`, `sanitizeSlug`, `hasSqlInjectionPatterns`, `isValidUuid`, `truncate`, `checkRateLimit`, `validateContentLength`. 25 tests. |
| `__tests__/auth-helpers.test.ts` | `parseInput` parse/error path. 4 tests. |
Total: 202 tests across 7 files, all green.
### 3.17 Vitest config fixed
- `vitest.setup.ts` had JSX inside a `.ts` file (esbuild refused: "Expected '>' but found '{'"). Replaced the `next/image` mock with `React.createElement`.
- `vitest.config.ts` was picking up Playwright e2e specs and trying to run them. Added explicit `include` and `exclude` globs.
### 3.18 Final dead-code sweep
Deleted:
- `app/auth/login/page.tsx` (dead duplicate login at `/auth/login` linking to non-existent `/auth/onboarding`).
- `components/app-shell.tsx` (no importer; hardcoded badge counts; no role gating).
### 3.19 Shared types consolidated in `lib/types.ts`
`lib/types.ts` already re-exported the generated row and enum types. Added:
- `MemberSummary`: common projection used by sidebars and props.
- `ActionResult<T>`: standard server-action return shape.
- Re-exports of `Role`, `ROLES`, `ADMIN_ROLES`, `TREASURER_ROLES`, `ALL_ROLES`, `ACTIVE_STATUSES`, `hasRole` from `lib/permissions.ts`.
- Re-exports of `Member`, `Result`, `MemberResult`, `ServerSupabase` from `lib/auth-helpers.ts`.
One-stop import for any consumer of typed entities.
### 3.20 Migration conventions documented
Added `scripts/README.md` covering: canonical vs incremental files, naming, the idempotency rules (`IF NOT EXISTS`, `DO ... EXCEPTION`, `DROP POLICY IF EXISTS`, etc), the security baseline for new tables, and verification queries for fresh deploys.
---
## 4. Open issues (not fixed in this pass)
These are documented for the next person, not silently shipped. Each one has a concrete location and a concrete fix.
### 4.1 (was open: dead scripts) RESOLVED in pass 2. See 3.10.
### 4.2 (was open: dead `components/app-shell.tsx`) RESOLVED in pass 3. See 3.18.
### 4.3 (was open: dead `app/auth/login`) RESOLVED in pass 3. See 3.18.
### 4.4 (was open: validation not wired everywhere) RESOLVED in pass 3. See 3.15.
### 4.4a Supabase TypeScript generic drift
`@supabase/ssr` and `@supabase/supabase-js` emit `SupabaseClient` with 3 generics. Our `types/database.ts` was generated against an older signature. The result: 38 `lib/actions/*` and 103 `app/(app)/*` TypeScript errors of the form `Argument of type '{...}' is not assignable to parameter of type 'never'`.
These errors already existed in the pre-refactor codebase. They are masked by `next.config.mjs: typescript.ignoreBuildErrors: true`. The runtime is unaffected: Supabase's PostgREST client accepts the values fine.
Fix: regenerate `types/database.ts` with the latest Supabase CLI (`supabase gen types typescript --project-id <ref>`). Then drop `typescript.ignoreBuildErrors`.
### 4.5 `signUp` metadata is not consumed by the auth trigger
`lib/auth-actions.ts:signUp` passes metadata keys `full_name, action, space_name, space_slug, city, invite_code`. The `handle_space_signup` trigger in `schema.sql` reads `space_id, role, display_name`. Different keys, so the trigger short-circuits and does nothing. That is actually fine because `createSpace` and `joinSpace` insert the membership themselves using the admin client. But the trigger is then dead code unless someone passes `space_id` in `data`. Either:
- Document the trigger as a future-use safety net and add a code comment, or
- Remove the trigger and stop pretending it does anything.
### 4.6 Several documented features are UI-only or unfinished
| Feature | Status | Location |
|---------|--------|----------|
| GitHub / Google OAuth | Client wired (`signInWithOAuth`) but needs provider config in Supabase Auth | `app/login/page.tsx:31` |
| Webhook endpoint | URL displayed in settings, no handler | (none) |
| Zeffy, Venmo, Stripe sync | Config storage only, no API code | `lib/actions.ts:saveIntegration` |
| CSV import | UI uploads a file but does not process it | `app/(app)/import/import-client.tsx` |
| Email notifications | None | (none) |
| Task recurrence auto-rollover | Cron job not implemented | (none) |
| Secrets encryption | Plain text in DB | `secrets.value` |
None of these are runtime errors. They are missing-feature stubs. They are listed here so deployers know what is and is not real. Pass 1's audit incorrectly listed OAuth as "UI only, no handlers"; the client-side OAuth call is actually wired. What is missing is the provider configuration in the Supabase dashboard.
### 4.7 `package.json` name is `"my-project"`
Cosmetic. Should be `"hackerspace-management"`. Not a deploy blocker.
### 4.8 `tsconfig.json` has `"strict": true` but `next.config.mjs` has `typescript.ignoreBuildErrors: true`
The build does not fail on type errors. This is a v0 default that should be removed once the codebase passes `pnpm exec tsc --noEmit` cleanly. The pass-2 schema and types edits move the needle. Remaining issues are minor.
### 4.9 Default channels: 3 vs 4
`schema.sql` `create_default_channels` inserts 3 channels: `general`, `announcements`, `ops`. Older docs (`DB_SCHEMA_MAP.md`, `docs/ARCHITECTURE.md`) say 4 channels (`general`, `announcements`, `random`, `facilities`). The schema is current; the older docs are stale. The signup page copy was fixed in pass 2. Update `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` on next docs pass.
### 4.10 (was open: members RLS privilege escalation) RESOLVED in pass 2. See 3.7.
---
## 5. Database column map versus code use
I cross-checked every column accessed in `lib/actions.ts`, `lib/auth-actions.ts`, `app/api/paypal/sync/route.ts`, and the client components named in `app/(app)/*`. Findings:
- `space_members.last_paid_at`: present in schema, used by `linkPaymentToMember` and `importMembers`. OK.
- `space_members.last_payment_at`: legacy duplicate. Schema keeps both. Code reads either with a fallback (`members-client.tsx`).
- `knowledge_base.is_pinned` vs `pinned`: schema has both, code uses `is_pinned`. OK.
- `secrets.title` vs `label`: schema has both, code writes both fields (`createSecret` at `lib/actions.ts:701`). OK.
- `payments.status` vs `link_status`: schema has both, code uses `link_status`. OK.
- `integrations.config` vs `credentials` / `settings`: schema has all three for legacy compatibility, code uses `config`. OK.
- `tasks.task_type`: column matches enum name. Server actions write `task_type`, never `type`. OK.
- `member_status` filter in app uses `('current', 'unverified', 'late')`. Schema enum matches. OK.
No drift between active code paths and the schema beyond what is in section 4.
---
## 6. Security posture
| Control | State |
|---------|-------|
| RLS on every table | Yes |
| Service role key isolated to server | Yes, but `lib/actions.ts:30-47` and other call sites should be reviewed periodically |
| Input validation | Partial. `createSpace` sanitises. `createTask` and `claimTask` use Zod. The rest do not |
| Rate limiting | Present in `lib/security.ts` as in-memory map. Wired into `signIn` and `createSpace`. Not persistent. For multi-instance deploys, switch to Redis (Upstash) |
| Secrets encryption at rest | None. `secrets.value` and `integrations.config` are plain text. Encrypt before merging to production |
| CSRF / Origin check | Implicit through Next server actions + same-origin cookies. No explicit Origin check |
| Audit log | `activity_log` table is populated by most write actions |
| Privilege escalation on `space_members` update | Possible per 4.11, fix before production |
---
## 7. Test posture
- `__tests__/utils.test.ts`: assertion-only tests of helpers, no real imports of project code. Useful as smoke tests, not as coverage.
- `__tests__/actions.test.ts`: mocked Supabase client, tests the call shape of server actions. Does not exercise RLS, does not hit a database.
- `__tests__/components.test.tsx`: not present, but listed in `TESTING.md`. The doc lies.
- `e2e/auth.spec.ts` and `e2e/critical-flows.spec.ts`: present, Playwright. Not verified to pass.
Recommended next steps: stand up a Supabase local instance for integration tests, add a CI job that runs `pnpm test` and `pnpm test:e2e`, fix `TESTING.md` to reflect reality.
---
## 8. Performance notes
- `app/(app)/layout.tsx` makes three sequential Supabase calls (member, taskCount, paymentCount). The latter two can run in `Promise.all`.
- `app/(app)/dashboard/page.tsx` runs multiple queries; check whether they parallelise. (Not audited line-by-line, see file.)
- Realtime: only `comms_messages` and `comms_channels` are in the publication. Good.
- Indexes: 22 indexes are defined in `schema.sql`, covering every foreign key and common filter (`tasks.status`, `payments.transaction_date`, `comms_messages.channel_id`). Good.
---
## 9. Deployment readiness summary
| Target | Ready? | Notes |
|--------|--------|-------|
| Local dev | Yes | `pnpm install && pnpm dev`, with `.env.local` filled |
| Vercel | Yes | Push to GitHub, connect, set env vars |
| DigitalOcean App Platform | Yes | Use `.do/app.yaml` or the UI flow |
| DigitalOcean Droplet (Docker) | Yes | Dockerfile and compose ship with the repo |
| Database fresh deploy | Yes | One file: `scripts/schema.sql` |
| Database upgrade from existing | Yes | Run new incremental migrations in `scripts/` only |
A fresh deploy will succeed. The open issues in section 4 are quality and feature gaps, not deploy blockers.
---
## 10. What changed across the three passes
### Pass 1 (deployment hardening)
Edited:
- `scripts/schema.sql`: user_id nullable, realtime idempotent, RLS policies idempotent.
- `middleware.ts`: `/api/health` added to public routes.
- `docs/DEPLOYMENT.md`: env section, App Platform section, Droplet section (no longer asks user to write the Dockerfile/compose), post-deploy checklist (health probe), upgrading section.
Added:
- `CLAUDE.md`
- `Dockerfile`, `.dockerignore`, `docker-compose.yml`
- `.do/app.yaml`
- `.env.example`
- `app/api/health/route.ts`
- `scripts/014_member_user_id_nullable.sql`
- `docs/AUDIT.md` (this file)
- `docs/HANDOFF.md`
- `README.md` rewritten
### Pass 2 (application repairs)
Edited:
- `app/(app)/dashboard/page.tsx`: open-task filter now lists open statuses positively instead of `neq('status','done')` (was leaking `completed` tasks).
- `app/signup/page.tsx`: default channel list in copy now reads `general, announcements, ops` to match the trigger.
- `lib/validations.ts`: aligned enums for tasks, projects, members, contacts, KB; tightened settings, area-lead, and cash-payment schemas.
- `lib/actions.ts`: removed duplicate `signIn`, `signOut`, `getUser` (kept in `auth-actions.ts`); wired `createProject`, `updateProjectStatus`, `addMember`, `logCashPayment`, `linkPaymentToMember` to their Zod schemas.
- `scripts/schema.sql`: added `prevent_member_self_role_change` trigger to stop privilege escalation on self-update.
- `types/database.ts`: `space_members.user_id` now `string | null` to match the schema.
Added:
- `scripts/015_prevent_member_self_role_change.sql`
Removed:
- `scripts/fix-actions.js`, `scripts/fix-sidebar.js`, `scripts/fix-sidebar.mjs`, `scripts/patch-dashboard.mjs`
No business rules, no UI styling, no test code were changed. Pass 2 edits are bug fixes, security hardening, schema alignment, and dead-code removal.
### Pass 3 (architecture refactor + real tests)
Added:
- `lib/permissions.ts` (role constants, `hasRole`)
- `lib/auth-helpers.ts` (`requireMember`, `requireMemberWithRole`, `parseInput`, `logActivity`)
- `lib/actions/` directory with 11 files (one per domain, plus `index.ts` barrel)
- `__tests__/validations.test.ts` (37 tests)
- `__tests__/permissions.test.ts` (10 tests)
- `__tests__/security.test.ts` (25 tests)
- `__tests__/auth-helpers.test.ts` (4 tests)
- `scripts/README.md`
Edited:
- `lib/types.ts` (now re-exports helpers + permissions for one-stop import)
- `vitest.config.ts` (proper include/exclude globs)
- `vitest.setup.ts` (replaced inline JSX in a .ts file with `React.createElement`)
Removed:
- `lib/actions.ts` (replaced by `lib/actions/` directory; barrel preserves the public import path)
- `app/auth/login/page.tsx` (dead duplicate)
- `components/app-shell.tsx` (unused, hardcoded badges)
Test suite: **202 tests, 7 files, all passing**.
Pass 3 changes UI nothing, schema nothing, business rules nothing. They are pure organisation, test coverage, and dead-code removal.