The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Podcast Guest Crm listing page.

4.2 million podcasts. $4B+ creator economy. Zero purpose-built workflow software.
We built the tool that should have existed for the last decade.
Built by Rudrendu Paul & Sourav Nandy · Engineered with Claude Code
Quick Start · The Problem · The Product · AI Layer · Architecture · Tech Stack
The app runs on seed data from first boot; 34 realistic guests across all six pipeline stages. No environment variables required.
[!NOTE] Zero-config only applies to this local dev mode running on seed data. A production deployment needs real Supabase and Anthropic credentials set in the environment: the Zod env schema in
packages/configcrashes the server at boot if a required secret is missing, by design.
Every tool a podcast host reaches for was built for a different job. HubSpot is a sales CRM. PodMatch is a discovery marketplace. Notion is a blank canvas that requires engineering to become anything useful. None of them model the guest lifecycle (the arc from discovery through outreach, scheduling, recording, publishing, and follow-up) as a first-class object.
This tool does. Guest fit scoring, personalized outreach drafting, interview prep, and follow-up sequences run on claude-sonnet-4-6, which is what makes automating those specific steps viable now in a way that wasn't a couple of years ago.
A full-stack AI-native CRM with six pages, eleven features, and zero compromises on craft.
Every guest moves through this lifecycle. Every transition is validated, logged, and acted on. The system knows where every guest is, when they last heard from you, and what needs to happen next. Without you having to remember.
| Feature | What it does |
|---|---|
| Cmd+K Palette | Search any guest by name, company, or topic. Navigate all six pages. Trigger actions. Entirely keyboard-driven. The fastest path to anything in the app. |
| Kanban Pipeline | Six-column drag-and-drop board. Optimistic updates. Lifecycle rules enforced at the service layer. You can't jump from Discover to Published. Confetti fires on every confirmed booking. |
| AI Email Composer | Select a guest, click Generate. Our AI streams a personalized 150–250 word pitch, character by character. Typewriter effect, not a spinner. Confidence score included. |
| Interview Brief | One-click pre-recording brief: bio intro, 5 tailored question types, talking points, closing hook. Copy-ready. |
| Social Posts | LinkedIn post, Twitter/X thread (5 tweets with character counts), Instagram caption. Platform tabs, one-click copy per platform. |
| Notification Center | Persistent bell dropdown. Shows guests without reply in 7+ days and upcoming recordings. Always visible. Always actionable. |
| Today's Focus | Dashboard section that surfaces exactly what needs attention today. No manual triage required. |
| Guest Detail | Animated fit score ring (counts from 0), lifecycle progress timeline, full contact links, AI action sidebar with three generative panels. |
| Analytics | Bar chart by stage, donut by topic, 12-week outreach activity timeline, conversion metrics. Runs without backend dependency. |
| Add Guest Modal | Cmd+N from anywhere. Name, email, title, company, bio, topics, LinkedIn, Twitter, stage, priority. Fit score auto-generated on create. |
| Smart Nudges | Toast appears on dashboard load when guests have been in outreach > 7 days without reply. Proactive, named, not nagging. |
The app is designed for keyboard-first workflows. Power users never touch the mouse for core tasks.
| Shortcut | Action |
|---|---|
⌘K | Command palette. Search guests, navigate, trigger actions |
⌘N | Add Guest modal directly |
↑ ↓ | Navigate palette results |
↵ | Select |
Esc | Close any modal |
All AI lives in packages/ai. The only place in the codebase that imports @anthropic-ai/sdk. Every feature calls a typed function. It never touches the SDK directly.
Two modes: completeJSON<T>() for structured output with generic type inference, stream() for the real-time typewriter effect. The outreach composer uses both simultaneously. Streaming for the live preview, JSON for the copy-ready result with confidence score.
| Feature | File | Output |
|---|---|---|
| Outreach Email | outreach-email.ts | Subject, 150–250 word body, confidence score (0–100), reasoning |
| Guest Fit Score | guest-research.ts | Score, alignment rationale, red flags, booking difficulty |
| Interview Brief | interview-brief.ts | Bio intro, 5 question types, talking points, closing hook |
| Topic Tagging | topic-tagging.ts | 3–8 tags from bio + LinkedIn, primary category, confidence |
| Follow-Up Sequence | follow-up-sequence.ts | 3-email arc: Day 7 bump, Day 14 follow-up, Day 21 final |
| Social Posts | social-post.ts | LinkedIn post, Twitter thread, Instagram caption. Tone varies by platform |
We're not prompting generically. Here's the actual constraint set from the outreach module. Specificity is the moat:
The fit scoring prompt evaluates guests against the show's actual topic taxonomy. Not generic relevance signals. The interview brief generates question types calibrated to the podcast format (depth, contrarian, forward-looking). This is prompt engineering as product design, not prompt engineering as a party trick.
1. Shared types in packages/types, zero inline definitions in apps/.
Every interface that crosses the API boundary (Guest, OutreachEmail, Workspace, AnalyticsOverview) lives in one package, imported by both the API and the web app. A TypeScript error on the frontend is a broken API contract caught before it ships.
2. Single AI seam in packages/ai.
ClaudeClient is the only place @anthropic-ai/sdk is imported. It handles exponential backoff on 429s and 5xx, token tracking per call, markdown stripping from JSON responses, and streaming via AsyncIterable. Feature code calls typed functions and never knows the SDK exists. Swapping models or providers is a one-file change.
3. Graceful degradation as a design requirement, not an afterthought. Every TanStack Query hook catches API errors and returns seed data. Every mutation has a synthetic fallback. The app is fully interactive without a running backend. This is deliberate: demos should never fail because a server is down.
4. Optimistic updates with enforced rollback.
Stage transitions on the kanban board are instant in the UI. The server confirms asynchronously. If the server rejects a transition (the lifecycle rules are strict, you cannot move from discover to published directly), the previous state is restored and an error toast fires. Users never wait for drag-drop feedback; errors surface clearly without corrupting state.
5. Zod at every boundary.
The env schema crashes the server at boot if a required secret is missing. Silent misconfiguration is worse than a loud failure. Every API route has a Zod schema for body, query, and params; the CI pipeline rejects routes without schemas. Shared schemas live in packages/config so frontend and backend enforce the identical contract.
This codebase was built using four specialized Claude Code sub-agents running in parallel, each scoped to a domain slice. This isn't a workflow preference. It's architectural isolation enforced at the tooling layer.
| Agent | Constraint file | What it owns |
|---|---|---|
| UI Agent | .claude/agents/ui-agent.md | apps/web/ only. use client only when required. Framer Motion on all list animations. |
| DB Agent | .claude/agents/db-agent.md | packages/db/ only. Schema, seeds, migrations. Cannot touch routes or UI. |
| AI Features Agent | .claude/agents/ai-features-agent.md | packages/ai/ only. Prompts, streaming, JSON mode. No any, no hardcoded show context. |
| Test Agent | .claude/agents/test-agent.md | Tests for everything other agents build. Coverage gate: >70% before merge. |
The UI agent cannot write a Drizzle query. The DB agent cannot create a React component. Constraint becomes architecture. You stop second-guessing whether a UI change silently mutated a schema.
Custom slash commands in .claude/commands/:
/new-feature <name>: scaffolds a full feature, API route + Zod schema + service + page + components + TanStack hook + tests/review-pr: runs a security, type safety, and MLP checklist before mergeThe "ship fast" advantage is gone. A capable developer scaffolds a CRM in a weekend; our solution compresses that to hours. The moat is now craft. The quality of what you build in that time.
We hold a Minimum Lovable Product bar on every PR. Elena Verna's framing: the threshold where a product earns genuine affection from its users, not just adequate utility.
Deliberately built moments:
MLP checklist, required on every PR:
Skeleton components, not blank screensTwo-tier SaaS. Simple pricing that grows with the customer.
| Plan | Price | Who it's for |
|---|---|---|
| Solo | $29/month | Independent podcast hosts managing 1 show, 20–100 guests/year |
| Agency | $99/month | Booking agencies managing 3+ shows and 200+ pitches/year |
Usage-based AI credits above the base tier: the first 200 AI calls/month (outreach email, fit score, brief, social post) are included, above that teams pay for what they use.
The gap isn't features. It's the mental model.
| Google Sheets | HubSpot / Pipedrive | PodMatch | Podcast Guest CRM | |
|---|---|---|---|---|
| Guest lifecycle (6-stage) | manual | custom fields required | ❌ | built-in, enforced |
| AI outreach (personalized) | ❌ | ❌ | ❌ | streaming, confidence score |
| Guest fit scoring | ❌ | ❌ | basic | AI-scored vs. your topics |
| Interview brief | ❌ | ❌ | ❌ | one click, copy-ready |
| Follow-up sequence (AI) | ❌ | add-on ($$$) | ❌ | 3-email arc, AI-written |
| Social post generator | ❌ | ❌ | ❌ | LinkedIn + Twitter + Instagram |
| Command palette (⌘K) | ❌ | ❌ | ❌ | full keyboard navigation |
| Smart notification center | ❌ | ❌ | ❌ | nudges + recording alerts |
| Agency multi-show workspace | ❌ | $$$ | ❌ | included |
| Price | $0 | $45–800/mo | $27–97/mo | $29–99/mo |
PodMatch solves discovery: finding guests. We solve workflow: the months-long process of pitching, following up, scheduling, prepping, recording, publishing, and staying in relationship. These are not the same problem. The companies that built discovery tools left the workflow problem untouched. That's the gap.
Every integration point sits behind an interface. MCP servers slot in without refactoring.
| MCP Server | Status | Integration point |
|---|---|---|
| GitHub MCP | Active in dev | .github/: PR automation, CI status, issue tracking from the terminal |
| Supabase MCP | Ready to wire | packages/db/: queries live schema before writing queries, eliminating field-name bugs |
| Gmail MCP | Ready to wire | apps/api/src/routes/outreach.ts: outreach sending is behind a sendEmail() interface |
| Google Calendar MCP | Ready to wire | apps/api/src/routes/guests.ts: booking confirmation and recording date sync |
| Exa Search MCP | Ready to wire | packages/ai/src/prompts/guest-research.ts: live web data in the fit-scoring pipeline |
With Gmail MCP active, outreach goes from drafted to sent in one click. With Calendar MCP, a guest moving to Scheduled creates the recording event automatically. With Exa, fit scoring pulls the guest's latest work from the web. Not just what's in their bio.
Every choice is defended. No resume-driven development.
| Layer | Technology | Why, honestly |
|---|---|---|
| Monorepo | Turborepo + pnpm workspaces | Remote build caching. workspace:* protocol. Single pnpm install at root wires everything. |
| Frontend | Next.js 14 App Router | RSC for static-first rendering. File-based routing. Built-in BFF pattern without a separate gateway. |
| UI | Tailwind CSS + shadcn/ui | shadcn copies into your repo. You own the code, not a version. No dependency hell on breaking releases. |
| Animations | Framer Motion | Layout animations on list reorders: one line. AnimatePresence handles mount/exit. Worth the bundle size. |
| Drag & Drop | @hello-pangea/dnd | Production-proven fork of react-beautiful-dnd. Maintained. Accessible. Drops in identically. |
| Server State | TanStack Query v5 | Stale-while-revalidate. Optimistic updates. Auto background refetch. The kanban board is instant because of this. |
| UI State | Zustand | Minimal API. Sidebar, modals, command palette, filters. All persisted to localStorage in one line of middleware. |
| API | Fastify v5 | ~2x faster than Express at the p99. First-class TypeScript. @fastify/swagger generates OpenAPI from route schemas automatically. |
| Validation | Zod | One schema = one TypeScript type + one runtime validator. On every route. No exceptions. |
| ORM | Drizzle ORM | No code generation. Schema is plain TypeScript. Migrations are plain SQL. Queries are fully type-safe. |
| Database | SQLite (dev) / Turso (prod) | Zero config locally. Identical schema to production. Turso adds global edge distribution when we need it. |
| AI | claude-sonnet-4-6 | Best structured JSON output and instruction-following depth available. The prompt patterns here require it. |
| Auth | Supabase Auth | JWT + Row Level Security. dev-mock-token in dev. RLS enforces workspace isolation at the DB layer in prod. |
| Resend + React Email | Templates as React components. Version controlled, testable, previewable in a browser. Mocked in dev. | |
| Charts | Recharts | React-native. Composable. TypeScript-friendly. Beat Chart.js on composability for our use case. |
| CI/CD | GitHub Actions | Lint → typecheck → test → audit on every PR. CodeQL on weekly schedule. No merge without green. |
OpenAPI documentation auto-generated at http://localhost:3001/docs.
[!WARNING]
PATCH /guests/:id/stage,POST /guests, andGET /guests/:idcurrently declare their response shape as a bare{ type: 'object' }with no listed properties inapps/api/src/routes/guests.ts. Fastify's JSON serializer strips the body down to{}on success as a result, even though the operation succeeded.guest listis unaffected. See the FAQ for how the CLI works around this.
Lifecycle transitions enforced at the service layer:
podcast-guest-crm-cli is a real TypeScript CLI (packages/cli) that wraps the same API above. Every command maps to a real route, no invented endpoints.
Add --json to any data-returning command for machine-readable output, meant for scripts and agents:
login authenticates directly against Supabase's own REST auth endpoint (POST <SUPABASE_URL>/auth/v1/token?grant_type=password), the same identity provider the web app uses. It never uses the dev-only Bearer dev-mock-token shortcut in apps/api/src/plugins/auth.ts, that bypass exists purely for local API testing. The resulting session is cached to ~/.config/podcast-guest-crm-cli/credentials.json (permissions 0600) and refreshed silently with the stored refresh token when it expires.


podcast-guest-crm-cli ships a Model Context Protocol server (not to be confused with the third-party MCP servers this app can integrate with, listed above). podcast-guest-crm-cli mcp starts it over stdio, exposing five tools that call straight into the same API seam every CLI command uses: list_guests, add_guest, update_guest_stage, draft_outreach_email, and get_analytics_summary.
A real tools/call for the core lifecycle tool, {"name": "update_guest_stage", "arguments": {"id": "guest_1", "stage": "outreach"}}, returns the same envelope guest stage <id> outreach --json prints on the CLI. See packages/cli's README for the full tool reference.
Production-grade controls from day one. We don't retrofit security.
| Control | Implementation |
|---|---|
| Authentication | JWT via @fastify/jwt. Every route: preHandler: [server.authenticate]. No exceptions, including in dev. |
| Workspace isolation | All queries filter by workspaceId from JWT payload. Supabase RLS enforces this at DB layer in production. |
| Rate limiting | 100 req/min per IP via @fastify/rate-limit. Configurable per environment. |
| Input validation | Zod on every route. Body, query, path params. No schema = no ship. |
| SQL injection | Drizzle ORM parameterized queries throughout. No raw SQL in this codebase. |
| XSS | Next.js default escaping + restrictive CSP headers in next.config.ts. |
| Secrets | Zod env schema crashes server at boot on missing required vars. Silent misconfiguration is a security bug. |
| CORS | Allowlist-based. No wildcard, ever. |
| SAST | CodeQL on every PR + weekly schedule. |
| Dependencies | pnpm audit in CI. Breaks the build on high-severity vulnerabilities. |
Near-term (next 60 days):
Medium-term:
Longer-term:
Rudrendu Paul and Sourav Nandy have built this production-ready AI-native software.
The stack used:
What is podcast-guest-crm-cli and how is it different from using the web app?
It's a real TypeScript command-line client (packages/cli) for the same API the Next.js web app calls. It wraps the guest lifecycle endpoints (guest list/add/show/stage), the AI outreach drafting endpoint (outreach draft), and the analytics endpoints (analytics summary/pipeline). The differentiator is agent-native output: every data-returning command supports --json, so a script or an AI agent can drive the same pipeline a human would drive from the dashboard, without scraping HTML or maintaining its own HTTP client.
What platforms and runtimes does it support?
The npm package (podcast-guest-crm-cli on npm, requires Node.js 20 or newer) runs on macOS, Linux, and Windows anywhere Node runs. A separate PyPI package of the same name (packages/cli-pypi-wrapper) is a thin wrapper for pip/pipx users: it doesn't reimplement the CLI in Python, it checks that node and npx are on PATH and shells out to the npm package, pinned to the wrapper's own version -- falling back to npm's latest release if that exact version was never published to npm, rather than failing outright.
How does login work?
podcast-guest-crm-cli login prompts for your email and password, then authenticates directly against your Supabase project's own REST endpoint (POST <SUPABASE_URL>/auth/v1/token?grant_type=password), the same identity provider the web app uses. You'll need your deployment's Supabase project URL and anon key (--supabase-url / --supabase-anon-key, or PODCAST_GUEST_CRM_SUPABASE_URL / PODCAST_GUEST_CRM_SUPABASE_ANON_KEY), matching the values your deployment already sets as NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY. The resulting access and refresh tokens are cached to ~/.config/podcast-guest-crm-cli/credentials.json with 0600 permissions, and the access token refreshes silently once it expires.
Why did a command print {"data": {}} instead of the fields I expected?
That's a real, current gap in a few of the API's own Fastify response schemas (apps/api/src/routes/guests.ts), not a CLI bug: routes like PATCH /guests/:id/stage, POST /guests, and GET /guests/:id declare their response shape as a bare { type: 'object' } with no listed properties, so Fastify's JSON serializer strips the body down to an empty object even on success. The CLI detects this and falls back to printing the raw (empty) response instead of crashing on a missing field. guest list isn't affected, since its schema declares an array with no fixed item shape.
Can I use this CLI in an automated pipeline or hand it to an AI agent?
Yes, that's the primary design goal. Every data-returning command accepts --json for structured output, exit codes are nonzero on failure, and error responses are JSON objects with error, message, and statusCode fields when --json is set. There's no interactive-only path required for any command except login's password prompt, which also accepts --email and --password flags for non-interactive use. For MCP-native agents (Claude Desktop, Claude Code), podcast-guest-crm-cli mcp starts a stdio MCP server exposing the same guest-lifecycle, outreach-drafting, and analytics capability as callable tools, see MCP Server above.
Can I use this CLI, or the rest of this codebase, commercially?
Yes. This repository (including packages/cli and packages/cli-pypi-wrapper) is MIT licensed, jointly owned by Rudrendu Paul and Sourav Nandy. See LICENSE for the full terms; commercial use, modification, and redistribution are all permitted.
Does the CLI ever store or transmit my password?
No. The password you enter at the login prompt is sent once, over HTTPS, directly to Supabase's password grant endpoint, and is never written to disk. Only the resulting access token, refresh token, and their expiry are cached locally.
What happens if my session expires while I'm running a command?
The CLI checks the cached access token's expiry (with a 30-second buffer) before every request. If it's expired, the CLI calls Supabase's refresh-token grant with the stored refresh token, saves the new session, and retries, all without prompting you to log in again. You'll only see login errors again once the refresh token itself is invalidated (for example, after a password change).
MIT. See LICENSE for full terms. Commercial use, modification, and redistribution are all permitted.