-- CREdispo Database Schema -- Run this in your Supabase SQL editor -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================ -- USERS TABLE -- ============================================ CREATE TABLE public.users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), auth_id UUID UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT UNIQUE NOT NULL, role TEXT NOT NULL DEFAULT 'agent' CHECK (role IN ('admin', 'agent', 'buyer')), name TEXT NOT NULL, company TEXT, phone TEXT, avatar_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- SELLER LEADS TABLE -- ============================================ CREATE TABLE public.seller_leads ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), agent_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, address TEXT NOT NULL, asset_type TEXT NOT NULL CHECK (asset_type IN ( 'Office', 'Industrial', 'Retail', 'Multifamily', 'Mixed-Use', 'Land', 'Hospitality', 'Other' )), asking_price NUMERIC(15, 2), cap_rate NUMERIC(5, 2), noi NUMERIC(15, 2), sqft INTEGER, units INTEGER, year_built INTEGER, market TEXT NOT NULL, city TEXT, state TEXT, notes TEXT, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'pending', 'matched', 'closed', 'withdrawn')), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- SELLER DOCUMENTS TABLE -- ============================================ CREATE TABLE public.seller_documents ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE, file_url TEXT NOT NULL, file_name TEXT NOT NULL, file_type TEXT NOT NULL, file_size INTEGER, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- BUYERS TABLE -- ============================================ CREATE TABLE public.buyers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), agent_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, user_id UUID REFERENCES public.users(id) ON DELETE SET NULL, name TEXT NOT NULL, company TEXT, email TEXT NOT NULL, phone TEXT, asset_types TEXT[] NOT NULL DEFAULT '{}', target_locations TEXT[] NOT NULL DEFAULT '{}', price_min NUMERIC(15, 2), price_max NUMERIC(15, 2), cap_rate_min NUMERIC(5, 2), cap_rate_max NUMERIC(5, 2), preferences TEXT, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'unresponsive', 'high-intent', 'inactive')), engagement_score INTEGER DEFAULT 0, last_contacted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- MATCHES TABLE -- ============================================ CREATE TABLE public.matches ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE, buyer_id UUID NOT NULL REFERENCES public.buyers(id) ON DELETE CASCADE, match_score NUMERIC(5, 2) NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'sent', 'interested', 'passed', 'closed')), agent_notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(lead_id, buyer_id) ); -- ============================================ -- OUTREACH TABLE -- ============================================ CREATE TABLE public.outreach ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), match_id UUID NOT NULL REFERENCES public.matches(id) ON DELETE CASCADE, channel TEXT NOT NULL CHECK (channel IN ('email', 'sms')), subject TEXT, body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'queued', 'sent', 'delivered', 'opened', 'clicked', 'replied', 'bounced', 'failed')), sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, opened_at TIMESTAMPTZ, clicked_at TIMESTAMPTZ, replied_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- NDAs TABLE -- ============================================ CREATE TABLE public.ndas ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), buyer_id UUID NOT NULL REFERENCES public.buyers(id) ON DELETE CASCADE, lead_id UUID NOT NULL REFERENCES public.seller_leads(id) ON DELETE CASCADE, signer_name TEXT NOT NULL, signer_email TEXT NOT NULL, signer_company TEXT, signed_at TIMESTAMPTZ DEFAULT NOW(), ip_address TEXT, user_agent TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(buyer_id, lead_id) ); -- ============================================ -- SUBSCRIPTIONS TABLE -- ============================================ CREATE TABLE public.subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, stripe_customer_id TEXT UNIQUE, stripe_subscription_id TEXT UNIQUE, status TEXT NOT NULL DEFAULT 'inactive' CHECK (status IN ('active', 'inactive', 'past_due', 'canceled', 'trialing')), plan TEXT DEFAULT 'pro', current_period_start TIMESTAMPTZ, current_period_end TIMESTAMPTZ, cancel_at_period_end BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- PROMO CODES TABLE -- ============================================ CREATE TABLE public.promo_codes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), code TEXT UNIQUE NOT NULL, discount_percent INTEGER CHECK (discount_percent BETWEEN 1 AND 100), discount_amount NUMERIC(10, 2), max_uses INTEGER, current_uses INTEGER DEFAULT 0, expires_at TIMESTAMPTZ, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================ -- INDEXES -- ============================================ CREATE INDEX idx_seller_leads_agent ON public.seller_leads(agent_id); CREATE INDEX idx_seller_leads_status ON public.seller_leads(status); CREATE INDEX idx_seller_leads_asset_type ON public.seller_leads(asset_type); CREATE INDEX idx_seller_leads_market ON public.seller_leads(market); CREATE INDEX idx_buyers_agent ON public.buyers(agent_id); CREATE INDEX idx_buyers_status ON public.buyers(status); CREATE INDEX idx_matches_lead ON public.matches(lead_id); CREATE INDEX idx_matches_buyer ON public.matches(buyer_id); CREATE INDEX idx_matches_status ON public.matches(status); CREATE INDEX idx_outreach_match ON public.outreach(match_id); CREATE INDEX idx_outreach_status ON public.outreach(status); CREATE INDEX idx_ndas_buyer ON public.ndas(buyer_id); CREATE INDEX idx_ndas_lead ON public.ndas(lead_id); CREATE INDEX idx_subscriptions_user ON public.subscriptions(user_id); CREATE INDEX idx_subscriptions_status ON public.subscriptions(status); -- ============================================ -- ROW LEVEL SECURITY (RLS) -- ============================================ ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; ALTER TABLE public.seller_leads ENABLE ROW LEVEL SECURITY; ALTER TABLE public.seller_documents ENABLE ROW LEVEL SECURITY; ALTER TABLE public.buyers ENABLE ROW LEVEL SECURITY; ALTER TABLE public.matches ENABLE ROW LEVEL SECURITY; ALTER TABLE public.outreach ENABLE ROW LEVEL SECURITY; ALTER TABLE public.ndas ENABLE ROW LEVEL SECURITY; ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY; -- Users can read their own data CREATE POLICY "Users can view own profile" ON public.users FOR SELECT USING (auth.uid() = auth_id); CREATE POLICY "Users can update own profile" ON public.users FOR UPDATE USING (auth.uid() = auth_id); -- Agents can manage their own seller leads CREATE POLICY "Agents can view own leads" ON public.seller_leads FOR SELECT USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); CREATE POLICY "Agents can insert leads" ON public.seller_leads FOR INSERT WITH CHECK (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); CREATE POLICY "Agents can update own leads" ON public.seller_leads FOR UPDATE USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); -- Agents can manage their own buyers CREATE POLICY "Agents can view own buyers" ON public.buyers FOR SELECT USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); CREATE POLICY "Agents can insert buyers" ON public.buyers FOR INSERT WITH CHECK (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); CREATE POLICY "Agents can update own buyers" ON public.buyers FOR UPDATE USING (agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())); -- Matches visible to relevant agents CREATE POLICY "Agents can view own matches" ON public.matches FOR SELECT USING ( lead_id IN (SELECT id FROM public.seller_leads WHERE agent_id IN (SELECT id FROM public.users WHERE auth_id = auth.uid())) ); -- Admin can see everything CREATE POLICY "Admins full access users" ON public.users FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins full access leads" ON public.seller_leads FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins full access buyers" ON public.buyers FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins full access matches" ON public.matches FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin') ); CREATE POLICY "Admins full access outreach" ON public.outreach FOR ALL USING ( EXISTS (SELECT 1 FROM public.users WHERE auth_id = auth.uid() AND role = 'admin') ); -- ============================================ -- STORAGE BUCKETS -- ============================================ INSERT INTO storage.buckets (id, name, public) VALUES ('documents', 'documents', false); INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', true); -- Storage policies CREATE POLICY "Authenticated users can upload documents" ON storage.objects FOR INSERT WITH CHECK (bucket_id = 'documents' AND auth.role() = 'authenticated'); CREATE POLICY "Users can view own documents" ON storage.objects FOR SELECT USING (bucket_id = 'documents' AND auth.role() = 'authenticated'); -- ============================================ -- FUNCTIONS -- ============================================ -- Auto-update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER update_seller_leads_updated_at BEFORE UPDATE ON public.seller_leads FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER update_buyers_updated_at BEFORE UPDATE ON public.buyers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER update_matches_updated_at BEFORE UPDATE ON public.matches FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON public.subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- Function to create user profile on signup CREATE OR REPLACE FUNCTION handle_new_user() RETURNS TRIGGER AS $$ BEGIN INSERT INTO public.users (auth_id, email, name, role) VALUES ( NEW.id, NEW.email, COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)), COALESCE(NEW.raw_user_meta_data->>'role', 'agent') ); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user();