Scheduled outreach email system built on Next.js (App Router) + Prisma + Inngest + Vercel Cron.
Vercel Cron (hourly)
→ GET /api/cron/outreach-dispatch [auth: CRON_SECRET]
→ finds active programs where next_run_at ≤ now
→ fires inngest event "outreach/program.due" per program
Inngest function: outreach-program-due
→ loads program, resolves listings from scope_payload
→ creates program_run + outreach_recipients rows
→ fans out "outreach/recipient.generate" per recipient
→ updates program.next_run_at
Inngest function: outreach-generate-recipient-email
→ builds LISTING or OWNER-rollup email
→ calls createGmailDraft() → stores draftId on recipient row
# 1. Install dependencies
npm install
# 2. Set up environment
cp .env.example .env.local
# Edit .env.local with your DATABASE_URL, CRON_SECRET, Inngest keys
# 3. Generate Prisma client and push schema
npm run db:generate
npm run db:push # or: npm run db:migrate (for migration files)
# 4. Run Next.js dev server
npm run dev
# 5. In a separate terminal, run the Inngest Dev Server
npx inngest-cli@latest devnpm testCovers:
computeNextRunAt— MONTHLY / QUARTERLY / SMART cadences and edge casesgroupListingsByOwner— recipient grouping for OWNER audience type
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string |
CRON_SECRET |
✅ | Bearer token for cron route auth |
INNGEST_EVENT_KEY |
✅ | Inngest event API key |
INNGEST_SIGNING_KEY |
✅ | Inngest signing key (production) |
GMAIL_CLIENT_ID |
When live | Google OAuth client ID |
GMAIL_CLIENT_SECRET |
When live | Google OAuth client secret |
GMAIL_REFRESH_TOKEN |
When live | Google OAuth refresh token |
src/lib/gmail.ts— replace stub with realgoogleapisintegrationresolveListingOwners()inoutreach-program-due.ts— query real owner dataresolveOwnerEmail()inoutreach-generate-recipient-email.ts— look up real email addresses- Email templates — inject real listing metrics / highlights
- SMART frequency — refine 30-day default with engagement-based logic
scopeTypehandling — addALL,TAG, or other scope types beyondFILTER
good-news-bot/
├── .env.example
├── vercel.json # Hourly Vercel Cron config
├── prisma/
│ └── schema.prisma # outreach_programs, program_runs, outreach_recipients
├── src/
│ ├── app/api/
│ │ ├── cron/outreach-dispatch/route.ts # Cron handler
│ │ └── inngest/route.ts # Inngest serve route
│ ├── inngest/
│ │ ├── client.ts
│ │ ├── index.ts
│ │ └── functions/
│ │ ├── outreach-program-due.ts
│ │ └── outreach-generate-recipient-email.ts
│ └── lib/
│ ├── db.ts # Prisma singleton
│ ├── gmail.ts # Gmail draft stub
│ └── scheduling.ts # next_run_at + groupListingsByOwner
└── __tests__/
├── scheduling.test.ts
└── recipient-grouping.test.ts