back to projects
// case study — flagship · project 01
Live demo available

Full-Stack AI Outreach SaaS

Upload your prospect list and get a personalized, deliverability-safe cold-email sequence running in minutes — no templates, no copy-paste. Per-contact AI personalization grounded in real scraped signals; crash-safe pipeline with idempotent state.

heads-up: the live demo auto-logs you into a pre-seeded sandbox workspace. Click through real campaigns, AI-written emails, the funnel dashboard, and the reply inbox — sending and account changes are disabled by a backend guard, so nothing real fires.

FastAPIPython 3Supabase (Postgres + Auth + RLS + Realtime)Groq · Claude · GPT-4oGroq Llama 3.1 8BOpenAI GPT-4on8nResendReoonHubSpotPlaywrightReact 18ViteTypeScriptTanStack QueryRenderVercel
// how it works

The system, in one frame.

Frontend talks to FastAPI. FastAPI owns all state. n8n does the work. External services handle the heavy lifting.

Frontend · Vercel
React + Vite SPA
TanStack Query · CSS Modules · Supabase Realtime client
↕ REST (JWT) + Supabase Realtime · no polling
API · Render
FastAPI
  • · Owns ALL state — single source of truth
  • · Idempotent mutating endpoints
  • · Auth · CSV ingest · AI orchestration · tracking pixel
↓ reads / writes↓ fires webhooks
Database
Supabase Postgres
  • · Per-workspace RLS (multi-tenant boundary)
  • · jobs table = work queue
  • · Realtime publications → live UI
Workers · Railway
n8n · 5 workflows
  • · Enrichment · Send Sequencer
  • · Reply Poller · Follow-up Scheduler
  • · Validate + A/B + CRM sync
rule: n8n NEVER writes the database directly — every state change goes through FastAPI (internal Bearer auth)
External services (called by FastAPI / n8n)
PlaywrightGroq · Claude · GPT-4oGroq Llama 3.1 8BOpenAI GPT-4oResendReoonGmail REST APIHubSpot
FastAPI is the single source of truth

n8n workers are stateless and only call back through idempotent endpoints — retries never double-send.

The jobs table IS the queue

Any worker can crash and resume exactly where it stopped. No Celery, no broker, no extra infra.

Multi-tenant by Supabase RLS

Every row is workspace-scoped at the database layer. Backend forgets a check, the DB refuses the operation.

100% free-tier stack

Real-time UI + deliverability-aware sending, all on free tiers. Scales by upgrading tiers, not rewriting.

// the pipeline

Ten stages, CSV in to analytics out.

Every stage is a retryable database job. Worker crashes mid-stage? Pipeline resumes from the next attempt.

01

CSV upload

FastAPI validates the file, inserts prospects, and creates one "enrich" job per row — all in one transaction.

02

Enrich

Playwright scrapes the prospect domain · Groq Llama 3.1 8B extracts structured signals (industry, tech stack, news, hiring).

03

Generate email

Groq Llama 3.3 70B writes a ≤120-word personalized email grounded in the scrape. No "I"-openers, no banned phrases, one CTA.

04

A/B variant

Optional subject-line variant assigned when A/B testing is on for the campaign.

05

Review & approve

Every draft lands in the Email Review screen. Human approval is required before anything sends.

06

Send

n8n picks a warmed-up sending account under its daily cap, injects the open pixel, and sends via Resend.

07

Track

Unique opens via 1×1 pixel · bounces & complaints via Resend webhook (Svix-signed) auto-dock reputation.

08

Detect replies

Reply Poller reads each sending account's inbox via the Gmail REST API · LLM classifies sentiment.

09

Follow-up / CRM

Auto follow-ups for non-repliers past the cadence window · positive replies sync to HubSpot.

10

Analytics

Nightly pg_cron snapshots + live denormalized counters keep the dashboard sub-second on free-tier Postgres.

// problem & solution
The Problem

Most cold-outreach tools mail-merge {{first_name}}into a static template — every prospect gets obviously-automated spam. The handoff between "send" and "reply came back" is broken too — reps lose replies in a shared inbox. Real personalization requires per-company research no one has time to do at scale.

The Solution

A SaaS that scrapes each prospect's site, extracts real business signals, and writes a genuinely specific email per contact with an LLM. Routes it through an approval step, sends on a warmed-up schedule, tracks opens, and detects + classifies replies so nothing falls through.

// the user journey

From CSV to live sequence.

What a real customer actually does. Seven steps from sign-up to active outreach with replies routed back.

01

Sign up / log in

Supabase Auth — email + password with full forgot/reset flow. Each user belongs to a workspace.

02

Upload prospect CSV

Required columns: company, domain. Optional: first_name, last_name, email, title, linkedin_url. The parser strips BOM, lowercases headers, skips invalid rows, and reports how many were inserted.

03

Configure the campaign

Pick a system template (e.g. "SaaS to SMB"), write a brand context (what you sell + value prop), choose a tone (Professional / Friendly / Direct / Casual), set follow-up cadence, optional custom AI instructions, toggle A/B subject testing.

04

AI personalizes each contact

Per prospect, a Playwright scrape feeds an extraction model, then a quality model writes a ≤120-word email using industry, product, tech stack, recent news, hiring signals, and a unique angle.

05

Review & approve

Every draft lands in the Email Review screen. The user reads, optionally edits inline, then approves individually or with one Approve-all click. Approval queues a send.

06

Sequences execute

n8n picks up send jobs, selects a warmed-up sending account under its daily limit, injects a tracking pixel, and sends via Resend. A scheduled poller checks for replies; a follow-up scheduler queues next steps for non-repliers.

07

Dashboard + replies

Live dashboard shows the funnel (prospects → enriched → approved → sent → opened → replied). Replies hit an Inbox with AI sentiment (positive/neutral/negative/unsubscribe) and a one-line summary. A reply auto-cancels pending follow-ups; unsubscribes are honored automatically.

// the proof — before & after

Real personalization. Real signals.

Two sanitized examples — what the AI sees, and what the prospect receives. Every opening line is grounded in a real scraped signal.

Northwind Logistics
Jordan Avery · VP Operations
Signals the AI saw
{
  "industry": "freight & logistics",
  "main_product": "regional LTL shipping",
  "recent_news": "launched real-time tracking portal v2 last month",
  "job_listings": ["Operations Analyst", "RevOps Manager"],
  "intent_signals": ["ops_maturity"],
  "unique_angle": "real-time visibility focus"
}
Email generated
Subject:Northwind's new tracking portal

Launching the v2 tracking portal last month says Northwind is doubling down on operational visibility — usually the same moment manual ops work starts eating the team alive.

We automate the repetitive ops layer (lead enrichment, outreach, CRM updates) so your analysts spend time on logistics, not spreadsheets — most teams claw back 10+ hours a week.

Worth a 15-minute look at where it'd save Northwind the most time?

Best, Kedrich — Kedrich Automation

personalization_hook: used the tracking-portal launch (recent_news) as proof of an operational-maturity moment
Brightwave Skincare
Mara Ellison · Founder
Signals the AI saw
{
  "industry": "DTC skincare",
  "main_product": "clean-ingredient skincare bundles",
  "team_size_signal": "small",
  "recent_news": "expanded into subscription bundles",
  "unique_angle": "founder-led, lean team"
}
Email generated
Subject:Brightwave's subscription push

Moving Brightwave into subscription bundles is a smart retention play — and a brutal amount of manual follow-up for a lean founder-led team.

We automate the outreach and follow-up busywork (personalized emails, replies routed for you) so you stay focused on product, not your inbox.

Open to a quick 15-minute walkthrough of how it'd fit Brightwave?

Best, Kedrich — Kedrich Automation

personalization_hook: tied the subscription expansion to a lean-team follow-up burden
// data model

Tenancy at the database.

Every tenant boundary is enforced by Supabase RLS. Even if the backend forgets a workspace check, the database refuses the read or write.

schema.txt
workspaces ──< workspace_members >── auth.users
   │
   ├──< campaigns ──< prospects ──< emails ──< replies
   │        │            │            └──< events
   │        └──< ab_variants
   ├──< sending_accounts        jobs (work-queue, entity_id → prospect/email)
   ├──< integrations            analytics_snapshots
   └──< unsubscribes
One-to-many

workspace → campaigns → prospects → emails → replies / events

Many-to-many

users ↔ workspaces via workspace_members

Multi-tenancy

Supabase RLS + backend membership check on every request. Every row carries workspace_id.

// tech breakdown

Each tool, its job.

FastAPI (>=0.115)
API / state owner
Async. Owns all DB writes — n8n never writes Supabase directly, so retries can't double-send.
Groq · Llama 3.3 70B
Email generation
Fast + free-tier; quality model for copy. Strict system prompt bans generic phrases.
Groq · Llama 3.1 8B
Signal extraction
Cheap/fast model for structured scrape parsing. Two-tier AI keeps cost near zero.
OpenAI GPT-4o
Demo / fallback
One AI_PROVIDER env var swaps providers — useful for high-stakes demos.
n8n (2.21.7, Railway)
Workflow workers
5 workflows: enrich, send, reply-poll, follow-up, validate+AB+CRM. Self-hosted, no per-execution cost.
Supabase
DB + Auth + RLS + Realtime
Postgres, ES256 JWT auth, per-workspace RLS, live UI subscriptions. The whole tenant boundary is enforced at the database.
Resend
Email sending
3k/mo free. Per-account API key. Bounce/complaint webhooks dock reputation.
Reoon
Email validation
Pre-send verification to cut bounces and protect deliverability.
HubSpot
CRM sync
Positive replies → contacts + deals. Fires from the reply classifier.
Playwright (in n8n)
Site scraping
Renders prospect sites JS-and-all so extraction sees the real content, not just SSR HTML.
Gmail REST API (httpx)
Reply detection
Replaced IMAP after Gmail throttling on datacenter IPs. Async, no discovery-doc fetch.
React 18 + Vite + TypeScript
Frontend
Custom SPA with CSS Modules. Not running Lovable as-is — the prototype was rewritten.
TanStack Query + Supabase Realtime
Data layer
REST reads + live subscriptions for status. No browser polling. Dashboard updates feel instant.
Render / Vercel / UptimeRobot
Hosting
API on Render, frontend on Vercel, UptimeRobot keepalive pings to dodge free-tier spin-down.
Fernet / slowapi
Security
Field-level credential encryption helper + rate limiting on auth endpoints.
// engineering decisions

The non-obvious calls.

Seven trade-offs that shaped the system more than the spec did.

Postgres jobs table over Celery

Every unit of work is a retryable DB row. Crashed n8n workers resume exactly where they stopped, and the free stack needs no broker (Redis/RabbitMQ). The cost of operating Celery on a portfolio-scale system isn't justified.

FastAPI owns all state

n8n never writes Supabase directly. Every status change goes through an idempotent FastAPI endpoint, so retries can't double-send or double-create. n8n is workflow logic; FastAPI is the source of truth.

Two-tier AI (8B + 70B)

Cheap 8B model handles structured extraction from messy scraped HTML; 70B writes the copy. One AI_PROVIDER env var swaps Groq→GPT-4o for demos. Prompt strategy bans generic phrases and forces one company-specific opener.

Gmail REST polling over IMAP

IMAP hangs from datacenter IPs (Render). Direct REST over httpx (no discovery-doc fetch) is reliable, async-native, and handles the dot/+tag address-equivalence problem cleanly.

Warmup ramp + per-account daily caps

5 → 90 sends/day over 10 days, with per-account limits and reputation scoring. Bounces auto-dock reputation and can auto-pause an account. Protects deliverability — most AI-email demos ignore this and burn their domains.

Idempotency keys on every email

A unique idempotency_key prevents n8n retries from double-creating or double-sending. Combined with the FastAPI-owned-state rule, the pipeline is exactly-once even with at-least-once delivery upstream.

Analytics from snapshots, not live aggregates

Nightly pg_cron rollups + denormalized counters keep the dashboard sub-second on free-tier Postgres. Running heavy aggregates on every dashboard load would have killed it at any real scale.

// challenges

What broke. How I fixed it.

IMAP dead on cloud IPs

Gmail throttles datacenter IMAP. Reply detection silently failed in production but worked locally. Rewrote the entire reply-detection layer on the Gmail REST API — async, no throttling, no discovery-doc fetch.

AI fabricating company facts

The model would invent plausible "v1.1 releases" when the scrape returned thin data. Fixed by constraining the prompt to ONLY reference scraped signals, with a safe fallback to generic-but-true angles when the scrape is empty.

Gmail dot/+tag address equivalence

Replies arrive from kedrichdev@ while the prospect is stored as kedrich.dev@. Matching now normalizes Gmail addresses (strip dots, strip +tag) before attribution.

Silent workspace mismatch

A sending account in a different workspace than its prospects made reply-matching return nothing — no errors, just zero matches. Now enforced and aligned at the DB layer. Cost hours to find.

CORS masking a 500

A real DB error surfaced in the browser as a CORS failure, because the 500 response skipped CORS middleware entirely. Fixed the underlying query; added explicit CORS headers to error responses.

Gmail image proxy over-counting opens

Gmail's image proxy prefetches the tracking pixel server-side, inflating open counts. Now counting unique opens (first load per email only).

// under the hood — engine room

The automation + the database.

Supplementary technical material — the 5 n8n workflows that do the work, plus the Supabase schema and the RLS policies that enforce multi-tenancy at the database layer. Click any to expand.

All 5 n8n workflows — Enrichment, Send Sequencer, Reply Poller, Follow-up Scheduler, Validate + AB + CRM
click to expand ⤢
All 5 n8n workflows — Enrichment, Send Sequencer, Reply Poller, Follow-up Scheduler, Validate + AB + CRM
Send Sequencer — warmup-aware account picker, pixel injection, Resend dispatch, idempotent state update
click to expand ⤢
Send Sequencer — warmup-aware account picker, pixel injection, Resend dispatch, idempotent state update
Enrichment Worker — Playwright scrape → FastAPI → 8B extraction → structured signals stored
click to expand ⤢
Enrichment Worker — Playwright scrape → FastAPI → 8B extraction → structured signals stored
Reply Poller — Gmail REST polling across all sending accounts, sentiment classification, prospect attribution
click to expand ⤢
Reply Poller — Gmail REST polling across all sending accounts, sentiment classification, prospect attribution
Follow-up Scheduler — finds non-repliers past their cadence window and generates the next step
click to expand ⤢
Follow-up Scheduler — finds non-repliers past their cadence window and generates the next step
Validate + AB + CRM — Reoon pre-send validation, A/B variant assignment, HubSpot sync on positive replies
click to expand ⤢
Validate + AB + CRM — Reoon pre-send validation, A/B variant assignment, HubSpot sync on positive replies
Supabase database schema — workspaces, campaigns, prospects, emails, replies, jobs, events, analytics_snapshots
click to expand ⤢
Supabase database schema — workspaces, campaigns, prospects, emails, replies, jobs, events, analytics_snapshots
Supabase Row-Level Security policies (1/3) — multi-tenant boundary enforced at the database layer
click to expand ⤢
Supabase Row-Level Security policies (1/3) — multi-tenant boundary enforced at the database layer
Supabase Row-Level Security policies (2/3) — every tenant table gated on workspace membership
click to expand ⤢
Supabase Row-Level Security policies (2/3) — every tenant table gated on workspace membership
Supabase Row-Level Security policies (3/3) — service-role bypass scoped to backend operations only
click to expand ⤢
Supabase Row-Level Security policies (3/3) — service-role bypass scoped to backend operations only
// notable

Things worth pointing out.

Prompt engineering that beats default prompting

Banned-phrase list, mandatory company-specific opener, ≤120 words, dual HTML/text output, and strict grounding in real scraped signals (not hallucinated specifics). The prompt is a contract, not a suggestion.

Crash-safe job architecture

DB-as-queue means any worker can die mid-run and the pipeline self-heals. FastAPI is the single source of truth with idempotency on every mutation — retries are safe, no double-sends.

Deliverability posture

Warmup ramp, per-account daily caps, reputation scoring, bounce auto-pause, Reoon pre-validation, honored unsubscribes. Most "AI email" demos skip all of this and torch their sender domains in week one.

Real-time, snapshot-backed dashboard

Supabase Realtime for live status with pre-computed analytics. Sub-second on a 100% free-tier stack. Heavy aggregates never run on the request path.

// results — the honest version

Built. Validated. Not yet public.

Full pipeline (enrich → generate → approve → send → open → reply → classify → analytics) confirmed end-to-end against real inboxes. No external users yet — the public read-only demo is the way to evaluate it today.

0
External users
12
Pipeline stages
5
n8n workflows
~$0
Cost per contact
RLS
Tenancy boundary
$0
Stack cost / mo
// what's next

The roadmap to public self-serve.

What lands next — in rough order of impact.

Reply-inbox onboarding + Gmail OAuth verification

The #1 blocker for true self-serve. Once Gmail OAuth is verified, users can connect their inbox in two clicks and the demo opens to the public.

Field-level credential encryption at rest

Helper already exists (Fernet); rolling it out across all stored credentials is on the near-term roadmap.

Multi-channel sequences

LinkedIn + SMS steps in a sequence (today: email only). Same n8n + jobs architecture handles it.

Team seats & roles

Schema already supports owner / admin / member. The UI surfaces the seats layer next.

Deeper CRM integrations

Pipedrive, native HubSpot two-way sync (currently one-way: replies → HubSpot).

Agent-assisted reply handling

Draft suggested responses from reply sentiment — let the user just review and send.

// let's build

Want outreach that
actually gets read?

Try the live demo first — see real campaigns, the AI personalization, and the reply inbox. Then book a call to talk about your stack.