Files
hackerspace.sh/DB_SCHEMA_MAP.md
T
Moheeb Zara 2687c1578d fix(secrets): honor ops.secrets.read permission for reveal/list
A regular member granted reveal access either via the ops.secrets.read
role permission or a per-secret ops_acl entry could not actually see or
reveal secrets: revealSecret hard-gated on built-in role admin/board
(ignoring both the permission and the ACL), and secrets_select RLS never
consulted the permission, and the Ops page short-circuited the secrets
fetch by built-in role.

Migration 031 additively adds an OR user_has_permission(...,
'ops.secrets.read') branch to secrets_select; the admin/board and
per-secret ops_acl branches are unchanged so deployment is access-neutral
unless an admin has granted the permission. revealSecret now resolves
the member and lets RLS be the boundary (a denied row simply does not
return). The Ops page always queries secrets (RLS filters the set) and
shows the section to admin/board, a permission holder, or anyone with an
ACL-granted secret. Write paths (create/update/delete) are unchanged.
Mirrored in schema.sql; migration docs and HANDOFF updated.
2026-05-16 19:07:06 -07:00

19 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)
028 Form slug unique PER SPACE (UNIQUE(space_id, slug), drops global UNIQUE(slug)); public URL /f/[space]/[slug]
029 space_invites.role (member_role enum, default member); invites can grant a role; admin-granting invites are admin-only (app-enforced)
030 certifications, member_certifications + certifications.manage / certifications.grant permissions (the latter = Instructor). Additive default-deny RLS; grants soft-revoked + immutable (no DELETE policy); expiry snapshotted at grant; no anonymous path
031 secrets_select additively also honors ops.secrets.read (previously admin/board OR per-secret ops_acl only). Reveal/list gates let RLS decide; writes unchanged. Access-neutral unless the permission is granted

Tables added by migrations 016-030 (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, role Multi-code invites; role (member_role, default member) is the role the invite grants on join
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 (UNIQUE per space_id), 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 /f/[space]/[slug] 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)
certifications space_id, name (UNIQUE per space, case-insensitive), description, validity_months (null = never expires), is_active Per-space certification types. SELECT = any space member; write-gated by certifications.manage
member_certifications space_id, member_id, certification_id, granted_by, granted_at, expires_at (snapshotted at grant), revoked_at, revoked_by, revoked_reason, note Per-member grants. Partial UNIQUE = one active grant per member+cert. SELECT = certifications.manage/certifications.grant (all) or member (own); INSERT/UPDATE = certifications.grant; NO DELETE policy (immutable history)

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.