Files
hackerspace.sh/DB_SCHEMA_MAP.md
T
Moheeb Zara 4c9b55147b chore: clear audit backlog (types sync, email norm, docs, members shortcut)
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.
2026-05-15 19:01:52 -07:00

417 lines
16 KiB
Markdown

# DB Schema Map — Quick Reference
> **Last Updated**: 2026-05-15
> **Full Documentation**: See [docs/DATABASE_SCHEMA.md](./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 |
---
## Tables added by migrations 016-024 (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) |
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.