Migration 032 adds classes (offering; optional payment_link and
grants_certification_id, is_active archive), class_sessions (scheduled
occurrence; denormalized space_id) and class_signups (member signup;
partial unique = one non-cancelled signup per member+session). RLS is
additive and default-deny: classes readable by managers (all) or members
(active only), class_sessions by any space member, class_signups by
managers/instructors (all) or the member (own) with instructor-only
UPDATE and no INSERT/DELETE policy so signup/cancel funnels through a
validated service-client action (capacity/waitlist/dedupe). New
classes.manage + classes.instruct permissions (group Classes) seeded to
board and backfilled. Mirrored idempotently in schema.sql; types and
schema docs updated.
Migration 030 adds certifications (per-space cert types, optional
validity_months, is_active archive) and member_certifications (per-member
grants; expires_at snapshotted at grant time; soft revoke; partial unique
on one active grant per member+cert). RLS is additive and default-deny:
cert types readable by any space member, write-gated by
certifications.manage; grants readable by managers/granters (all) or the
member (own), written by certifications.grant, with no DELETE policy so
the history is immutable. No anonymous path.
Two new permission codes in the catalog (group Certifications):
certifications.manage and certifications.grant (the Instructor
capability, assignable to any role via the existing additive model).
Seeded to board and backfilled. Mirrored idempotently in schema.sql;
types and schema docs updated.
Invites
- migration 029: space_invites.role (member_role enum, default
'member' — legacy invites and the spaces.invite_code path keep
granting member). schema.sql + types/database.ts mirrored
- lib/invite-logic.ts (pure, tested): canAssignInviteRole — admin may
grant any role; board may grant anything except admin; nobody else.
Privilege-escalation guard on top of the existing admin/board gate
- createInvite/updateInvite take + enforce role via that guard;
createInviteSchema/updateInviteSchema validate the enum
- joinSpace applies the invite's granted role (validated), default
member; uses_count/max_uses/expiry already enforced
- invites panel: role picker (filtered to what the creator may grant),
one-click "single use", per-invite role badge
- space-scoped invite link /join/<space>?code=… via a presentational
app/join/[space] landing that hands off to the proven
/signup?invite= flow; /join added to middleware PUBLIC_ROUTES
Audit hardening (from this pass's review)
- /login now honors a validated same-origin ?next= (was hardcoded to
/dashboard) so the public_auth form sign-in round-trips; OAuth
threads next through the callback (which already honored it)
- submitForm requires formId; submitFormSchema drops the now-ambiguous
slug path (form slugs are per-space after 028). Both callers already
pass formId. Tests updated
337 tests; build verified (Compiled successfully, exit 0). No deploy.
Adds the forms and form_submissions tables behind a custom form/waiver
builder. Phase 1 is schema + RLS only; server actions, builder UI, the
public /f/[slug] page, onboarding integration, and retro-linking follow
in later phases.
- scripts/026_forms.sql (idempotent), mirrored as schema.sql Section 17
- forms: globally-unique slug, kind (form/waiver), visibility
(public_anon/public_auth/members), status, jsonb field schema,
legal_text, version
- form_submissions: per-row snapshot of schema/legal-text/version for
immutable waiver records, plus ip/user_agent for e-sign defensibility
- RLS additive + default-deny: forms write gated by forms.manage via
user_has_permission; form_submissions has no write policy, so it is
service-client-only and immutable; anon gets no grant on forms
- new forms.manage permission seeded to board + backfilled
- types/database.ts and DATABASE_SCHEMA/DB_SCHEMA_MAP/ARCHITECTURE/HANDOFF
updated
Finishes all remaining tracked work from the full-app audit.
- types/database.ts synced: added the 8 missing tables (forum_threads,
comments, space_tiers, space_invites, space_onboarding_steps,
space_role_labels, space_custom_roles, space_member_custom_roles),
the comment_entity_type enum (type + Constants), and the missing
columns (space_members.tier_id/onboarding_completed_at/
onboarding_progress, secrets.encrypted_value/encryption_version,
knowledge_base.render_markdown).
- Email canonicalization: new emailField() helper (trim+lowercase)
applied to signIn/signUp/addMember/updateMember/createContact and
the import path; prevents case-variant duplicates and sign-in
mismatches.
- Members directory: per-row "make area lead" picker (admin/board)
assigns a member to any area-lead role; mirrors Customize.
- Docs refreshed and re-dated: DATABASE_SCHEMA.md (migrations 014-024
summary), DB_SCHEMA_MAP.md (new-table quick map), API_REFERENCE.md
(all server actions from migrations 016-024). schema.sql named as
source of truth.
Backlog is now empty. 271/271 tests pass, build clean. No migration.
Migration 023 (folded into schema.sql section 15). ADDITIVE and
backward compatible: with no permission/ACL rows the app behaves
exactly as before.
Schema:
- space_role_permissions (space_id, subject, permission); ops_acl
(space_id, entity_type secret|kb|process|area_lead, entity_id,
role). Full RLS, writes admin/board.
- user_effective_roles(uid,sid): built-in role + custom-role slugs +
area_lead:<id> for areas the member leads. SECURITY DEFINER.
- user_has_permission(uid,sid,perm): admin implicit-all else a grant
via an effective role. SECURITY DEFINER.
- secrets/knowledge_base SELECT rewritten as
`exact existing rule OR ops_acl match` (only widens; zero change
with no ACL rows). Seed + backfill default role permissions.
Area-lead roles reuse the existing area_leads table rather than
duplicating the concept: a row is a role, lead_id NULL = Vacant,
assigning sets lead_id, the member then holds area_lead:<id>.
App:
- lib/permissions-catalog.ts (codes + per-role defaults + guardrails),
Zod schemas, lib/actions/permissions.ts (setRolePermissions,
setOpsAcl, createAreaLeadRole, assign/unassign/delete).
- Customize gains Permissions (role x permission matrix, admin shown
implicit-all/locked) and Area leads (create/Vacant/assign) panels,
modular under customize/panels/. Reusable
components/ops/ops-acl-editor.tsx wired into the Secrets list.
Audit-found latent fixes:
- revealSecret errors on encrypted-but-no-ciphertext corruption
instead of silently returning the stale plaintext column.
- upsertAreaLead rejects null/empty area_code (NULLs are distinct in
unique constraints; allowed duplicate rows).
types/database.ts: added the two new tables. 271/271 tests pass,
build clean. KB-modal ACL wiring + members-row shortcut deferred and
tracked in HANDOFF.
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.