-- ============================================================ -- AI Client Acquisition System — Supabase Schema -- Run this in Supabase SQL Editor -- ============================================================ -- Enable pgcrypto for UUID generation CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- for fuzzy name matching -- ─── ENUMS ────────────────────────────────────────────────── CREATE TYPE company_status AS ENUM ( 'discovered', 'researching', 'profiled', 'qualified', 'nurture', 'archived', 'suppressed' ); CREATE TYPE contact_status AS ENUM ( 'found', 'email_verified', 'email_invalid', 'linkedin_only', 'suppressed' ); CREATE TYPE lead_tier AS ENUM ('hot', 'warm', 'nurture', 'archive'); CREATE TYPE outreach_channel AS ENUM ('email', 'linkedin'); CREATE TYPE outreach_status AS ENUM ( 'queued', 'sent', 'opened', 'replied', 'bounced', 'failed', 'review_needed' ); CREATE TYPE intent_type AS ENUM ( 'interested', 'question', 'not_now', 'not_interested', 'out_of_office', 'wrong_person', 'unknown' ); CREATE TYPE review_status AS ENUM ('pending', 'approved', 'rejected', 'edited'); -- ─── CORE TABLES ──────────────────────────────────────────── -- ICP Configuration (editable from dashboard) CREATE TABLE icp_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL DEFAULT 'default', min_employees INTEGER NOT NULL DEFAULT 50, industries TEXT[] NOT NULL DEFAULT ARRAY['technology','manufacturing','logistics','healthcare','finance'], exclude_industries TEXT[] NOT NULL DEFAULT ARRAY['government','non-profit','education'], geographies TEXT[] NOT NULL DEFAULT ARRAY['US','UK','AU','UAE','SA'], keywords TEXT[] NOT NULL DEFAULT ARRAY['automation','digital transformation','AI','operations'], tech_signals TEXT[] DEFAULT ARRAY['salesforce','hubspot','legacy_erp','sap'], score_threshold INTEGER NOT NULL DEFAULT 70, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Weekly rotation state CREATE TABLE rotation_state ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), week_number INTEGER NOT NULL, -- 1=USA, 2=UK, 3=AU, 4=Gulf region TEXT NOT NULL, started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, companies_found INTEGER DEFAULT 0, leads_qualified INTEGER DEFAULT 0 ); -- Companies discovered CREATE TABLE companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain TEXT NOT NULL UNIQUE, name TEXT NOT NULL, industry TEXT, employee_count INTEGER, employee_range TEXT, -- "50-200", "200-500" etc description TEXT, website_url TEXT, linkedin_url TEXT, country TEXT, region TEXT, tech_stack TEXT[], growth_signals JSONB DEFAULT '[]', -- job posts, news, funding raw_data JSONB DEFAULT '{}', source TEXT NOT NULL, -- 'serper', 'linkedin', 'manual' status company_status NOT NULL DEFAULT 'discovered', discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_companies_domain ON companies(domain); CREATE INDEX idx_companies_status ON companies(status); CREATE INDEX idx_companies_country ON companies(country); CREATE INDEX idx_companies_name_trgm ON companies USING GIN (name gin_trgm_ops); -- Contacts (decision-makers) CREATE TABLE contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, full_name TEXT NOT NULL, first_name TEXT, last_name TEXT, title TEXT NOT NULL, seniority TEXT, -- 'c_suite','vp','director','manager' email TEXT, email_verified BOOLEAN DEFAULT FALSE, email_source TEXT, -- 'hunter','snov','pattern' linkedin_url TEXT, linkedin_verified BOOLEAN DEFAULT FALSE, status contact_status NOT NULL DEFAULT 'found', suppressed BOOLEAN NOT NULL DEFAULT FALSE, suppressed_at TIMESTAMPTZ, suppressed_reason TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_contacts_company ON contacts(company_id); CREATE INDEX idx_contacts_email ON contacts(email); CREATE INDEX idx_contacts_suppressed ON contacts(suppressed); -- Evidence gathered during research CREATE TABLE evidence ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, type TEXT NOT NULL, -- 'job_posting','news','social_post','website_text' content TEXT NOT NULL, source_url TEXT, ai_signal BOOLEAN DEFAULT FALSE, -- does this mention AI/automation? collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_evidence_company ON evidence(company_id); -- Lead profiles (LLM output) CREATE TABLE lead_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, profile_summary TEXT NOT NULL, pain_points TEXT[] DEFAULT '{}', ai_use_case TEXT, ai_readiness TEXT NOT NULL DEFAULT 'medium', -- low/medium/high outreach_angle TEXT, llm_model TEXT NOT NULL, llm_confidence NUMERIC(3,2), is_fallback BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Lead scores CREATE TABLE lead_scores ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id), total_score INTEGER NOT NULL CHECK (total_score BETWEEN 0 AND 100), tier lead_tier NOT NULL, company_fit INTEGER, -- 0-25 ai_readiness INTEGER, -- 0-25 decision_maker INTEGER, -- 0-20 growth_signal INTEGER, -- 0-15 engagement_potential INTEGER, -- 0-15 score_reasoning TEXT, scored_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_scores_company ON lead_scores(company_id); CREATE INDEX idx_scores_tier ON lead_scores(tier); CREATE INDEX idx_scores_total ON lead_scores(total_score DESC); -- ─── OUTREACH TABLES ──────────────────────────────────────── CREATE TABLE outreach_sequences ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES contacts(id), current_step INTEGER NOT NULL DEFAULT 0, total_steps INTEGER NOT NULL DEFAULT 5, next_action_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'active', -- active/paused/completed/stopped stopped_reason TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE outreach_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), sequence_id UUID REFERENCES outreach_sequences(id), company_id UUID NOT NULL REFERENCES companies(id), contact_id UUID NOT NULL REFERENCES contacts(id), channel outreach_channel NOT NULL, step_number INTEGER NOT NULL, template_id TEXT, message_hash TEXT NOT NULL UNIQUE, -- prevent duplicate sends subject TEXT, status outreach_status NOT NULL DEFAULT 'queued', provider_id TEXT, -- external message ID from Resend/LinkedIn sent_at TIMESTAMPTZ, opened_at TIMESTAMPTZ, replied_at TIMESTAMPTZ, bounced_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_outreach_company ON outreach_log(company_id); CREATE INDEX idx_outreach_hash ON outreach_log(message_hash); CREATE INDEX idx_outreach_status ON outreach_log(status); -- ─── ENGAGEMENT TABLES ────────────────────────────────────── CREATE TABLE engagement_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), outreach_id UUID REFERENCES outreach_log(id), company_id UUID NOT NULL REFERENCES companies(id), contact_id UUID NOT NULL REFERENCES contacts(id), signal_type TEXT NOT NULL, -- 'open','reply','bounce','linkedin_accept' intent intent_type, raw_content TEXT, -- actual reply text (for NLP) detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── SYSTEM TABLES ────────────────────────────────────────── CREATE TABLE suppression_list ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT, domain TEXT, reason TEXT NOT NULL, -- 'unsubscribed','bounced','manual' added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_suppression_email ON suppression_list(email); CREATE INDEX idx_suppression_domain ON suppression_list(domain); CREATE TABLE human_review_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), type TEXT NOT NULL, -- 'outreach_approval','score_anomaly','escalation' company_id UUID REFERENCES companies(id), contact_id UUID REFERENCES contacts(id), payload JSONB NOT NULL, -- full context for reviewer status review_status NOT NULL DEFAULT 'pending', reviewer_notes TEXT, resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_review_status ON human_review_queue(status); CREATE TABLE api_usage_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), provider TEXT NOT NULL, -- 'serper','hunter','snov','reoon' endpoint TEXT, credits_used INTEGER DEFAULT 1, success BOOLEAN NOT NULL, error_msg TEXT, called_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id UUID, actor TEXT NOT NULL DEFAULT 'system', details JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ─── SEED DATA ────────────────────────────────────────────── INSERT INTO icp_config (name, min_employees, industries, geographies, keywords, score_threshold) VALUES ( 'default', 50, ARRAY['technology','software','manufacturing','logistics','supply_chain','healthcare','finance','real_estate_tech','retail_tech'], ARRAY['US','UK','AU','UAE','SA','SG'], ARRAY['automation','digital transformation','AI','machine learning','operations','workflow','efficiency'], 70 ); INSERT INTO rotation_state (week_number, region) VALUES (1, 'US');