← Back

Project deep dive

The Nudge Products

Two production SMS reminder apps — Sunday Nudge (personal) and Nudge Together (group) — built on a shared pnpm monorepo with Claude Haiku AI parsing, DST-safe scheduling, and full TCPA compliance. AI wrote most of the code. I designed the architecture, caught the edge cases, and shipped it.

Next.jsTypeScriptDrizzle ORMPostgreSQLTwilioClaude HaikuVitestVercelpnpm monorepo
Sunday Nudge ↗Nudge Together ↗

Overview

The Nudge products start from a simple premise: SMS is the most reliable notification channel that exists, and nobody should need to download an app just to get a reminder. Sunday Nudge is the personal version — text it naturally, it figures out when you mean, and texts you back. Nudge Together extends that to groups.

Both apps live in a single pnpm monorepo and share a TypeScript core package (@nudge/core) that centralizes scheduling, timezone conversion, AI parsing, Twilio integration, rate limiting, and logging. The shared package is compiled directly by each app via transpilePackages — no separate build or publish step.

The hardest parts weren't the features — they were the edge cases that only show up in production: DST transitions drifting scheduled times, AI parses that are probably right but not certainly right, and browser bundles breaking because a shared package imported a Node-only module. Getting those right is what separates a deployed app from a demo.

Sunday Nudge

What it does

A production SMS reminder app running at sundaynudge.com on a verified Twilio toll-free number. Real users, real reminders.

Natural-language SMS scheduling

Claude Haiku parses free-text messages ("Call Acme Plumbing Tue 2pm") into structured scheduling intents using tool-use mode with Zod-validated output. Confidence scoring below 0.8 routes to a clarification state instead of silently committing bad data.

DST-safe wall clock scheduling

Most scheduling systems store only UTC and drift after daylight saving transitions. Sunday Nudge stores remindAtLocal alongside UTC and re-anchors the UTC value on each cron pass — so a 9am reminder stays at 9am year-round.

Multi-pass cron delivery

A five-pass cron job handles DST re-anchoring, legacy record normalization, primary delivery, 30–90 minute follow-up for ignored reminders, and 14-day purge of stale records. Idempotency guards on reminderSentAt prevent duplicate sends under concurrent execution.

14+ SMS commands

LIST, NEXT, DONE, UNDO, DELETE, SNOOZE, STOP, START, HELP, WEB/LOGIN, QUIET, TIMEZONE, PAUSE, RESUME. Fast-path command detection bypasses AI parsing entirely for known keywords — reducing latency and API cost.

Persistent conversation state machine

Multi-turn SMS flows (create, edit, delete, snooze) are handled by a state machine with JSONB-stored pending payloads. States include TTL-backed expiry to prevent stale context from causing incorrect writes.

Full web dashboard

Authenticated dashboard for reminder CRUD, history pagination, timezone config, quiet hours, pause windows, and Sunday check-in settings. Email+password auth with bcrypt, httpOnly cookies, session expiration, and constant-time dummy-hash login.

Nudge Together

The group version

Nudge Together extends the same SMS-native architecture to group accountability. Invite a team, family, or crew — everyone receives the reminder, everyone can mark it done. Built on the same @nudge/core shared package with separate database isolation per product for clean consent and compliance boundaries.

Shared group reminders delivered via SMS — no app, no login required for members
Consent-gated onboarding: members blocked until affirmative YES reply (TCPA)
STOP/START compliance with immediate unsubscribe and reactivation semantics
Separate Neon database instance for clean per-product consent records
Admin tooling: user inspection, consent monitoring, plan tier management (trial, friends_family, paid)

Technical Architecture

Under the hood

Monorepo

  • pnpm monorepo with two production Next.js apps and a shared @nudge/core TypeScript package
  • transpilePackages in each app lets Next.js compile TypeScript directly — no separate build step for the shared package
  • Sub-path exports (@nudge/core/timezone, @nudge/core/recurrence) prevent Node-only modules (Twilio's fs/tls dependencies) from leaking into browser bundles
  • Separate Neon database instances per app for clean compliance and consent isolation

AI Integration

  • Claude Haiku in tool-use mode for structured extraction: intent, title, datetime, recurrence, confidence
  • Zod schema validation on all AI outputs — malformed responses never reach business logic
  • Deterministic inference: temperature 0, explicit system prompt rules, locked tool_choice
  • Timezone-aware prompts inject the user's local time and timezone to avoid day-boundary errors on relative dates

Scheduling

  • Recurrence engine: daily, weekly, monthly, yearly, weekdays, and custom day-list patterns (days:mon,wed,fri)
  • Calendar edge case handling: month overflow, leap-year rollover, DST transition correction
  • Quiet hours (quietStart/quietEnd) postpone sends to next window exit — never dropped silently
  • Pause semantics (pausedUntil) suspend delivery without data loss

Security & Compliance

  • Twilio webhook signature validation on all inbound routes to reject spoofed traffic
  • Mandatory consent gating (consentedAt) — unconsented users blocked until affirmative YES reply
  • STOP/START compliance with immediate unsubscribe/reactivation semantics (TCPA)
  • Custom in-memory rate limiter in @nudge/core with progressive warnings before blocking
  • Cron endpoints secured behind bearer-secret authorization (CRON_SECRET)

Testing

  • 55+ passing Vitest unit tests covering SMS commands, recurrence, timezone conversion, rate limiting, AI parsing, and state transitions
  • Mock strategy targets internal package files by direct path rather than barrel imports — ensuring relative imports inside @nudge/core are correctly intercepted
  • Critical edge cases covered: DST/local-date conversions, leap-year recurrence, AI parser timeout/fallback, command normalization

Engineering Challenges

Where it got interesting

01

DST and wall clock intent

Storing only UTC timestamps causes wall-clock drift when daylight saving transitions shift the offset. The solution: store remindAtLocal at creation time, then add a cron pre-pass that re-anchors the UTC remindAt on every DST boundary. This is the kind of problem most scheduling systems get wrong the first time — and only discover in production.

02

AI confidence gating

Calling an LLM and trusting the output is easy. Handling failure modes gracefully is not. Parses scoring below 0.8 don't auto-commit — they route to a clarification state where the user confirms before anything writes to the database. This required designing the state machine around the AI's uncertainty, not just its success path.

03

Browser bundle contamination

Importing from a shared package barrel (@nudge/core) caused Twilio's Node-only dependencies (fs, tls) to leak into browser bundles, breaking the frontend build. The fix was sub-path exports that create explicit boundaries between Node and browser code — a non-obvious architectural decision that required understanding how Next.js resolves module graphs.

04

Production lifecycle bug in rescheduling

Rescheduled reminders that had already fired were getting permanently stuck. The root cause: reminderSentAt was not being cleared on reschedule, so the cron's WHERE reminderSentAt IS NULL clause never picked them up again. Diagnosing it required tracing the interaction between event status, reminderSentAt, and the cron query — not obvious from any single layer.

← Back to portfolio
Sunday Nudge ↗Nudge Together ↗