PayNudge
AI drafts the awkward email. You still hit send.

380+
automated tests
3
AI tone tiers
0
emails sent without approval
100%
RLS-covered tables
chasing money is uncomfortable
freelancers avoid the follow-up email longer than they should, and invoices quietly go from due to forgotten.
manual chasing doesn't scale
past a handful of clients, remembering who's overdue — and how firmly to word the next email — turns into a spreadsheet nobody maintains.
full automation feels risky
nobody wants a bot emailing their clients unsupervised. the trust gap is real, and it's the reason most people give up on automating this.
human approves, AI drafts
every follow-up is generated first, reviewed second. nothing reaches a client's inbox without an explicit click.
a state machine, not a prompt
the draft pipeline runs on LangGraph — structured nodes for drafting, validating, and retrying — instead of one long prompt hoping for the best.
tone escalates on a schedule
polite → firm → final notice, gated by a strict tier-increase guard so the same tone never fires twice in a row.
business rules stay pure functions
escalation logic, invoice numbering, and validation are side-effect-free and tested with property-based tests (fast-check) — thousands of random inputs, not a handful of examples.
RLS as the actual security boundary
every table enforces Row Level Security in Postgres. a bug in a route handler still can't leak another user's invoices.
PDFs with no browser in the loop
invoices render in-memory with pdfkit — no headless Chrome, no filesystem writes, no cold-start tax.
- frontend
- Next.js 14, React 18, TypeScript, Tailwind, shadcn/ui
- backend
- Express.js (TypeScript, ESM)
- database
- Supabase — Postgres, Auth, Storage, RLS
- ai
- Google Gemini via LangGraph state machine
- Resend (transactional delivery)
- pdfkit — in-memory generation
- testing
- Vitest + fast-check (property-based), 380+ tests
why LangGraph instead of one Gemini call?
a single generateContent() call works until it doesn't. the graph gives structured retries, failure counts, and a tier guard as named nodes — each debuggable on its own.
why pure validators instead of Zod for business rules?
Zod is great at parsing shapes. "escalation tier must strictly increase" isn't a shape problem, it's a logic problem — a plain function is easier to test and reason about.
why pdfkit instead of a headless browser?
pdfkit renders a clean invoice from pure data in ~50ms. no Chrome, no Docker, no cold starts — a fair trade for a single-page layout.
why RLS instead of app-level auth checks?
defense in depth. even a buggy route handler can't leak another tenant's rows, because Postgres enforces the boundary, not application code.