File size: 19,967 Bytes
32bc095 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | -- 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;
|