Spaces:
Paused
Paused
| -- ============================================================================ | |
| -- OpticParse & PhishVision — Supabase Database Schema | |
| -- Run this migration in your Supabase SQL Editor (Database → SQL Editor) | |
| -- ============================================================================ | |
| -- Enable UUID generation | |
| CREATE EXTENSION IF NOT EXISTS "pgcrypto"; | |
| -- --------------------------------------------------------------------------- | |
| -- users: linked to Supabase Auth, stores subscription tier + billing info | |
| -- --------------------------------------------------------------------------- | |
| CREATE TABLE IF NOT EXISTS public.users ( | |
| id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, | |
| email TEXT NOT NULL UNIQUE, | |
| tier TEXT NOT NULL DEFAULT 'free' | |
| CHECK (tier IN ('free', 'pro', 'enterprise')), | |
| monthly_limit INTEGER NOT NULL DEFAULT 100, | |
| current_usage INTEGER NOT NULL DEFAULT 0, | |
| usage_reset_at TIMESTAMPTZ NOT NULL DEFAULT (date_trunc('month', now()) + interval '1 month'), | |
| lemon_customer_id TEXT, | |
| lemon_subscription_id TEXT, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| updated_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| -- Auto-create a user row when someone signs up via Supabase Auth | |
| 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; | |
| 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(); | |
| -- --------------------------------------------------------------------------- | |
| -- api_keys: SHA-256 hashed keys bound to user profiles | |
| -- --------------------------------------------------------------------------- | |
| CREATE TABLE IF NOT EXISTS public.api_keys ( | |
| id UUID PRIMARY KEY DEFAULT gen_random_uuid(), | |
| user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, | |
| key_hash TEXT NOT NULL UNIQUE, | |
| key_prefix TEXT NOT NULL, -- e.g. 'op_live_a1b2c3d4' | |
| is_active BOOLEAN NOT NULL DEFAULT true, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now(), | |
| revoked_at TIMESTAMPTZ | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON public.api_keys (key_hash) WHERE is_active = true; | |
| CREATE INDEX IF NOT EXISTS idx_api_keys_user ON public.api_keys (user_id); | |
| -- --------------------------------------------------------------------------- | |
| -- usage_logs: per-request audit trail | |
| -- --------------------------------------------------------------------------- | |
| CREATE TABLE IF NOT EXISTS public.usage_logs ( | |
| id BIGSERIAL PRIMARY KEY, | |
| user_id UUID NOT NULL REFERENCES public.users(id), | |
| api_key_id UUID REFERENCES public.api_keys(id), | |
| endpoint TEXT NOT NULL, | |
| service TEXT NOT NULL CHECK (service IN ('opticparse', 'phishvision')), | |
| status_code INTEGER, | |
| response_time_ms INTEGER, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_usage_user_month | |
| ON public.usage_logs (user_id, created_at); | |
| -- --------------------------------------------------------------------------- | |
| -- security_events: fraud detection and key suspension log | |
| -- --------------------------------------------------------------------------- | |
| CREATE TABLE IF NOT EXISTS public.security_events ( | |
| id BIGSERIAL PRIMARY KEY, | |
| api_key_id UUID REFERENCES public.api_keys(id), | |
| user_id UUID REFERENCES public.users(id), | |
| event_type TEXT NOT NULL | |
| CHECK (event_type IN ('abuse_detected', 'key_suspended', 'key_revoked', 'key_regenerated', 'tier_upgraded', 'tier_downgraded')), | |
| details JSONB, | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| -- --------------------------------------------------------------------------- | |
| -- Row Level Security (RLS) policies | |
| -- --------------------------------------------------------------------------- | |
| -- Users can only read their own row | |
| ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY "Users read own" ON public.users | |
| FOR SELECT USING (auth.uid() = id); | |
| CREATE POLICY "Users update own" ON public.users | |
| FOR UPDATE USING (auth.uid() = id); | |
| -- API keys: users can only see their own | |
| ALTER TABLE public.api_keys ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY "Keys read own" ON public.api_keys | |
| FOR SELECT USING (auth.uid() = user_id); | |
| -- Usage logs: users can only see their own | |
| ALTER TABLE public.usage_logs ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY "Usage read own" ON public.usage_logs | |
| FOR SELECT USING (auth.uid() = user_id); | |
| -- Security events: users can only see their own | |
| ALTER TABLE public.security_events ENABLE ROW LEVEL SECURITY; | |
| CREATE POLICY "Events read own" ON public.security_events | |
| FOR SELECT USING (auth.uid() = user_id); | |
| -- --------------------------------------------------------------------------- | |
| -- Monthly usage reset function (call via Supabase cron or pg_cron) | |
| -- --------------------------------------------------------------------------- | |
| CREATE OR REPLACE FUNCTION public.reset_monthly_usage() | |
| RETURNS void AS $$ | |
| BEGIN | |
| UPDATE public.users | |
| SET current_usage = 0, | |
| usage_reset_at = date_trunc('month', now()) + interval '1 month', | |
| updated_at = now() | |
| WHERE usage_reset_at <= now(); | |
| END; | |
| $$ LANGUAGE plpgsql SECURITY DEFINER; | |