AI-powered lead qualification, enrichment, and personalized email generation with real-time SSE streaming
AI SDR is a production-ready AI Sales Development Representative system built to demonstrate full-stack AI engineering skills. It uses the Claude API to qualify leads against an ICP rubric, enrich company profiles from public data, and generate personalized cold emails — all streamed in real time via Server-Sent Events. Eight pre-seeded leads with hand-authored AI outputs let recruiters explore the full feature set immediately with no setup.
Features delivered:
Tech stack:
Sales Development Representatives spend the majority of their time on tasks that follow predictable patterns but require significant cognitive effort:
AI SDR replaces the three most time-consuming SDR tasks with a sequential AI pipeline that runs in real time and streams progress back to the user:
AI SDR uses a standalone two-app architecture — a Next.js 16 frontend and a NestJS 11 backend — deployed as separate Docker containers on Coolify. This proves the ability to architect either a standalone repo or a Turborepo monorepo depending on project requirements.
ai-sdr/web/ — Next.js 16 frontend with App Router, Server Components, and Client Components for SSEai-sdr/api/ — NestJS 11 backend with modular architecture (LeadsModule, PipelineModule, ClaudeModule)ai-sdr/api/prisma/ — Schema, migrations, and seed script — no shared packages required| Decision | Rationale |
|---|---|
| SSE over WebSockets vs. bidirectional channel | Unidirectional pipeline events (qualify→enrich→personalize) need no bidirectional channel. SSE is simpler, works through Nginx with X-Accel-Buffering: no, and has no reconnect complexity. Native browser EventSource handles reconnection automatically if needed. |
| Structured outputs (Zod + zodOutputFormat) vs. prompt-engineered JSON | Qualify and enrich steps return typed JSON via Anthropic's native structured output. Temperature 0 ensures deterministic ICP scores across repeated calls for the same input — the ICP rubric is auditable and consistent. |
| Standalone repo (not Turborepo monorepo) vs. shared workspace packages | AI SDR is fully self-contained to prove ability to architect either way. No shared packages, simpler Docker build context (./ai-sdr), and no turbo prune needed. Simpler CI/CD without workspace dependency graphs. |
| Pre-seeded demo data vs. live scraping on demand | Live scraping fails 20-30% of sites and takes 45-90 seconds per lead. The seed script writes hand-authored AI outputs directly to Postgres so recruiters see a populated app immediately with zero latency and 100% reliability. |
| In-process pipeline (no Redis/BullMQ) vs. distributed job queue | Sequential steps (qualify → enrich → personalize) run synchronously in PipelineService. A callback bridge pattern connects StepCallback to an SSE Observable without any queue infrastructure — correct for single-user demo scale, simpler to reason about. |
| NEXT_PUBLIC_API_URL as Docker build ARG vs. runtime ENV | Next.js bakes NEXT_PUBLIC_ variables into the client bundle during next build. Docker ARG passes the API URL at image build time — runtime ENV has no effect on client bundle, which would silently break the EventSource URL in Client Components. |
Nginx buffers SSE responses by default, delivering all tokens in a single batch at the end instead of incrementally. This breaks the real-time streaming experience — locally SSE appears to work (no proxy), but production fails silently.
Solution: Set X-Accel-Buffering: no as a per-request response header in NestJS (leads.controller.ts). Nginx 1.1.4+ honors this header per-connection without requiring server config changes — zero-config Coolify deployment.
Learned: Nginx buffering is silent — SSE appears to work locally but fails in production. The X-Accel-Buffering header is the correct solution; Nginx configuration changes would require Coolify infrastructure access that defeats the point of a managed platform.
NestJS @Sse() expects an Observable<MessageEvent>, but the pipeline runs via async callbacks (StepCallback). Bridging the two without leaking Claude API connections on client disconnect was non-obvious.
Solution: Wrapped the pipeline in new Observable<MessageEvent>(subscriber => ...) and used a closed boolean flag plus subscriber.closed guard in the onStep callback to prevent "Cannot call next on closed subscriber" errors. Added res.on('close', () => closed = true) for explicit disconnect detection.
Learned: The Observable/callback bridge pattern is the cleanest way to connect imperative async code to reactive streams in NestJS. The closed flag is essential — without it, a client disconnect during the email streaming step causes an unhandled error.
The @anthropic-ai/sdk's zodOutputFormat() calls z.toJSONSchema() which only exists in Zod v4+. Zod v3 (default install) causes a silent failure where the structured output schema is undefined, and the API call silently returns unstructured text.
Solution: Upgraded to zod@4.3.6. Also confirmed that nullable() (not optional()) must be used for optional EnrichOutput fields — Anthropic structured outputs require all fields in the JSON schema's required array; optional() would cause the API to reject the schema.
Learned: Structured output schema compatibility is fragile when mixing Zod versions. Pinning to zod@^4.x in package.json prevents silent schema failures. The nullable() vs optional() distinction is a non-obvious Anthropic API constraint.

ICP Score BarsColor-coded bars (green 70+, amber 40-69, red below 40) show qualification fit at a glance — no column sorting needed to identify top prospects.
Pre-Seeded Demo DataEight leads spanning FinTech, Healthcare, DevTools, and Logistics are loaded on first login — no setup required for recruiters to explore the full feature set.
Status ColumnThe status badge (pending / processing / complete / failed) reflects the pipeline state so reps know which leads have AI outputs ready to review.

Step Progress IndicatorsEach pipeline step (qualify, enrich, personalize) shows a spinner while Claude API runs, then a checkmark when complete — giving the user live feedback without polling.
SSE StreamingProgress arrives via Server-Sent Events — the NestJS backend streams step completion events as the pipeline runs, not in a single batch at the end.
Non-Blocking SubmissionThe lead is created immediately with status pending, then the pipeline triggers on first SSE connection — the form submission returns instantly with no artificial wait.

ICP Score BarThe horizontal bar renders in green (70+), amber (40-69), or red (below 40) and shows the numeric score — communicating qualification strength without any jargon.
Why This Score AccordionThe collapsible card lists matched ICP criteria (green checkmarks) and weak criteria (amber warnings) — so reps understand why Claude scored the lead, not just what the score is.
Structured OutputScore and reasoning come from Claude structured outputs via Zod schemas at temperature 0 — the same input always produces the same score, making the ICP rubric auditable and deterministic.

Streaming Email PreviewThe email arrives token-by-token from Claude via SSE — the user watches it being written in real time rather than waiting for a completed response.
Company-Specific CopyThe email references company-specific facts from the enrichment step (funding stage, tech stack, pain points) — not generic templates that buyers ignore.
One-Click CopyThe Copy Email button uses navigator.clipboard.writeText() to transfer the full email to clipboard instantly — ready to paste into any outreach tool.
AI SDR successfully demonstrates production-ready AI engineering capabilities across backend pipeline design, Claude API integration, and real-time streaming UX:
Demo credentials are shown on the login page. Log in to see 8 pre-seeded leads with ICP scores across industries. Submit a new lead to watch Claude qualify, enrich, and write a personalized email in real time.
Launch Demo