Skip to content
Generated from server/database/schema.ts

Data model

70 tables, introspected from the Drizzle schema on every build. Grouped by bounded context; each table links to its columns, keys, indexes and the code that uses it.

All tables

Every table and foreign key. Open a context below for keys and columns.

Rendering diagram…

Diagram source
erDiagram
  ai_usage
  attendances
  availability_slots
  bookable_spaces
  booking_inquiries
  booking_requests
  brand_looks
  cities
  city_battle_votes
  city_videos
  community_group_reports
  community_groups
  dancer_goals
  dancers
  deliveries
  dinner_group_members
  dinner_groups
  dinner_signups
  dinners
  discover_swipes
  election_candidates
  election_vote_history
  election_votes
  event_flyers
  event_posts
  events
  festival_ride_shares
  festival_roommate_lookups
  festival_signups
  festival_submissions
  festival_waves
  festivals
  follows
  gig_applications
  gigs
  giveaway_entries
  giveaway_proposals
  giveaways
  guideline_versions
  handles
  hangout_rsvps
  hangouts
  inbound_requests
  legacy_follows
  mcp_audit
  mcp_rate_limits
  moderator_elections
  network_links
  oauth_clients
  oauth_codes
  oauth_grants
  oauth_tokens
  organizer_subscriptions
  plan_items
  profile_access_log
  profile_affiliations
  profile_claims
  profile_contacts
  profile_invites
  profile_members
  profile_sites
  profile_suggestions
  profiles
  recommendation_requests
  referrals
  reviews
  sessions
  tracked_link_actions
  tracked_links
  video_votes
  events |o--o{ ai_usage : "event_id"
  event_flyers |o--o{ ai_usage : "flyer_id"
  dancers |o--o{ ai_usage : "dancer_id"
  events ||--o{ attendances : "event_id"
  dancers |o--o{ attendances : "dancer_id"
  bookable_spaces ||--o{ availability_slots : "space_id"
  profiles ||--o{ bookable_spaces : "profile_id"
  profiles ||--o{ booking_inquiries : "profile_id"
  dancers |o--o{ booking_inquiries : "requester_id"
  bookable_spaces ||--o{ booking_requests : "space_id"
  profiles ||--o{ booking_requests : "profile_id"
  profiles |o--o{ booking_requests : "organizer_id"
  dancers |o--o{ booking_requests : "requester_id"
  dancers |o--o{ booking_requests : "moderated_by_id"
  profiles ||--o{ brand_looks : "profile_id"
  dancers |o--o{ brand_looks : "created_by"
  dancers |o--o{ city_battle_votes : "voter_dancer_id"
  dancers |o--o{ city_videos : "dancer_id"
  community_groups ||--o{ community_group_reports : "group_id"
  dancers ||--o{ community_group_reports : "dancer_id"
  dancers ||--o{ dancer_goals : "dancer_id"
  dinner_groups ||--o{ dinner_group_members : "group_id"
  dancers ||--o{ dinner_group_members : "dancer_id"
  dinners ||--o{ dinner_groups : "dinner_id"
  dinners ||--o{ dinner_signups : "dinner_id"
  dancers ||--o{ dinner_signups : "dancer_id"
  festivals ||--o{ dinners : "festival_id"
  dancers ||--o{ discover_swipes : "dancer_id"
  moderator_elections ||--o{ election_candidates : "election_id"
  dancers ||--o{ election_candidates : "dancer_id"
  moderator_elections ||--o{ election_vote_history : "election_id"
  dancers ||--o{ election_vote_history : "voter_dancer_id"
  moderator_elections ||--o{ election_votes : "election_id"
  dancers ||--o{ election_votes : "voter_dancer_id"
  election_candidates ||--o{ election_votes : "candidate_id"
  events ||--o{ event_flyers : "event_id"
  event_posts |o--o{ event_flyers : "post_id"
  brand_looks |o--o{ event_flyers : "look_id"
  dancers |o--o{ event_flyers : "created_by"
  events ||--o{ event_posts : "event_id"
  dancers |o--o{ event_posts : "created_by"
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  festivals ||--o{ festival_ride_shares : "festival_id"
  dancers ||--o{ festival_ride_shares : "dancer_id"
  festivals ||--o{ festival_roommate_lookups : "festival_id"
  dancers ||--o{ festival_roommate_lookups : "dancer_id"
  festivals ||--o{ festival_signups : "festival_id"
  dancers |o--o{ festival_signups : "dancer_id"
  dancers |o--o{ festival_submissions : "submitted_by_id"
  dancers ||--o{ festival_waves : "dancer_id"
  dancers ||--o{ festival_waves : "target_dancer_id"
  dancers ||--o{ follows : "dancer_id"
  profiles |o--o{ follows : "profile_id"
  dancers |o--o{ follows : "target_dancer_id"
  gigs ||--o{ gig_applications : "gig_id"
  dancers ||--o{ gig_applications : "dancer_id"
  profiles |o--o{ gig_applications : "profile_id"
  dancers |o--o{ gig_applications : "decided_by"
  dancers |o--o{ gigs : "dancer_id"
  profiles |o--o{ gigs : "poster_profile_id"
  giveaways ||--o{ giveaway_entries : "giveaway_id"
  dancers |o--o{ giveaway_entries : "dancer_id"
  events ||--o{ giveaway_proposals : "event_id"
  dancers |o--o{ giveaway_proposals : "proposed_by"
  profiles ||--o{ giveaway_proposals : "profile_id"
  giveaways |o--o{ giveaway_proposals : "giveaway_id"
  dancers |o--o{ giveaway_proposals : "decided_by"
  profiles ||--o{ guideline_versions : "profile_id"
  moderator_elections |o--o{ guideline_versions : "election_id"
  dancers |o--o{ guideline_versions : "moderator_dancer_id"
  dancers |o--o{ handles : "dancer_id"
  profiles |o--o{ handles : "profile_id"
  hangouts ||--o{ hangout_rsvps : "hangout_id"
  dancers ||--o{ hangout_rsvps : "dancer_id"
  events |o--o{ hangouts : "event_id"
  dancers ||--o{ hangouts : "dancer_id"
  dancers |o--o{ inbound_requests : "dancer_id"
  events |o--o{ inbound_requests : "event_id"
  profiles |o--o{ inbound_requests : "profile_id"
  gigs |o--o{ inbound_requests : "gig_id"
  dancers ||--o{ legacy_follows : "dancer_id"
  oauth_grants |o--o{ mcp_audit : "grant_id"
  dancers |o--o{ mcp_audit : "dancer_id"
  profiles |o--o{ mcp_audit : "profile_id"
  profiles ||--o{ moderator_elections : "profile_id"
  profiles ||--o{ network_links : "profile_id"
  profiles ||--o{ network_links : "other_profile_id"
  dancers |o--o{ network_links : "created_by"
  oauth_grants ||--o{ oauth_codes : "grant_id"
  dancers ||--o{ oauth_grants : "dancer_id"
  oauth_clients ||--o{ oauth_grants : "client_id"
  oauth_grants ||--o{ oauth_tokens : "grant_id"
  profiles ||--o{ organizer_subscriptions : "profile_id"
  dancers ||--o{ organizer_subscriptions : "dancer_id"
  dancers ||--o{ plan_items : "dancer_id"
  profiles ||--o{ profile_access_log : "profile_id"
  dancers |o--o{ profile_access_log : "actor_id"
  dancers |o--o{ profile_access_log : "subject_id"
  profiles ||--o{ profile_affiliations : "artist_profile_id"
  profiles ||--o{ profile_affiliations : "organizer_profile_id"
  profiles ||--o{ profile_claims : "profile_id"
  dancers ||--o{ profile_claims : "dancer_id"
  dancers |o--o{ profile_claims : "decided_by"
  profiles ||--o{ profile_contacts : "profile_id"
  dancers |o--o{ profile_contacts : "dancer_id"
  profiles ||--o{ profile_invites : "profile_id"
  events |o--o{ profile_invites : "event_id"
  dancers |o--o{ profile_invites : "created_by"
  profiles ||--o{ profile_members : "profile_id"
  dancers ||--o{ profile_members : "dancer_id"
  dancers |o--o{ profile_members : "added_by"
  profiles ||--o{ profile_sites : "profile_id"
  dancers |o--o{ profile_sites : "updated_by"
  profiles ||--o{ profile_suggestions : "profile_id"
  events |o--o{ profile_suggestions : "event_id"
  dancers |o--o{ profile_suggestions : "suggested_by"
  dancers ||--o{ recommendation_requests : "asker_id"
  festivals ||--o{ referrals : "festival_id"
  dancers ||--o{ referrals : "referrer_id"
  dancers ||--o{ referrals : "referee_id"
  dancers ||--o{ reviews : "dancer_id"
  dancers ||--o{ sessions : "dancer_id"
  tracked_links ||--o{ tracked_link_actions : "link_id"
  dancers |o--o{ tracked_link_actions : "dancer_id"
  events ||--o{ tracked_links : "event_id"
  profiles ||--o{ tracked_links : "profile_id"
  dancers |o--o{ tracked_links : "created_by"
  city_videos ||--o{ video_votes : "winner_video_id"
  city_videos ||--o{ video_votes : "loser_video_id"
  dancers |o--o{ video_votes : "voter_dancer_id"

Identity & auth

Dancer accounts, credentials and login sessions, and the apps (MCP assistants) dancers connect via OAuth.

ER diagram for Identity & auth

Rendering diagram…

Diagram source
erDiagram
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  mcp_audit {
    uuid id PK
    uuid grant_id FK
    uuid dancer_id FK
    uuid profile_id FK
    text confirm_nonce UK
  }
  mcp_rate_limits {
  }
  oauth_clients {
    text id PK
  }
  oauth_codes {
    text code_hash PK
    uuid grant_id FK
  }
  oauth_grants {
    uuid id PK
    uuid dancer_id FK
    text client_id FK
  }
  oauth_tokens {
    uuid id PK
    uuid grant_id FK
    text token_hash UK
  }
  profiles {
    uuid id PK
    text username UK
  }
  sessions {
    uuid id PK
    uuid dancer_id FK
    text token UK
  }
  oauth_grants |o--o{ mcp_audit : "grant_id"
  dancers |o--o{ mcp_audit : "dancer_id"
  profiles |o--o{ mcp_audit : "profile_id"
  oauth_grants ||--o{ oauth_codes : "grant_id"
  dancers ||--o{ oauth_grants : "dancer_id"
  oauth_clients ||--o{ oauth_grants : "client_id"
  oauth_grants ||--o{ oauth_tokens : "grant_id"
  dancers ||--o{ sessions : "dancer_id"

Festivals & ticketing

Festival listings, sign-ups and verified tickets, referrals, organiser submissions, and the per-festival social layer (waves, rides, roommates).

ER diagram for Festivals & ticketing

Rendering diagram…

Diagram source
erDiagram
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  festival_ride_shares {
    uuid id PK
    uuid festival_id FK
    uuid dancer_id FK
  }
  festival_roommate_lookups {
    uuid id PK
    uuid festival_id FK
    uuid dancer_id FK
  }
  festival_signups {
    uuid id PK
    uuid festival_id FK
    uuid dancer_id FK
    text tickettailor_order_id UK
  }
  festival_submissions {
    uuid id PK
    uuid submitted_by_id FK
  }
  festival_waves {
    uuid id PK
    uuid dancer_id FK
    uuid target_dancer_id FK
  }
  festivals {
    uuid id PK
    text slug UK
  }
  referrals {
    uuid id PK
    uuid festival_id FK
    uuid referrer_id FK
    uuid referee_id FK
  }
  festivals ||--o{ festival_ride_shares : "festival_id"
  dancers ||--o{ festival_ride_shares : "dancer_id"
  festivals ||--o{ festival_roommate_lookups : "festival_id"
  dancers ||--o{ festival_roommate_lookups : "dancer_id"
  festivals ||--o{ festival_signups : "festival_id"
  dancers |o--o{ festival_signups : "dancer_id"
  dancers |o--o{ festival_submissions : "submitted_by_id"
  dancers ||--o{ festival_waves : "dancer_id"
  dancers ||--o{ festival_waves : "target_dancer_id"
  festivals ||--o{ referrals : "festival_id"
  dancers ||--o{ referrals : "referrer_id"
  dancers ||--o{ referrals : "referee_id"

Festival dinners

Dinner slots at festivals, sign-ups and the generated dinner groups.

ER diagram for Festival dinners

Rendering diagram…

Diagram source
erDiagram
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  dinner_group_members {
    uuid id PK
    uuid group_id FK
    uuid dancer_id FK
  }
  dinner_groups {
    uuid id PK
    uuid dinner_id FK
  }
  dinner_signups {
    uuid id PK
    uuid dinner_id FK
    uuid dancer_id FK
  }
  dinners {
    uuid id PK
    uuid festival_id FK
  }
  festivals {
    uuid id PK
    text slug UK
  }
  dinner_groups ||--o{ dinner_group_members : "group_id"
  dancers ||--o{ dinner_group_members : "dancer_id"
  dinners ||--o{ dinner_groups : "dinner_id"
  dinners ||--o{ dinner_signups : "dinner_id"
  dancers ||--o{ dinner_signups : "dancer_id"
  festivals ||--o{ dinners : "festival_id"

Events & plans

Dated public events (v4 archive + live v3 mirror), their flyers and the hosts' saved flyer looks, each dancer's week/year plan, and the unified attendance model.

events
41 columns · 2 FKs · used by 92

Events — dated, public dance events (socials, classes, workshops, festivals). Created outside Drizzle on 2026-07-15 by scripts/migrate/00-schema.mjs to hold the 9,580 historical v4 events (archived=true, hidden). Since 2026-10 it also holds the LIVE upcoming events mirrored from wedance.vip (v3 Firestore) by scripts/sync-v3-events.ts — those carry source='wedance-v3' + source_id=<post id> and archived=false, published=true while still upcoming in v3. Times: start_date / end_date are `timestamp without time zone` holding UTC (the convention 04-events.mjs established). `timezone` is the IANA zone the event happens in — render wall-clock times in it, never in the viewer's zone. Rollback for synced rows: DELETE FROM events WHERE source = 'wedance-v3'.

plan_items
7 columns · 1 FKs · used by 29
discover_swipes
7 columns · 1 FKs · used by 5

Discover-deck decisions on /my-plan ("Widen your year"). One row per dancer per card: a Yes or a Skip on another dancer, an event or a festival, so a card never comes back once decided (migration 0046). Kept apart from festival_waves on purpose: a wave is a message the other dancer sees, a deck Yes is private bookkeeping. `festival_slug` records the festival that put a dancer on the card — Yes on two or more dancers heading to the same festival raises the "add it to your year?" nudge.

attendances
16 columns · 2 FKs · used by 13

Attendances (ADR-008, migration 0044). One row per dancer per event: replaces the festival/event rows of `plan_items` (Going clicks) and `festival_signups` (ticket holders). Status ladder: interested → going → ticket_holder. One visibility setting replaces both `plan_items.share_going` and `festival_signups.roster_visibility`. Default visibility = hidden (opt-in). Phase 4 expand step: this table is created alongside the old tables. New Going clicks dual-write here and to `plan_items`; the old reads stay on `plan_items`/`festival_signups` until the switch step. `order_id` is nullable and has no FK yet (the `orders` table ships in Phase 3 Step 22).

dancer_goals
9 columns · 1 FKs · used by 4

Dancer goals (ADR-009, migration 0044). Goals were stored as plan_items with item_type='goal' and the goal itself in metadata. Now they have their own table with typed columns.

brand_looks
10 columns · 2 FKs · used by 18
event_flyers
23 columns · 4 FKs · used by 17
event_posts
12 columns · 2 FKs · used by 20

Content plan (migration 0037): an event's posts, each with its own flyer versions (event_flyers.post_id) and one kept. Kinds: shared/utils/posts.ts.

ai_usage
17 columns · 3 FKs · used by 15

AI usage log (migration 0038): every Gemini call, for /admin/ai-usage.

ER diagram for Events & plans

Rendering diagram…

Diagram source
erDiagram
  ai_usage {
    uuid id PK
    uuid event_id FK
    uuid flyer_id FK
    uuid dancer_id FK
  }
  attendances {
    uuid id PK
    uuid event_id FK
    uuid dancer_id FK
  }
  brand_looks {
    uuid id PK
    uuid profile_id FK
    uuid created_by FK
  }
  dancer_goals {
    uuid id PK
    uuid dancer_id FK
  }
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  discover_swipes {
    uuid id PK
    uuid dancer_id FK
  }
  event_flyers {
    uuid id PK
    uuid event_id FK
    uuid post_id FK
    uuid look_id FK
    uuid created_by FK
  }
  event_posts {
    uuid id PK
    uuid event_id FK
    uuid created_by FK
  }
  events {
    uuid id PK
    uuid host_profile_id FK
    uuid edited_by FK
  }
  plan_items {
    uuid id PK
    uuid dancer_id FK
  }
  profiles {
    uuid id PK
    text username UK
  }
  events |o--o{ ai_usage : "event_id"
  event_flyers |o--o{ ai_usage : "flyer_id"
  dancers |o--o{ ai_usage : "dancer_id"
  events ||--o{ attendances : "event_id"
  dancers |o--o{ attendances : "dancer_id"
  profiles ||--o{ brand_looks : "profile_id"
  dancers |o--o{ brand_looks : "created_by"
  dancers ||--o{ dancer_goals : "dancer_id"
  dancers ||--o{ discover_swipes : "dancer_id"
  events ||--o{ event_flyers : "event_id"
  event_posts |o--o{ event_flyers : "post_id"
  brand_looks |o--o{ event_flyers : "look_id"
  dancers |o--o{ event_flyers : "created_by"
  events ||--o{ event_posts : "event_id"
  dancers |o--o{ event_posts : "created_by"
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  dancers ||--o{ plan_items : "dancer_id"

Profiles & cities

Venue / artist / organiser profiles under /@handle, canonical cities, follows and the per-city community-group directory.

profiles
34 columns · 0 FKs · used by 132

Professional profiles — venues, artists, organizers — under the unified /@<username> handle namespace (v4-style). Dancers keep their own row + handle in `dancers`; the /@handle route resolves either. Reviews attach polymorphically by (type, username).

profile_members
9 columns · 3 FKs · used by 110

Who manages a profile (hosting mode). Created when an admin approves a claim or when a dancer creates a new profile. `claimed` on the profile mirrors "has at least one owner" so the v3 sync never overwrites it.

profile_claims
14 columns · 3 FKs · used by 8

"This is me / my venue" requests for existing profiles. An admin approves (→ profile_members owner row + profiles.claimed) or rejects.

profile_access_log
8 columns · 3 FKs · used by 10

Who got, lost or changed access to a profile, and who did it (0027) — the facts an admin needs to judge a reclaim dispute.

profile_suggestions
9 columns · 3 FKs · used by 9

Artist confirmation (migration 0033). An organizer's photos, video and name for an artist in their line-up are suggestions on the artist's profile until the artist approves them through a personal link (profile_invites).

profile_invites
19 columns · 3 FKs · used by 15
follows
5 columns · 3 FKs · used by 11

Follows — unified follow graph (ADR-004, Step 15). A dancer follows either a professional profile (venue/artist/organizer) OR another dancer. Exactly one of profile_id / target_dancer_id is set per row. Renamed from `artist_follows` (migration 0044). Mutual dancer→dancer follows = friends.

legacy_follows
5 columns · 1 FKs · used by 0

Legacy follows from v4 that couldn't be resolved to a profile or dancer during the Step 15 migration. Kept for provenance; no app code reads this.

profile_affiliations
5 columns · 2 FKs · used by 0

Step 14 (ADR-004): artist ↔ organiser affiliations ("teaches at", "resident DJ at", "member of"). Links an artist profile to an organiser/venue profile.

handles
4 columns · 2 FKs · used by 0

Handle registry (ADR-004, Step 12). One namespace for dancer and profile URLs: `/@<handle>` resolves through this table. citext for case-insensitive uniqueness. Exactly one of dancer_id / profile_id is set.

organizer_subscriptions
11 columns · 2 FKs · used by 5

--- Organizer subscriptions (RAZ-106) ---------------------------------------- An organizer profile can hold one active annual subscription plan. The Network plan (€199/year) covers unlimited events under a single annual price. Stripe runs the billing; this table mirrors the subscription state so entitlement checks stay inside the DB without a Stripe API call.

network_links
6 columns · 3 FKs · used by 8

Business "Network" curation (migration 0042, RAZ-408). The network itself is derived from shared events; a row here means a page's team ADDED someone by hand or HID a derived contact. One row per (my page, their page).

profile_sites
4 columns · 2 FKs · used by 6

A profile's own website, managed on WeDance (RAZ-412, migration 0045): the page's sections and words in every language (shared/types/profileSite.ts). The site renders GET /api/profiles/<username>/site.

profile_contacts
12 columns · 2 FKs · used by 7

People who signed up on a profile's website (RAZ-412, migration 0045): the profile's contacts on WeDance. One row per email per profile; signing up again updates it. Linked to a dancer when the email has a WeDance account.

cities
13 columns · 0 FKs · used by 23

Cities — canonical, Google-Places-backed (migration 0024). One row per Google place: `name` is the English name ("Munich"), `local_name` the name in the country's language ("München"), `alt_names` every other spelling we have resolved or seen typed ("Мюнхен", "Munique", "munchen"). The CityCombobox only ever saves one of these rows, so "München", "Мюнхен" and "munich" all end up as the same city. Free-text `city` columns elsewhere are migrated to a `city_id` FK later — plan: docs/research/2026-10-12-city-canonicalisation.md.

community_groups
11 columns · 0 FKs · used by 4

Community groups (WhatsApp / Telegram / …) per city — the cold-start directory for cities where WeDance has no events yet.

community_group_reports
4 columns · 2 FKs · used by 4

Per-user report tracking for community groups — prevents a single user from hiding a group by reporting it multiple times.

ER diagram for Profiles & cities

Rendering diagram…

Diagram source
erDiagram
  cities {
    uuid id PK
  }
  community_group_reports {
    uuid id PK
    uuid group_id FK
    uuid dancer_id FK
  }
  community_groups {
    uuid id PK
  }
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  events {
    uuid id PK
    uuid host_profile_id FK
    uuid edited_by FK
  }
  follows {
    uuid id PK
    uuid dancer_id FK
    uuid profile_id FK
    uuid target_dancer_id FK
  }
  handles {
    text handle PK
    uuid dancer_id FK,UK
    uuid profile_id FK,UK
  }
  legacy_follows {
    uuid id PK
    uuid dancer_id FK
  }
  network_links {
    uuid id PK
    uuid profile_id FK
    uuid other_profile_id FK
    uuid created_by FK
  }
  organizer_subscriptions {
    uuid id PK
    uuid profile_id FK
    uuid dancer_id FK
    text stripe_subscription_id UK
  }
  profile_access_log {
    uuid id PK
    uuid profile_id FK
    uuid actor_id FK
    uuid subject_id FK
  }
  profile_affiliations {
    uuid id PK
    uuid artist_profile_id FK
    uuid organizer_profile_id FK
  }
  profile_claims {
    uuid id PK
    uuid profile_id FK
    uuid dancer_id FK
    uuid decided_by FK
  }
  profile_contacts {
    uuid id PK
    uuid profile_id FK
    uuid dancer_id FK
  }
  profile_invites {
    uuid id PK
    uuid profile_id FK
    uuid event_id FK
    uuid created_by FK
  }
  profile_members {
    uuid id PK
    uuid profile_id FK
    uuid dancer_id FK
    uuid added_by FK
  }
  profile_sites {
    uuid profile_id PK,FK
    uuid updated_by FK
  }
  profile_suggestions {
    uuid id PK
    uuid profile_id FK
    uuid event_id FK
    uuid suggested_by FK
  }
  profiles {
    uuid id PK
    text username UK
  }
  community_groups ||--o{ community_group_reports : "group_id"
  dancers ||--o{ community_group_reports : "dancer_id"
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  dancers ||--o{ follows : "dancer_id"
  profiles |o--o{ follows : "profile_id"
  dancers |o--o{ follows : "target_dancer_id"
  dancers |o--o{ handles : "dancer_id"
  profiles |o--o{ handles : "profile_id"
  dancers ||--o{ legacy_follows : "dancer_id"
  profiles ||--o{ network_links : "profile_id"
  profiles ||--o{ network_links : "other_profile_id"
  dancers |o--o{ network_links : "created_by"
  profiles ||--o{ organizer_subscriptions : "profile_id"
  dancers ||--o{ organizer_subscriptions : "dancer_id"
  profiles ||--o{ profile_access_log : "profile_id"
  dancers |o--o{ profile_access_log : "actor_id"
  dancers |o--o{ profile_access_log : "subject_id"
  profiles ||--o{ profile_affiliations : "artist_profile_id"
  profiles ||--o{ profile_affiliations : "organizer_profile_id"
  profiles ||--o{ profile_claims : "profile_id"
  dancers ||--o{ profile_claims : "dancer_id"
  dancers |o--o{ profile_claims : "decided_by"
  profiles ||--o{ profile_contacts : "profile_id"
  dancers |o--o{ profile_contacts : "dancer_id"
  profiles ||--o{ profile_invites : "profile_id"
  events |o--o{ profile_invites : "event_id"
  dancers |o--o{ profile_invites : "created_by"
  profiles ||--o{ profile_members : "profile_id"
  dancers ||--o{ profile_members : "dancer_id"
  dancers |o--o{ profile_members : "added_by"
  profiles ||--o{ profile_sites : "profile_id"
  dancers |o--o{ profile_sites : "updated_by"
  profiles ||--o{ profile_suggestions : "profile_id"
  events |o--o{ profile_suggestions : "event_id"
  dancers |o--o{ profile_suggestions : "suggested_by"

Venue booking & governance

Bookable spaces, availability and booking requests, plus moderator elections and guideline versions for free community spaces.

bookable_spaces
10 columns · 1 FKs · used by 9

A profile (venue) can offer N bookable areas — e.g. Pinakothek's 5 spaces.

availability_slots
7 columns · 1 FKs · used by 5

Recurring weekly availability windows per bookable space. A venue publishes when each space is open for bookings (e.g. "Monday 18:00–23:00"). When slots exist for a space, booking requests are validated against them; when none exist the space is treated as always-available (backward-compat).

booking_requests
24 columns · 5 FKs · used by 11

Booking requests — connector model (no payment held). An organizer requests a date for a space; the venue responds. T&C accepted at request time.

booking_inquiries
17 columns · 2 FKs · used by 7

Booking inquiries (migration 0039): anyone asks to book an artist or venue from their page, no account needed. The page's team answers by email.

moderator_elections
11 columns · 1 FKs · used by 9

The election itself + its lifecycle (G801). One active (non-closed) election per profile is enforced in the router.

election_candidates
6 columns · 2 FKs · used by 8

A self-nominated candidate + the guidelines they run on (G802). One candidacy per dancer per election; a real (verified) profile is required — enforced in the router by requiring the dancer to have a username.

election_votes
6 columns · 3 FKs · used by 7

The CURRENT vote per voter — one row per (election, voter), upserted on change (G803). One real profile = one vote (G801 anti-stuffing). The tally uses this table (latest vote per voter); the full trail lives in electionVoteHistory.

election_vote_history
6 columns · 2 FKs · used by 5

Immutable append-only log of every cast/change (G803). Makes "is my vote still X?" verifiable — a vote is never silently overwritten, each change is a row.

guideline_versions
8 columns · 3 FKs · used by 5

Per-term guideline versions (G804): on close, the winner's guidelines become the space's active ruleset; every term is preserved so governance history is auditable across elections.

inbound_requests
12 columns · 4 FKs · used by 0

Inbound requests (ADR-010, Step 16). Every public-facing request — event enquiries, problem reports, gig applications, general contact — is stored here first, then delivered to Linear / email by the delivery outbox.

deliveries
11 columns · 0 FKs · used by 0

Delivery outbox (ADR-010, Step 16). Every notification that leaves the app (Linear issue, email, push). A cron picks up pending deliveries, sends them, and marks them delivered. Retries capped at 3.

ER diagram for Venue booking & governance

Rendering diagram…

Diagram source
erDiagram
  availability_slots {
    uuid id PK
    uuid space_id FK
  }
  bookable_spaces {
    uuid id PK
    uuid profile_id FK
  }
  booking_inquiries {
    uuid id PK
    uuid profile_id FK
    uuid requester_id FK
  }
  booking_requests {
    uuid id PK
    uuid space_id FK
    uuid profile_id FK
    uuid organizer_id FK
    uuid requester_id FK
    uuid moderated_by_id FK
  }
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  deliveries {
    uuid id PK
  }
  election_candidates {
    uuid id PK
    uuid election_id FK
    uuid dancer_id FK
  }
  election_vote_history {
    uuid id PK
    uuid election_id FK
    uuid voter_dancer_id FK
  }
  election_votes {
    uuid id PK
    uuid election_id FK
    uuid voter_dancer_id FK
    uuid candidate_id FK
  }
  events {
    uuid id PK
    uuid host_profile_id FK
    uuid edited_by FK
  }
  gigs {
    uuid id PK
    uuid dancer_id FK
    uuid poster_profile_id FK
  }
  guideline_versions {
    uuid id PK
    uuid profile_id FK
    uuid election_id FK
    uuid moderator_dancer_id FK
  }
  inbound_requests {
    uuid id PK
    uuid dancer_id FK
    uuid event_id FK
    uuid profile_id FK
    uuid gig_id FK
  }
  moderator_elections {
    uuid id PK
    uuid profile_id FK
  }
  profiles {
    uuid id PK
    text username UK
  }
  bookable_spaces ||--o{ availability_slots : "space_id"
  profiles ||--o{ bookable_spaces : "profile_id"
  profiles ||--o{ booking_inquiries : "profile_id"
  dancers |o--o{ booking_inquiries : "requester_id"
  bookable_spaces ||--o{ booking_requests : "space_id"
  profiles ||--o{ booking_requests : "profile_id"
  profiles |o--o{ booking_requests : "organizer_id"
  dancers |o--o{ booking_requests : "requester_id"
  dancers |o--o{ booking_requests : "moderated_by_id"
  moderator_elections ||--o{ election_candidates : "election_id"
  dancers ||--o{ election_candidates : "dancer_id"
  moderator_elections ||--o{ election_vote_history : "election_id"
  dancers ||--o{ election_vote_history : "voter_dancer_id"
  moderator_elections ||--o{ election_votes : "election_id"
  dancers ||--o{ election_votes : "voter_dancer_id"
  election_candidates ||--o{ election_votes : "candidate_id"
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  dancers |o--o{ gigs : "dancer_id"
  profiles |o--o{ gigs : "poster_profile_id"
  profiles ||--o{ guideline_versions : "profile_id"
  moderator_elections |o--o{ guideline_versions : "election_id"
  dancers |o--o{ guideline_versions : "moderator_dancer_id"
  dancers |o--o{ inbound_requests : "dancer_id"
  events |o--o{ inbound_requests : "event_id"
  profiles |o--o{ inbound_requests : "profile_id"
  gigs |o--o{ inbound_requests : "gig_id"
  profiles ||--o{ moderator_elections : "profile_id"

Community engagement

City video voting and battles, giveaways (and the ones promoters propose), reviews, "ask locals" recommendations, and promoters' tracked links with what they brought.

city_videos
13 columns · 1 FKs · used by 10
video_votes
7 columns · 3 FKs · used by 5
city_battle_votes
9 columns · 1 FKs · used by 6

Cross-city battle votes — the tier above per-city voting. Each row is one vote in a head-to-head between two cities' champion videos (the current month's highest-ELO approved video per city). Ranks CITIES, not videos, into the "Top dance cities" leaderboard. videoIds are stored for provenance only (plain uuid, no FK — champions rotate as ELO shifts, and this must survive video deletion).

giveaways
13 columns · 0 FKs · used by 5
giveaway_entries
5 columns · 2 FKs · used by 4
giveaway_proposals
14 columns · 5 FKs · used by 6

A promoter/influencer proposes a giveaway for an event; only admins create giveaways directly, so the event's organizer approves it and it becomes one.

reviews
13 columns · 1 FKs · used by 6

Reviews & ratings — polymorphic by (targetType, targetSlug) so a review can attach to ANY entity (festival / venue / artist / organizer), including on-demand stubs created by "ask locals" recommendations, without needing a full entity table for each. `source` separates a normal review from an auto-5★ recommendation. One review per dancer per target.

recommendation_requests
6 columns · 1 FKs · used by 5

"Ask locals" — a question posted for a city, answered by recommendations (which write into `reviews` with source='recommendation').

tracked_links
7 columns · 3 FKs · used by 15

A promoter's (or influencer's) personal link to one event: /events/<slug>?via=<code>. One per promoter page + event. The caption cached here is the kit caption written for this promoter (promo_kit), so it isn't rewritten on every call.

tracked_link_actions
7 columns · 2 FKs · used by 12

What a tracked link brought: a visit (per browser), a sign-up, a plan add or a ticket (per dancer). Each counts once per link. Last touch wins: the link in the visitor's `wd_via` cookie when they act.

ER diagram for Community engagement

Rendering diagram…

Diagram source
erDiagram
  city_battle_votes {
    uuid id PK
    uuid voter_dancer_id FK
  }
  city_videos {
    uuid id PK
    uuid dancer_id FK
  }
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  events {
    uuid id PK
    uuid host_profile_id FK
    uuid edited_by FK
  }
  giveaway_entries {
    uuid id PK
    uuid giveaway_id FK
    uuid dancer_id FK
  }
  giveaway_proposals {
    uuid id PK
    uuid event_id FK
    uuid proposed_by FK
    uuid profile_id FK
    uuid giveaway_id FK
    uuid decided_by FK
  }
  giveaways {
    uuid id PK
  }
  profiles {
    uuid id PK
    text username UK
  }
  recommendation_requests {
    uuid id PK
    uuid asker_id FK
  }
  reviews {
    uuid id PK
    uuid dancer_id FK
  }
  tracked_link_actions {
    uuid id PK
    uuid link_id FK
    uuid dancer_id FK
  }
  tracked_links {
    uuid id PK
    text code UK
    uuid event_id FK
    uuid profile_id FK
    uuid created_by FK
  }
  video_votes {
    uuid id PK
    uuid winner_video_id FK
    uuid loser_video_id FK
    uuid voter_dancer_id FK
  }
  dancers |o--o{ city_battle_votes : "voter_dancer_id"
  dancers |o--o{ city_videos : "dancer_id"
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  giveaways ||--o{ giveaway_entries : "giveaway_id"
  dancers |o--o{ giveaway_entries : "dancer_id"
  events ||--o{ giveaway_proposals : "event_id"
  dancers |o--o{ giveaway_proposals : "proposed_by"
  profiles ||--o{ giveaway_proposals : "profile_id"
  giveaways |o--o{ giveaway_proposals : "giveaway_id"
  dancers |o--o{ giveaway_proposals : "decided_by"
  dancers ||--o{ recommendation_requests : "asker_id"
  dancers ||--o{ reviews : "dancer_id"
  tracked_links ||--o{ tracked_link_actions : "link_id"
  dancers |o--o{ tracked_link_actions : "dancer_id"
  events ||--o{ tracked_links : "event_id"
  profiles ||--o{ tracked_links : "profile_id"
  dancers |o--o{ tracked_links : "created_by"
  city_videos ||--o{ video_votes : "winner_video_id"
  city_videos ||--o{ video_votes : "loser_video_id"
  dancers |o--o{ video_votes : "voter_dancer_id"

Gigs & hangouts

The gig board (open roles and offered services, artists' applications) and tonight's hangouts with RSVPs.

ER diagram for Gigs & hangouts

Rendering diagram…

Diagram source
erDiagram
  dancers {
    uuid id PK
    text email UK
    text neon_auth_id UK
    text username UK
  }
  events {
    uuid id PK
    uuid host_profile_id FK
    uuid edited_by FK
  }
  gig_applications {
    uuid id PK
    uuid gig_id FK
    uuid dancer_id FK
    uuid profile_id FK
    uuid decided_by FK
  }
  gigs {
    uuid id PK
    uuid dancer_id FK
    uuid poster_profile_id FK
  }
  hangout_rsvps {
    uuid id PK
    uuid hangout_id FK
    uuid dancer_id FK
  }
  hangouts {
    uuid id PK
    uuid event_id FK
    uuid dancer_id FK
  }
  profiles {
    uuid id PK
    text username UK
  }
  profiles |o--o{ events : "host_profile_id"
  dancers |o--o{ events : "edited_by"
  gigs ||--o{ gig_applications : "gig_id"
  dancers ||--o{ gig_applications : "dancer_id"
  profiles |o--o{ gig_applications : "profile_id"
  dancers |o--o{ gig_applications : "decided_by"
  dancers |o--o{ gigs : "dancer_id"
  profiles |o--o{ gigs : "poster_profile_id"
  hangouts ||--o{ hangout_rsvps : "hangout_id"
  dancers ||--o{ hangout_rsvps : "dancer_id"
  events |o--o{ hangouts : "event_id"
  dancers ||--o{ hangouts : "dancer_id"

Source: server/database/schema.ts. Context grouping: scripts/architecture/domains.ts (a test fails when a table is not assigned).