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.
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.
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.
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
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.
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.
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.
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.