-- Migration 002: Gamification — Xu system, XP, streak, leaderboard -- Run in Supabase Dashboard → SQL Editor (after 001_user_progress.sql) -- ============================================================ -- User gamification state (one row per user) -- ============================================================ CREATE TABLE IF NOT EXISTS user_gamification ( user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, xp INT NOT NULL DEFAULT 0, level TEXT NOT NULL DEFAULT 'beginner' CHECK (level IN ('beginner', 'bronze', 'silver', 'gold', 'master')), streak INT NOT NULL DEFAULT 0, longest_streak INT NOT NULL DEFAULT 0, last_active DATE, xu INT NOT NULL DEFAULT 50, -- welcome bonus freeze_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now() ); -- ============================================================ -- Xu transaction log (immutable audit trail) -- ============================================================ CREATE TABLE IF NOT EXISTS xu_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, type TEXT NOT NULL CHECK (type IN ( 'earn_welcome', 'earn_daily', 'earn_streak', 'earn_ads', 'spend_freeze', 'spend_writing', 'spend_test' )), amount INT NOT NULL, -- positive = earn, negative = spend balance INT NOT NULL, -- xu balance after this transaction description TEXT, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_xu_transactions_user_date ON xu_transactions(user_id, created_at DESC); -- ============================================================ -- Weekly leaderboard (reset every week) -- ============================================================ CREATE TABLE IF NOT EXISTS weekly_leaderboard ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, week_start DATE NOT NULL, xp_earned INT NOT NULL DEFAULT 0, rank INT, UNIQUE (user_id, week_start) ); CREATE INDEX IF NOT EXISTS idx_leaderboard_week_xp ON weekly_leaderboard(week_start, xp_earned DESC); -- ============================================================ -- Row Level Security -- ============================================================ -- user_gamification ALTER TABLE user_gamification ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can read own gamification" ON user_gamification FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert own gamification" ON user_gamification FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own gamification" ON user_gamification FOR UPDATE USING (auth.uid() = user_id); -- xu_transactions (immutable — no UPDATE/DELETE) ALTER TABLE xu_transactions ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can read own xu transactions" ON xu_transactions FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can insert own xu transactions" ON xu_transactions FOR INSERT WITH CHECK (auth.uid() = user_id); -- weekly_leaderboard (public read, own write) ALTER TABLE weekly_leaderboard ENABLE ROW LEVEL SECURITY; CREATE POLICY "Anyone can read leaderboard" ON weekly_leaderboard FOR SELECT USING (true); CREATE POLICY "Users can insert own leaderboard" ON weekly_leaderboard FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own leaderboard" ON weekly_leaderboard FOR UPDATE USING (auth.uid() = user_id); -- ============================================================ -- Trigger: auto-create gamification row on new user signup -- ============================================================ CREATE OR REPLACE FUNCTION handle_new_user_gamification() RETURNS TRIGGER AS $$ BEGIN INSERT INTO user_gamification (user_id, xu) VALUES (NEW.id, 50); INSERT INTO xu_transactions (user_id, type, amount, balance, description) VALUES (NEW.id, 'earn_welcome', 50, 50, 'Welcome bonus'); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER on_auth_user_created_gamification AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user_gamification();