Case Study · Blitz · 2026

Athletes set
the terms.
The system
runs the
outreach.

Built by
Jacob Oliker · solo
Status
● MVP — shipped on Vercel
Stack
Next.js 16 · Supabase · n8n · Anthropic · Resend · Mapbox
7Postgres tables · RLS-isolated per athlete
4Stages in the outbound automation
1Builder · solo MVP
Athletes the system can run in parallel
§01 · The system

Four pillars, one shared database.

Blitz splits cleanly across a Next.js application that athletes log into, a Postgres database in Supabase that every part of the system reads and mutates, an n8n automation layer that runs the outbound work in the background, and a stack of integrations that handle email, search, and AI.

§ 01·a — Application

Next.js 16

Athlete onboarding, dashboards, deal pipeline, and a unified inbox for replies. App Router, React 19, server components, middleware-enforced auth. Deployed on Vercel.

App Router · React 19 · Tailwind v4 · Supabase SSR
§ 01·b — Data

Supabase

Postgres + Auth + Storage. Seven tables hold the entire system state: athletes, their deliverables and preferences, target companies, contacts, campaigns, and deals. Row-level security isolates every athlete's data.

Postgres · RLS · Auth · Storage · MCP server
§ 01·c — Automation

n8n

Runs the outbound work asynchronously: company discovery, contact scraping, personalization, send. Connects to the same Supabase database the app reads from. Trigger model is per-athlete — when an athlete activates outreach, n8n picks them up.

Webhook + scheduled · idempotent · same DB
§ 01·d — Outbound stack

Integrations

Resend for transactional and outbound email with proper deliverability. Anthropic Claude for the personalization step. Mapbox for visualizing target companies geographically. Google APIs for inbox sync.

Resend · Claude · Mapbox · Google APIs
§02 · Architecture

One athlete, one autonomous campaign.

From the moment an athlete finishes onboarding to the moment a brand replies and the conversation hands back to the human. Two parallel halves of the system — the application the athlete sees, and the automation that runs in the background — meeting in the middle at a single Postgres database.

ATHLETE-FACING SHARED STATE AUTOMATION-FACING Athlete SIGNS UP · ONBOARDS /onboarding PERSONAL · SOCIAL DELIVERABLES · PRICING [ multi-step ] /dashboard METRICS · ACTIVE DEALS /inbox REPLIES · HAND-OFF /deals PIPELINE · PAYMENT /brands TARGET COMPANIES [ supabase ssr · cookie session ] Supabase Postgres 7 TABLES · ROW-LEVEL SECURITY athletes deliverables athlete_preferences target_companies company_contacts outreach_campaigns deals RLS · athletes only see their own data SUPABASE STORAGE · headshots · media kits [ single source of truth ] n8n trigger ATHLETE ACTIVATES Discover COMPANY SOURCING Scrape contacts DECISION-MAKER LOOKUP Personalize CLAUDE · ATHLETE-SPECIFIC PITCH Send RESEND · TRACKED Inbox sync REPLY DETECTION [ writes back to DB ] positive reply → notify athlete · hand-off
scroll to pan

The shared database is the integration. The Next.js app and the n8n workflows never call each other directly — they both read and mutate Postgres. Athletes can edit their preferences mid-campaign and the next run picks up the change. n8n can write a new contact and the athlete sees it in /brands a moment later. Decoupling falls out for free.

§03 · Data model

Seven tables. Every piece of state.

The database is the spine of the whole system. Every page in the app queries it, every n8n step reads or writes to it, every deal moves through it. RLS policies at the row level mean each athlete only ever sees their own data — not enforced by application code, but by the database itself.

athletes core · 1 row per user
  • iduuidPK
  • user_iduuidFK auth.users
  • first_name · last_name
  • school · sport · year
  • socialsjsonb
  • headshot_url
  • media_kit_url
  • onboarding_completedbool
deliverables athlete offerings
  • iduuidPK
  • athlete_iduuidFK
  • typeenum
  • platform
  • price_centsint
  • turnaround_days
  • description
  • is_activebool
athlete_preferences targeting filters
  • athlete_iduuidPK
  • target_industriestext[]
  • geo_radius_miles
  • geo_centergeog
  • company_size_range
  • excluded_brandstext[]
  • toneenum
target_companies discovered prospects
  • iduuidPK
  • athlete_iduuidFK
  • name
  • domain · website
  • industry
  • locationgeog
  • size_range
  • discovery_source
  • match_scorenumeric
company_contacts decision makers
  • iduuidPK
  • company_iduuidFK
  • first_name · last_name
  • title
  • email · linkedin_url
  • verifiedbool
  • scrape_source
outreach_campaigns sent messages · replies
  • iduuidPK
  • athlete_id · contact_idFK
  • subject · body
  • sent_attimestamptz
  • statusenum
  • opened_at · replied_at
  • resend_message_id
  • personalizationjsonb
deals won opportunities
  • iduuidPK
  • athlete_id · campaign_idFK
  • brand_name
  • deliverable_idsuuid[]
  • total_value_cents
  • commission_cents
  • statusenum
  • closed_at
How to read this

The fields shown are reconstructed from PROJECT.md and the application's middleware-protected route surface, not the live migration files. Field names follow the conventions visible in the codebase. Treat this as the architectural shape — the deployed schema may have drifted in details.

§04 · The pipeline

Discover. Scrape. Personalize. Send.

Four asynchronous stages, each one writing back to the same Postgres database. n8n runs the whole sequence per-athlete, idempotent, with each stage operating on whatever rows are due for it. New athletes get their first run within minutes of activating. Existing athletes get re-evaluated on a schedule.

i.
Stage 1 · DISCOVERCompany sourcing
Pulls the athlete's targeting preferences (industries, geo radius, size range, excluded brands) and surfaces matching companies. Each match writes a row into target_companies with a discovery source and a match score the system can rank by.
athlete_preferences → target_companies match_score
ii.
Stage 2 · SCRAPEDecision-maker lookup
For each target company, find the right person to email — typically a marketing or partnerships lead, sometimes the founder for smaller brands. Verifies emails before they enter the campaign queue. Skips contacts that already exist for the same athlete.
target_companies → company_contacts verified · email
iii.
Stage 3 · PERSONALIZEPer-prospect pitch generation
Hands the contact + the athlete's deliverables, sport, school, and tone preference to Anthropic Claude. Returns a subject line and a 60–90 word body that reads like the athlete wrote it themselves — never a template. The personalization payload gets stored on the campaign row so it can be audited or A/B tested later.
@anthropic-ai/sdk athletes + contacts → outreach_campaigns
iv.
Stage 4 · SENDTracked outbound + reply detection
Resend handles the actual email send, with bounce + open + click tracking. The Resend message ID gets stored on the campaign row for later correlation. A separate inbox sync flow watches for replies and routes positive ones into the athlete's /inbox; negative or unsubscribe responses suppress the contact from future runs.
resend resend_message_id → /inbox · hand-off
§05 · LLM role

One model. One job.

Claude does exactly one thing in Blitz: write a single email per prospect that doesn't sound like a template. That's it. The rest of the system is deterministic Postgres queries and n8n flows. Keeping the AI surface narrow is the point — fewer places for an LLM mistake to break the system, more concentrated impact where it matters.

Layer 01 · The personalization step

Anthropic Claude

@anthropic-ai/sdk · structured prompt · ≈ 200 tokens

The prompt fuses the athlete's profile (school, sport, year, deliverables) with the prospect's profile (company, industry, role) and asks for a subject line + body in the athlete's voice. Strict rules: no hype words, no “I'm reaching out because”, no fake compliments. Output drops directly into the campaign row.

The personalization payload — what the model saw and what it returned — gets stored on the campaign row so the system can attribute reply rates to specific prompt variants later.

Layer 02 · Why not more

Deterministic everywhere else

Postgres + filters · no LLM in the discovery loop

Discovery and scraping are deliberately rule-based. Match scores come from comparing the athlete's preferences against the company's metadata — distance, industry overlap, exclusion list. No semantic match, no embedding similarity. That's a future feature; for the MVP, deterministic targeting is more debuggable, cheaper to run, and lets the personalization step be the moment the AI earns its keep.

If the personalization fails, the row is skipped and surfaced in the admin panel for manual review. The campaign never sends template-y junk.

§06 · The stack

Modern defaults, where they earn it.

Twelve dependencies that actually do something. The picks were calibrated for a solo build — minimum infrastructure, maximum leverage from managed services, and an automation layer that doesn't require running a server.

FrontendNext.js 16App Router, React 19, server components. Bleeding edge but the upgrades have been worth it for the streaming + cookie-aware patterns.
Auth + DBSupabasePostgres, Auth, Storage, and Row-Level Security in one product. The MCP server is a nice bonus for local development with Claude.
StylingTailwind v4No design system to invent. Configuration-as-CSS in v4 is dramatically faster than the JS-config era.
DeployVercelZero-config deploy from the same repo. Preview branches let me ship UI changes safely.
Automationn8nVisual workflow builder that connects to the same database the app reads from. No backend code to maintain for the discovery and outreach flows.
PersonalizationAnthropic ClaudeThe most reliable model I've used for first-person voice + strict-rule prompts. Direct SDK integration in the n8n step.
EmailResendTransactional email that doesn't require fighting deliverability on day one. Tracking, bounces, and a clean React Email integration.
MapsMapbox GLVisualizing target companies on the map makes the geo-radius preference tangible to athletes during onboarding.
ChartsRechartsDashboard widgets for outreach stats and deal pipeline. Composable, server-component-friendly.
Inbox syncGoogle APIsWatches replies on athlete-connected Gmail accounts. Routes positive replies into /inbox.
CSV bulk opscsv-parse / stringifyFor exporting deals and importing seed lists during onboarding.
Drag & dropdnd-kitUsed in the deliverables setup so athletes can rank their offerings.
§07 · The build

Six choices I can defend.

Decisions that don't show up on a feature list, but that separate “I bolted some tools together” from “I designed this”.

i.

The shared database is the API

The Next.js app and the n8n flows never call each other directly. Both read and write Postgres. The athlete edits a preference in the app, the next n8n run picks it up. n8n discovers a company, the athlete sees it in /brands a second later. No internal API to version, no webhook contracts to maintain.

ii.

RLS in the database, not the application

Athlete data isolation is enforced by Postgres row-level security policies, not by application code that filters by athlete_id. A bug in a Next.js route handler can't leak another athlete's data — the database itself refuses to return rows that don't belong to the requesting session.

iii.

Discovery is rule-based, not semantic

Match scores compare the athlete's preferences (industries, geo radius, size range, exclusion list) against company metadata. No embeddings, no semantic similarity. Cheaper, debuggable, and lets the AI step concentrate where it matters: writing the email.

iv.

The personalization payload is stored, not just used

Every campaign row keeps the JSON of what was sent to Claude and what came back. This makes it possible to run prompt-variant analysis later — which prompt template correlates with higher reply rates — without re-running anything.

v.

Replies hand back to the human, not the bot

Once a brand replies, the conversation moves to the athlete's /inbox and the automation stops. Blitz doesn't try to negotiate deals — it gets the door open. The athlete closes from there, the platform tracks the deal, and commission gets deducted on payout. Clean separation between “machine work” and “human work”.

vi.

An admin allowlist via env, not a role table

The middleware checks NEXT_PUBLIC_ADMIN_EMAILS for admin routes. For an MVP with a single admin (me) it's the right tradeoff: zero schema, no role-management UI, instant. When the user count justifies a real role table, the middleware swap is one PR.

§08 · End of case study

One more build on file.

Blitz is the athlete-side mirror of Georgia. Same problem at the core — running personalized outreach at scale — different stack, different audience, different scaffolding around the AI.

Or return to the index for contact and other work.