Spaces:
Sleeping
Sleeping
| -- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- LabCard AI β Supabase Schema | |
| -- Run this in Supabase SQL Editor: https://supabase.com/dashboard/project/_/sql | |
| -- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- ββ User Profiles βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Extends auth.users β created automatically on first report / payment | |
| CREATE TABLE IF NOT EXISTS public.user_profiles ( | |
| id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY, | |
| tier TEXT NOT NULL DEFAULT 'free' | |
| CHECK (tier IN ('free', 'premium')), | |
| reports_analyzed INTEGER NOT NULL DEFAULT 0, | |
| premium_expires_at TIMESTAMPTZ, | |
| created_at TIMESTAMPTZ DEFAULT NOW(), | |
| updated_at TIMESTAMPTZ DEFAULT NOW() | |
| ); | |
| -- Auto-update updated_at on any change | |
| CREATE OR REPLACE FUNCTION update_updated_at() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| NEW.updated_at = NOW(); | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| CREATE TRIGGER user_profiles_updated_at | |
| BEFORE UPDATE ON public.user_profiles | |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); | |
| -- ββ Reports βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Full LabReport JSON stored in report_json JSONB for future trend analysis. | |
| -- Summary columns (score, grade, etc.) allow fast SQL aggregates without | |
| -- parsing the full JSON every time. | |
| CREATE TABLE IF NOT EXISTS public.reports ( | |
| id UUID DEFAULT gen_random_uuid() PRIMARY KEY, | |
| user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, | |
| -- Extracted patient fields (denormalized for fast queries) | |
| patient_name TEXT, | |
| patient_age TEXT, | |
| patient_gender TEXT, | |
| lab_name TEXT, | |
| report_date TEXT, | |
| -- Health metrics (all deterministic β not from AI) | |
| health_score INTEGER CHECK (health_score >= 0 AND health_score <= 100), | |
| health_grade TEXT, | |
| biological_age INTEGER, | |
| chronological_age INTEGER, | |
| has_critical_alert BOOLEAN DEFAULT FALSE, | |
| -- Processing metadata | |
| tier_used TEXT DEFAULT 'free', | |
| biomarker_count INTEGER, | |
| abnormal_count INTEGER, | |
| processing_time_ms INTEGER, | |
| -- Full report blob (enables trend queries + future re-analysis) | |
| report_json JSONB NOT NULL, | |
| created_at TIMESTAMPTZ DEFAULT NOW() | |
| ); | |
| -- Indexes for common access patterns | |
| CREATE INDEX IF NOT EXISTS idx_reports_user_id | |
| ON public.reports(user_id); | |
| CREATE INDEX IF NOT EXISTS idx_reports_created_at | |
| ON public.reports(created_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_reports_health_score | |
| ON public.reports(health_score); | |
| -- GIN index on JSONB for fast biomarker trend queries | |
| CREATE INDEX IF NOT EXISTS idx_reports_json | |
| ON public.reports USING GIN (report_json); | |
| -- ββ Payments ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Audit trail for all Razorpay transactions. | |
| -- razorpay_order_id is UNIQUE β idempotent upsert is safe. | |
| CREATE TABLE IF NOT EXISTS public.payments ( | |
| id UUID DEFAULT gen_random_uuid() PRIMARY KEY, | |
| user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, | |
| razorpay_order_id TEXT UNIQUE NOT NULL, | |
| razorpay_payment_id TEXT UNIQUE, | |
| razorpay_signature TEXT, | |
| plan TEXT NOT NULL | |
| CHECK (plan IN ('per_report', 'monthly')), | |
| amount_paise INTEGER NOT NULL, | |
| status TEXT NOT NULL DEFAULT 'pending' | |
| CHECK (status IN ('pending', 'success', 'failed')), | |
| verified_at TIMESTAMPTZ, | |
| created_at TIMESTAMPTZ DEFAULT NOW() | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_payments_user_id | |
| ON public.payments(user_id); | |
| -- ββ Row Level Security ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Users can only read their own data. | |
| -- Backend uses service role key β bypasses RLS for writes. | |
| ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.payments ENABLE ROW LEVEL SECURITY; | |
| -- user_profiles: users see and update only their own row | |
| CREATE POLICY "Users see own profile" | |
| ON public.user_profiles FOR ALL | |
| USING (auth.uid() = id); | |
| -- reports: users can read their own reports | |
| CREATE POLICY "Users see own reports" | |
| ON public.reports FOR SELECT | |
| USING (auth.uid() = user_id); | |
| -- reports: backend service role inserts (WITH CHECK true allows service key) | |
| CREATE POLICY "Service role inserts reports" | |
| ON public.reports FOR INSERT | |
| WITH CHECK (true); | |
| -- payments: users can only see their own payment records | |
| CREATE POLICY "Users see own payments" | |
| ON public.payments FOR SELECT | |
| USING (auth.uid() = user_id); | |
| -- payments: backend service role inserts | |
| CREATE POLICY "Service role inserts payments" | |
| ON public.payments FOR INSERT | |
| WITH CHECK (true); | |
| -- ββ Helper: increment reports_analyzed counter ββββββββββββββββββββββββββββββββ | |
| -- Called via trigger when a new report is inserted | |
| CREATE OR REPLACE FUNCTION increment_reports_analyzed() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| IF NEW.user_id IS NOT NULL THEN | |
| INSERT INTO public.user_profiles (id, reports_analyzed) | |
| VALUES (NEW.user_id, 1) | |
| ON CONFLICT (id) DO UPDATE | |
| SET reports_analyzed = user_profiles.reports_analyzed + 1, | |
| updated_at = NOW(); | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql SECURITY DEFINER; | |
| CREATE TRIGGER on_report_inserted | |
| AFTER INSERT ON public.reports | |
| FOR EACH ROW EXECUTE FUNCTION increment_reports_analyzed(); | |
| -- ββ Trend analysis view (bonus β useful for future dashboard) βββββββββββββββββ | |
| -- Returns latest health score per user for trend charts | |
| CREATE OR REPLACE VIEW public.user_health_trends AS | |
| SELECT | |
| r.user_id, | |
| r.id AS report_id, | |
| r.created_at, | |
| r.health_score, | |
| r.health_grade, | |
| r.biological_age, | |
| r.chronological_age, | |
| r.abnormal_count, | |
| r.lab_name | |
| FROM public.reports r | |
| WHERE r.user_id IS NOT NULL | |
| ORDER BY r.user_id, r.created_at DESC; | |
| -- ββ Verify setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SELECT | |
| schemaname, | |
| tablename, | |
| tableowner | |
| FROM pg_tables | |
| WHERE schemaname = 'public' | |
| AND tablename IN ('user_profiles', 'reports', 'payments'); | |