Spaces:
Running
on
Zero
Running
on
Zero
| -- Portfolio Intelligence Platform Database Schema | |
| -- For Supabase PostgreSQL with Auth Integration | |
| -- Enable UUID extension (if not already enabled) | |
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | |
| -- Helper function to convert strings to UUIDs (for demo portfolio IDs) | |
| CREATE OR REPLACE FUNCTION string_to_portfolio_uuid(input_string TEXT) | |
| RETURNS UUID | |
| LANGUAGE plpgsql | |
| IMMUTABLE | |
| SECURITY INVOKER | |
| SET search_path = '' | |
| AS $$ | |
| BEGIN | |
| -- Use URL namespace UUID constant (6ba7b811-9dad-11d1-80b4-00c04fd430c8) | |
| -- uuid_ns_url() is a constant in PostgreSQL, not a function | |
| RETURN public.uuid_generate_v5('6ba7b811-9dad-11d1-80b4-00c04fd430c8'::uuid, input_string); | |
| END; | |
| $$; | |
| -- Grant execute to authenticated and anon roles | |
| GRANT EXECUTE ON FUNCTION string_to_portfolio_uuid TO authenticated, anon; | |
| -- Users table (linked to Supabase Auth - CRITICAL FIX) | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, | |
| email VARCHAR(255) UNIQUE NOT NULL, | |
| username VARCHAR(100), | |
| is_active BOOLEAN DEFAULT true, | |
| is_demo BOOLEAN DEFAULT false, | |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_users_id ON users(id); | |
| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); | |
| -- Portfolios table | |
| CREATE TABLE IF NOT EXISTS portfolios ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, | |
| name VARCHAR(200) NOT NULL, | |
| description TEXT, | |
| risk_tolerance VARCHAR(20) DEFAULT 'moderate', | |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_portfolios_user_id ON portfolios(user_id); | |
| CREATE INDEX IF NOT EXISTS idx_portfolios_created_at ON portfolios(created_at DESC); | |
| -- Portfolio inputs table (for auto-save history - keeps last 3 per user) | |
| CREATE TABLE IF NOT EXISTS portfolio_inputs ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, | |
| description TEXT NOT NULL, | |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_portfolio_inputs_user_created ON portfolio_inputs(user_id, created_at DESC); | |
| -- Trigger function to limit portfolio inputs to 3 per user | |
| CREATE OR REPLACE FUNCTION limit_portfolio_inputs_per_user() | |
| RETURNS TRIGGER | |
| LANGUAGE plpgsql | |
| SECURITY INVOKER | |
| SET search_path = '' | |
| AS $$ | |
| BEGIN | |
| -- Keep only the 3 most recent entries per user | |
| DELETE FROM public.portfolio_inputs | |
| WHERE id IN ( | |
| SELECT id | |
| FROM ( | |
| SELECT id, | |
| ROW_NUMBER() OVER ( | |
| PARTITION BY user_id | |
| ORDER BY created_at DESC | |
| ) AS row_num | |
| FROM public.portfolio_inputs | |
| WHERE user_id = NEW.user_id | |
| ) ranked | |
| WHERE row_num > 3 | |
| ); | |
| RETURN NULL; | |
| END; | |
| $$; | |
| -- Create trigger for automatic cleanup | |
| DROP TRIGGER IF EXISTS trigger_limit_portfolio_inputs ON portfolio_inputs; | |
| CREATE TRIGGER trigger_limit_portfolio_inputs | |
| AFTER INSERT ON portfolio_inputs | |
| FOR EACH ROW EXECUTE FUNCTION limit_portfolio_inputs_per_user(); | |
| -- Portfolio holdings table | |
| CREATE TABLE IF NOT EXISTS portfolio_holdings ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE, | |
| ticker VARCHAR(20) NOT NULL, | |
| quantity NUMERIC(20, 8) NOT NULL, | |
| cost_basis NUMERIC(20, 2), | |
| asset_type VARCHAR(20) DEFAULT 'stock', | |
| added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_holdings_portfolio_id ON portfolio_holdings(portfolio_id); | |
| CREATE INDEX IF NOT EXISTS idx_holdings_ticker ON portfolio_holdings(ticker); | |
| -- Portfolio analyses table | |
| CREATE TABLE IF NOT EXISTS portfolio_analyses ( | |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), | |
| portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE, | |
| analysis_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(), | |
| -- Analysis results (JSONB for flexibility) | |
| holdings_snapshot JSONB NOT NULL, | |
| market_data JSONB, | |
| risk_metrics JSONB, | |
| optimisation_results JSONB, | |
| ai_synthesis TEXT, | |
| recommendations JSONB, | |
| -- Metadata | |
| reasoning_steps JSONB, | |
| mcp_calls JSONB, | |
| sentiment_data JSONB, | |
| execution_time_ms INTEGER, | |
| model_version VARCHAR(50), | |
| -- Computed fields | |
| total_value NUMERIC(20, 2), | |
| health_score INTEGER CHECK (health_score >= 0 AND health_score <= 10), | |
| risk_level VARCHAR(20), | |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_analyses_portfolio_id ON portfolio_analyses(portfolio_id); | |
| CREATE INDEX IF NOT EXISTS idx_analyses_date ON portfolio_analyses(analysis_date DESC); | |
| CREATE INDEX IF NOT EXISTS idx_analyses_health_score ON portfolio_analyses(health_score); | |
| CREATE INDEX IF NOT EXISTS idx_analyses_sentiment_data ON portfolio_analyses USING GIN (sentiment_data); | |
| -- Function to automatically create user profile on signup (CRITICAL FOR AUTH) | |
| -- Uses SECURITY DEFINER with empty search_path to bypass RLS on INSERT | |
| -- Fully qualifies table name (public.users) to work with empty search_path | |
| CREATE OR REPLACE FUNCTION public.handle_new_user() | |
| RETURNS TRIGGER | |
| LANGUAGE plpgsql | |
| SECURITY DEFINER SET search_path = '' | |
| AS $$ | |
| BEGIN | |
| INSERT INTO public.users (id, email, username, is_active, is_demo) | |
| VALUES ( | |
| NEW.id, | |
| NEW.email, | |
| COALESCE( | |
| NEW.raw_user_meta_data ->> 'username', | |
| split_part(NEW.email, '@', 1) | |
| ), | |
| true, | |
| false | |
| ); | |
| RETURN NEW; | |
| EXCEPTION | |
| WHEN OTHERS THEN | |
| -- Log error but don't block signup | |
| RAISE WARNING 'Failed to create user profile: %', SQLERRM; | |
| RETURN NEW; | |
| END; | |
| $$; | |
| -- Create trigger for automatic profile creation | |
| DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; | |
| CREATE TRIGGER on_auth_user_created | |
| AFTER INSERT ON auth.users | |
| FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); | |
| -- Function to update updated_at timestamp | |
| CREATE OR REPLACE FUNCTION update_updated_at_column() | |
| RETURNS TRIGGER | |
| LANGUAGE plpgsql | |
| SECURITY INVOKER | |
| SET search_path = '' | |
| AS $$ | |
| BEGIN | |
| NEW.updated_at = NOW(); | |
| RETURN NEW; | |
| END; | |
| $$; | |
| -- Triggers for updated_at | |
| 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_column(); | |
| DROP TRIGGER IF EXISTS update_portfolios_updated_at ON portfolios; | |
| CREATE TRIGGER update_portfolios_updated_at BEFORE UPDATE ON portfolios | |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); | |
| -- Grant permissions (Supabase uses RLS - Row Level Security) | |
| -- These are basic permissions; adjust based on your auth setup | |
| ALTER TABLE users ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE portfolios ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE portfolio_inputs ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE portfolio_holdings ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE portfolio_analyses ENABLE ROW LEVEL SECURITY; | |
| -- RLS policies with performance optimizations (wrapped auth.uid() = 20x faster) | |
| -- Users table policies | |
| DROP POLICY IF EXISTS users_read_own ON users; | |
| CREATE POLICY users_read_own ON users | |
| FOR SELECT | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = id); | |
| DROP POLICY IF EXISTS users_update_own ON users; | |
| CREATE POLICY users_update_own ON users | |
| FOR UPDATE | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = id) | |
| WITH CHECK ((SELECT auth.uid()) = id); | |
| DROP POLICY IF EXISTS users_insert_own ON users; | |
| CREATE POLICY users_insert_own ON users | |
| FOR INSERT | |
| TO authenticated | |
| WITH CHECK ((SELECT auth.uid()) = id); | |
| -- Portfolios table policies | |
| DROP POLICY IF EXISTS portfolios_read_own ON portfolios; | |
| CREATE POLICY portfolios_read_own ON portfolios | |
| FOR SELECT | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = user_id); | |
| DROP POLICY IF EXISTS portfolios_insert_own ON portfolios; | |
| CREATE POLICY portfolios_insert_own ON portfolios | |
| FOR INSERT | |
| TO authenticated | |
| WITH CHECK ((SELECT auth.uid()) = user_id); | |
| DROP POLICY IF EXISTS portfolios_update_own ON portfolios; | |
| CREATE POLICY portfolios_update_own ON portfolios | |
| FOR UPDATE | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = user_id) | |
| WITH CHECK ((SELECT auth.uid()) = user_id); | |
| DROP POLICY IF EXISTS portfolios_delete_own ON portfolios; | |
| CREATE POLICY portfolios_delete_own ON portfolios | |
| FOR DELETE | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = user_id); | |
| -- Portfolio inputs table policies | |
| DROP POLICY IF EXISTS portfolio_inputs_read_own ON portfolio_inputs; | |
| CREATE POLICY portfolio_inputs_read_own ON portfolio_inputs | |
| FOR SELECT | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = user_id); | |
| DROP POLICY IF EXISTS portfolio_inputs_insert_own ON portfolio_inputs; | |
| CREATE POLICY portfolio_inputs_insert_own ON portfolio_inputs | |
| FOR INSERT | |
| TO authenticated | |
| WITH CHECK ((SELECT auth.uid()) = user_id); | |
| DROP POLICY IF EXISTS portfolio_inputs_delete_own ON portfolio_inputs; | |
| CREATE POLICY portfolio_inputs_delete_own ON portfolio_inputs | |
| FOR DELETE | |
| TO authenticated | |
| USING ((SELECT auth.uid()) = user_id); | |
| -- Holdings policies (optimized subquery pattern) | |
| DROP POLICY IF EXISTS holdings_access_own ON portfolio_holdings; | |
| CREATE POLICY holdings_access_own ON portfolio_holdings | |
| FOR ALL | |
| TO authenticated | |
| USING ( | |
| portfolio_id IN ( | |
| SELECT id FROM portfolios | |
| WHERE user_id = (SELECT auth.uid()) | |
| ) | |
| ) | |
| WITH CHECK ( | |
| portfolio_id IN ( | |
| SELECT id FROM portfolios | |
| WHERE user_id = (SELECT auth.uid()) | |
| ) | |
| ); | |
| -- Analyses policies | |
| DROP POLICY IF EXISTS analyses_access_own ON portfolio_analyses; | |
| CREATE POLICY analyses_access_own ON portfolio_analyses | |
| FOR ALL | |
| TO authenticated | |
| USING ( | |
| portfolio_id IN ( | |
| SELECT id FROM portfolios | |
| WHERE user_id = (SELECT auth.uid()) | |
| ) | |
| ) | |
| WITH CHECK ( | |
| portfolio_id IN ( | |
| SELECT id FROM portfolios | |
| WHERE user_id = (SELECT auth.uid()) | |
| ) | |
| ); | |
| -- Grant permissions | |
| GRANT USAGE ON SCHEMA public TO anon, authenticated; | |
| GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated; | |
| GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated; | |
| GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated; | |