-- VYNDR Initial Schema -- Feature 1.4 — All tables, indexes, RLS policies, triggers -- All timestamps use TIMESTAMPTZ (UTC) -- ============================================================ -- TABLE: users -- Extends auth.users with app-specific profile data -- ============================================================ CREATE TABLE public.users ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT NOT NULL, tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'analyst', 'desk')), scan_count INT NOT NULL DEFAULT 0, scan_reset_date TIMESTAMPTZ NOT NULL DEFAULT (date_trunc('month', now()) + interval '1 month'), stripe_customer_id TEXT, founder_status BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; CREATE POLICY "users_select_own" ON public.users FOR SELECT USING (auth.uid() = id); CREATE POLICY "users_update_own" ON public.users FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id); CREATE POLICY "users_insert_own" ON public.users FOR INSERT WITH CHECK (auth.uid() = id); -- ============================================================ -- TABLE: picks -- Individual prop analysis results from scans -- ============================================================ CREATE TABLE public.picks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, player TEXT NOT NULL, stat_type TEXT NOT NULL, line NUMERIC(5,1) NOT NULL, book TEXT NOT NULL, direction TEXT NOT NULL CHECK (direction IN ('over', 'under')), grade TEXT NOT NULL CHECK (grade IN ('A', 'B', 'C', 'D')), edge_pct NUMERIC(5,2), reasoning TEXT, kill_conditions TEXT[], confidence NUMERIC(4,2), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_picks_user_id ON public.picks(user_id); CREATE INDEX idx_picks_created_at ON public.picks(created_at); ALTER TABLE public.picks ENABLE ROW LEVEL SECURITY; CREATE POLICY "picks_select_own" ON public.picks FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "picks_insert_own" ON public.picks FOR INSERT WITH CHECK (auth.uid() = user_id); -- ============================================================ -- TABLE: scan_sessions -- Groups picks into a single scan/parlay analysis session -- ============================================================ CREATE TABLE public.scan_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, legs UUID[] NOT NULL DEFAULT '{}', final_grade TEXT CHECK (final_grade IN ('A', 'B', 'C', 'D')), kill_conditions TEXT[], correlation_notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_scan_sessions_user_id ON public.scan_sessions(user_id); ALTER TABLE public.scan_sessions ENABLE ROW LEVEL SECURITY; CREATE POLICY "scan_sessions_select_own" ON public.scan_sessions FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "scan_sessions_insert_own" ON public.scan_sessions FOR INSERT WITH CHECK (auth.uid() = user_id); -- ============================================================ -- TABLE: bets -- User-submitted bets (screenshot, quick slip, sportsbook sync) -- ============================================================ CREATE TABLE public.bets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, amount NUMERIC(10,2) NOT NULL, potential_payout NUMERIC(10,2), slip_data JSONB NOT NULL, book TEXT NOT NULL, bet_type TEXT NOT NULL CHECK (bet_type IN ('straight', 'parlay', 'teaser', 'round_robin')), submission_method TEXT NOT NULL CHECK (submission_method IN ('screenshot', 'quickslip', 'sync')), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'won', 'lost', 'push', 'void')), placed_at TIMESTAMPTZ NOT NULL DEFAULT now(), settled_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_bets_user_id ON public.bets(user_id); CREATE INDEX idx_bets_status ON public.bets(status); CREATE INDEX idx_bets_placed_at ON public.bets(placed_at); ALTER TABLE public.bets ENABLE ROW LEVEL SECURITY; CREATE POLICY "bets_select_own" ON public.bets FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "bets_insert_own" ON public.bets FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "bets_update_own" ON public.bets FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); -- ============================================================ -- TABLE: outcomes -- Actual results for each pick after game is played -- ============================================================ CREATE TABLE public.outcomes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), pick_id UUID NOT NULL REFERENCES public.picks(id) ON DELETE CASCADE, result TEXT NOT NULL CHECK (result IN ('hit', 'miss', 'push')), actual_value NUMERIC(5,1) NOT NULL, logged_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX idx_outcomes_pick_id ON public.outcomes(pick_id); ALTER TABLE public.outcomes ENABLE ROW LEVEL SECURITY; CREATE POLICY "outcomes_select_own" ON public.outcomes FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.picks WHERE picks.id = outcomes.pick_id AND picks.user_id = auth.uid() ) ); -- ============================================================ -- TABLE: performance -- Aggregated performance metrics per user per period -- ============================================================ CREATE TABLE public.performance ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, period TEXT NOT NULL CHECK (period IN ('weekly', 'monthly', 'all_time')), roi NUMERIC(6,2), win_rate NUMERIC(5,2), sample_size INT NOT NULL DEFAULT 0, total_wagered NUMERIC(10,2) DEFAULT 0, total_profit NUMERIC(10,2) DEFAULT 0, calculated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_performance_user_id ON public.performance(user_id); CREATE UNIQUE INDEX idx_performance_user_period ON public.performance(user_id, period); ALTER TABLE public.performance ENABLE ROW LEVEL SECURITY; CREATE POLICY "performance_select_own" ON public.performance FOR SELECT USING (auth.uid() = user_id); -- ============================================================ -- TRIGGERS -- ============================================================ -- 1. Auto-create user profile on signup CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.users (id, email) VALUES (NEW.id, NEW.email); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); -- 2. Auto-update updated_at on users table CREATE OR REPLACE FUNCTION public.update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER users_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.update_updated_at(); -- 3. Monthly scan count reset CREATE OR REPLACE FUNCTION public.reset_scan_count() RETURNS TRIGGER AS $$ BEGIN IF NEW.scan_reset_date <= now() THEN NEW.scan_count = 0; NEW.scan_reset_date = date_trunc('month', now()) + interval '1 month'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER users_scan_reset BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.reset_scan_count();