clienttarget / supabase /migrations /001_initial_schema.sql
iDevBuddy
feat: Phase 1 β€” AI Client Acquisition System
bd28470
-- ============================================================
-- 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');