Skip to content

Daily reminders, with the restraint to keep them switched on - #9

Merged
BleakMidwinter90 merged 1 commit into
mainfrom
feat/notifications
Aug 10, 2026
Merged

Daily reminders, with the restraint to keep them switched on#9
BleakMidwinter90 merged 1 commit into
mainfrom
feat/notifications

Conversation

@BleakMidwinter90

Copy link
Copy Markdown
Owner

Push notifications came out of the research as the most requested feature in every chore app — and they are also the fastest way for one to get permanently muted. So the rules matter considerably more than the plumbing, and they live in lib/domain/reminders as pure functions with 12 tests.

The rules

Four gates, all of which must pass:

  1. The person asked to be reminded.
  2. They have something to be reminded about. Nobody wants a notification congratulating them on an empty list.
  3. Their chosen hour has arrived.
  4. They have not already been reminded today.

That fourth gate is load-bearing well beyond politeness. Because the decision is idempotent per day, the dispatcher is safe to call as often as you like — which is exactly what lets a self-hosted instance run a plain fifteen-minute timer instead of requiring a cron daemon. Telling someone running a container on a NAS to "also set up a cron job" is the point at which they stop. There's a test asserting that property directly.

The wording

States the fact and stops. No exclamation marks, no "Don't forget!", no counting how many days something has slipped.

There is a test asserting the message never contains lazy, should, overdue, late, or behind. The rest of the app is careful never to become ammunition in a domestic argument; a notification is the single easiest place in a product to break that promise, so it's guarded explicitly.

Zero-setup keys

VAPID keys are generated on first use and stored beside the data. Asking a self-hoster to run web-push generate-vapid-keys and paste two base64 strings into a compose file before notifications work at all is another place people give up. Environment variables still win when set.

Key creation uses onConflictDoNothing and then re-reads, so two requests racing on a cold instance can't end up with keys that disagree with what a browser was just handed.

Details that matter

  • The reminder stamp is written before sending, not after. If delivery throws, a missed reminder is a far smaller failure than notifying someone repeatedly on every subsequent tick.
  • Dead subscriptions are pruned on 404/410 only. A timeout or a 502 from a push service is transient and must not cost someone their subscription.
  • Reminder time is per member. One person is up at six; another would rather not be spoken to until the evening.
  • Changing your time clears the stamp, so a new setting can take effect today rather than silently waiting until tomorrow.
  • Permission is requested only on a button press, never on page load. An unprompted permission dialog is the fastest route to a permanent denial that the page can never undo.

Capability detection runs through useSyncExternalStore rather than an effect, so hydration stays quiet. The plain-HTTP case — a first-class deployment here — is explained in plain language rather than failing silently.

Verified

Against the running server: 401 unauthenticated, a valid 87-character P-256 public key that stays stable across calls (proving it's persisted rather than regenerated per request), 400 on a malformed subscription, and a cron pass that considered 10 members and correctly sent nothing.

131 tests, lint and typecheck clean, build passes.

Push notifications are the most requested feature in every chore app I
looked at, and the fastest way for one to get muted. The rules matter more
than the plumbing, so they live in lib/domain/reminders as pure functions
with 12 tests.

Four gates, all of which must pass: the person asked to be reminded, they
have something to be reminded about, their chosen hour has arrived, and
they have not already been reminded today.

That last gate is load-bearing beyond politeness. Because the decision is
idempotent per day, the dispatcher is safe to call as often as you like -
which is what lets a self-hosted instance run a plain fifteen-minute timer
instead of requiring a cron daemon. Telling someone running a container on
a NAS to 'also set up cron' is where they stop.

The wording states the fact and stops. No exclamation marks, no counting
days, no 'don't forget'. There is a test asserting it never uses words
like lazy, should, or overdue - the same restraint the rest of the app
shows, applied to the one surface where products usually abandon it.

VAPID keys are generated on first use and stored beside the data. Asking a
self-hoster to run a key-generation command and paste base64 into a
compose file before notifications work is another place people give up.
Environment variables still win when set. Key creation uses
onConflictDoNothing and re-reads, so two requests racing on a cold start
cannot end up with keys that disagree.

Reminder time is per member, not per household: one person is up at six
and another would rather not be spoken to until the evening.

The stamp is written before sending, not after. If delivery throws, a
missed reminder is a far smaller failure than notifying someone
repeatedly on every subsequent tick.

Dead subscriptions are pruned on 404/410 only. A timeout or a 502 from a
push service is transient and must not cost someone their subscription.

Client-side, permission is requested only on a button press, never on load
- an unprompted dialog is the fastest route to a permanent denial that the
page can never undo. Capability detection runs through
useSyncExternalStore so hydration stays quiet, and the plain-HTTP case is
explained rather than failing silently.

Verified against the running server: 401 unauthenticated, a valid 87-char
P-256 key that stays stable across calls, 400 on malformed input, and a
cron pass that considered 10 members without sending anything it
shouldn't. 131 tests, lint and typecheck clean.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@BleakMidwinter90
BleakMidwinter90 merged commit 90bc430 into main Aug 10, 2026
2 checks passed
@BleakMidwinter90
BleakMidwinter90 deleted the feat/notifications branch August 10, 2026 01:30
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