Case Study: How We Automated Lead Routing and Client Onboarding Using n8n and Supabase

By Simeon Matheka, Founder & Creative Director · Published 2026-08-04 · Updated 2026-08-04 · 18 min read

How we replaced manual multi-channel lead triage with a self-hosted n8n + Supabase pipeline: sub-minute routing, schema-safe storage, automated onboarding, and a dead-letter queue under $80/month.

Architecture collage of n8n workflows and Supabase database driving automated lead routing and client onboarding

High-growth B2B service firms and digital agencies hit the same wall: multi-channel inbound leads pile up faster than people can triage them. Forms, WhatsApp Cloud API webhooks, and direct email land in separate inboxes. Staff copy fields into a CRM, guess budget fit, and fire calendar links by hand. Response latency stretches into hours. Conversion drops. Marketing ROI bleeds out.

We engineered and deployed a unified lead orchestration and client onboarding pipeline for a rapidly scaling service client. We replaced rigid, metered third-party SaaS middleware with self-hosted n8n and a hardened Supabase (PostgreSQL) backend. The result was a schema-validated, event-driven pipeline with agentic qualification sitting cleanly inside deterministic security and storage layers.

Executive Summary and Key Results

The deployment produced immediate, measurable gains across response speed, data quality, onboarding time, reliability, and monthly compute cost.

Operational MetricLegacy Manual StateEngineered Pipeline (n8n + Supabase)Measured Gain
Lead response velocity4+ hours average manual triage<45 seconds automated parse and dispatch99.7% reduction in lead latency
Data verification and accuracy~12% transcription error rate across channels100% schema-validated PostgreSQL recordsElimination of malformed lead records
Client onboarding latency2 business days for credentials and portal setup<5 seconds post-trigger dynamic setupReal-time workspace provisioning
System compute overhead$1,200+/month task-metered Zapier/Make plans<$80/month self-hosted cloud instance93.3% reduction in software overhead
Pipeline reliabilityUnnotified drops from upstream rate limits0% data loss via dead-letter queue loggingFull auditability and self-healing retries

The Bottleneck: Manual Lead Triage and Fragmented Tools

Before we rebuilt the pipeline, the client ran a fragmented multi-channel capture stack. Prospective clients arrived through website forms, structured WhatsApp chats, and inbound business email. Ops staff evaluated each request by hand, transcribed contacts into a CRM, assessed budget viability, and manually generated scheduling links.

That manual architecture had four structural weak points:

  • High latency and lost inbound momentum: Submissions outside East African Time business hours sat unprocessed for up to 14 hours. In competitive B2B services, delays past a few minutes cut qualified conversion probability hard.
  • Context degradation and schema drift: Unvalidated webhooks and inconsistent human entry produced corrupted fields, free-form budget strings, and missing phone numbers. Downstream automation then had nothing reliable to act on.
  • Fragile, expensive middleware: Early experiments with Zapier and Make failed under scale. Task pricing climbed with every loop. Those platforms also lacked the depth we needed for PostgreSQL Row Level Security, custom edge signature checks, and granular handling of upstream rate limits.
  • Onboarding bottlenecks: After a deal closed, account managers spent up to two days creating directories, issuing portal credentials, and sending legal packs. The client relationship started with operational friction.

We designed an open-source, database-first system that validates webhooks in real time, runs agentic lead scoring, enforces strict PostgreSQL constraints, and asynchronously provisions client portal infrastructure.

If you want the architecture pattern for deterministic workflows versus large language model (LLM) agents, read Deterministic Workflows vs. LLM Agents. This case study is that hybrid pattern in production: deterministic edge and storage, probabilistic qualification in the middle.

Solution Architecture

We decoupled ingestion, orchestration, persistence, and provisioning into specialized layers. Cloudflare Workers handle edge security and rate limiting. Self-hosted n8n owns complex workflow orchestration. Supabase (PostgreSQL) provides transactional storage, identity, and event-driven database triggers.

System ComponentInfrastructure ChoiceCore Operational Function
Edge security layerCloudflare Workers gatewayRejects invalid payloads, enforces rate limits, validates HMAC SHA-256 headers
Workflow engineSelf-hosted n8n (Docker)State logic, API integrations, branching, global error handling
Central databaseSupabase (PostgreSQL 15+)Structural integrity, triggers, Row Level Security
Identity and authenticationSupabase Auth Admin APIcreateUser, roles, portal access
Non-blocking messagingpg_net PostgreSQL extensionAsync DB webhooks from row updates without blocking transactions

End-to-End Pipeline

flowchart TD
    Web["Web form"] --> Edge["Cloudflare Workers<br/>HMAC · rate limits · 401 on fail"]
    WA["WhatsApp Cloud API"] --> Edge
    Email["Inbound email channel"] --> Edge
    Edge --> N8n["Self-hosted n8n<br/>normalize · LLM score · switch"]
    N8n --> SB[("Supabase PostgreSQL<br/>leads · RLS · constraints")]
    N8n -->|score high| Slack["Slack alert + calendar link"]
    N8n -->|score low| Nurture["Nurture sequence flags"]
    SB -->|closed_won insert| Trigger["pg_net trigger<br/>async HTTP webhook"]
    Trigger --> Prov["n8n provisioning workflow"]
    Prov --> Auth["Supabase Auth Admin<br/>createUser · magic link"]
    Prov --> Storage["Storage buckets + portal URL"]
    N8n -.->|failures| DLQ[("dead_letter_events")]
    DLQ --> Cron["Hourly retry cron"]
    Cron --> N8n
  1. Ingest and edge verification: Workers intercept web form and WhatsApp webhooks, validate HMAC signatures, and block bad traffic before it reaches n8n.
  2. Orchestration and AI logic: n8n parses verified JSON, normalizes fields, and sends unstructured message text to an LLM node for intent extraction and budget scoring.
  3. Secure storage: High-intent leads insert into Supabase with RLS isolating tenant records and restricting writes to authenticated service-role contexts.
  4. Automated routing: n8n branches on score. High-priority leads get dynamic booking links and Slack alerts. Lower scores go to nurture state flags.
  5. Asynchronous provisioning: When status becomes closed_won, a PostgreSQL trigger via pg_net fires a non-blocking HTTP call to start account setup.
  6. Portal and credentials: n8n calls the Supabase Admin Auth API to create users, seed storage buckets, and issue secure onboarding magic links.

Technical Deep Dive

Data Ingestion and Edge Security

Exposing n8n webhooks directly to the public internet invites spam, replay attacks, and denial-of-service noise. We put a Cloudflare Workers validation layer in front.

For WhatsApp traffic, the Worker acts as the gateway: it reads the X-Hub-Signature-256 header, computes a SHA-256 HMAC over the raw body with the app secret, compares with a constant-time check, validates request timestamps against replay windows, and normalizes the payload shape before any orchestration runs. Invalid or missing signatures return HTTP 401 from the Worker itself. Unauthorized calls never burn n8n executions or write database rows. On success, n8n finishes the pipeline and the Worker is still the party that returns 200 Accepted to the client.

sequenceDiagram
    autonumber
    actor Client as Form / WhatsApp
    box Edge security
      participant CF as Cloudflare Worker
    end
    box Automation
      participant N8n as n8n
    end
    box AI enrichment
      participant LLM as LLM score node
    end
    box Persistence
      participant DB as Supabase
    end

    Client->>+CF: POST /ingest payload + HMAC (HTTPS)
    CF->>CF: Verify signature · validate timestamp · normalize
    alt Invalid signature or expired timestamp
      CF-->>-Client: 401 Unauthorized
    else Valid gateway pass
      CF->>+N8n: POST verified JSON (HTTPS)
      N8n->>+LLM: Unstructured text for scoring
      LLM-->>-N8n: Structured JSON {intent, budget_range, lead_score, confidence, urgency, extracted_services}
      N8n->>+DB: UPSERT lead ON CONFLICT (idempotency_key)
      DB-->>-N8n: Upserted row
      N8n-->>-CF: 200 workflow complete
      CF-->>-Client: 200 Accepted
    end

Database Schema and Row Level Security

We enforce structure in PostgreSQL, not only in the workflow canvas. Constraints, foreign keys, and RLS policies make malformed data un-writable even if a node misbehaves.

-- Core leads table
CREATE TABLE public.leads (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT UNIQUE NOT NULL,
    full_name TEXT NOT NULL,
    phone TEXT,
    channel TEXT NOT NULL
      CHECK (channel IN ('web_form', 'whatsapp', 'email')),
    intent_category TEXT,
    budget_tier TEXT,
    lead_score INT DEFAULT 0
      CHECK (lead_score BETWEEN 0 AND 100),
    status TEXT NOT NULL DEFAULT 'new'
      CHECK (status IN ('new', 'qualified', 'disqualified', 'closed_won')),
    metadata JSONB DEFAULT '{}'::jsonb,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE public.leads ENABLE ROW LEVEL SECURITY;

CREATE POLICY service_role_full_access ON public.leads
    FOR ALL TO service_role
    USING (true) WITH CHECK (true);

-- Dead-letter queue for failed workflow executions
CREATE TABLE public.dead_letter_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    workflow_id TEXT NOT NULL,
    execution_id TEXT NOT NULL,
    error_message TEXT NOT NULL,
    failed_node TEXT,
    payload JSONB NOT NULL,
    attempt_count INT DEFAULT 1,
    status TEXT NOT NULL DEFAULT 'open'
      CHECK (status IN ('open', 'retrying', 'resolved', 'dead')),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Client onboarding isolation
CREATE TABLE public.client_onboarding (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    lead_id UUID UNIQUE REFERENCES public.leads(id) ON DELETE CASCADE,
    auth_user_id UUID UNIQUE,
    workspace_slug TEXT NOT NULL UNIQUE,
    provisioning_status TEXT NOT NULL DEFAULT 'pending'
      CHECK (provisioning_status IN ('pending', 'active', 'failed')),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

With RLS enabled, anonymous clients cannot poke around lead rows. Writes and reads go through the service role or explicit SECURITY DEFINER functions that n8n and Auth workflows invoke on purpose.

Agentic Qualification and n8n Routing

After edge checks pass, n8n runs classification. We extract project scope, budget hints, and timeline language, then send that text to an LLM node configured for strict structured output. The model returns a richer JSON object than three loose fields, for example:

  • intent / intent_category: standardized string such as Enterprise Web Architecture, AI Process Automation, or Out of Scope.
  • budget_range / budget_tier: mapped band such as under $5k, $5k–$15k, or above $15k.
  • lead_score: integer priority used by downstream Switch rules (0–100).
  • confidence: float between 0.0 and 1.0 for how sure the model is.
  • urgency: relative priority signal extracted from language about timelines and deadlines.
  • extracted_services: normalized list of services the lead is asking for, used for routing and CRM context.

A Switch node applies fixed business rules. Valid intent plus budget over $5,000 yields a lead_score above 80. High-value branches generate a dynamic calendar link, set status to qualified in Supabase, and push rich context into Slack. Lower scores mark disqualified or flip nurture flags in PostgreSQL. The model proposes structure; the workflow owns money-adjacent and CRM side effects.

flowchart LR
    In["Verified JSON payload"] --> LLM["LLM structured output"]
    LLM --> SW{"Business rules<br/>intent + budget + confidence"}
    SW -->|score ≥ 80| HQ["qualified<br/>calendar · Slack · CRM"]
    SW -->|score low| LQ["disqualified or nurture flag"]
    HQ --> DB[("leads.status = qualified")]
    LQ --> DB2[("leads.status = disqualified / nurture")]

Automated Client Onboarding Pipeline

When a deal closes, an admin sets lead status to closed_won and inserts a client_onboarding row. We refused cron polling against the database. Instead, PostgreSQL detects the change with a trigger plus pg_net for a non-blocking HTTP POST into n8n.

-- pg_net trigger: fire async provisioning without blocking the transaction
CREATE OR REPLACE FUNCTION public.fn_onboard_client_trigger()
RETURNS TRIGGER AS $$
BEGIN
    PERFORM net.http_post(
        url := 'https://n8n.internal.example/webhook/client-provisioned',
        body := jsonb_build_object(
            'onboarding_id', NEW.id,
            'lead_id', NEW.lead_id,
            'workspace_slug', NEW.workspace_slug,
            'timestamp', NOW()
        )
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER trg_client_onboard_after_insert
    AFTER INSERT ON public.client_onboarding
    FOR EACH ROW
    EXECUTE FUNCTION public.fn_onboard_client_trigger();

n8n then calls supabase.auth.admin.createUser, provisions storage folders, builds portal URLs, and emails a secure credential/magic-link message. Provisioning that used to take two business days now completes in seconds after the status flip.

Failure Handling, Security, and Edge Cases

Retry Logic and Idempotency

Webhooks fail. APIs return 429s. Targets go dark. A quiet failure here means a lost sale or a half-created client. We built three recovery layers into every production workflow.

  • Node-level exponential backoff: Outbound nodes retry up to five times on 5xx/timeouts, with delays stepping from roughly 2,000ms to 5,000ms. On HTTP 429, a Code node reads Retry-After and waits accordingly.
  • Idempotent database writes: Every write carries an idempotency key (SHA-256 of email + submission timestamp). Unique constraints turn retries into safe upserts instead of duplicate clients or double emails.
  • Dead-letter queue: Past max retries or permanent errors leave the main path so one bad payload cannot clog the system.
Error ClassificationRoot Cause TriggerAutomated Recovery
Permanent failuresHTTP 400/403/404, invalid schema, bad credentialsMark dead, skip retry storms, page engineering on Slack
Transient failuresHTTP 429, 5xx gateway errors, API timeoutsLog open, hourly cron re-injects via n8n retry API

Dead-Letter Queue Pattern

A global Error Trigger workflow captures unhandled exceptions across production flows. We log execution ID, error message, failed node, and original payload into dead_letter_events with status open. An hourly cron selects unresolved rows where attempt_count is under 5, re-injects them via the n8n executions retry API, and marks resolved when clean. The table becomes both safety net and audit trail.

flowchart TD
    Fail["Workflow error / max retries"] --> Err["Global Error Trigger"]
    Err --> Log[("dead_letter_events<br/>status = open")]
    Log --> Class{"Permanent vs transient?"}
    Class -->|Permanent| Dead["status = dead<br/>Slack page"]
    Class -->|Transient| Cron["Hourly n8n cron"]
    Cron --> Retry["POST /api/v1/executions/retry/:id"]
    Retry --> Main["Main pipeline"]
    Main -->|success| Res["status = resolved"]
    Main -->|fail again| Inc["attempt_count++"]

PII Protection and Compliance Basics

Leads are people. We keep API keys and service tokens in Supabase Vault and server environment variables, never hardcoded on the canvas. Webhooks require TLS 1.3 in transit. Storage encryption at rest is AES-256 on the database side. Combined with RLS, that keeps client records off the public path by default.

Strategic Takeaways

For growing B2B teams, self-hosted n8n plus Supabase beats task-metered middleware once volume and compliance matter. Metered platforms get expensive and blunt exactly when you need custom edge checks, transactional constraints, and controlled retries.

  • Separate storage from orchestration so PostgreSQL owns integrity and RLS, while n8n owns branching and integrations.
  • Keep agents inside a deterministic shell: edge HMAC, schema validation, policy thresholds, then static side effects.
  • Design for failure first: backoff, idempotency keys, DLQ, and replay, not hope.
  • Trigger onboarding from the database when state is truth; do not poll for closed-won forever.

Platform choice for the shell still matters. Our comparison of n8n vs Zapier vs Make explains when self-hosted n8n wins on volume and control, which is exactly why we used it here.

We keep shipping modular pipelines like this for clients that need inbound and onboarding to run like infrastructure, not like a full-time triage job.

Tags: case study, n8n, Supabase, lead routing, client onboarding, AI automation, workflow automation, PostgreSQL