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>
This commit is contained in:
357
dancechamp-claude-code/docs/DATABASE.md
Normal file
357
dancechamp-claude-code/docs/DATABASE.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user