A new member who joins via invite code now walks through an admin-defined onboarding sequence before reaching the app. Schema (migration 022, folded into schema.sql Section 14): - space_onboarding_steps: step_key, step_type (welcome|code_of_conduct|profile|payment|content), title, body, config jsonb, is_enabled, is_required, is_system, sort_order, unique per (space, step_key). Full RLS: members read, admin/board write, admin delete non-system. - space_members.onboarding_completed_at + onboarding_progress jsonb. - Trigger seeds four default steps per space (welcome, code of conduct [required], profile, dues). Backfill seeds existing spaces and marks all existing members complete so the new gate never traps them. Flow: - /onboarding (top-level route, outside the (app) group so no redirect loop). Progress bar, step counter, back/continue, per-type rendering. Profile step calls updateMyProfile; payment step is a non-blocking nudge with optional link and "Remind me later". - (app)/layout.tsx redirects to /onboarding while onboarding_completed_at is null. - createSpace marks the founder complete: founders configure the space, they do not run the member flow (matches admin-vs-end-user persona research). - finishOnboarding re-checks required steps server-side so a tampered client cannot skip a required code-of-conduct acknowledgement. skipOnboarding only works when nothing is required. Admin config: - Settings -> Onboarding tab: reorder, edit title/body/payment link, toggle enabled/required, add custom content steps, delete non-system steps. Safety: - components/safe-markdown.tsx renders admin-authored markdown and a sanitized subset of HTML (rehype-raw + rehype-sanitize). Added those two deps. Also: - bio added to updateMyProfileSchema (members can self-set it; not blocked by the migration-015 trigger). - docs/UX_AND_PERSONAS.md: 4 personas and prioritized UX audit findings. Onboarding was finding #1; 2-6 deferred and tracked. 266/266 unit tests pass. Production build clean, /onboarding in the route table. Migration 022 applies idempotently on the next deploy.
58 KiB
Handoff Log
Append-only. Newest entries on top. Keep each entry to one screen.
2026-05-15 (pass 12) — Configurable member onboarding + UX research
Branch: main.
Context carried in
Pass 11's feature batch (forum, comments, tiers, invites, secrets encryption) deployed to production after one landmine: the .gitignore rule secrets/ also matched lib/secrets/, so the first deploy failed on Module not found: '@/lib/secrets/crypto'. Fixed by anchoring the rule to /secrets/ (commit 96a1fb9); redeploy succeeded. Migration 021 is applied in prod (_migrations_applied has 8 rows). SECRETS_ENCRYPTION_KEY is set in /opt/hackerspace-ops/.env.production on the Droplet.
What this pass does
UX research + persona work, a deep UI/UX audit, and the configurable member-onboarding feature.
What shipped
- Migration
scripts/022_onboarding.sql(folded intoschema.sqlSection 14):space_onboarding_steps(step_key, step_type in welcome|code_of_conduct|profile|payment|content, title, body, config jsonb, is_enabled, is_required, is_system, sort_order; unique per space+key).space_members.onboarding_completed_atandonboarding_progress jsonb. Trigger seeds 4 default steps per space; backfill seeds existing spaces and marks all existing members complete (so the new gate does not trap them). RLS: members read, admin/board write, admin delete non-system. lib/actions/onboarding.ts: admin CRUD (createOnboardingStep/updateOnboardingStep/deleteOnboardingStep), member progress (markOnboardingStepDone,finishOnboardingwhich server-side-enforces required steps,skipOnboardingwhich only works when nothing is required). Validations added;bioadded toupdateMyProfileSchema./onboardingroute (top-level, outside the(app)group so no redirect loop).OnboardingFlowclient stepper: progress bar, step counter, back, per-type rendering (welcome/coc/content viaSafeMarkdown, profile form ->updateMyProfile, payment nudge with optional link + "Remind me later"). Gate added inapp/(app)/layout.tsx: redirect to/onboardingwhenonboarding_completed_atis null.createSpacenow setsonboarding_completed_atfor the founder.components/safe-markdown.tsx: rehype-raw + rehype-sanitize. Addedrehype-rawandrehype-sanitizedeps.- Settings -> Onboarding admin tab: reorder (sort number), edit title/body/payment link, toggle enabled/required, add custom content step, delete non-system steps.
docs/UX_AND_PERSONAS.md: 4 personas (founder, new member, operator, casual) and prioritized UX findings. Onboarding was finding #1; findings 2-6 (sidebar icon collision, inconsistent empty states, heading hierarchy, getting-started affordance, role-filtered nav) deferred.
Open items / deferred
- UX findings 2-6 in
docs/UX_AND_PERSONAS.md(icon collision, empty-state component, page-title primitives, admin checklist, role-filtered sidebar). - Custom role labels / custom roles still have no Settings UI (data layer from pass 11 exists).
importMembers/importPaymentsCsvstill lack Zod schemas (pass-10 audit item).
Verification
pnpm test266/266.pnpm buildgreen,/onboardingin the route table.- Migration 022 applies via
deploy.sh's idempotent runner on next deploy.
2026-05-14 (pass 11) — Forum, comments, secrets encryption, custom tiers/invites, logo
Branch: main. Pushed.
What I did
Major feature expansion + security fix surfaced by audit.
Schema migration scripts/021_forum_tiers_roles_invites_secrets.sql, folded into scripts/schema.sql Section 13:
forum_threads(title, body, category, pinned, locked, comment_count, last_comment_at). Full RLS.commentspolymorphic over (forum_thread|proposal|incident|policy) withparent_idfor nesting. Trigger maintainsforum_threads.comment_countandlast_comment_at.space_tiersper-space (slug, name, description, monthly_price_cents, billing_cadence, is_system, sort_order). Backfillsplus/basic/associatefor every existing space and seeds them onINSERT spacesvia trigger.space_members.tier_idFK added and backfilled from the legacy enum.space_role_labels(rename + recolor built-in roles per space).space_custom_roles(non-privileged extra labels).space_member_custom_rolesm2m.space_inviteswithcode,label,expires_at,max_uses,uses_count,is_enabled. Legacyspaces.invite_codebackfilled as a permanent enabled invite.secrets.encrypted_value bytea+encryption_version smallint. Secrets RLS tightened.knowledge_base.render_markdownboolean (default true).comms_channelsINSERT policy: any member of the space can create. UPDATE/DELETE: admin/board OR creator (default channels protected).
Server actions:
lib/actions/forum.ts:createForumThread,updateForumThread(pin/lock moderator-gated),deleteForumThread,addComment,editComment,deleteComment.lib/actions/comms.ts:createChannel,renameChannel,deleteChannel.lib/actions/tiers.ts:createTier,updateTier,deleteTier.lib/actions/roles.ts:upsertRoleLabel,createCustomRole,updateCustomRole,deleteCustomRole,assignCustomRole,unassignCustomRole.lib/actions/invites.ts:createInvite,updateInvite,deleteInvite.lib/actions/secrets.ts: rewritten to use AES-256-GCM at rest. NewrevealSecret(id)returns plaintext only via server action and logssecret.revealedtoactivity_log. NewupdateSecret. List endpoint in/ops/page.tsxno longer selectsvalueorencrypted_value— fixes the screenshot bug where ciphertext was already in the page bundle before "Reveal".lib/secrets/crypto.ts: AES-256-GCM helpers. ReadsSECRETS_ENCRYPTION_KEYenv (64 hex chars). Falls back to plaintext storage if the env var isn't set.
UI:
/forum(index with pinned/locked badges, last-activity sort),/forum/new,/forum/[id].components/comments/CommentThread+loadCommentshelper. Embedded on/proposals/[id],/incidents/[id],/policies/[slug]./commssidebar: inline "+ New channel" form (name + description + type)./opsSecrets tab: Reveal now calls therevealSecretaction and respects encryption./opsKB: row click opens a modal that rendersentry.contentas markdown via react-markdown + remark-gfm./settings: new Tiers tab (list/create/edit/archive/delete custom tiers, inline price edit). New Invites tab (list/create/disable/delete; copy code).- Landing page nav + footer now use
AtlasLogoinstead ofIcoTerminal.public/logo.svgis the favicon (referenced byapp/layout.tsxmetadata). - Sidebar gets a
Forumlink belowComms.
Auth flow: joinSpace honors codes from space_invites first (enforces enabled/expired/max-uses), falls back to legacy spaces.invite_code, and increments uses_count on success.
Env: SECRETS_ENCRYPTION_KEY added to .env.example and to secrets/base.env locally (gitignored).
Validations: added updateSecretSchema, createForumThreadSchema, updateForumThreadSchema, createCommentSchema, updateCommentSchema, createTierSchema, updateTierSchema, upsertRoleLabelSchema, createCustomRoleSchema, updateCustomRoleSchema, createInviteSchema, updateInviteSchema, createChannelSchema.
Open items (deferred for follow-up)
- Custom role labels and custom roles UI in Settings. Data layer + RLS + server actions all in place; UI surface not yet added. Schema rows can be created via PostgREST or psql today.
importMembersandimportPaymentsCsvlack Zod schemas (audit-flagged). Logic still filters on type, but should adopt schemas.- Add
SECRETS_ENCRYPTION_KEYto the droplet's.env.productionbefore the next deploy. Without it, secrets fall back to legacy plaintext storage and the screenshot bug returns. (Update done locally; deploy env update pending.) - Migration 021 must be applied on production. The deploy script runs it idempotently; first deploy after this commit will write
_migrations_appliedrow.
Verification
pnpm test-> 266/266 pass.pnpm build-> green, all new routes in the route table (/forum,/forum/new,/forum/[id]).
2026-05-14 (pass 10) — Production deploy + open-source release prep
Branch: main. Pushed (8ad1ca4).
Self-hosted production deploy completed on a single DigitalOcean Droplet at https://hackerspace.sh. Supabase (db/auth/rest/realtime/kong/meta/studio) runs in Docker with the Postgres data directory on a persistent block volume at /mnt/data/postgres. Caddy 2 terminates TLS with automatic Let's Encrypt for the apex, www, supabase, and studio hostnames. Studio is gated by HTTP basic auth (bcrypt). Daily pg_dumpall cron writes to /mnt/data/backups with 14-day retention. GitHub Actions auto-deploys every push to main over SSH via /opt/hackerspace-ops/deploy.sh which applies pending migrations idempotently (via public._migrations_applied), rebuilds, and restarts the systemd service. Resend SMTP is wired into GoTrue.
Open-source release scaffolding shipped: LICENSE (MIT), CONTRIBUTING.md, SECURITY.md, CHANGELOG.md, docs/WEBHOOKS.md, refreshed README.md and docs/DEPLOYMENT.md. All v0/Vercel references removed from app code; @vercel/analytics dependency dropped. GitHub icon link to the public repo in the landing nav + footer.
Audit fixes in the same commit:
- Webhook signing secret now has show/hide + copy in
/settings; the broken externaldocs.hackerspace.sh/webhookslink replaced withdocs/WEBHOOKS.md. updateSpaceSettingsparameter type now includesmission_statement(was silently dropped)./membersenforcesmember_directory_visibilityfor non-admin viewers.- Mobile responsiveness pass: Projects Kanban, Members + Payments tables (column hiding on narrow), Proposal tally + voting buttons.
2026-05-14 (pass 9) — Datetime fix + configurable areas
Branch: main. Author: Claude (no commits made).
What I did
Two user-reported issues from running the local app:
- "Invalid datetime" error when creating a task with a due_date. HTML
<input type="date">sends"2026-05-22"(date-only) butz.string().datetime()requires full RFC 3339. Eight other schemas had the same latent bug (createProjectSchema.due_date,addMemberSchema.joined_at,logCashPaymentSchema.transaction_date, both proposal voting timestamps,createPolicySchema.effective_at,supersedePolicySchema.effective_at,meetingMinutesSchema.meeting_date). - Areas were hardcoded constants in
tasks-client.tsx(9 strings) andprojects-client.tsx(9 different strings). Not configurable per-space. No defaults seeded for new spaces.
Fix 1: shared flexibleDateTime preprocessor
lib/validations.ts exports flexibleDateTime(). A z.preprocess that maps ""/null/undefined → null, passes through full RFC 3339 unchanged, normalises YYYY-MM-DD and YYYY-MM-DDTHH:MM through new Date(v).toISOString(), and leaves unparseable input for Zod to reject. Wired into all nine date fields. The original bug screenshot is gone: "2026-05-22" validates and inserts as "2026-05-22T00:00:00.000Z". Added 7 new tests.
Fix 2: space_areas table + Settings UI
Migration scripts/020_areas.sql:
space_areastable:(id, space_id, code, name, icon, sort_order, is_archived). UNIQUE per(space_id, code)and(space_id, name).- Sort index
(space_id, sort_order, name). - RLS: members read, admin/board write, admin delete.
seed_default_areas()trigger fires after spaces INSERT, seeding ten defaults: 3D Printing, Electronics, Woodshop, Laser, Metal Shop, Facilities, Software, Kitchen, Admin, General. Codes are stable slugs; names are display labels.- Backfill populates every existing space that has no areas. Idempotent.
- Folded into
scripts/schema.sqlas Section 12.
Library surface:
lib/types.ts:SpaceAreatype.lib/validations.ts:createAreaSchema,updateAreaSchema(slug regex, length caps).lib/actions/areas.ts:createArea,updateArea,deleteArea. UserequireMemberWithRole(ADMIN_ROLES). Re-exported via the barrel.
UI wiring:
app/(app)/tasks/page.tsxandapp/(app)/projects/page.tsxfetchspace_areasin parallel and passareas: string[]to the client.tasks-client.tsxandprojects-client.tsxaccept the new prop; old hardcoded arrays renamedDEFAULT_AREASas a fallback.app/(app)/settings/page.tsxfetches the full area list and passes toSettingsClient.settings-client.tsx: new Areas card on the Space tab. Lists every area with inline-editable name + sort_order, archive/restore toggle, admin-only delete with confirm prompt. "Add area" form takes code + display name.
Verification (live local stack)
- Migration applied to the running Postgres. 20 area rows across 2 spaces (Demo Hackerspace + the user's HeatSync Labs space created earlier through the UI), 10 each.
- Test user fetched
space_areasvia REST with their JWT; got the 10 defaults sorted bysort_order. RLS clean, no cross-tenant leak. - 266 tests across 8 files (was 259; +7 for flexibleDateTime).
pnpm buildclean, 31 routes.- Dev server still running, hot-reloaded the changes.
Files changed
Added:
scripts/020_areas.sql
lib/actions/areas.ts
Edited:
lib/validations.ts (flexibleDateTime + area schemas)
lib/types.ts (SpaceArea)
lib/actions/index.ts (export areas)
scripts/schema.sql (Section 12)
scripts/README.md (020 listed)
app/(app)/tasks/page.tsx (fetch + pass areas)
app/(app)/tasks/tasks-client.tsx (accept areas prop)
app/(app)/projects/page.tsx (fetch + pass areas)
app/(app)/projects/projects-client.tsx (accept areas prop)
app/(app)/settings/page.tsx (fetch areas)
app/(app)/settings/settings-client.tsx (Areas management card)
__tests__/validations.test.ts (7 new tests)
types/database.ts (regenerated; includes space_areas)
docs/HANDOFF.md (this entry)
In the browser: /tasks → New Task → due_date 2026-05-22 saves cleanly. /settings → Space tab → scroll to Areas → add a custom area; it appears in the Tasks and Projects dropdowns on next render.
2026-05-14 (pass 8) — Local stack verified end-to-end, seed + LOCAL_DEV docs
Branch: main. Author: Claude (no commits made).
What I did
Brought the system from "passes tests + builds" to "runs against a real Supabase stack on this machine, end-to-end."
- Installed Supabase CLI via
brew install supabase/tap/supabase. Started a local stack (supabase start) — Postgres, GoTrue, PostgREST, Realtime, Storage, Studio, Mailpit all up in Docker. - Applied
scripts/schema.sqlfrom scratch against the empty local DB. 18 tables created. - Wrote
scripts/seed.sqland applied it: 1 demo space, 5 offline members across all roles, 1 active code-of-conduct policy v1, 3 tasks, 1 project, 1 KB entry, 3 proposals (open / draft / decided) with 7 votes already cast, 1 open incident, 8 activity-log rows. - Migration 019 (
scripts/019_proposal_expiry.sql):public.expire_proposals()flips open proposals past their deadline todecided(if quorum met) orexpired. Schedules itself hourly viapg_cronif the extension is enabled. Folded intoschema.sqlas Section 11. - Wrote
docs/LOCAL_DEV.md: 8-step quickstart, wipe + re-seed recipe, troubleshooting section. - Regenerated
types/database.tsfrom the live local DB. 1696 lines, 18 tables, 1 view, 3 RPC functions. - Wrote
.env.localwith the local Supabase URL and keys. - Started
pnpm dev. Health endpoint returns 200, public routes return 200,/dashboardcorrectly 307s to/login.
Verification done with real data
- Anon-key RLS gate confirmed (empty array on no-session).
- Service-role bypass confirmed.
- Created
[email protected]via Auth Admin API, signed in via/auth/v1/token, got a real ES256 JWT, queried proposals + channels + policies — all RLS-scoped, all returning seeded data. - Privilege-escalation guard (migration 015): simulated a plain-member self-promotion via JWT-claimed authenticated role. Postgres rejected with the exact
Members cannot change their own role, tier, status, approval, card access, or space.message. expire_proposals()runs cleanly, returns 0.- HTML smoke:
/logintitle correct,/signupcontains expected copy. - 259 tests across 8 files passing.
Files changed
Added:
supabase/config.toml (from supabase init)
scripts/019_proposal_expiry.sql
scripts/seed.sql
docs/LOCAL_DEV.md
.env.local (gitignored)
Edited:
scripts/schema.sql (Section 11)
types/database.ts (regenerated)
2026-05-14 (pass 7) — Tier 2/3 UI closure + Markdown + proxy rename
Branch: main
Author: Claude (no commits made).
What I did
Finishing the user-facing surface for the schema work passes 5 and 6 added.
- Renamed
middleware.ts→proxy.tsfor Next.js 16. Renamed the exportedmiddlewarefunction toproxy. The deprecation warning that the build had been emitting since pass 4 is now gone. - Markdown rendering: installed
react-markdown+remark-gfm. Createdcomponents/markdown.tsx(MarkdownBody) with safe defaults — HTML escaped, norehypeRaw, every link getsrel="noopener noreferrer"andtarget="_blank". Wired it into the three governance detail pages so proposal bodies, incident bodies, incident dispositions, incident updates, and policy formal/plain text now render headings, lists, links, code, blockquotes, tables, strikethrough. /profileroute (Tier 3.1 + 3.2 UI closure):- Server page reads the caller's
space_membersrow. ProfileForm(client) lets a member edit display_name, handle, phone, skills, interests, willing_to. Uses a tiny customChipInputcomponent for tag-style multi-value fields withEnter/,to commit andBackspaceto delete.AffiliationsForm(client) lets the member disclose COI affiliations. Submit stampscoi_last_disclosed_atviadiscloseAffiliations. Surfaces an orange callout for privileged-role members who haven't disclosed yet.- The
willing_tofield ships with suggestion chips:board_candidate,treasurer_candidate,host_volunteer,area_lead_candidate,event_organizer,safety_committee,docs_steward. Members can also free-form.
- Server page reads the caller's
/recruitmentroute (Tier 3 succession view):- Board / admin only (server-side redirect for others).
- Queries
space_membersfor the current space; groups by eachwilling_totag; renders one card per tag with the willing members listed underneath. - For privileged-role members (admin / board / treasurer) the card shows a "COI on file" or "no COI" badge based on
coi_last_disclosed_at. - A separate "Skills declared (no role opt-in)" section catches members who declared skills without willing_to.
- Settings extensions (Tier 2.1 + Tier 2.2 + mission statement):
- Added
mission_statementfield toupdateSpaceSettingsSchema(max 5000 chars, nullable). - The Space tab on
/settingsnow has a Mission Statement textarea inside the existing space-settings form. - A new Visibility card on the Space tab exposes two dropdowns:
financial_visibility(3 options) andmember_directory_visibility(4 options). Saves via the existingupdateSpaceVisibilityaction from pass 6.
- Added
- Mission statement display in the app shell: when
space.mission_statementis set, the app layout renders it in a small italic footer on every protected page — the "stated-mission visibility on every page" principle fromdocs/GOVERNANCE_FEATURES.mdsection 1. - Dashboard widget: above the main grid, two side-by-side cards.
- Open proposals awaiting your vote: queries open proposals, joins against the caller's
proposal_votesto filter out ones they've already voted on, shows up to 5 with the voting-window close timestamp and a live quorum percentage. - Incidents needing response (admin/board only): shows
receivedandunder_reviewincidents, sorted newest first, with a red "respond by" date if the SLA has elapsed. - The whole section only renders if at least one of the two has content.
- Open proposals awaiting your vote: queries open proposals, joins against the caller's
- Sidebar: added "My profile" (
UserCircleicon) and admin-gated "Recruitment" (UserSearchicon) under the People group. - 4 new tests in
__tests__/governance.test.tsfor the mission_statement extension toupdateSpaceSettingsSchema. Total now 259 across 8 files.
Verification
pnpm vitest run→ 259 tests pass across 8 files.pnpm build→ success. Build emits 31 routes (added/profile,/recruitment).- The middleware-deprecation warning is gone. Only remaining build warning is the stray lockfile at
/Users/obsidian/Projects/package-lock.json, which is outside the repo and harmless on DO.
Open items, priority order
- Apply pending migrations on production (015, 017, 018) if not already done. Fresh deploys get them all via
scripts/schema.sql. - Regenerate
types/database.tswith the Supabase CLI to clear the ~217 TS errors thattypescript.ignoreBuildErrorsmasks. Once clean, removeignoreBuildErrorsso future drift surfaces in CI. - Configure OAuth providers in Supabase if you want the GitHub / Google buttons to work.
- Auto-expire open proposals: when
voting_closes_at < now(), status should auto-flip fromopentoexpired. Currently manual via the admin's "Mark decided" button. A cron-triggered server action would close this. - Mark KB entry as meeting minutes: schema (column added in 018) and validation schema in place, but no UI control yet.
- Notifications on incident status change: activity_log row written but no in-app comms message or email triggered. Add when notification infra is ready.
- Hosting calendar, sponsor / partner registry, community surveys: Tier 4, deferred indefinitely until adoption signals demand.
- Secrets encryption at rest:
secrets.valueandintegrations.configstill plain text. Address before storing real production credentials.
Files changed this pass
Added:
proxy.ts (replaces middleware.ts)
components/markdown.tsx
app/(app)/profile/page.tsx
app/(app)/profile/profile-form.tsx
app/(app)/profile/affiliations-form.tsx
app/(app)/profile/chip-input.tsx
app/(app)/recruitment/page.tsx
Removed:
middleware.ts (renamed to proxy.ts)
Edited:
app/(app)/layout.tsx (mission statement footer)
app/(app)/dashboard/page.tsx (governance widget)
app/(app)/settings/settings-client.tsx (mission statement + visibility section)
app/(app)/proposals/[id]/page.tsx (MarkdownBody)
app/(app)/incidents/[id]/page.tsx (MarkdownBody, ×3)
app/(app)/policies/[slug]/page.tsx (MarkdownBody, ×2)
components/app-sidebar.tsx (Profile, Recruitment links)
lib/validations.ts (mission_statement on updateSpaceSettingsSchema)
__tests__/governance.test.ts (4 new tests)
package.json (react-markdown, remark-gfm)
docs/HANDOFF.md (this entry)
How to verify locally
pnpm install # picks up react-markdown + remark-gfm
pnpm vitest run # 259 tests, 8 files
pnpm build # 31 routes, no middleware deprecation warning
In the browser:
- Visit
/profile, edit skills / willing_to (chip input), save. - Visit
/recruitmentas admin/board — see members grouped by willing_to. - Visit
/settings, scroll to "Visibility", change financial_visibility totreasurer_only, save, then visit/financialsas a plain member — should be blocked. Switch back toall_members_visible— visible again. - Set a mission statement on
/settings. It appears as a footer on every protected page. - Open a proposal, edit body to include
# heading\n\n- list\n- items\n\n[link](https://example.com). Renders rich.
2026-05-14 (pass 6) — Audit fixes + Tier 2/3 partial-ship
Branch: main
Author: Claude (no commits made).
Audit findings
Read my pass-5 work hard. Four real defects, plus several lower-severity items I documented but didn't change.
Critical (fixed in migration 017):
proposal_votes.votes_insertaccepted a vote wheremember_idwas aspace_membersrow from space A but the proposal was in space B, because the policy never joined them. Fixed: insert WITH CHECK now requiresm.space_id = p.space_idfor the joined member.votes_updatehad the same bug. Same fix.incidents.incidents_insertlet the reporter setreporter_idto anyspace_members.idthey owned — across spaces. Fixed: WITH CHECK requires the row'sspace_idmatch the incident's.incident_updates.incident_updates_insertdid not validateauthor_idat all. Fixed: WITH CHECK requires the author is the calling user and in the incident's space.
Plus: added partial unique index policies_one_active_per_slug ON policies (space_id, slug) WHERE status = 'active' so two admins concurrently activating different drafts cannot leave two active rows.
Lower-severity (documented, deferred):
requireMember().single()makes multi-space membership effectively impossible. The app design has always assumed one membership per user.appealIncidentdoes insert-then-update without a transaction; mid-flight failure leaves an orphaned proposal. Acceptable risk.supersedePolicyrace between two admins computingversion + 1simultaneously is caught by the existingUNIQUE (space_id, slug, version).- Reporter can post
visibility='board_only'on their own incident_update via raw API and then can't read it. Self-inflicted. /proposalsand/incidentslists don't paginate. Defer.- Incident detail never shows the reporter's name to admin/board. Future ergonomic improvement.
- Dashboard doesn't surface open proposals or pending incidents.
Built in this pass (audit fixes + Tier 2/3 schema + one Tier 2 UI)
- Migration 017
scripts/017_governance_rls_hardening.sql: the four RLS fixes + the partial unique index. Mirrored intoscripts/schema.sql. - Migration 018
scripts/018_member_state_and_visibility.sql:space_membersgainsskills,interests,willing_to,affiliations(alltext[]) andcoi_last_disclosed_at. GIN indexes onskillsandwilling_to.spacesgainsfinancial_visibilityandmember_directory_visibilityenums.knowledge_basegainsis_meeting_minutesandmeeting_date(partial index).schedule_card_reviewtrigger inserts a 180-day task wheneverhas_card_accessflips on.inactive_membersview: members in good standing with noactivity_logentry in 180 days.
- New server actions:
members.ts → updateMyProfile({...}): member self-edit of display_name, handle, phone, skills, interests, willing_to. The privilege-escalation trigger from migration 015 keeps role/tier/status off-limits.members.ts → discloseAffiliations({ affiliations }): stampscoi_last_disclosed_atand logs to activity_log.settings.ts → updateSpaceVisibility({...}): admin-only.
- Validation schemas:
updateMyProfileSchema,discloseAffiliationsSchema,updateSpaceVisibilitySchema,meetingMinutesSchema, plus exported tuples (financialVisibilities,directoryVisibilities). - 13 new tests in
__tests__/governance.test.ts. Total now 255 across 8 files. /financialsroute: server-component aggregations frompayments(this month, this year, lifetime, linked vs unlinked, by platform with sparkline-style bars, last 12 months). Gated byspace.financial_visibilityagainst caller's role.- Sidebar: added "Financials" under People with the
LineCharticon.
Verification
pnpm vitest run→ 255 tests pass across 8 files.pnpm build→ success. 29 routes emit (added/financials).pnpm exec tsc --noEmit→ same Supabase generic-drift count as before. No new error categories.
Deployment steps for this pass
Run on every existing database, in order:
\i scripts/017_governance_rls_hardening.sql
\i scripts/018_member_state_and_visibility.sql
Then restart PostgREST (docker compose exec rest restart on self-hosted; managed Supabase auto-refreshes).
Fresh deploys get both via scripts/schema.sql (sections 9.8 and 10). No additional config needed; new columns ship with sensible defaults.
Open items, priority order
- Apply 017 on production immediately if any chance of multi-space membership (admin client could have set it up).
- Apply 018 for the Tier 2/3 columns and the card-review trigger.
- Profile / COI / settings-visibility UI: server actions are in place, no UI yet. A
/profileroute + extending Settings would close Tier 3.1, 3.2 and the financial-visibility toggle. - Recruitment view: query
space_membersbywilling_tofor board-only succession planning. - Mark KB entry as meeting minutes from UI: schema ready, no UI control.
- Earlier-pass items still open: regenerate
types/database.ts, run migration 015 if not already, renamemiddleware.ts→proxy.tsfor Next.js 16.
Files changed this pass
Added (schema):
scripts/017_governance_rls_hardening.sql
scripts/018_member_state_and_visibility.sql
Added (UI):
app/(app)/financials/page.tsx
Edited:
scripts/schema.sql (sections 9.8 + 10)
scripts/README.md (017, 018 listed)
lib/types.ts (FinancialVisibility + DirectoryVisibility)
lib/validations.ts (4 new schemas + tuples)
lib/actions/members.ts (updateMyProfile, discloseAffiliations)
lib/actions/settings.ts (updateSpaceVisibility)
components/app-sidebar.tsx (Financials nav link)
__tests__/governance.test.ts (13 new tests)
docs/GOVERNANCE_FEATURES.md (Tier 2 + 3 marked partial-shipped)
docs/HANDOFF.md (this entry)
2026-05-14 (pass 5) — Tier 1 governance kernel shipped
Branch: main
Author: Claude (no commits made).
What I did
Built the full Tier 1 governance kernel from docs/GOVERNANCE_FEATURES.md. Three new tightly integrated modules: proposals, incidents, policies. Schema + server actions + validations + tests + UI + sidebar nav.
- Schema:
scripts/016_governance_kernel.sql(and mirrored intoscripts/schema.sqlas Section 9). 5 new tables (proposals,proposal_votes,incidents,incident_updates,policies), 8 new enums, 6 new columns onspacesfor governance defaults (quorum percent/floor, voting window, incident SLA, mission statement). Triggers:compute_proposal_quorum,refresh_proposal_tally,compute_incident_sla,touch_updated_at. Full RLS: any member proposes / files / reads; admin+board transition status; votes are immutable post-window; incidents readable only to reporter + admin/board. Idempotent re-run safe (DROP POLICY IF EXISTS,CREATE OR REPLACE,EXCEPTION WHEN duplicate_object). - Types in
lib/types.ts: hand-typedProposal,ProposalVote,Incident,IncidentUpdateRow,Policyand all 8 governance enums.types/database.tsshould be regenerated via Supabase CLI to fold these in. - Validations in
lib/validations.ts: 11 new Zod schemas (createProposalSchema,castVoteSchema,decideProposalSchema,withdrawProposalSchema,openProposalSchema,fileIncidentSchema,updateIncidentStatusSchema,addIncidentUpdateSchema,appealIncidentSchema,createPolicySchema,supersedePolicySchema,updatePolicyStatusSchema). Tested before being wired into actions (test-driven). - Server actions:
lib/actions/proposals.ts,lib/actions/incidents.ts,lib/actions/policies.ts. Use the existingrequireMember/requireMemberWithRole/parseInput/logActivityhelpers. Re-exported via the barrel inlib/actions/index.ts. Notable:fileIncidentgenerates an opaquereporter_tokenwhen anonymous;appealIncidentatomically spawns a draftmembership_voteproposal and flips the incident toappealed;supersedePolicyinserts a new version withprior_version_idset rather than mutating the prior row;updatePolicyStatus(... , 'active')atomically supersedes any prior active version with the same slug. - Tests:
__tests__/governance.test.tsadds 40 tests covering every new Zod schema (valid input, invalid enum values, required field enforcement, recusal-requires-reason cross-field validation, slug character set, etc). The cross-cutting "governance contract surface" suite enforces that the enum tuples we export match the DB enums in workflow order. - UI: server-component-driven, minimal client islands.
/proposals: open / draft / archive sections with live tally and quorum hints./proposals/[id]: title + body + status, tally panel with quorum progress, vote form (yes/no/abstain/recused-with-required-reason), management buttons (open / withdraw / decide) gated by role, vote list with comments and recusal reasons./proposals/new: form with type, threshold, "open immediately" checkbox./incidents: open / decided / closed sections. Members see only their own; admin/board sees all (RLS enforced)./incidents/[id]: title + body + disposition + update thread, status transition form (admin/board), post-update form (everyone with access), one-click "request membership appeal" for reporter on dismissed incidents./incidents/new: title, body, category, severity, optional subjects multi-select, anonymous checkbox with honest disclaimer; on anonymous submit, surfaces the opaque tracking token to the reporter once./policies: one row per slug showing the active (or latest) version./policies/[slug]: plain-language + formal text + version history, with "activate" / "supersede" / "deprecate" buttons for admins./policies/new: slug + title + section ref + plain + formal text. Admin-only route.
- Sidebar: new "Governance" section with three links (Proposals, Incidents, Policies). Lucide icons:
Vote,ShieldAlert,ScrollText. - Docs updated:
docs/GOVERNANCE_FEATURES.md: Tier 1 marked SHIPPED with implementation pointers.scripts/README.md: migration 016 listed.- This handoff entry.
Verification
pnpm vitest run→ 242 tests pass across 8 files (was 202 / 7). The 40 new tests are in__tests__/governance.test.ts.pnpm build→ success. 9 new routes emit cleanly (/proposals,/proposals/[id],/proposals/new,/incidents,/incidents/[id],/incidents/new,/policies,/policies/[slug],/policies/new). 28 routes total.pnpm exec tsc --noEmit→ 217 errors (was 157). All 60 new errors are the same Supabase generic-drift pattern (Property 'X' does not exist on type 'never') that the pre-existing 157 are. Runtime is unaffected. Regeneratingtypes/database.tswith the Supabase CLI clears them all.
Open items, priority order
- Apply
scripts/016_governance_kernel.sqlto any existing production database. Fresh deploys get it viascripts/schema.sqlautomatically. - Regenerate
types/database.tswithsupabase gen types typescript --project-id <ref>. Clears the 60 + 157 = 217 TS errors. Then removetypescript.ignoreBuildErrorsfromnext.config.mjs. - Pass 4 priority 1 still applies: run
scripts/015_prevent_member_self_role_change.sqlbefore external users sign in. - Auto-expiry of open proposals: when
voting_closes_at < now(), the status should flip fromopentoexpired(ordecidedif board acts). Currently manual. A cron-triggered server action or DB job can do this. - Markdown rendering: proposals' body is currently rendered as
<pre>whitespace-preserved text. Pulling inreact-markdownwould give links, lists, and the[policy:slug#section]citation syntax from the roadmap doc. Defer until adoption is real. - No notifications on incident status change. The activity_log row is inserted but no in-app comms message or email is sent. Add later when notification infra is ready.
- Rename
middleware.ts→proxy.tsfor Next.js 16 (still works, deprecation warning only).
Decisions pending
- Whether to back-port Tier 2 features (skill/interest fields, COI disclosure, mid-tenure card review trigger, financial dashboard visibility settings) in the next pass.
- Whether to add markdown rendering with citation hover-cards now or wait.
- Whether to add a worker / cron to auto-flip expired proposals.
Files changed this pass
Added (schema):
scripts/016_governance_kernel.sql
Added (lib):
lib/actions/proposals.ts
lib/actions/incidents.ts
lib/actions/policies.ts
Added (tests):
__tests__/governance.test.ts
Added (UI):
app/(app)/proposals/page.tsx
app/(app)/proposals/proposal-badges.tsx
app/(app)/proposals/new/page.tsx
app/(app)/proposals/new/new-proposal-form.tsx
app/(app)/proposals/[id]/page.tsx
app/(app)/proposals/[id]/proposal-actions.tsx
app/(app)/incidents/page.tsx
app/(app)/incidents/new/page.tsx
app/(app)/incidents/new/incident-form.tsx
app/(app)/incidents/[id]/page.tsx
app/(app)/incidents/[id]/incident-actions.tsx
app/(app)/policies/page.tsx
app/(app)/policies/[slug]/page.tsx
app/(app)/policies/[slug]/policy-actions.tsx
app/(app)/policies/new/page.tsx
app/(app)/policies/new/new-policy-form.tsx
Edited:
scripts/schema.sql (added Section 9: governance kernel)
scripts/README.md (listed migration 016)
lib/types.ts (governance entity types + enum unions)
lib/validations.ts (11 new Zod schemas + enum exports)
lib/actions/index.ts (re-export the three new modules)
components/app-sidebar.tsx (Governance section with 3 links)
docs/GOVERNANCE_FEATURES.md (Tier 1 marked SHIPPED)
docs/HANDOFF.md (this entry)
Deployment steps for this pass
On any existing database:
-- In Supabase SQL editor or psql:
\i scripts/016_governance_kernel.sql
For a fresh deploy, the canonical scripts/schema.sql already includes it (Section 9). No app-level config changes needed; the new spaces columns ship with sensible defaults (default_quorum_percent=10, default_quorum_floor=1, default_voting_window_hours=216 (9 days), default_threshold='simple_majority', incident_sla_hours=72).
After applying, restart PostgREST (docker compose restart rest on self-hosted) so the schema cache refreshes.
2026-05-14 (pass 4) — Deploy readiness + self-hosted DO guide
Branch: main
Author: Claude (no commits made).
What I did
- Verified deploy readiness end to end:
pnpm vitest run: 202 tests pass across 7 files.pnpm build: success. 19 routes emitted (including/api/health,/api/paypal/sync,/auth/callback,(landing) /, all(app)/*routes).pnpm exec tsc --noEmit: 157 errors, all pre-existing supabase-js generic drift, all masked bytypescript.ignoreBuildErrors: trueinnext.config.mjs. No new errors introduced.
- Fixed one self-inflicted test bug:
__tests__/auth-helpers.test.tswas written in pass 3 using the oldr.erroraccess pattern, which does not narrow now thatResult<T>is a tagged union (ok: true | false). Rewrote each test to gate onif (r.ok)/if (!r.ok)before readingdataorerror. - Surfaced two non-blocking build warnings that the user should know about:
- Next.js 16 deprecates the
middlewarefile convention in favour ofproxy. Build still works withmiddleware.ts; rename toproxy.tswhenever convenient. - Next.js detected a stray
/Users/obsidian/Projects/package-lock.jsonoutside the repo (workspace root inference). Local-only nuisance, does not affect CI or DO builds.
- Next.js 16 deprecates the
- Wrote
docs/DEPLOY_DO_SELFHOSTED.md: complete end-to-end guide for running the entire stack on one DigitalOcean Droplet with self-hosted open-source Supabase. 16 sections, ~700 lines, copy-pasteable commands throughout. Covers:- Architecture and cost (~$48/mo for 8 GB Droplet).
- Droplet provisioning, hardening, Docker/Nginx/Certbot install.
- DNS records for
yourdomain.com,supabase.yourdomain.com,studio.yourdomain.com. - Self-hosting Supabase: cloning
supabase/supabase, generatingJWT_SECRET/ANON_KEY/SERVICE_ROLE_KEYvia a Node one-liner, full.envtemplate, port-remap override (Studio defaults to 3000 and collides with the Next.js app; doc rebinds it to 3001 and forces all internal ports to127.0.0.1to keep them off the public internet). - Applying
scripts/schema.sql,014_*.sql,015_*.sqlviadocker compose exec db psql. - Building the app image with
NEXT_PUBLIC_*build args (otherwise the client bundle ships withundefinedfor them — common gotcha). - Nginx server blocks for app, Supabase API, and basic-auth-gated Studio.
- Let's Encrypt SSL covering all four hostnames.
- Post-deploy verification including a regression test for migration 015's privilege-escalation guard.
- Daily
pg_dumpbackups with 14-day local rotation and an off-site path to DO Spaces. - Operations: app upgrades, incremental schema migrations (with
docker compose restart restto refresh PostgREST's schema cache), Supabase version upgrades. - Hardening checklist (14 items) and a troubleshooting section covering the eight problems people actually hit.
- Linked the new doc from
README.md(deployment table + body section) and from the top ofdocs/DEPLOYMENT.md. Made it explicit thatDEPLOYMENT.mdcovers managed-Supabase paths andDEPLOY_DO_SELFHOSTED.mdcovers full self-hosting.
Deployment quick reference (so the next session does not have to re-read everything)
| Path | When to use | Guide |
|---|---|---|
| Vercel + managed Supabase | Fastest path, no infra to manage | docs/DEPLOYMENT.md section 4 |
| DO App Platform + managed Supabase | Want DO but not infra | docs/DEPLOYMENT.md section 5, spec at .do/app.yaml |
| DO Droplet + managed Supabase | Container-only on your VPS, Supabase Cloud for DB | docs/DEPLOYMENT.md section 6 |
| DO Droplet + self-hosted Supabase | Full data ownership, single Droplet | docs/DEPLOY_DO_SELFHOSTED.md |
Migrations to run on every fresh deploy, in order:
scripts/schema.sql(canonical, idempotent)scripts/014_member_user_id_nullable.sqlscripts/015_prevent_member_self_role_change.sql(security; do not skip)
Open items, priority order
- Run migration 015 on any live database before external users sign in. Without it, any authenticated member can
UPDATE space_members SET role = 'admin'on their own row via direct PostgREST. - Rename
middleware.tstoproxy.tsto clear the Next.js 16 deprecation. Single-file rename; the export and matcher contract is unchanged. - Regenerate
types/database.tsviasupabase gen types typescript --project-id <ref>(managed) or against the self-hosted Postgres. Clears the 157 TS errors. Then droptypescript.ignoreBuildErrors: truefromnext.config.mjsso future type drift surfaces in CI. - Wire OAuth providers in Supabase dashboard (GitHub, Google). The client code already calls
supabase.auth.signInWithOAuth; only the provider configuration is missing. - Encrypt
secrets.valueandintegrations.configbefore storing real production credentials. - Stale docs:
DB_SCHEMA_MAP.mdanddocs/ARCHITECTURE.mdstill claim 4 default channels. Schema creates 3 (general,announcements,ops). signUptrigger metadata mismatch inscripts/schema.sql(handle_space_signupreadsspace_idbut the app never passes it). Benign; trigger short-circuits. Either remove the trigger or rewire it.
Decisions pending from the user
- Whether to rename
middleware.ts→proxy.tsnow or wait for a future Next.js major version that removes the alias. - Whether to regenerate
types/database.tsagainst managed Supabase (supabase gen typesrequires the project ref) or wait until self-hosting is up and run it against that. - Whether to commit the pass 1–4 changes as one large commit, four sequenced commits matching the pass boundaries, or branch-and-PR per concern.
How to verify locally
pnpm install
pnpm vitest run # 202 tests, 7 files
pnpm build # success in ~3s; warnings about middleware/lockfile are non-blocking
pnpm exec tsc --noEmit 2>&1 | grep -v never | wc -l # remaining non-Supabase errors should be 0
Files changed this pass
Added:
docs/DEPLOY_DO_SELFHOSTED.md
Edited:
__tests__/auth-helpers.test.ts
README.md
docs/DEPLOYMENT.md
docs/HANDOFF.md
No application code, no schema, no UI, no business rules were changed in pass 4. The pass added a major deployment guide and confirmed the work from passes 1–3 produces a clean build and a green test suite.
2026-05-14 (pass 3) — Architecture refactor + real tests
Branch: main
Author: Claude (no commits made).
What I did
- Split
lib/actions.ts(914 lines, 29 functions) into 10 domain files underlib/actions/. Each file owns one entity:tasks.ts,projects.ts,members.ts,contacts.ts,payments.ts,knowledge-base.ts,secrets.ts,area-leads.ts,settings.ts,imports.tslib/actions/index.tsre-exports everything viaexport * from './foo'- Every client import
from '@/lib/actions'still resolves correctly.
- Created
lib/permissions.ts: role constants (ROLES,ADMIN_ROLES,TREASURER_ROLES,ACTIVE_STATUSES),Roletype alias,hasRole(role, allowed)helper. Removes magic-string role chains. - Created
lib/auth-helpers.ts:getAuthMember,requireMember,requireMemberWithRole,parseInput,logActivity. Result types use the canonicalok: booleandiscriminator so TypeScript narrows onif (!r.ok)reliably. - Wired validation into every server action that has a schema (was previously only
createTaskandclaimTask). 18 more actions now reject bad input at the boundary instead of hoping the database rejects it. - Expanded
lib/types.tswith re-exports forRole,Member,MemberSummary,Result,MemberResult,ActionResult, and the role/permission constants. One-stop import for typed work. - Deleted dead code:
app/auth/login/page.tsx(duplicate login linking to nowhere),components/app-shell.tsx(unused, hardcoded badge counts). - Wrote real tests: 76 new tests across four files exercise real project code instead of inline assertions.
__tests__/validations.test.ts: 37 tests verifying every Zod schema accepts real DB enums and rejects legacy ones.__tests__/permissions.test.ts: 10 tests for role constants andhasRole.__tests__/security.test.ts: 25 tests forsanitize*,escapeHtml,checkRateLimit,isValidUuid.__tests__/auth-helpers.test.ts: 4 tests forparseInput.- Full suite: 202 tests, 7 files, all passing.
- Fixed vitest config:
vitest.setup.tshad inline JSX in a.tsfile (esbuild refuses). Replaced withReact.createElement.vitest.config.tswas picking up Playwright e2e specs. Addedinclude: ['__tests__/**/*.{test,spec}.{ts,tsx}']andexclude: ['e2e/**'].
- Added
scripts/README.mddocumenting the migration convention (canonicalschema.sql+ numbered incremental files, idempotency rules, security baseline, verification queries).
Open items, priority order
- TypeScript drift between
@supabase/supabase-jsgenerics and ourDatabasetype: 38 errors inlib/actions/*of the form'X' is not assignable to parameter of type 'never'. These existed in the originallib/actions.tstoo (the build masks them vianext.config.mjs: typescript.ignoreBuildErrors: true). Fix is to regeneratetypes/database.tswith the latest supabase CLI so the generatedDatabaseshape matches what@supabase/ssrexpects. - 103 app/ TypeScript errors in client components (
comms-client.tsx,members-client.tsx, etc). Pre-existing v0 codegen output; same root cause as item 1. Untouched in this pass to avoid UI regressions. getCurrentMemberwas removed fromlib/actions.tswhen I split it; no caller referenced it. If anything starts to need it, define a server action inlib/actions/index.tsor usegetAuthMemberfromlib/auth-helpers.ts.- Pass 2 priority 1 (run
scripts/015_*.sqlon production) is still outstanding until the user runs it. signUptrigger metadata mismatch remains documented but unfixed.- Stale docs:
DB_SCHEMA_MAP.mdanddocs/ARCHITECTURE.mdsay 4 default channels. Schema creates 3.
Decisions pending
- Whether to regenerate
types/database.tsvia the Supabase CLI to clear the remaining 141 TS errors. - Whether to also run
next.config.mjswithouttypescript.ignoreBuildErrors: trueonce types are clean. - Whether to add CI to run
pnpm vitestandpnpm exec tsc --noEmiton every push.
Files changed this pass
Added:
lib/permissions.ts
lib/auth-helpers.ts
lib/actions/tasks.ts
lib/actions/projects.ts
lib/actions/members.ts
lib/actions/contacts.ts
lib/actions/payments.ts
lib/actions/knowledge-base.ts
lib/actions/secrets.ts
lib/actions/area-leads.ts
lib/actions/settings.ts
lib/actions/imports.ts
lib/actions/index.ts
__tests__/validations.test.ts
__tests__/permissions.test.ts
__tests__/security.test.ts
__tests__/auth-helpers.test.ts
scripts/README.md
Edited:
lib/types.ts
vitest.config.ts
vitest.setup.ts
Removed:
lib/actions.ts (replaced by lib/actions/ directory)
app/auth/login/page.tsx (dead duplicate)
components/app-shell.tsx (dead, unused)
How to verify locally
pnpm install
pnpm vitest run
# expect: 202 tests passing across 7 files
2026-05-13 (pass 2) — Application repairs
Branch: main
Author: Claude (no commits made).
What I did
- Fixed dashboard "Quick Chores" bug (
app/(app)/dashboard/page.tsx:49). The filter was.neq('status','done')butcompleteTask()writes'completed'. Completed tasks were leaking into the open list. Replaced with a positive filter on the open statuses. - Fixed signup copy: said default channels were "general, announcements, random". Schema creates "general, announcements, ops". Aligned.
- Fixed
lib/validations.tsenum drift: nine schemas had values that did not match the database. Now aligned: task type and recurrence, project status, member role, contact type, KB visibility, cash payment shape, settings, area lead. - Wired validation into high-risk server actions:
createProject,updateProjectStatus,addMember,logCashPayment,linkPaymentToMember. Other actions still pass rawformData; documented indocs/AUDIT.mdsection 4.4. - Patched privilege escalation in
space_members: added aBEFORE UPDATEtrigger (prevent_member_self_role_change) that rejects self-updates ofrole,tier,status,approved,has_card_access,space_idunless the user is already admin/board/treasurer in that space. Applied toschema.sql. Shipped as incremental migrationscripts/015_prevent_member_self_role_change.sqlfor existing databases. Run this on any production database immediately. - Removed duplicate auth functions in
lib/actions.ts(signIn,signOut,getUser). Canonical versions live inlib/auth-actions.ts. Verified no caller imports them fromactions.ts. - Deleted dead scripts:
fix-actions.js,fix-sidebar.js,fix-sidebar.mjs,patch-dashboard.mjs. All had hardcoded/vercel/share/v0-projectpaths. - Synced
types/database.tsforspace_members.user_idtostring | nullmatching the schema fix from pass 1.
Open items, priority order
- Run
scripts/015_prevent_member_self_role_change.sqlon production before any external user can authenticate. Until then, any member can self-promote to admin via direct PostgREST. app/auth/login/page.tsxis a dead duplicate login page linking to non-existent/auth/onboarding. Delete the directory.components/app-shell.tsxis dead (no importer). Delete.- Wire validation into remaining actions listed in
docs/AUDIT.md4.4 (updateMember, contacts, KB, secrets, area lead, settings, integrations, imports). - Secrets at rest: still plain text.
signUptrigger metadata mismatch is benign (trigger short-circuits) but should be removed or re-aligned. Seedocs/AUDIT.md4.5.- Stale docs:
DB_SCHEMA_MAP.mdanddocs/ARCHITECTURE.mdstill say 4 default channels. Schema creates 3.
Decisions pending
- Whether to wire validation into the remaining actions in the next session.
- Whether to delete
app/auth/login/andcomponents/app-shell.tsxnow. - Whether to regenerate
types/database.tsproperly viasupabase gen typesinstead of the manual edit applied here.
How to verify the security fix
-- Connect to your Supabase project's SQL editor as a regular user (NOT service role):
-- The following should now fail with "Members cannot change their own role..."
UPDATE public.space_members SET role = 'admin' WHERE user_id = auth.uid();
Files changed this pass
Edited:
app/(app)/dashboard/page.tsx
app/signup/page.tsx
lib/validations.ts
lib/actions.ts
scripts/schema.sql
types/database.ts
docs/AUDIT.md
docs/HANDOFF.md
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
2026-05-13 (pass 1) — Deep audit + fresh-deploy hardening
Branch: main
Author: Claude (per CLAUDE.md, no commits made this session).
What I did
- Wrote
CLAUDE.mdat the repo root. It is the standing brief: no Claude attribution in commits, no emojis, no em dashes, no AI tells, test-driven where practical, always document, handoff after long sessions, smart separation of concerns. - Audited every source file. Findings in
docs/AUDIT.md. - Fixed three idempotency / fresh-deploy issues in
scripts/schema.sql:space_members.user_idis now nullable (offline members work).- Realtime
ALTER PUBLICATIONwrapped inDO ... EXCEPTIONblocks. - All RLS policies preceded by
DROP POLICY IF EXISTS.
- Added
scripts/014_member_user_id_nullable.sqlfor upgrading existing databases. - Added every missing deployment artifact:
Dockerfile,.dockerignore,docker-compose.yml,.do/app.yaml,.env.example,app/api/health/route.ts. - Updated
middleware.tsto let/api/healththrough without auth. - Updated
docs/DEPLOYMENT.mdto reference bundled artifacts instead of asking the user to write their own.
What I did not touch
- Application logic (
lib/actions.ts,lib/auth-actions.ts, all*-client.tsx). - UI components.
- Tests.
- Validation schemas (they are wrong in places, see audit section 4.1, but fixing them is a behavior change).
Open items, in priority order
- Privilege escalation on
space_members.role(audit 4.11). A self-update via themembers_updateRLS policy lets a member rewrite their own row, includingrole. Fix before production. - Validation schema enum mismatches (audit 4.1). Six schemas in
lib/validations.tsreject valid database values. Wire schemas into actions and align the enums. - Dead code: four
scripts/fix-*.{js,mjs}files with hardcoded/vercel/sharepaths, andcomponents/app-shell.tsxwhich no page imports. Delete on next cleanup. - Duplicate auth functions:
lib/actions.tslines 30 to 47 redeclaresignIn,signOut,getUserthat already live inlib/auth-actions.ts. Consolidate. - Secrets at rest:
secrets.valueandintegrations.configare plain text. Encrypt before production. - UI-only features: OAuth buttons, CSV import processor, webhook endpoint, payment platform syncs (except PayPal), email notifications. See audit 4.7.
package.jsonname is"my-project". Cosmetic.- Older docs reference 4 default chat channels; schema creates 3 (
general,announcements,ops).DB_SCHEMA_MAP.mdanddocs/ARCHITECTURE.mdare stale on this point.
Decisions pending from the user
- Whether to delete dead scripts now (low risk) or in a separate change.
- Whether to apply
scripts/014_member_user_id_nullable.sqlagainst the live database (production may already have user_id nullable perDB_SCHEMA_MAP.md, this migration is idempotent). - Whether to wire validation schemas into existing server actions in the next session.
How to verify the fresh-deploy fixes locally
# 1. Spin up a fresh Supabase project at supabase.com
# 2. In Supabase SQL Editor, paste scripts/schema.sql and run it. Should report no errors.
# 3. Re-run scripts/schema.sql. Should still report no errors (idempotent).
# 4. Copy .env.example to .env.local, fill in Supabase URL + anon + service role.
pnpm install
pnpm dev
# 5. Visit http://localhost:3000, sign up, create a space, confirm /dashboard renders.
# 6. curl http://localhost:3000/api/health -> {"status":"ok"}
How to verify the Docker build
# Requires .env.local populated.
docker compose build
docker compose up -d
curl http://localhost:3000/api/health
docker compose logs -f app
Files changed this session
Edited:
middleware.ts
scripts/schema.sql
docs/DEPLOYMENT.md
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
docs/HANDOFF.md
No git commits were made. The working tree carries all changes; the user decides what to commit and when.