# DanceChamp — Database Schema ## Overview Backend: **Supabase** (PostgreSQL + Auth + Storage + Realtime) All tables use `uuid` primary keys generated by `gen_random_uuid()`. All tables have `created_at` and `updated_at` timestamps. --- ## Tables ### users Extended from Supabase Auth. This is a `public.users` table that mirrors `auth.users` via trigger. ```sql create table public.users ( id uuid primary key references auth.users(id) on delete cascade, email text not null, name text not null, role text not null check (role in ('admin', 'organization', 'member')), city text, instagram_handle text, experience_years integer, disciplines text[] default '{}', -- ['Pole Exotic', 'Pole Art'] auth_provider text default 'email', -- 'email' | 'google' | 'instagram' avatar_url text, status text not null default 'active' check (status in ('active', 'warned', 'blocked')), warn_reason text, block_reason text, created_at timestamptz default now(), updated_at timestamptz default now() ); ``` ### organizations One-to-one with a user (role = 'organization'). ```sql create table public.organizations ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users(id) on delete cascade, name text not null, instagram_handle text, email text, city text, logo_url text, verified boolean not null default false, status text not null default 'pending' check (status in ('active', 'pending', 'blocked')), block_reason text, created_at timestamptz default now(), updated_at timestamptz default now() ); ``` ### championships Belongs to an organization. Core entity. ```sql create table public.championships ( id uuid primary key default gen_random_uuid(), org_id uuid not null references public.organizations(id) on delete cascade, name text not null, subtitle text, event_date text, -- "May 30, 2026" or ISO date reg_start text, -- registration opens reg_end text, -- registration closes (must be before event_date) location text, -- "Minsk, Belarus" venue text, -- "Prime Hall" accent_color text default '#D4145A', image_emoji text default '💃', status text not null default 'draft' check (status in ('draft', 'pending_approval', 'live', 'completed', 'blocked')), -- configurable sections progress config_info boolean not null default false, config_categories boolean not null default false, config_fees boolean not null default false, config_rules boolean not null default false, config_judges boolean not null default false, -- links form_url text, -- Google Forms URL rules_doc_url text, -- Rules document URL created_at timestamptz default now(), updated_at timestamptz default now() ); ``` ### disciplines Championship has many disciplines. Each discipline has levels. ```sql create table public.disciplines ( id uuid primary key default gen_random_uuid(), championship_id uuid not null references public.championships(id) on delete cascade, name text not null, -- "Exotic Pole Dance" levels text[] default '{}', -- ['Beginners', 'Amateur', 'Semi-Pro', 'Profi', 'Elite'] sort_order integer default 0, created_at timestamptz default now() ); ``` ### styles Championship-level styles (not per-discipline). ```sql create table public.styles ( id uuid primary key default gen_random_uuid(), championship_id uuid not null references public.championships(id) on delete cascade, name text not null, -- "Classic", "Flow", "Theater" sort_order integer default 0, created_at timestamptz default now() ); ``` ### fees One-to-one with championship. ```sql create table public.fees ( id uuid primary key default gen_random_uuid(), championship_id uuid not null unique references public.championships(id) on delete cascade, video_selection text, -- "50 BYN / 1,500 RUB" solo text, -- "280 BYN / 7,500 RUB" duet text, -- "210 BYN / 5,800 RUB pp" "group" text, -- "190 BYN / 4,500 RUB pp" refund_note text, created_at timestamptz default now(), updated_at timestamptz default now() ); ``` ### rules Championship has many rules across sections. ```sql create table public.rules ( id uuid primary key default gen_random_uuid(), championship_id uuid not null references public.championships(id) on delete cascade, section text not null check (section in ('general', 'costume', 'scoring', 'penalty')), name text not null, -- rule text or criterion name value text, -- for scoring: "10" (max), for penalty: "-2" or "DQ" sort_order integer default 0, created_at timestamptz default now() ); ``` ### judges Championship has many judges. ```sql create table public.judges ( id uuid primary key default gen_random_uuid(), championship_id uuid not null references public.championships(id) on delete cascade, name text not null, instagram text, bio text, photo_url text, sort_order integer default 0, created_at timestamptz default now() ); ``` ### registrations Links a member to a championship. Tracks the 10-step progress. ```sql create table public.registrations ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users(id) on delete cascade, championship_id uuid not null references public.championships(id) on delete cascade, discipline_id uuid references public.disciplines(id), level text, -- "Semi-Pro" style text, -- "Classic" participation_type text default 'solo' check (participation_type in ('solo', 'duet', 'group')), -- Progress steps (step 1–10) step_rules_reviewed boolean default false, step_category_selected boolean default false, step_video_recorded boolean default false, step_form_submitted boolean default false, step_video_fee_paid boolean default false, -- confirmed by org step_video_fee_receipt_url text, -- uploaded receipt step_results text check (step_results in ('pending', 'passed', 'failed')), step_champ_fee_paid boolean default false, step_champ_fee_receipt_url text, step_about_me_submitted boolean default false, step_insurance_confirmed boolean default false, step_insurance_doc_url text, -- Video video_url text, -- Meta current_step integer default 1, created_at timestamptz default now(), updated_at timestamptz default now(), unique(user_id, championship_id) ); ``` ### notifications Push to member's in-app feed. ```sql create table public.notifications ( id uuid primary key default gen_random_uuid(), user_id uuid not null references public.users(id) on delete cascade, championship_id uuid references public.championships(id) on delete set null, type text not null check (type in ( 'category_changed', 'payment_confirmed', 'results', 'deadline_reminder', 'registration_confirmed', 'announcement', 'champ_approved', 'champ_rejected', 'org_approved', 'org_rejected' )), title text not null, message text not null, read boolean not null default false, created_at timestamptz default now() ); ``` ### activity_logs Admin audit trail. ```sql create table public.activity_logs ( id uuid primary key default gen_random_uuid(), actor_id uuid references public.users(id) on delete set null, action text not null, -- "org_approved", "user_blocked", "champ_auto_approved" target_type text not null, -- "organization", "championship", "user" target_id uuid, target_name text, -- denormalized for display details jsonb, -- extra context created_at timestamptz default now() ); ``` --- ## Relationships Diagram ``` users (1) ──── (1) organizations │ │ has many ▼ championships ┌────┼────┬────┬────┐ │ │ │ │ │ disciplines styles fees rules judges │ registrations ─┘ (user + championship) │ notifications ``` --- ## Row Level Security (RLS) Enable RLS on all tables. ### users ```sql -- Members can read/update their own row create policy "Users can read own" on users for select using (auth.uid() = id); create policy "Users can update own" on users for update using (auth.uid() = id); -- Org admins can read members registered to their championships create policy "Orgs can read their members" on users for select using ( id in ( select r.user_id from registrations r join championships c on r.championship_id = c.id join organizations o on c.org_id = o.id where o.user_id = auth.uid() ) ); -- Admin can read/update all create policy "Admin full access" on users for all using ( exists (select 1 from users where id = auth.uid() and role = 'admin') ); ``` ### championships ```sql -- Anyone can read live championships create policy "Public read live" on championships for select using (status = 'live'); -- Org can CRUD their own create policy "Org manages own" on championships for all using ( org_id in (select id from organizations where user_id = auth.uid()) ); -- Admin full access create policy "Admin full access" on championships for all using ( exists (select 1 from users where id = auth.uid() and role = 'admin') ); ``` ### registrations ```sql -- Members can read/create their own create policy "Member own registrations" on registrations for select using (user_id = auth.uid()); create policy "Member can register" on registrations for insert with check (user_id = auth.uid()); -- Org can read/update registrations for their championships create policy "Org manages registrations" on registrations for all using ( championship_id in ( select c.id from championships c join organizations o on c.org_id = o.id where o.user_id = auth.uid() ) ); -- Admin full access create policy "Admin full access" on registrations for all using ( exists (select 1 from users where id = auth.uid() and role = 'admin') ); ``` ### notifications ```sql -- Users can read their own notifications create policy "Read own" on notifications for select using (user_id = auth.uid()); -- Users can mark their own as read create policy "Update own" on notifications for update using (user_id = auth.uid()); ``` --- ## Storage Buckets ``` receipts/ -- Payment receipt screenshots {user_id}/{registration_id}/receipt.jpg insurance/ -- Insurance documents {user_id}/{registration_id}/insurance.pdf judge-photos/ -- Judge profile photos {championship_id}/{judge_id}.jpg org-logos/ -- Organization logos {org_id}/logo.jpg ``` --- ## Seed Data For development, seed with: - 1 admin user - 2 organizations (1 verified, 1 unverified/pending) - 2 championships for verified org (1 live, 1 draft) - 1 championship for unverified org (pending_approval) - 7 member users with registrations at various progress stages - Sample notifications, activity logs This matches the prototype demo data.