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.

Open Supabase Dashboard

Data Flow

developer_signups
Entry point
internal_signals
Usage data
+
company_enrichment
External data
lead_scores
Prioritization
lead_dashboard
View for UI
stakeholders
Buying committee
expansion_signals
Intent triggers
ae_activities
Outreach tracking

Tables

developer_signups

Core user data - developer accounts signing up for Supabase

8 rows
sample data

Purpose: Entry point for PLG funnel. Tracks who signed up and from where.

ColumnTypeNotes
idUUIDPrimary key
emailTEXTUnique user email
full_nameTEXTDeveloper name
company_domainTEXTExtracted from email or provided
signup_dateTIMESTAMPTZWhen they signed up
signup_sourceTEXTorganic, github, google, referral
is_company_emailBOOLEAN⚡ GENERATED - filters out personal emails

internal_signals

Usage metrics from the Supabase platform

8 rows
sample data

Purpose: The heart of PLG scoring - shows actual product adoption and growth.

ColumnTypeNotes
developer_idUUIDFK → developer_signups
project_countINTTotal projects created
uses_auth / uses_storage / uses_edge_functions / uses_realtime / uses_vector / uses_cronBOOLEANFeature adoption flags
billing_tierTEXTfree, pro, team, enterprise
team_member_countINTTeam size indicator
api_calls_30dBIGINTCurrent period usage
api_calls_prev_30dBIGINTPrevious period for comparison
api_call_growth_pctNUMERIC⚡ GENERATED - auto-calculates MoM growth

company_enrichment

External data from Apollo, Clearbit, LinkedIn

6 rows
sample data

Purpose: Enriches developer signups with company context for enterprise qualification.

ColumnTypeNotes
domainTEXTPrimary key - company domain
company_nameTEXTDisplay name
industry / sub_industryTEXTVertical classification
employee_count / employee_rangeINT / TEXTCompany size
funding_stageTEXTseed through series_d+
total_funding_usdBIGINTTotal raised
tech_stackJSONB⚡ JSONB array of technologies
headquarters_city / headquarters_countryTEXTLocation

stakeholders

Key people at each company

16 rows
sample data

Purpose: Maps the buying committee - who to contact beyond the developer champion.

ColumnTypeNotes
company_domainTEXTFK → company_enrichment
full_name / titleTEXTPerson details
departmentTEXTengineering, product, security, finance, executive
seniorityTEXTic, manager, director, vp, c-level
is_decision_maker / is_technical / is_budget_holderBOOLEANRole flags for outreach strategy
linkedin_url / emailTEXTContact info

expansion_signals

External triggers indicating expansion opportunity

14 rows
sample data

Purpose: Captures hiring, funding, and tech migration signals that indicate buying intent.

ColumnTypeNotes
company_domainTEXTFK → company_enrichment
signal_typeTEXThiring, funding, tech_migration, news, competitor_churn
signal_title / signal_detailTEXTWhat happened
signal_urlTEXTSource link
signal_strengthTEXTlow, medium, high - affects scoring weight

lead_scores

Computed scores for prioritization

8 rows
sample data

Purpose: The scoring engine - weights internal + external signals into actionable tiers.

ColumnTypeNotes
developer_idUUIDFK → developer_signups
company_scoreINT25% weight - enterprise fit
usage_scoreINT30% weight - product adoption (highest!)
expansion_scoreINT20% weight - buying signals
champion_scoreINT15% weight - developer engagement
timing_scoreINT10% weight - recent activity
total_scoreINT⚡ GENERATED - weighted sum of components
tierTEXT⚡ GENERATED - hot (80+), warm (60+), nurture (40+), monitor
suggested_actionTEXTNext best action for AE

ae_activities

AE engagement tracking

0 rows
sample data

Purpose: Tracks outreach history to prevent duplicate contacts and measure conversion.

ColumnTypeNotes
developer_idUUIDFK → developer_signups
activity_typeTEXTviewed, contacted, meeting_scheduled, demo_completed, opportunity_created
activity_notesTEXTContext for handoffs
created_byTEXTWhich 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
) STORED

Why 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.