Database Schema
The lead scoring system is built on 7 tables and 2 views in Postgres, demonstrating how to prioritize PLG signups for enterprise expansion.
View the actual Supabase project
Supabase team: You can access the live database, table editor, and SQL editor directly.
Data Flow
Tables
developer_signups
Core user data - developer accounts signing up for Supabase
Purpose: Entry point for PLG funnel. Tracks who signed up and from where.
| Column | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| TEXT | Unique user email | |
| full_name | TEXT | Developer name |
| company_domain | TEXT | Extracted from email or provided |
| signup_date | TIMESTAMPTZ | When they signed up |
| signup_source | TEXT | organic, github, google, referral |
| is_company_email | BOOLEAN | ⚡ GENERATED - filters out personal emails |
internal_signals
Usage metrics from the Supabase platform
Purpose: The heart of PLG scoring - shows actual product adoption and growth.
| Column | Type | Notes |
|---|---|---|
| developer_id | UUID | FK → developer_signups |
| project_count | INT | Total projects created |
| uses_auth / uses_storage / uses_edge_functions / uses_realtime / uses_vector / uses_cron | BOOLEAN | Feature adoption flags |
| billing_tier | TEXT | free, pro, team, enterprise |
| team_member_count | INT | Team size indicator |
| api_calls_30d | BIGINT | Current period usage |
| api_calls_prev_30d | BIGINT | Previous period for comparison |
| api_call_growth_pct | NUMERIC | ⚡ GENERATED - auto-calculates MoM growth |
company_enrichment
External data from Apollo, Clearbit, LinkedIn
Purpose: Enriches developer signups with company context for enterprise qualification.
| Column | Type | Notes |
|---|---|---|
| domain | TEXT | Primary key - company domain |
| company_name | TEXT | Display name |
| industry / sub_industry | TEXT | Vertical classification |
| employee_count / employee_range | INT / TEXT | Company size |
| funding_stage | TEXT | seed through series_d+ |
| total_funding_usd | BIGINT | Total raised |
| tech_stack | JSONB | ⚡ JSONB array of technologies |
| headquarters_city / headquarters_country | TEXT | Location |
stakeholders
Key people at each company
Purpose: Maps the buying committee - who to contact beyond the developer champion.
| Column | Type | Notes |
|---|---|---|
| company_domain | TEXT | FK → company_enrichment |
| full_name / title | TEXT | Person details |
| department | TEXT | engineering, product, security, finance, executive |
| seniority | TEXT | ic, manager, director, vp, c-level |
| is_decision_maker / is_technical / is_budget_holder | BOOLEAN | Role flags for outreach strategy |
| linkedin_url / email | TEXT | Contact info |
expansion_signals
External triggers indicating expansion opportunity
Purpose: Captures hiring, funding, and tech migration signals that indicate buying intent.
| Column | Type | Notes |
|---|---|---|
| company_domain | TEXT | FK → company_enrichment |
| signal_type | TEXT | hiring, funding, tech_migration, news, competitor_churn |
| signal_title / signal_detail | TEXT | What happened |
| signal_url | TEXT | Source link |
| signal_strength | TEXT | low, medium, high - affects scoring weight |
lead_scores
Computed scores for prioritization
Purpose: The scoring engine - weights internal + external signals into actionable tiers.
| Column | Type | Notes |
|---|---|---|
| developer_id | UUID | FK → developer_signups |
| company_score | INT | 25% weight - enterprise fit |
| usage_score | INT | 30% weight - product adoption (highest!) |
| expansion_score | INT | 20% weight - buying signals |
| champion_score | INT | 15% weight - developer engagement |
| timing_score | INT | 10% weight - recent activity |
| total_score | INT | ⚡ GENERATED - weighted sum of components |
| tier | TEXT | ⚡ GENERATED - hot (80+), warm (60+), nurture (40+), monitor |
| suggested_action | TEXT | Next best action for AE |
ae_activities
AE engagement tracking
Purpose: Tracks outreach history to prevent duplicate contacts and measure conversion.
| Column | Type | Notes |
|---|---|---|
| developer_id | UUID | FK → developer_signups |
| activity_type | TEXT | viewed, contacted, meeting_scheduled, demo_completed, opportunity_created |
| activity_notes | TEXT | Context for handoffs |
| created_by | TEXT | Which AE took action |
Views
lead_dashboard
Main view joining all tables for the dashboard UI
SELECT
d.id, d.email, d.full_name AS champion_name,
c.company_name, c.industry, c.employee_count,
i.billing_tier, i.api_calls_30d, i.api_call_growth_pct,
s.total_score, s.tier, s.suggested_action,
-- Feature array for easy display
ARRAY_REMOVE(ARRAY[
CASE WHEN i.uses_auth THEN 'Auth' END,
CASE WHEN i.uses_edge_functions THEN 'Edge Functions' END,
...
], NULL) AS features_used
FROM developer_signups d
LEFT JOIN company_enrichment c ON c.domain = d.company_domain
LEFT JOIN internal_signals i ON i.developer_id = d.id
LEFT JOIN lead_scores s ON s.developer_id = d.id
WHERE d.is_company_email = true
ORDER BY s.total_score DESC;hot_leads
Filtered view of tier = "hot" leads for urgent follow-up
SELECT * FROM lead_dashboard WHERE tier = 'hot' ORDER BY total_score DESC;
Key Postgres Patterns Used
GENERATED ALWAYS AS
Computed columns that auto-calculate on write
is_company_email BOOLEAN GENERATED ALWAYS AS ( email NOT LIKE '%@gmail.com' AND email NOT LIKE '%@yahoo.com' -- etc. ) STORED
Why it matters: No application code needed - database handles the logic. Always consistent.
Weighted Scoring Formula
Total score calculated from component scores with business-defined weights
total_score INT GENERATED ALWAYS AS ( (company_score * 0.25) + -- 25% weight (usage_score * 0.30) + -- 30% weight (PLG priority!) (expansion_score * 0.20) + -- 20% weight (champion_score * 0.15) + -- 15% weight (timing_score * 0.10) -- 10% weight ) STORED
Why it matters: Scoring logic lives in the database, not scattered across application code.
Tier Assignment
Automatic lead qualification based on score thresholds
tier TEXT GENERATED ALWAYS AS (
CASE
WHEN total_score >= 80 THEN 'hot'
WHEN total_score >= 60 THEN 'warm'
WHEN total_score >= 40 THEN 'nurture'
ELSE 'monitor'
END
) STOREDWhy it matters: Consistent tier assignment. Change thresholds in one place.
Row Level Security (RLS)
Access control at the database level
ALTER TABLE developer_signups ENABLE ROW LEVEL SECURITY; CREATE POLICY "public read" ON developer_signups FOR SELECT USING (true);
Why it matters: Security enforced in Postgres, not application. Works with any client.
This is a working Supabase project
Not just mockups - this dashboard queries real Postgres tables with RLS policies. The scoring logic runs in the database, not the application.