Files
hackerspace.sh/DB_SCHEMA_MAP.md
T
Moheeb Zara 295d220849 feat(forms): onboarding form/waiver step with required enforcement (Phase 4)
- scripts/027 (idempotent CHECK replace) + schema.sql Section 14: allow
  space_onboarding_steps.step_type = 'form'; referenced form id lives in
  the step's existing config.form_id (no new column)
- finishOnboarding: a required form step is satisfied when a submission
  for config.form_id by the member exists (any version — re-sign is
  non-blocking / auto-satisfy); fails open if the form is missing or
  unpublished so a misconfig cannot trap a member. Probe uses the
  service client (submissions are forms.manage-only under RLS)
- onboarding page enriches form steps with the published form def +
  prior-submission flag; onboarding-flow renders FormRenderer + waiver
  legal text/consent and submits via submitForm then marks the step done
- customize: forms fetched and threaded to the onboarding panel; new
  "+ Form step" button + form picker writing config.form_id
- validations/types/casts include 'form'; forms.test.ts +2
- docs: DATABASE_SCHEMA/DB_SCHEMA_MAP/API_REFERENCE/ARCHITECTURE/HANDOFF

287/287 tests, build exit 0. No deploy.
2026-05-16 15:15:17 -07:00

18 KiB

DB Schema Map — Quick Reference

Last Updated: 2026-05-15
Full Documentation: See docs/DATABASE_SCHEMA.md for complete reference
Source of Truth: scripts/schema.sql (canonical, idempotent)


ENUMS — exact live values (queried 2026-03-10)

Enum Live Values
member_role admin, board, treasurer, member, associate
member_tier plus, basic, associate
member_status current, late, inactive, unverified — NOT active or pending
task_type chore, task — DB column is task_type (enum name and column name match)
task_status open, claimed, in_progress, overdue, due_today, completed, done, blocked
recurrence_type daily, weekly, biweekly, monthly, none — NO quarterly
project_status backlog, in_progress, review, done, blocked
kb_visibility all_members, board, admin_only — NOT admins_only
contact_type vendor, supplier, partner, landlord, city — NO contractor
area_lead_status active, vacant, handoff
payment_platform paypal, zeffy, venmo, cash
payment_link_status linked, unlinked
channel_type general, area, ops, project — NO board
contact_type vendor, supplier, partner, landlord, city
payment_platform paypal, zeffy, venmo, cash
payment_link_status linked, unlinked
channel_type general, area, ops, project

TABLES — Every column, exact names

spaces

Column Type Notes
id uuid PK
name text NOT NULL
slug text UNIQUE NOT NULL
city text nullable
require_approval boolean default true
public_member_directory boolean default false
invite_code text UNIQUE NOT NULL
webhook_secret text nullable
created_at timestamptz
updated_at timestamptz

Does NOT have: description, logo, timezone


space_members

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
user_id uuid FK → auth.users NULLABLE — offline members have no user_id
role text default 'member'
tier text default 'basic'
display_name text NOT NULL
email text nullable
handle text nullable
phone text nullable
status text default 'active'
payment_status text nullable
payment_note text nullable
joined_at timestamptz
last_payment_at timestamptz nullable — legacy, also has last_paid_at
last_paid_at timestamptz nullable — added script 013; used by importMembers / linkPaymentToMember
has_card_access boolean default false
approved boolean default true (fixed script 008)
UNIQUE (space_id, user_id)
UNIQUE (space_id, email) added script 009

Does NOT have: last_payment_at (has both last_payment_at and last_paid_at), approved_at, bio, avatar_url


tasks

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
title text NOT NULL
description text nullable
task_type task_type default 'task' — column is task_type, matching the enum name
status task_status default 'open' — use 'completed' not 'done'
area text nullable
recurrence recurrence_type default 'none'
due_date timestamptz nullable
assigned_to uuid FK → auth.users nullable
assigned_to_name text nullable
claimed_by uuid FK → auth.users nullable
claimed_by_name text nullable
requested_by uuid FK → auth.users nullable
requested_by_name text nullable
subtask_total integer default 0
subtask_completed integer default 0
last_done_at timestamptz nullable
completed_at timestamptz nullable
created_at timestamptz
updated_at timestamptz

Does NOT have: type (use task_type), progress, blocked, priority


projects

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
title text NOT NULL
description text nullable
status project_status default 'backlog'
area text nullable
tags text[] nullable
assignee_names text[] nullable
task_count integer default 0
tasks_completed integer default 0
due_date timestamptz nullable
created_at timestamptz
updated_at timestamptz

Does NOT have: progress, blocked, priority, owner_id


knowledge_base

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
title text NOT NULL
content text nullable
icon text nullable
visibility kb_visibility default 'all_members'
area text nullable
tags text[] nullable
is_pinned boolean default false — column is is_pinned, NOT pinned
updated_by_id uuid FK → auth.users nullable
updated_by_name text nullable — NOT updated_by
created_at timestamptz
updated_at timestamptz

Does NOT have: description, pinned, updated_by, summary


secrets

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
title text NOT NULL
icon text nullable
description text nullable
value text NOT NULL
area text nullable
created_at timestamptz
updated_at timestamptz

area_leads

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
area_name text NOT NULL
area_code text NOT NULL
lead_id uuid FK → auth.users nullable
lead_handle text nullable
status area_lead_status default 'active'
created_at timestamptz
updated_at timestamptz
UNIQUE (space_id, area_code) added script 007

contacts

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
name text NOT NULL
code text NOT NULL auto-generated 3-char prefix + random 3 digits
contact_type contact_type NOT NULL
email text nullable
phone text nullable
details text nullable
note text nullable — added script 007
group_label text nullable — added script 007
tags text[] nullable
created_at timestamptz
updated_at timestamptz

payments

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
platform payment_platform NOT NULL
amount numeric(10,2) NOT NULL
from_identifier text nullable
from_note text nullable
member_id uuid FK → space_members nullable
member_name text nullable
link_status payment_link_status default 'unlinked'
transaction_date timestamptz NOT NULL
created_at timestamptz

Does NOT have: description, reference, currency


comms_channels

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
name text NOT NULL
icon text nullable
channel_type channel_type default 'general'
area_reference text nullable
project_id uuid FK → projects nullable, cascade delete
member_count integer default 0
created_at timestamptz

Does NOT have: description, topic, unread_count, is_archived

Default channels created by trigger on space INSERT: general, announcements, random, facilities


comms_messages

Column Type Notes
id uuid PK
channel_id uuid FK → comms_channels NOT NULL, cascade delete
space_id uuid FK → spaces NOT NULL
user_id uuid FK → auth.users nullable — NOT sender_id
display_name text NOT NULL
handle text nullable
content text NOT NULL NOT message, NOT body
created_at timestamptz

Does NOT have: message, body, sender_id, edited_at, reactions


integrations

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
name text NOT NULL
platform text NOT NULL
description text nullable
is_connected boolean default false
config jsonb nullable
created_at timestamptz
updated_at timestamptz
UNIQUE (space_id, platform) added script 007

activity_log

Column Type Notes
id uuid PK
space_id uuid FK → spaces NOT NULL
user_id uuid FK → auth.users nullable
display_name text nullable
action text NOT NULL
entity_type text nullable
entity_id uuid nullable
details text nullable
created_at timestamptz

USER FLOWS

Sign Up → Create Space

  1. /signup form → signUp() → Supabase auth.signUp
  2. After email confirm / immediate: createSpace({ spaceName, spaceSlug, displayName }) (admin client, bypasses RLS)
  3. Inserts spaces → trigger fires → inserts 4 default comms_channels
  4. Inserts space_members as role=admin, tier=plus, status=active, approved=true
  5. Redirect → /dashboard

Sign Up → Join Space

  1. /signup form → signUp()
  2. joinSpace({ inviteCode, displayName }) (admin client)
  3. Looks up spaces.invite_code → inserts space_members with role=member, status=active (or pending if require_approval=true), approved=true
  4. Redirect → /dashboard

Login

  1. /login → signIn(email, password) → Supabase auth
  2. Middleware checks session cookie
  3. App layout RSC calls getCurrentMembership() → queries space_members WHERE user_id = auth.uid() AND status IN ('active', 'pending')
  4. If no membership found → redirect /signup

Dashboard

  • RSC queries: tasks (open/in_progress), projects (in_progress), recent payments, activity_log, all scoped to space_id

Tasks

  • List: tasks WHERE space_id = ? ORDER BY created_at DESC
  • Create: createTask() → status=open, type=task|chore
  • Claim: claimTask() → status=claimed
  • Complete: completeTask() → status=completed (NOT done)
  • Delete: deleteTask()

Projects

  • Kanban columns: backlog | in_progress | review | done
  • Create: createProject() → status=backlog
  • Move: updateProjectStatus(id, status) where status ∈ project_status enum

Members (admin/board write)

  • List: all space_members for space
  • Add: addMember() (RLS: user_has_role_in_space admin/board)
  • Edit: updateMember()
  • Approve: approveMember() → status=active, approved=true
  • Remove: removeMember() (admin only)
  • CSV: importMembers() → upsert on (space_id, email)

Payments (treasurer/admin/board)

  • List: payments JOIN space_members for display_name
  • Log cash: logCashPayment() → platform=cash
  • Link: linkPaymentToMember() → updates member_id, link_status=linked, member last_paid_at
  • CSV: importPaymentsCsv()

Ops / Knowledge Base

  • List: knowledge_base ordered by is_pinned DESC, created_at DESC
  • Create: createKbEntry() → columns: title, content, area, visibility, is_pinned, tags, icon, updated_by_id, updated_by_name
  • Update: updateKbEntry() → same columns
  • Secrets: createSecret(), deleteSecret() — admin/board only via RLS
  • Area leads: upsertAreaLead() on conflict (space_id, area_code)

Comms

  • Channels: RSC fetches comms_channels WHERE space_id = ?
  • Messages: client fetches comms_messages WHERE channel_id = ? ORDER BY created_at
  • Realtime: Supabase channel subscription on comms_messages for channel_id=eq.{id}
  • Send: INSERT comms_messages (channel_id, space_id, user_id, display_name, handle, content)

Settings (admin)

  • Space: updateSpaceSettings()
  • Integrations: saveIntegration() upsert on (space_id, platform), disconnectIntegration()
  • Webhook: rotateWebhookSecret()

RLS SUMMARY

Table SELECT INSERT UPDATE DELETE
spaces own space members admin client only (bootstrap) admin —
space_members own space self OR admin/board self OR admin admin
tasks members members members members
projects members members members members
knowledge_base members (visibility-scoped) members members board/admin
secrets admin/board/treasurer admin admin admin
area_leads members board/admin board/admin board/admin
contacts members members members members
payments treasurer/board/admin treasurer/admin treasurer/admin treasurer/admin
comms_channels members board/admin board/admin board/admin
comms_messages members members (user_id=auth.uid()) — —
integrations admin admin admin admin
activity_log members members — —

MIGRATIONS APPLIED

Script Change
001 Full schema creation
002 Schema fixes + create_default_channels trigger (original — had bug)
003 Signup trigger
004 Admin helper functions
005 Additional fixes
006 Comprehensive RLS rewrite
007 Added task_status done; contacts note/group_label; unique on integrations(space_id,platform) and area_leads(space_id,area_code)
008 space_members.approved default=true; backfilled NULLs — fixed login loop
009 space_members INSERT RLS: allow admin/board inserts; added UNIQUE(space_id,email)
010 Fixed create_default_channels trigger — removed non-existent description column
011 Renamed tasks type column to task_type (canonical enum column name)
012 Canonical sync — task_type enum, cleaned duplicate columns
013 Added space_members.last_paid_at timestamptz — required by importMembers and linkPaymentToMember
014-025 See docs/DATABASE_SCHEMA.md migrations table (governance kernel, areas, forum/tiers/roles/invites, onboarding, permissions/ACLs, self-change hardening, incidents_insert re-assert)
026 forms, form_submissions + forms.manage permission (additive, default-deny RLS; submissions immutable + service-client-only)
027 space_onboarding_steps.step_type CHECK adds 'form' (onboarding can embed a form/waiver; form id in step config.form_id)

Tables added by migrations 016-026 (quick map)

Table Key columns Purpose
proposals / proposal_votes space_id, status, quorum_, voting_; member_id, position Async governance voting
incidents / incident_updates space_id, status, sla_*; visibility Incident reports + log
policies space_id, slug, version, status, body_* Versioned policy library
space_areas space_id, code, name, sort_order, is_archived Per-space configurable areas
forum_threads space_id, author_id, title, category, pinned, locked, comment_count Forum
comments space_id, entity_type, entity_id, author_id, parent_id, body Polymorphic comments (forum/proposal/incident/policy)
space_tiers space_id, slug, name, monthly_price_cents, billing_cadence, is_system Custom membership tiers (space_members.tier_id FK)
space_role_labels space_id, role, display_name, color Rename/recolor built-in roles
space_custom_roles / space_member_custom_roles space_id, slug, name, color / member_id, custom_role_id Custom non-privileged roles + assignment
space_invites space_id, code, label, expires_at, max_uses, uses_count, is_enabled Multi-code invites
space_onboarding_steps space_id, step_key, step_type, title, body, config, is_enabled, is_required Configurable member onboarding
space_role_permissions space_id, subject, permission Role/custom-role permission grants
ops_acl space_id, entity_type, entity_id, role Per-item Ops access list (secret/kb/process/area_lead)
forms space_id, slug (global UNIQUE), kind (form/waiver), visibility (public_anon/public_auth/members), status (draft/published/closed), schema jsonb, legal_text, version Custom forms + waivers. Write-gated by forms.manage; public page served via service-client server action (no anon grant)
form_submissions form_id, space_id, member_id, submitter_email, answers jsonb, form_snapshot jsonb, legal_text_snapshot, form_version, ip, user_agent Append-only submissions. Per-row snapshot = immutable waiver record. SELECT = forms.manage; NO write policy (service-client only; immutable)

Column additions: space_members.tier_id, onboarding_completed_at, onboarding_progress; secrets.encrypted_value, encryption_version; knowledge_base.render_markdown, is_meeting_minutes, meeting_date. New enum comment_entity_type.

Helper functions: user_effective_roles(uid,sid), user_has_permission(uid,sid,perm) (both SECURITY DEFINER, search_path=public), plus governance quorum/tally/SLA functions.