FollowThrough is currently a one-page concept site for an SMS-first reminder agent. Visitors can try an interactive, browser-based commitment flow and join a waitlist.
The repository also retains a larger server-backed reminder application and its test suite. That implementation includes authentication, commitments, SMS delivery, scheduling, and account controls, but those product pages are not part of the current visitor experience.
The active application has one visitor-facing product route:
/- landing page, interactive demo, product explanation, and waitlist
The hosted Sites application also has an operational route:
/admin/waitlist- owner-only signup count and newest-first table of position, email, source, submission time, and alert-delivery state
The admin route requires ChatGPT sign-in and then checks the signed-in email against the server-side ADMIN_EMAILS allowlist before reading waitlist data.
These former application routes redirect to /:
/dashboard/settings/onboarding/auth/sign-in/commitments/*/dev/simulator
There is no separate dashboard or simulator page in the current experience. The route source files remain in the repository as part of the retained server-backed prototype, but Next.js redirects visitors before those pages render.
The page lets a visitor:
- enter a commitment and trigger a simulated check-in;
- reply with natural phrases such as
done,I'm working, orgive me 15 minutes; - advance the demo clock by 5 or 15 minutes;
- see a scheduled follow-up arrive in the simulated phone conversation; and
- join the FollowThrough waitlist.
The interactive conversation is intentionally client-side. It does not create an account, write commitments to PostgreSQL, send an SMS, call OpenAI, or enqueue a background job. Demo state lives in React memory and resets when the page is reloaded.
The demo reply parser is deterministic and currently recognizes four outcomes:
DONE- marks the current demo commitment complete;SNOOZE- schedules another demo check-in, capped at 180 minutes;PROGRESS- acknowledges progress and schedules a 15-minute follow-up; andUNKNOWN- asks the visitor to clarify whether they are working, done, or need more time.
The one-page experience is maintained in two implementations:
| Surface | Location | Runtime | Waitlist storage |
|---|---|---|---|
| Main local application | repository root | Next.js 16 | Canonical hosted D1 through a server-side proxy |
| Hosted Sites application | sites/followthrough |
Vinext on Cloudflare Workers | Cloudflare D1 through Drizzle |
Both render the same landing-page demo and feed the same hosted waitlist. The Sites application exists so the one-page experience can be hosted independently from the larger Node/PostgreSQL application.
The Sites project can remain public because /admin/waitlist has its own server-side authentication and owner authorization checks.
Requirements:
- Node.js 22 or newer
- npm
From PowerShell:
cd C:\Projects\Personal-Assistant
npm install
npm run demo:setup
npm run devOpen http://localhost:3000.
npm run demo:setup creates an ignored .env with local URLs and random 32-byte phone-encryption and lookup keys. It refuses to overwrite an existing .env. If the environment has already been created, skip that command.
Use npm run demo:setup -- --force only when you intentionally want to replace the existing environment and encryption keys. Previously encrypted local phone data cannot be read with new keys and must be reseeded.
The interactive demo and active waitlist do not require local Docker or PostgreSQL. The root waitlist route forwards valid submissions to the canonical hosted endpoint. A local PostgreSQL database is required only for the retained server-backed services.
Docker Desktop must be running.
npm run db:up
npm run db:migrateThe development database runs on port 5433. db:up also starts the disposable integration-test database on port 5434.
Useful database commands:
npm run db:generate
npm run db:migrate
npm run db:deploy
npm run db:seed
npm run db:studio
npm run db:downdb:seed creates retained prototype/demo records for service and integration testing. It does not unlock the redirected dashboard or /dev/simulator routes.
The root Next.js application posts the form to /api/waitlist. That server route validates and normalizes the submission, ignores honeypot traffic, and forwards the request to the canonical hosted waitlist. WAITLIST_API_URL can override the canonical endpoint for controlled local testing; without an override, a real submission reaches the public hosted waitlist.
The hosted endpoint is the source of truth. For each new normalized email, it atomically:
- trims and lowercases the email address;
- validates the address and enforces a 254-character maximum;
- limits a pseudonymous request fingerprint to eight attempts per hour;
- stores one D1 row per unique email;
- assigns the next permanent waitlist position from a deletion-safe counter;
- records the signup source and creation time; and
- queues one idempotent owner alert through Resend after returning the public response.
The alert is sent to waitlist@fernando-ace.com. Its subject identifies FollowThrough and the ordinal position, for example FollowThrough — 9th person joined the waitlist. The message does not include the entrant's email address. A duplicate email keeps its original position and does not create or send another alert. New and duplicate valid submissions receive the same public status and message so the endpoint does not disclose whether an address is already registered.
The signup is still accepted if alert delivery fails. D1 retains pending, sending, sent, failed, or unknown delivery state, the attempt count, and the provider message ID when confirmed. The protected owner view can retry confirmed failures. Ambiguous outcomes can be retried only during a 23-hour safety window inside Resend's 24-hour idempotency retention; older ambiguous attempts require provider review before another alert is sent.
Existing rows receive stable positions during migration and are marked legacy; they do not trigger retroactive alerts.
The owner-only /admin/waitlist route reads D1 and displays the total signup count plus a newest-first table. It also exposes a narrowly scoped retry control for unsent alerts.
Anonymous visitors are redirected through Sites-owned ChatGPT sign-in. Signed-in users who are not listed in the hosted ADMIN_EMAILS runtime variable receive a not-found response, and the D1 query does not run.
Set these server-side values before announcing the waitlist:
ADMIN_EMAILS- owner emails allowed to open/admin/waitlist;RESEND_API_KEY- secret Resend API key;WAITLIST_NOTIFICATION_FROM- a sender on a domain verified in Resend, such asFollowThrough <waitlist@updates.example.com>.
The first controlled signup should be verified in both the owner view and the destination inbox before marketing begins.
- Joining does not send the entrant a confirmation email; the only automated email is the owner alert.
- Product updates and unsubscribe requests are handled manually through
waitlist@fernando-ace.comfor now. - The owner view has no search, filtering, CSV export, editing, or deletion controls.
- Position assignment is global to the canonical D1 waitlist, not to a local PostgreSQL database.
The hosted implementation has its own dependencies and lockfile:
cd C:\Projects\Personal-Assistant\sites\followthrough
npm install
npm run devIts useful checks are:
npm run lint
npm run build
npm testUse npm run db:generate after changing the hosted Drizzle schema.
The D1 binding is declared as DB in sites/followthrough/.openai/hosting.json. Sites owns the hosted database resource and deployment wiring. Publishing is performed through the Sites workflow rather than the root application scripts. Apply the committed Drizzle migrations as part of the Sites deployment workflow.
The repository still contains a substantial implementation of the originally planned SMS product. It is useful as a tested backend foundation, but it should not be confused with the active one-page UI.
Retained capabilities include:
- Supabase email magic-link authentication;
- onboarding, phone verification, SMS consent, quiet hours, tone, and follow-up preferences;
- commitment creation, editing, pausing, completion, cancellation, and conversation history;
- deterministic natural-language intent parsing and a conversation state machine;
- PostgreSQL-backed scheduling, idempotency, stale-job recovery, delivery locks, and daily limits;
- Twilio outbound messaging plus signed inbound and delivery-status webhooks;
- Upstash QStash dispatch and reconciliation endpoints;
- optional OpenAI structured-output classification for ambiguous progress replies;
- JSON account export and hardened account deletion; and
- local provider adapters and extensive unit/integration tests.
The backend remains deterministic by default. OpenAI is optional and cannot directly execute tools or change schedules.
Server Actions / Twilio webhook / QStash job
|
authenticated services
|
intent parser -> conversation engine
|
PostgreSQL + provider adapters
Important modules:
src/domain/intent.ts- compliance-first deterministic parsing and timezone-aware date handlingsrc/domain/engine.ts- pure conversation decisions and state transitionssrc/services/conversation.ts- inbound association, persistence, and reschedulingsrc/services/dispatcher.ts- transactional claim, deferral, send, and follow-up lifecyclesrc/services/scheduling.ts- durable check-in scheduling and provider coordinationsrc/providers/- local/Twilio messaging, local/QStash scheduling, and optional OpenAI classificationprisma/schema.prisma- users, preferences, consent, commitments, check-ins, messages, webhooks, rate limits, demo state, deleted identities, and waitlist signups
A retained backend commitment starts in SCHEDULED. A dispatched check-in moves it to AWAITING_STATUS. Replies can complete the commitment, mark progress, request another time, enter a confirmation or clarification state, or schedule a replacement check-in. Exhausted no-response follow-ups move the commitment to NEEDS_ATTENTION.
Compliance keywords are handled before other intents. STOP opts the user out globally, records a consent event, and cancels pending SMS delivery. START opts the user back in but does not silently recreate canceled jobs.
The root application validates its server environment at runtime, and the root .env.example is its canonical template. The hosted one-page application has a separate template at sites/followthrough/.env.example.
Active one-page waitlist variables:
WAITLIST_API_URL- optional root-app override for the canonical hosted endpoint;ADMIN_EMAILS- hosted owner allowlist;RESEND_API_KEY- hosted secret used only for owner alerts; andWAITLIST_NOTIFICATION_FROM- hosted verified sender identity.
Core variables:
APP_URLAPP_MODE-demoorproductionDATABASE_URLDIRECT_URLTEST_DATABASE_URLPHONE_ENCRYPTION_KEYPHONE_LOOKUP_HMAC_KEY
Production-only retained integrations:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, or the legacyNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_MESSAGING_SERVICE_SID, orTWILIO_FROM_NUMBERQSTASH_TOKENQSTASH_CURRENT_SIGNING_KEYQSTASH_NEXT_SIGNING_KEY
Optional integrations:
AI_PROVIDER=none|openaiOPENAI_API_KEYOPENAI_MODELRATE_LIMIT_PROVIDER=database|upstashUPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN
APP_MODE=demo is rejected when starting a production server. The build includes a post-build assertion that request-sensitive legacy routes were not accidentally prerendered.
Never expose the Supabase service role, Twilio auth token, QStash signing keys, phone keys, database credentials, or OpenAI key to browser code.
These integrations are not used by the active in-browser demo. They apply only if the server-backed product is re-enabled and deployed.
- Create a Supabase project.
- Use the pooled PostgreSQL URL for
DATABASE_URLand the direct URL forDIRECT_URL. - Run
npm run db:deployagainst the production database. - Enable email magic links and allow
${APP_URL}/auth/callbackas a redirect URL. - Set the publishable and server-only service-role keys.
- Run with
APP_MODE=production.
Application data access stays server-side through Prisma. Migrations revoke direct table access from Supabase anon and authenticated roles when those roles exist.
- Create a Messaging Service and attach a sender, or configure a direct From number.
- Set the Twilio environment variables and deploy over HTTPS.
- Configure inbound messages to
POST https://YOUR_DOMAIN/api/webhooks/twilio/sms. - Configure Advanced Opt-Out for STOP/START/HELP if desired.
The inbound route validates X-Twilio-Signature, normalizes the sender, looks up its HMAC, rate limits requests, deduplicates MessageSid, and stores only a payload hash for webhook auditing.
Each production outbound message registers https://YOUR_DOMAIN/api/webhooks/twilio/status as its callback. The signed callback persists accepted, delivered, or failed status while ignoring duplicate and out-of-order regressions.
QStash publishes check-in IDs to:
POST https://YOUR_DOMAIN/api/jobs/reminders/dispatch
The handler verifies Upstash-Signature, accepts a strict UUID-only payload, and treats duplicate or stale delivery as a safe no-op.
A production deployment also needs a recurring QStash schedule that sends POST {} to /api/jobs/reminders/reconcile every five minutes with retries enabled. Reconciliation repairs transient enqueue failures, stale worker leases, and expired database rate-limit buckets.
Root commands:
npm run lint
npm run typecheck
npm run test:unit
npm run test:integration
npm run test:e2e
npm run build
npm run validateTest responsibilities are now split:
- unit tests cover deterministic parsing, environment validation, provider adapters, route guards, rate limiting, navigation, the client-side demo classifier, ordinal formatting, and the email-provider request contract;
- integration tests exercise the retained PostgreSQL conversation, scheduling, delivery, recovery, verification, and deletion services;
- hosted storage tests apply the D1 migration in SQLite and exercise stable positions, duplicate suppression, exclusive delivery claims, and the durable rate-limit counter;
- Playwright exercises the active one-page demo, privacy and crawl routes, legacy-route redirects, waitlist dialog accessibility, narrow mobile overflow, and the create/reply/advance/complete browser flow; and
- the production build checks compilation and rejects accidental private-route prerendering.
npm run test:integration and the full npm run validate require the disposable PostgreSQL test service on port 5434. Start it with npm run db:up.
- The active interactive demo keeps commitment and conversation state in the browser's memory and loses it on refresh.
- Waitlist email addresses are the only visitor data intentionally persisted by the one-page experience.
- The root and hosted forms share one canonical waitlist and deduplicate normalized email addresses.
- A pseudonymous fingerprint derived from the request's network address is stored in hourly D1 rate-limit buckets; expired buckets are removed as later signup requests arrive, and the waitlist row does not store the raw address.
- Owner-alert emails contain the signup's ordinal position, source, and timestamp, but not the entrant's email address.
- Retained phone numbers are normalized to E.164, encrypted with AES-256-GCM, and located through a separate HMAC.
- Retained SMS consent and opt-out transitions are auditable.
- Retained account export omits credential material.
- Retained account deletion quiesces delivery, removes database artifacts, purges relational data, deletes the external identity when possible, and leaves a tombstone if identity deletion must be retried.
- The crisis-language guard is a narrow, static safety response. FollowThrough is not therapy, treatment, crisis support, or a substitute for professional care.
- Provider timeouts or missed callbacks can leave a delivery outcome unknown. Durable message idempotency prevents blind retries that could double-text.
src/app/ Root Next.js routes and retained route handlers
src/components/demo-workspace.tsx Active one-page interactive demo
src/domain/demo-conversation.ts Client-side demo reply classifier
src/domain/ Retained deterministic backend domain logic
src/services/ Retained application and delivery services
src/providers/ Retained external-provider adapters
prisma/ PostgreSQL schema and migrations
tests/ Root unit, integration, and Playwright suites
sites/followthrough/ Hosted one-page Sites application
The most immediate product work is operational rather than adding more hidden backend surface:
- configure the verified Resend sender and secret, then confirm one controlled numbered alert end to end;
- invite a small first cohort to try the public demo and join the waitlist;
- define the interest threshold that warrants finishing the production product;
- add confirmation, update, and automated unsubscribe workflows before sending product email at scale; and
- decide whether the retained authenticated application should be restored as a product surface or remain backend research code.
If the full SMS product is restored, the next reliability milestone is polling Twilio for messages whose delivery callback never arrives, followed by real-provider smoke testing for authentication, verification, STOP/START, dispatch, reconciliation, export, and account deletion.