Skip to content

feat(site): Send the daily reminder by browser notification - #17

Merged
WillieCubed merged 5 commits into
mainfrom
feat/daily-reminders
Sep 24, 2026
Merged

WillieCubed merged 5 commits into
mainfrom
feat/daily-reminders

Conversation

@WillieCubed

Copy link
Copy Markdown
Contributor

TL;DR

Participants can turn on one browser notification each morning of Week Without Driving, October 1 to
8, from My week, and stop it with one tap. Each notification asks them to leave the car at home and
share that day's trip, and tapping it opens My week. The Worker sends them itself, encrypted and
signed with Web Crypto, with no new dependency, and the production guide says exactly what a
maintainer needs to run to turn it on.

Overview of Changes

The reminder message list

Each morning, a participant who turns on reminders gets one short message asking them to leave the
car at home and share that day's trip on My week, with one idea to try. The eight messages live in
one place, apps/site/src/data/reminders.json, word for word as the campaign wrote them: each day's
notification title and body, and the text message a future text channel will send, with {link}
where the person's own "Open my week" link goes. Every build and every pnpm dev checks the list and
stops with the message's date and the broken rule when an edit breaks one — eight messages in date
order, titles and texts that name their day, the mail-in line on days 1 and 8, and texts that fit in
one standard text message with the 45-character link, in plain characters only.

Turning reminders on and off

My week's "Remind me to share my trip" section opens with its heading and a line saying what the
reminder is, then "Browser notifications." Tapping "Turn on notifications" answers the browser's own
question, subscribes with the Worker's public key and saves the subscription for the signed-in
person; "Stop reminders" cancels the subscription in the browser first, so it works offline, then
deletes the record. An iPhone outside the Home Screen gets the existing "Add to your Home Screen"
steps; a browser that can't show notifications gets a plain line instead of a button. From 8:00 am on
October 8 the section shows only its closing line. Signing out leaves a phone's reminders on, as
designed. The old yes-or-no choices for notifications, texts and email never sent anything and
nobody had saved one, so their table and route are gone.

How the morning send works

Subscriptions live in the new push_subscriptions table. A Cron Trigger fires on the odd minutes
from 8:01 to 8:59 am Las Vegas time, October 1 to 8, and each run sends that date's message to the
next 40 browsers that turned reminders on before 8:00 am, six at a time, encrypted for each. A
browser gets a day's message once; one its push service reports gone is deleted, and anything else is
tried again on the next run. The Worker's scheduled handler now picks the job by its Cron Trigger, so
the daily cleanup and the reminders never run each other's work, and a text channel can be added
later beside browser notifications on the even minutes. Volunteers get a "Send test reminder" button
in /admin, and the API preview sends its pinned day's reminder to each new browser every five
minutes.

Encryption and keys

Each notification is encrypted for its browser alone (RFC 8291, aes128gcm) and signed with a 12-hour
VAPID token for its push service (RFC 8292, ES256), both written with Web Crypto alone in
apps/site/worker/push/, so nothing new is added to run on Workers. platform.json declares one new
secret, VAPID_PRIVATE_KEY, generated once by pnpm bootstrap --production; the Worker works the
public key out from the private key and serves it at GET /api/push/key, so the two can never
disagree. The tests reproduce RFC 8291's worked example byte for byte, derive the RFC's sender public
key from its private key, read messages back with a stand-in browser's key, and verify the token's
signature.

What production needs

A maintainer runs pnpm bootstrap --production once after this merges; it applies migration 0006
and generates VAPID_PRIVATE_KEY without anything to type or copy. The production guide now lists
what reminders need and where each value lives: VAPID_PRIVATE_KEY (generated once, never replaced
on a later run), the public key the Worker derives from it, VAPID_SUBJECT and the send times
already in wrangler.jsonc, and the subscriptions table the same command's migrations add. It says
the owner does nothing by hand, how to replace a leaked key on purpose, and how to test a phone
before October 1 with "Send test reminder." The Privacy page says what turning on reminders keeps,
who delivers them, and that the record is deleted when you stop or on November 30, 2026.

lvwwd.org/keep-going

Day 8's reminder points to "Keep going after the week" on My week. /keep-going reaches it from
anywhere: on a phone signed in to My week, a small script opens My week at the card; on any other
phone it opens LVBT's join page. Both replace the page, so Back doesn't bounce into the redirect
again. Without JavaScript, the page shows the card's heading and line with both links, and it asks
search engines not to index it.

How it was tested

Vitest against a local D1 covers the RFC 8291 vector and VAPID tokens; subscribing and the 401, 400
and 410 replies; the schedule (40 a run then the rest, the 8:00 am cutoff, nothing outside the week
or on another Cron Trigger); gone and failing push services; sign-out leaving reminders on; and
deletion on November 30. The service worker tests show and tap each of the eight reminders. Playwright
covers the section's states, turning on and off, /keep-going signed in and out and with JavaScript
off. On the API preview, Chrome signed up, turned reminders on, and received the pinned day's message
a few minutes later through Google's push service. pnpm check passes.

Follow-ups

  • Send the daily reminder by text message once LVBT's Twilio registration is approved.
  • Record the reminder_opt_in analytics event once LVBT's analytics allow lvwwd.org's events.

🤖 Generated with Claude Code

WillieCubed and others added 5 commits September 23, 2026 19:42
Each morning of the week, October 1 to 8, a participant who turns on
reminders gets one short message that asks them to leave the car at
home and share that day's trip on My week, with one idea to try. The
eight messages now live in one place, src/data/reminders.json: each
day's notification title and body, and the text message a future
text channel sends, with {link} where the person's own link goes.

Every build and every dev server start checks the list and stops with
the message's date and the broken rule when an edit breaks one: eight
messages in date order, titles and texts that name their day, the
mail-in line on days 1 and 8, and texts that fit in one standard text
with the 45-character link, in plain characters only.

The tests hold the list to the campaign's wording word for word, put
a random personal link into each text to confirm it stays one GSM
text, and feed the check each kind of broken list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Day 8's reminder points participants to "Keep going after the week"
on My week. /keep-going is one short address that reaches it from
anywhere: on a phone signed in to My week, a small script opens My
week at the card (now anchored as #keep-going); on any other phone it
opens LVBT's join page. Both replace the page, so Back doesn't bounce
into the redirect again. Without JavaScript, the page shows the card's
heading and line with both links, and it stays out of search engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Daily reminders reach phones as web push notifications, which need
two pieces of standard cryptography. Both are written with Web Crypto
alone, so they run on Workers with no new dependency.

- Encryption (RFC 8291, aes128gcm): each message gets a new P-256 key
  pair and salt, and only the browser that subscribed can read it.
- VAPID (RFC 8292): each push carries a 12-hour ES256 token for its
  push service, signed with VAPID_PRIVATE_KEY. The Worker works out
  the public key from the private key, so the two can't disagree and
  setup only has to generate one secret.

The tests reproduce RFC 8291's worked example byte for byte, derive
the RFC's sender public key from its private key, read messages back
with a stand-in browser's key, and verify the token's signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
My week's "Remind me to share my trip" section now turns reminders on
and off for real. It opens with the section's heading and line, then
the "Browser notifications" part: one tap on "Turn on notifications"
asks the browser's own question, subscribes with the Worker's public
key and saves the subscription for the person signed in; "Stop
reminders" cancels it in the browser first, so it works offline, then
deletes the record. iPhones outside the Home Screen get the steps to
add it, and blocked or unsupported browsers get a plain line. From
8:00 am on October 8 the section shows only its closing line. The old
yes-or-no choices for notifications, texts and email never sent
anything and nobody had saved one, so their table and route are gone.

Subscriptions live in the new push_subscriptions table. A Cron
Trigger on the odd minutes from 8:01 to 8:59 am Las Vegas time,
October 1 to 8, sends that date's message to the next 40 browsers
that turned reminders on before 8:00 am, six at a time, encrypted for
each. Each browser gets a day's message once; one a push service says
is gone is deleted, and anything else is tried again on the next run.
Signing out leaves a phone's reminders on, as before. The service
worker shows each reminder, one at a time, and a tap opens My week.

The Worker picks each Cron Trigger's job, and reminder channels share
one schedule, so a text message channel can be added beside browser
notifications on the even minutes. Volunteers get a "Send test
reminder" button in /admin, and the API preview sends its pinned
day's reminder to each new browser every five minutes.
VAPID_PRIVATE_KEY is a generated secret in platform.json; the Privacy
page now says what reminders keep and who delivers them, and every
subscription is deleted on November 30 with the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The production guide now lists what the reminders need and where each
value lives: VAPID_PRIVATE_KEY, which pnpm bootstrap --production
generates once and never replaces on a later run; the public key,
which the Worker works out from it; VAPID_SUBJECT and the send times,
already in wrangler.jsonc; and the subscriptions table, which the same
command's migrations add. It says the owner does nothing by hand, how
to replace a leaked key on purpose, and how to test a phone before
October 1 with "Send test reminder". The README points to the message
list and the preview's reminder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@WillieCubed
WillieCubed merged commit b4f587f into main Sep 24, 2026
1 check passed
@WillieCubed
WillieCubed deleted the feat/daily-reminders branch September 28, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant