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>
358 lines
11 KiB
Markdown
358 lines
11 KiB
Markdown
# 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.
|