-- ============================================================ -- Idempotent migration — safe to run multiple times. -- Uses IF NOT EXISTS for tables/indexes and DROP IF EXISTS -- for policies/triggers (Postgres has no CREATE POLICY IF NOT EXISTS). -- ============================================================ -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================================ -- PROFILES -- ============================================================ CREATE TABLE IF NOT EXISTS profiles ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, full_name TEXT NOT NULL, email TEXT NOT NULL, avatar_url TEXT, role TEXT DEFAULT 'user', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id) ); ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can view own profile" ON profiles; DROP POLICY IF EXISTS "Users can update own profile" ON profiles; DROP POLICY IF EXISTS "Users can insert own profile" ON profiles; CREATE POLICY "Users can view own profile" ON profiles FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Users can update own profile" ON profiles FOR UPDATE USING (auth.uid() = user_id); CREATE POLICY "Users can insert own profile" ON profiles FOR INSERT WITH CHECK (auth.uid() = user_id); -- ============================================================ -- CONTACTS -- ============================================================ CREATE TABLE IF NOT EXISTS contacts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, phone TEXT NOT NULL, name TEXT, email TEXT, company TEXT, avatar_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_contacts_user_id ON contacts(user_id); CREATE INDEX IF NOT EXISTS idx_contacts_phone ON contacts(phone); ALTER TABLE contacts ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own contacts" ON contacts; CREATE POLICY "Users can manage own contacts" ON contacts FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- TAGS -- ============================================================ CREATE TABLE IF NOT EXISTS tags ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#3b82f6', created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE tags ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own tags" ON tags; CREATE POLICY "Users can manage own tags" ON tags FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- CONTACT_TAGS (many-to-many) -- ============================================================ CREATE TABLE IF NOT EXISTS contact_tags ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(contact_id, tag_id) ); CREATE INDEX IF NOT EXISTS idx_contact_tags_contact ON contact_tags(contact_id); CREATE INDEX IF NOT EXISTS idx_contact_tags_tag ON contact_tags(tag_id); ALTER TABLE contact_tags ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage contact tags" ON contact_tags; CREATE POLICY "Users can manage contact tags" ON contact_tags FOR ALL USING (EXISTS (SELECT 1 FROM contacts WHERE contacts.id = contact_tags.contact_id AND contacts.user_id = auth.uid())); -- ============================================================ -- CUSTOM_FIELDS -- ============================================================ CREATE TABLE IF NOT EXISTS custom_fields ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, field_name TEXT NOT NULL, field_type TEXT NOT NULL DEFAULT 'text', field_options JSONB, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE custom_fields ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own custom fields" ON custom_fields; CREATE POLICY "Users can manage own custom fields" ON custom_fields FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- CONTACT_CUSTOM_VALUES -- ============================================================ CREATE TABLE IF NOT EXISTS contact_custom_values ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, custom_field_id UUID NOT NULL REFERENCES custom_fields(id) ON DELETE CASCADE, value TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(contact_id, custom_field_id) ); ALTER TABLE contact_custom_values ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage custom values" ON contact_custom_values; CREATE POLICY "Users can manage custom values" ON contact_custom_values FOR ALL USING (EXISTS (SELECT 1 FROM contacts WHERE contacts.id = contact_custom_values.contact_id AND contacts.user_id = auth.uid())); -- ============================================================ -- CONTACT_NOTES -- ============================================================ CREATE TABLE IF NOT EXISTS contact_notes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, note_text TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE contact_notes ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own notes" ON contact_notes; CREATE POLICY "Users can manage own notes" ON contact_notes FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- CONVERSATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS conversations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'pending', 'closed')), assigned_agent_id UUID, last_message_text TEXT, last_message_at TIMESTAMPTZ, unread_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id); CREATE INDEX IF NOT EXISTS idx_conversations_contact_id ON conversations(contact_id); ALTER TABLE conversations ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own conversations" ON conversations; CREATE POLICY "Users can manage own conversations" ON conversations FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- MESSAGES -- ============================================================ CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_type TEXT NOT NULL CHECK (sender_type IN ('customer', 'agent', 'bot')), sender_id UUID, content_type TEXT NOT NULL DEFAULT 'text' CHECK (content_type IN ('text', 'image', 'document', 'audio', 'video', 'location', 'template')), content_text TEXT, media_url TEXT, template_name TEXT, message_id TEXT, status TEXT NOT NULL DEFAULT 'sent' CHECK (status IN ('sending', 'sent', 'delivered', 'read', 'failed')), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_messages_message_id ON messages(message_id); ALTER TABLE messages ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can view own messages" ON messages; DROP POLICY IF EXISTS "Service role can insert messages" ON messages; CREATE POLICY "Users can view own messages" ON messages FOR ALL USING (EXISTS (SELECT 1 FROM conversations WHERE conversations.id = messages.conversation_id AND conversations.user_id = auth.uid())); CREATE POLICY "Service role can insert messages" ON messages FOR INSERT WITH CHECK (true); -- ============================================================ -- WHATSAPP_CONFIG -- ============================================================ CREATE TABLE IF NOT EXISTS whatsapp_config ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, phone_number_id TEXT NOT NULL, waba_id TEXT, access_token TEXT NOT NULL, verify_token TEXT, status TEXT NOT NULL DEFAULT 'disconnected' CHECK (status IN ('connected', 'disconnected')), connected_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id) ); ALTER TABLE whatsapp_config ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own config" ON whatsapp_config; CREATE POLICY "Users can manage own config" ON whatsapp_config FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- MESSAGE_TEMPLATES -- ============================================================ CREATE TABLE IF NOT EXISTS message_templates ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, category TEXT NOT NULL DEFAULT 'Marketing' CHECK (category IN ('Marketing', 'Utility', 'Authentication')), language TEXT DEFAULT 'en_US', header_type TEXT CHECK (header_type IN ('text', 'image', 'video', 'document')), header_content TEXT, body_text TEXT NOT NULL, footer_text TEXT, buttons JSONB, status TEXT DEFAULT 'Draft' CHECK (status IN ('Draft', 'Pending', 'Approved', 'Rejected')), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE message_templates ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own templates" ON message_templates; CREATE POLICY "Users can manage own templates" ON message_templates FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- PIPELINES -- ============================================================ CREATE TABLE IF NOT EXISTS pipelines ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE pipelines ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own pipelines" ON pipelines; CREATE POLICY "Users can manage own pipelines" ON pipelines FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- PIPELINE_STAGES -- ============================================================ CREATE TABLE IF NOT EXISTS pipeline_stages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, color TEXT NOT NULL DEFAULT '#3b82f6', created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_pipeline_stages_pipeline ON pipeline_stages(pipeline_id); ALTER TABLE pipeline_stages ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage pipeline stages" ON pipeline_stages; CREATE POLICY "Users can manage pipeline stages" ON pipeline_stages FOR ALL USING (EXISTS (SELECT 1 FROM pipelines WHERE pipelines.id = pipeline_stages.pipeline_id AND pipelines.user_id = auth.uid())); -- ============================================================ -- DEALS -- ============================================================ CREATE TABLE IF NOT EXISTS deals ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, pipeline_id UUID NOT NULL REFERENCES pipelines(id) ON DELETE CASCADE, stage_id UUID NOT NULL REFERENCES pipeline_stages(id), contact_id UUID NOT NULL REFERENCES contacts(id), conversation_id UUID REFERENCES conversations(id), title TEXT NOT NULL, value NUMERIC(12,2) NOT NULL DEFAULT 0, currency TEXT DEFAULT 'USD', notes TEXT, expected_close_date DATE, status TEXT DEFAULT 'active', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_deals_pipeline ON deals(pipeline_id); CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage_id); ALTER TABLE deals ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own deals" ON deals; CREATE POLICY "Users can manage own deals" ON deals FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- BROADCASTS -- ============================================================ CREATE TABLE IF NOT EXISTS broadcasts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, template_name TEXT NOT NULL, template_language TEXT NOT NULL DEFAULT 'en_US', template_variables JSONB, audience_filter JSONB, scheduled_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'scheduled', 'sending', 'sent', 'failed')), total_recipients INTEGER DEFAULT 0, sent_count INTEGER DEFAULT 0, delivered_count INTEGER DEFAULT 0, read_count INTEGER DEFAULT 0, replied_count INTEGER DEFAULT 0, failed_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE broadcasts ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own broadcasts" ON broadcasts; CREATE POLICY "Users can manage own broadcasts" ON broadcasts FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- BROADCAST_RECIPIENTS -- ============================================================ CREATE TABLE IF NOT EXISTS broadcast_recipients ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), broadcast_id UUID NOT NULL REFERENCES broadcasts(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'delivered', 'read', 'replied', 'failed')), sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, read_at TIMESTAMPTZ, replied_at TIMESTAMPTZ, error_message TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_broadcast_recipients_broadcast ON broadcast_recipients(broadcast_id); ALTER TABLE broadcast_recipients ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage broadcast recipients" ON broadcast_recipients; CREATE POLICY "Users can manage broadcast recipients" ON broadcast_recipients FOR ALL USING (EXISTS (SELECT 1 FROM broadcasts WHERE broadcasts.id = broadcast_recipients.broadcast_id AND broadcasts.user_id = auth.uid())); -- ============================================================ -- UPDATED_AT TRIGGER FUNCTION -- ============================================================ CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Apply to tables with updated_at — drop existing triggers first to avoid conflicts DROP TRIGGER IF EXISTS set_updated_at ON profiles; DROP TRIGGER IF EXISTS set_updated_at ON contacts; DROP TRIGGER IF EXISTS set_updated_at ON conversations; DROP TRIGGER IF EXISTS set_updated_at ON whatsapp_config; DROP TRIGGER IF EXISTS set_updated_at ON message_templates; DROP TRIGGER IF EXISTS set_updated_at ON deals; DROP TRIGGER IF EXISTS set_updated_at ON broadcasts; CREATE TRIGGER set_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON contacts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON whatsapp_config FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON message_templates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON deals FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER set_updated_at BEFORE UPDATE ON broadcasts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================ -- AUTO-CREATE PROFILE ON USER SIGNUP -- Uses SECURITY DEFINER with owner=postgres (bypasses RLS). -- EXCEPTION block ensures signup still succeeds even if profile -- insert fails — profile can be created later if needed. -- ============================================================ DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; DROP FUNCTION IF EXISTS public.handle_new_user(); CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ BEGIN INSERT INTO public.profiles (user_id, full_name, email) VALUES ( NEW.id, COALESCE(NEW.raw_user_meta_data->>'full_name', ''), NEW.email ); RETURN NEW; EXCEPTION WHEN OTHERS THEN RAISE WARNING 'Failed to create profile for user %: %', NEW.id, SQLERRM; RETURN NEW; END; $$; ALTER FUNCTION public.handle_new_user() OWNER TO postgres; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); -- ============================================================ -- ENABLE REALTIME for key tables (idempotent via DO block) -- ============================================================ DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'messages' ) THEN ALTER PUBLICATION supabase_realtime ADD TABLE messages; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'conversations' ) THEN ALTER PUBLICATION supabase_realtime ADD TABLE conversations; END IF; END $$;