Files
PoleDanceApp/dancechamp-claude-code/docs/DATABASE.md
Dianaka123 789d2bf0a6 Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin
Backend (FastAPI + SQLAlchemy + SQLite):
- JWT auth with access/refresh tokens, bcrypt password hashing
- User model with member/organizer/admin roles, auto-approve members
- Championship, Registration, ParticipantList, Notification models
- Alembic async migrations, seed data with test users
- Registration endpoint returns tokens for members, pending for organizers
- /registrations/my returns championship title/date/location via eager loading
- Admin endpoints: list users, approve/reject organizers

Mobile (React Native + Expo + TypeScript):
- Zustand auth store, Axios client with token refresh interceptor
- Role-based registration (Member vs Organizer) with contextual form labels
- Tab navigation with Ionicons, safe area headers, admin tab for admin role
- Championships list with status badges, detail screen with registration progress
- My Registrations with championship title, progress bar, and tap-to-navigate
- Admin panel with pending/all filter, approve/reject with confirmation
- Profile screen with role badge, Ionicons info rows, sign out
- Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:46:50 +03:00

11 KiB
Raw Blame History

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.

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').

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.

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.

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).

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.

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.

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.

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.

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 110)
  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.

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.

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

-- 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

-- 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

-- 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

-- 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.