-- ============================================================================= -- Ultimate Investment Tool — Supabase Schema -- Paste this entire file into Supabase SQL Editor and click Run. -- ============================================================================= -- ----------------------------------------------------------------------------- -- 1. TABLES -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS licenses ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, license_key TEXT UNIQUE NOT NULL, hwid TEXT, -- NULL until first activation tier TEXT NOT NULL DEFAULT 'monthly', -- 'weekly' | 'monthly' | 'lifetime' channel TEXT NOT NULL DEFAULT 'stable', -- 'stable' | 'beta' expiry_date TIMESTAMPTZ, -- NULL = lifetime active BOOLEAN NOT NULL DEFAULT true, machines_allowed INTEGER NOT NULL DEFAULT 1, notes TEXT, -- customer name / order ref created_at TIMESTAMPTZ DEFAULT NOW(), activated_at TIMESTAMPTZ ); -- Add channel column to existing tables (safe to run on already-created tables) ALTER TABLE licenses ADD COLUMN IF NOT EXISTS channel TEXT NOT NULL DEFAULT 'stable'; CREATE TABLE IF NOT EXISTS app_versions ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, version TEXT UNIQUE NOT NULL, -- e.g. '1.0.1' channel TEXT NOT NULL DEFAULT 'stable', -- 'stable' | 'beta' gui_py_url TEXT, -- Supabase Storage URL for screener_gui.py screener_py_url TEXT, -- Supabase Storage URL for stock_screener.py release_notes TEXT, is_latest BOOLEAN NOT NULL DEFAULT true, released_at TIMESTAMPTZ DEFAULT NOW() ); -- Add channel column to existing app_versions tables (safe to run on already-created tables) ALTER TABLE app_versions ADD COLUMN IF NOT EXISTS channel TEXT NOT NULL DEFAULT 'stable'; CREATE TABLE IF NOT EXISTS auth_log ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, license_key TEXT, hwid TEXT, result TEXT, -- 'valid' | 'invalid' reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS analyst_cache ( ticker TEXT PRIMARY KEY, pe_forward DOUBLE PRECISION, eps_forward DOUBLE PRECISION, analyst_norm DOUBLE PRECISION, analyst_upside DOUBLE PRECISION, analyst_count INTEGER, analyst_target DOUBLE PRECISION, recommendation TEXT, news_sentiment DOUBLE PRECISION, sector TEXT, industry TEXT, updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS fundamentals_cache ( ticker TEXT PRIMARY KEY, -- Valuation pe_trailing DOUBLE PRECISION, pb_ratio DOUBLE PRECISION, ev_ebitda DOUBLE PRECISION, eps_trailing DOUBLE PRECISION, -- Growth revenue DOUBLE PRECISION, revenue_growth DOUBLE PRECISION, earnings_growth DOUBLE PRECISION, -- Quality roe DOUBLE PRECISION, roa DOUBLE PRECISION, debt_to_equity DOUBLE PRECISION, total_debt DOUBLE PRECISION, total_cash DOUBLE PRECISION, book_value DOUBLE PRECISION, current_ratio DOUBLE PRECISION, -- Profitability profit_margin DOUBLE PRECISION, operating_margin DOUBLE PRECISION, fcf_yield DOUBLE PRECISION, dividend_yield DOUBLE PRECISION, -- Market market_cap DOUBLE PRECISION, shares_outstanding DOUBLE PRECISION, ev_revenue DOUBLE PRECISION, -- Risk short_percent DOUBLE PRECISION, -- Meta filer_type TEXT, -- 'us-gaap-quarterly' | 'us-gaap-annual' | 'ifrs-annual' | 'none' updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS sector_stats ( sector TEXT PRIMARY KEY, -- Valuation pe_trailing_med DOUBLE PRECISION, pe_trailing_mad DOUBLE PRECISION, pb_ratio_med DOUBLE PRECISION, pb_ratio_mad DOUBLE PRECISION, ev_ebitda_med DOUBLE PRECISION, ev_ebitda_mad DOUBLE PRECISION, ev_revenue_med DOUBLE PRECISION, ev_revenue_mad DOUBLE PRECISION, -- Growth revenue_growth_med DOUBLE PRECISION, revenue_growth_mad DOUBLE PRECISION, earnings_growth_med DOUBLE PRECISION, earnings_growth_mad DOUBLE PRECISION, eps_growth_med DOUBLE PRECISION, eps_growth_mad DOUBLE PRECISION, -- Quality roe_med DOUBLE PRECISION, roe_mad DOUBLE PRECISION, roa_med DOUBLE PRECISION, roa_mad DOUBLE PRECISION, debt_to_equity_med DOUBLE PRECISION, debt_to_equity_mad DOUBLE PRECISION, current_ratio_med DOUBLE PRECISION, current_ratio_mad DOUBLE PRECISION, -- Profitability profit_margin_med DOUBLE PRECISION, profit_margin_mad DOUBLE PRECISION, operating_margin_med DOUBLE PRECISION, operating_margin_mad DOUBLE PRECISION, fcf_yield_med DOUBLE PRECISION, fcf_yield_mad DOUBLE PRECISION, -- Analyst analyst_upside_med DOUBLE PRECISION, analyst_upside_mad DOUBLE PRECISION, -- Meta ticker_count INTEGER, updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Add columns that may be missing if sector_stats was created before the current schema ALTER TABLE sector_stats ADD COLUMN IF NOT EXISTS eps_growth_med DOUBLE PRECISION; ALTER TABLE sector_stats ADD COLUMN IF NOT EXISTS eps_growth_mad DOUBLE PRECISION; ALTER TABLE sector_stats ADD COLUMN IF NOT EXISTS ticker_count INTEGER; ALTER TABLE sector_stats ENABLE ROW LEVEL SECURITY; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'sector_stats' AND policyname = 'anon_select' ) THEN CREATE POLICY anon_select ON sector_stats FOR SELECT TO anon USING (true); END IF; END $$; ALTER TABLE fundamentals_cache ENABLE ROW LEVEL SECURITY; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'fundamentals_cache' AND policyname = 'anon_select' ) THEN CREATE POLICY anon_select ON fundamentals_cache FOR SELECT TO anon USING (true); END IF; END $$; -- Seed the initial version row so the updater has something to compare against. INSERT INTO app_versions (version, is_latest, release_notes) SELECT '1.0.0', true, 'Initial release' WHERE NOT EXISTS (SELECT 1 FROM app_versions WHERE version = '1.0.0'); -- ----------------------------------------------------------------------------- -- 2. ROW LEVEL SECURITY — no direct table access for anon -- ----------------------------------------------------------------------------- ALTER TABLE licenses ENABLE ROW LEVEL SECURITY; ALTER TABLE app_versions ENABLE ROW LEVEL SECURITY; ALTER TABLE auth_log ENABLE ROW LEVEL SECURITY; ALTER TABLE analyst_cache ENABLE ROW LEVEL SECURITY; -- No RLS policies on licenses/app_versions/auth_log = anon cannot access directly. -- All sensitive access goes through SECURITY DEFINER functions below. -- analyst_cache is public market data — allow the anon key to read it. DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'analyst_cache' AND policyname = 'anon_select' ) THEN CREATE POLICY anon_select ON analyst_cache FOR SELECT TO anon USING (true); END IF; END $$; -- ----------------------------------------------------------------------------- -- 3. RPC: verify_license(p_key, p_hwid) → JSON -- ----------------------------------------------------------------------------- -- Called by the app on every launch with the stored license key + machine HWID. -- Handles: first-time HWID binding, expiry, disabled keys, HWID mismatch. -- Returns: { valid, reason, tier, expiry_date } -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION verify_license(p_key TEXT, p_hwid TEXT) RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE lic RECORD; BEGIN -- Look up the key SELECT * INTO lic FROM licenses WHERE license_key = p_key; IF NOT FOUND THEN INSERT INTO auth_log (license_key, hwid, result, reason) VALUES (p_key, p_hwid, 'invalid', 'key_not_found'); RETURN json_build_object( 'valid', false, 'reason', 'License key not found.' ); END IF; -- Check disabled IF NOT lic.active THEN INSERT INTO auth_log (license_key, hwid, result, reason) VALUES (p_key, p_hwid, 'invalid', 'key_disabled'); RETURN json_build_object( 'valid', false, 'reason', 'This license has been disabled. Please contact support.' ); END IF; -- Check expiry (NULL = lifetime, never expires) IF lic.expiry_date IS NOT NULL AND lic.expiry_date < NOW() THEN INSERT INTO auth_log (license_key, hwid, result, reason) VALUES (p_key, p_hwid, 'invalid', 'expired'); RETURN json_build_object( 'valid', false, 'reason', 'Your subscription has expired. Please renew to continue.' ); END IF; -- HWID check IF lic.hwid IS NULL THEN -- First activation — bind this machine UPDATE licenses SET hwid = p_hwid, activated_at = NOW() WHERE license_key = p_key; ELSIF lic.hwid <> p_hwid THEN INSERT INTO auth_log (license_key, hwid, result, reason) VALUES (p_key, p_hwid, 'invalid', 'hwid_mismatch'); RETURN json_build_object( 'valid', false, 'reason', 'This license is registered to a different machine. Contact support to transfer.' ); END IF; -- All checks passed INSERT INTO auth_log (license_key, hwid, result, reason) VALUES (p_key, p_hwid, 'valid', 'ok'); RETURN json_build_object( 'valid', true, 'reason', 'ok', 'tier', lic.tier, 'channel', lic.channel, 'expiry_date', lic.expiry_date ); END; $$; -- ----------------------------------------------------------------------------- -- 4. RPC: get_latest_version() → JSON -- ----------------------------------------------------------------------------- -- Called by the updater after a successful license check. -- Returns: { version, gui_py_url, screener_py_url, release_notes } -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION get_latest_version(p_channel TEXT DEFAULT 'stable') RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE ver RECORD; BEGIN SELECT * INTO ver FROM app_versions WHERE is_latest = true AND channel = p_channel ORDER BY released_at DESC LIMIT 1; IF NOT FOUND THEN RETURN json_build_object('version', '1.0.0'); END IF; RETURN json_build_object( 'version', ver.version, 'gui_py_url', ver.gui_py_url, 'screener_py_url', ver.screener_py_url, 'release_notes', ver.release_notes ); END; $$; -- ----------------------------------------------------------------------------- -- 5. GRANTS — allow anon key to call the two RPC functions only -- ----------------------------------------------------------------------------- GRANT EXECUTE ON FUNCTION verify_license(TEXT, TEXT) TO anon; GRANT EXECUTE ON FUNCTION get_latest_version(TEXT) TO anon;