BakoAI / supabase_schema.sql
icanedit2's picture
Deploy backend to HF with player video visibility fix
32bc095
Raw
History Blame Contribute Delete
20 kB
-- Supabase Database Schema for Basketball Analysis Platform
-- Run these in Supabase SQL Editor
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- USERS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
hashed_password TEXT NOT NULL,
account_type TEXT NOT NULL CHECK (account_type IN ('team', 'personal', 'coach')),
full_name TEXT,
avatar_url TEXT,
phone TEXT,
organization_id UUID, -- Explicitly added for linking to orgs
staff_role TEXT, -- e.g., 'Main Coach', 'Assistant Coach'
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for email lookups
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- ============================================
-- ORGANIZATIONS TABLE (TEAM accounts)
-- ============================================
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
description TEXT,
logo_url TEXT,
primary_color TEXT DEFAULT '#FF5733',
secondary_color TEXT DEFAULT '#333333',
jersey_style TEXT DEFAULT 'Solid',
home_court TEXT,
website TEXT,
phone TEXT,
email TEXT,
twitter_handle TEXT,
instagram_handle TEXT,
competition_settings JSONB DEFAULT '{}',
roster_settings JSONB DEFAULT '{}',
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_organizations_owner ON organizations(owner_id);
-- ============================================
-- PLAYERS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS players (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
jersey_number INTEGER,
position TEXT,
height_cm REAL,
weight_kg REAL,
date_of_birth DATE,
avatar_url TEXT,
phone TEXT,
address TEXT,
experience_years TEXT,
bio TEXT,
status TEXT DEFAULT 'active',
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_players_org ON players(organization_id);
CREATE INDEX IF NOT EXISTS idx_players_user ON players(user_id);
-- ============================================
-- VIDEOS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS videos (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
uploader_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT,
description TEXT,
analysis_mode TEXT NOT NULL CHECK (analysis_mode IN ('team', 'personal')),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
storage_path TEXT NOT NULL,
duration_seconds REAL,
frame_count INTEGER,
fps REAL,
width INTEGER,
height INTEGER,
file_size_bytes BIGINT,
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
error_message TEXT,
progress_percent REAL,
current_step TEXT,
annotated_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_videos_uploader ON videos(uploader_id);
CREATE INDEX IF NOT EXISTS idx_videos_status ON videos(status);
CREATE INDEX IF NOT EXISTS idx_videos_org ON videos(organization_id);
-- ============================================
-- DETECTIONS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS detections (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
frame INTEGER NOT NULL,
object_type TEXT NOT NULL CHECK (object_type IN ('player', 'ball')),
track_id INTEGER NOT NULL,
bbox REAL[] NOT NULL,
confidence REAL NOT NULL,
keypoints JSONB,
team_id INTEGER,
has_ball BOOLEAN DEFAULT FALSE,
tactical_x REAL,
tactical_y REAL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_detections_video ON detections(video_id);
CREATE INDEX IF NOT EXISTS idx_detections_frame ON detections(video_id, frame);
-- ============================================
-- ANALYSIS RESULTS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS analysis_results (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
total_frames INTEGER NOT NULL,
duration_seconds REAL NOT NULL,
fps REAL DEFAULT 30.0,
players_detected INTEGER,
-- Team analysis specific
team_1_possession_percent REAL,
team_2_possession_percent REAL,
total_passes INTEGER,
total_interceptions INTEGER,
defensive_actions INTEGER,
-- Personal analysis specific
shot_attempts INTEGER,
overall_shooting_percentage REAL,
shot_form_consistency REAL,
dribble_count INTEGER,
dribble_frequency_per_minute REAL,
total_distance_meters REAL,
avg_speed_kmh REAL,
max_speed_kmh REAL,
acceleration_events INTEGER,
avg_knee_bend_angle REAL,
avg_elbow_angle_shooting REAL,
training_load_score REAL,
-- Events (stored as JSONB)
events JSONB DEFAULT '[]',
processing_time_seconds REAL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_analysis_video ON analysis_results(video_id);
-- ============================================
-- ANALYTICS TABLE (time-series metrics)
-- ============================================
CREATE TABLE IF NOT EXISTS analytics (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
video_id UUID REFERENCES videos(id) ON DELETE SET NULL,
metric_type TEXT NOT NULL,
value REAL NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_analytics_player ON analytics(player_id);
CREATE INDEX IF NOT EXISTS idx_analytics_type ON analytics(player_id, metric_type);
CREATE INDEX IF NOT EXISTS idx_analytics_video ON analytics(video_id);
-- ============================================
-- ROW LEVEL SECURITY POLICIES
-- ============================================
-- Enable RLS on all tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE players ENABLE ROW LEVEL SECURITY;
ALTER TABLE videos ENABLE ROW LEVEL SECURITY;
ALTER TABLE detections ENABLE ROW LEVEL SECURITY;
ALTER TABLE analysis_results ENABLE ROW LEVEL SECURITY;
ALTER TABLE analytics ENABLE ROW LEVEL SECURITY;
-- Users can only see their own data
DROP POLICY IF EXISTS "Users can view own profile" ON users;
DROP POLICY IF EXISTS "Users can update own profile" ON users;
DROP POLICY IF EXISTS "Enable insert for registration" ON users;
DROP POLICY IF EXISTS "Users can manage own profile" ON users;
CREATE POLICY "Users can manage own profile" ON users
FOR ALL USING (true) WITH CHECK (true);
-- Organizations: owners only
DROP POLICY IF EXISTS "Owners can manage organizations" ON organizations;
CREATE POLICY "Owners can manage organizations" ON organizations
FOR ALL USING (true) WITH CHECK (true);
-- Videos: uploaders only
DROP POLICY IF EXISTS "Uploaders can manage videos" ON videos;
CREATE POLICY "Uploaders can manage videos" ON videos
FOR ALL USING (true) WITH CHECK (true);
-- Players: org owners or personal users
DROP POLICY IF EXISTS "Users can manage their players" ON players;
CREATE POLICY "Users can manage their players" ON players
FOR ALL USING (true) WITH CHECK (true);
-- Detections: via video ownership
DROP POLICY IF EXISTS "Users can view own detections" ON detections;
CREATE POLICY "Users can manage detections" ON detections
FOR ALL USING (true) WITH CHECK (true);
-- Analysis results: via video ownership
DROP POLICY IF EXISTS "Users can view own analysis" ON analysis_results;
CREATE POLICY "Users can manage analysis" ON analysis_results
FOR ALL USING (true) WITH CHECK (true);
-- Analytics: via player ownership
DROP POLICY IF EXISTS "Users can view own analytics" ON analytics;
CREATE POLICY "Users can manage analytics" ON analytics
FOR ALL USING (true) WITH CHECK (true);
-- ============================================
-- FUNCTIONS
-- ============================================
-- Auto-update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql
SET search_path = '';
-- Apply trigger to relevant tables
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
DROP TRIGGER IF EXISTS update_organizations_updated_at ON organizations;
CREATE TRIGGER update_organizations_updated_at
BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
DROP TRIGGER IF EXISTS update_players_updated_at ON players;
CREATE TRIGGER update_players_updated_at
BEFORE UPDATE ON players
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
DROP TRIGGER IF EXISTS update_videos_updated_at ON videos;
CREATE TRIGGER update_videos_updated_at
BEFORE UPDATE ON videos
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ============================================
-- SCHEDULES TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS schedules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
type TEXT NOT NULL,
location TEXT,
description TEXT,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Announcements Table
CREATE TABLE IF NOT EXISTS announcements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_schedules_org ON schedules(organization_id);
-- ============================================
-- MATCHES TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS matches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
opponent TEXT NOT NULL,
date TIMESTAMPTZ NOT NULL,
location TEXT,
result TEXT,
score_us INTEGER,
score_them INTEGER,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_matches_org ON matches(organization_id);
-- ============================================
-- NOTIFICATIONS TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
recipient_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
message TEXT NOT NULL,
type TEXT DEFAULT 'info',
read BOOLEAN DEFAULT FALSE,
action_link TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_id);
-- ============================================
-- RLS POLICIES FOR NEW TABLES
-- ============================================
ALTER TABLE schedules ENABLE ROW LEVEL SECURITY;
ALTER TABLE matches ENABLE ROW LEVEL SECURITY;
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
ALTER TABLE announcements ENABLE ROW LEVEL SECURITY;
-- Schedules: viewable by org members (including owners)
DROP POLICY IF EXISTS "Users can view schedules" ON schedules;
CREATE POLICY "Users can view schedules" ON schedules
FOR SELECT USING (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text
)
);
DROP POLICY IF EXISTS "Owners and Staff can manage schedules" ON schedules;
CREATE POLICY "Owners and Staff can manage schedules" ON schedules
FOR ALL WITH CHECK (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach')
)
);
-- Matches: similar to schedules
DROP POLICY IF EXISTS "Users can view matches" ON matches;
CREATE POLICY "Users can view matches" ON matches
FOR SELECT USING (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text
)
);
DROP POLICY IF EXISTS "Owners and Staff can manage matches" ON matches;
CREATE POLICY "Owners and Staff can manage matches" ON matches
FOR ALL WITH CHECK (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach')
)
);
-- Announcements: viewable by org members
DROP POLICY IF EXISTS "Users can view announcements" ON announcements;
CREATE POLICY "Users can view announcements" ON announcements
FOR SELECT USING (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text
)
);
DROP POLICY IF EXISTS "Owners and Staff can manage announcements" ON announcements;
CREATE POLICY "Owners and Staff can manage announcements" ON announcements
FOR ALL WITH CHECK (
organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
UNION
SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach')
)
);
-- Notifications: generic
DROP POLICY IF EXISTS "Users can manage own notifications" ON notifications;
CREATE POLICY "Users can manage own notifications" ON notifications
FOR ALL USING (recipient_id::text = (SELECT auth.uid())::text);
-- Triggers
DROP TRIGGER IF EXISTS update_schedules_updated_at ON schedules;
CREATE TRIGGER update_schedules_updated_at
BEFORE UPDATE ON schedules
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
DROP TRIGGER IF EXISTS update_matches_updated_at ON matches;
CREATE TRIGGER update_matches_updated_at
BEFORE UPDATE ON matches
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ============================================
-- ACTIVITIES TABLE
-- ============================================
CREATE TABLE IF NOT EXISTS activities (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
player_id UUID REFERENCES players(id) ON DELETE CASCADE,
type TEXT NOT NULL,
description TEXT,
date TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_activities_player ON activities(player_id);
ALTER TABLE activities ENABLE ROW LEVEL SECURITY;
-- Activities: viewable by self or org owner
DROP POLICY IF EXISTS "Users can view activities" ON activities;
CREATE POLICY "Users can view activities" ON activities
FOR SELECT USING (
player_id IN (
SELECT id FROM players WHERE user_id::text = (SELECT auth.uid())::text
OR organization_id IN (
SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text
)
)
);
DROP POLICY IF EXISTS "Players can manage own activities" ON activities;
CREATE POLICY "Players can manage own activities" ON activities
FOR ALL WITH CHECK (
player_id IN (
SELECT id FROM players WHERE user_id::text = (SELECT auth.uid())::text
)
);
-- ============================================
-- PERSONAL ANALYSES TABLE (Standalone for personal accounts)
-- ============================================
CREATE TABLE IF NOT EXISTS personal_analyses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
job_id UUID UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL,
results_json JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_personal_analyses_user ON personal_analyses(user_id);
CREATE INDEX IF NOT EXISTS idx_personal_analyses_job ON personal_analyses(job_id);
ALTER TABLE personal_analyses ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users can manage own personal analyses" ON personal_analyses;
CREATE POLICY "Users can manage own personal analyses" ON personal_analyses
FOR ALL USING ((SELECT auth.uid())::text = user_id::text);
-- Triggers
DROP TRIGGER IF EXISTS update_personal_analyses_updated_at ON personal_analyses;
CREATE TRIGGER update_personal_analyses_updated_at
BEFORE UPDATE ON personal_analyses
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ==========================================
-- STORAGE BUCKETS SETUP
-- ==========================================
-- Create the team-analysis-videos bucket for annotated outputs
INSERT INTO storage.buckets (id, name, public)
VALUES ('team-analysis-videos', 'team-analysis-videos', true)
ON CONFLICT (id) DO NOTHING;
-- RLS Policies for the storage.objects table for the new bucket
-- Drop existing policies first to avoid errors if re-run
DROP POLICY IF EXISTS "Public Access Team Videos" ON storage.objects;
DROP POLICY IF EXISTS "Allow Uploads Team Videos" ON storage.objects;
DROP POLICY IF EXISTS "Allow Updates Team Videos" ON storage.objects;
DROP POLICY IF EXISTS "Allow Deletes Team Videos" ON storage.objects;
-- Allow public access to read files
CREATE POLICY "Public Access Team Videos" ON storage.objects
FOR SELECT
USING (bucket_id = 'team-analysis-videos');
-- Allow all users (including anon backend) to upload files
CREATE POLICY "Allow Uploads Team Videos" ON storage.objects
FOR INSERT
WITH CHECK (bucket_id = 'team-analysis-videos');
-- Allow updates
CREATE POLICY "Allow Updates Team Videos" ON storage.objects
FOR UPDATE
USING (bucket_id = 'team-analysis-videos');
-- Allow deletes
CREATE POLICY "Allow Deletes Team Videos" ON storage.objects
FOR DELETE
USING (bucket_id = 'team-analysis-videos');
-- End of schema
-- ==========================================
-- UPDATE EXISTING TABLES
-- ==========================================
-- Run this to update your existing videos table
ALTER TABLE videos ADD COLUMN IF NOT EXISTS annotated_url TEXT;