Mobile responsiveness: several flex rows had a `truncate` child without `min-w-0`, so a long value (member/door name, channel name, published form URL) would not shrink and pushed the row past the viewport on a phone. Added `min-w-0` (with `flex-1`/`shrink-0` where needed) on the dashboard presence + door-self-entry rows, the comms channel list, and the form-builder URL. Door inbound poll: `/api/cron/door-ingest` now polls connections with Promise.allSettled instead of a sequential loop, so one tick is bounded to the slowest single poll rather than the sum of all per-call timeouts; each connection stays isolated and the run is idempotent. Docs: refreshed ARCHITECTURE (door epic marked complete P1-P5), the README access-control feature bullet + project-status pointer, a historical banner on the older AUDIT.md, a door-epic CHANGELOG entry, and the rolling HANDOFF audit log (passes 65-68). No migration; no behavior change for existing users.
474 lines
27 KiB
Markdown
474 lines
27 KiB
Markdown
# Codebase Audit
|
|
|
|
> **Historical snapshot.** This is a full-codebase audit as of 2026-05-14, before the certifications, classes, equipment, Stripe-dues, notifications, member-portal, and door (cards / connections / inbound ingest / API-call buttons) work landed. For audit findings, deploy state, and the running session log AFTER this date, see [HANDOFF.md](./HANDOFF.md) (newest entry on top) — it carries the rolling per-pass security audits.
|
|
|
|
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.
|