#Dev Log#CareReady#Web Push#Privacy#PWA
Privacy-First Reminders: Web Push Where the Schedule Never Leaves the Device
CareReady keeps every family schedule on the device. Reminders threatened that promise — so we inverted Web Push: the server sends an empty daily wake-up, and the Service Worker decides on-device whether to notify.
CareReady is a belongings checklist for family caregivers. Everything a user enters — who they’re preparing for, where they’re going, when — lives on the device, in IndexedDB. No account, no server copy. That was a deliberate promise, and it made one feature awkward: reminders.
A useful reminder is time-based. “Your mother’s short-stay is in two days — want to start packing?” The obvious way to send that is a scheduled server job: the client uploads the outing date, a Lambda wakes up daily, finds the dates that are close, and pushes a notification. Simple. It also quietly breaks the promise: now the server knows when and where every user is going.
So we inverted it.
The trick: a payload-less “wake-up” push
Web Push lets a server deliver a message to a browser even when the tab is closed, via the browser’s push service. The message can carry an encrypted payload — or nothing at all. A payload-less push still wakes the Service Worker; it just arrives with event.data === null.
That “nothing at all” is the whole design:
- The server stores only an anonymous push subscription (an opaque endpoint + keys). It knows nothing about outings.
- A daily job sends the same empty “check” push to every subscription.
- On receipt, the Service Worker reads the local data itself and decides whether there’s anything worth showing.
The schedule — the sensitive part — never leaves the device. The server is a dumb daily alarm clock.
// service worker
self.addEventListener('push', (event) => {
event.waitUntil(maybeNotify());
});
async function maybeNotify() {
const outings = await idbGet('careready', 'kv', 'specialOutings');
if (!Array.isArray(outings)) return;
const today = new Date(); today.setHours(0, 0, 0, 0);
let soonest = null;
for (const o of outings) {
if (!o.date) continue;
const days = Math.round((new Date(o.date + 'T00:00:00') - today) / 86400000);
if (days >= 0 && days <= 2 && (!soonest || days < soonest.days)) {
soonest = { name: o.name, days };
}
}
if (!soonest) return; // most days: stay silent
const when = soonest.days === 0 ? 'today' : soonest.days === 1 ? 'tomorrow' : `in ${soonest.days} days`;
await self.registration.showNotification('CareReady', {
body: `${soonest.name} is ${when}. Shall we start packing?`,
tag: 'careready-outing',
});
}
The Service Worker can open the same IndexedDB the app uses, so the decision logic runs entirely on-device.
What you trade
Nothing is free. Two costs are worth naming:
- You push to everyone, every day. The server can’t pre-filter by date, so it fans out a daily no-op to every subscriber. Push services are built for this volume and the payload is empty, but it’s less efficient than a targeted send. For a small user base it’s a rounding error; at scale you’d revisit it.
- The reminder logic lives in the Service Worker, coupled to your storage layout. A schema change in the app has to stay compatible with what the SW reads. We keep that surface tiny — one key, one shape.
Keys and secrets
Web Push needs a VAPID key pair. The public key ships in the client (it’s meant to be public). The private key signs the push and must never touch the repo — it goes in a secrets manager, read only by the sending function. If you generate keys with a one-liner and paste the private one into a config file “just for now,” you’ve already lost; treat it like any other signing key.
Why bother
For a care app, “we never learn your family’s schedule” isn’t a marketing line — it’s the reason a nervous caregiver trusts you with the boring, private logistics of someone else’s day. The reminder is a small feature. Keeping its data on the device is the point.
Takeaway: if a reminder only needs the server to say “check now,” don’t let it say anything more.
This is part of building CareReady, an in-development web app at VEAI LAB. The implementation is public in the CareReady repository; my broader technical-PM profile is at hire-veai.com.

Thanks for reading — built with care, for caregivers.