Feature compatibility
Your server updates when you pull a new image; the app updates when the App Store says so. The two are rarely on the same build, so optional backend-backed features are negotiated at runtime rather than guessed from a version number.
Feature ids live in shared/types.ts as SERVER_FEATURES. The server advertises the
ids it supports in the /health response, the app probes that on connect and
refreshes it on a slow timer, and it caches the answer alongside the local snapshot
so a cold start isn’t a blind one. Anything the server leaves out, the app hides and
stops sending.
Reminders are the current example: on a server that doesn’t list taskReminders, the
app hides the Reminders row and omits task.reminders from its pushes, so an older
backend is never handed a field it would drop on the floor.
Unknown is its own case. A server that answered and left an id out is
unsupported; a /health probe that failed is merely unknown, and the app handles the
two differently. POST /sync upserts whole rows, so sending a field the server never
heard of is harmless — it’s ignored — while omitting one it does support overwrites
the stored value with an empty one. So against an unknown server the app hides the UI
(wrong on screen, and it self-corrects on the next probe) but still sends the field
(a strip can’t be undone). Only a server that actually answered gets fields stripped.
Adding a backend-backed feature:
- Add a stable id to
SERVER_FEATURES. Ids outlive deployed clients, so they’re never renamed or recycled. - Advertise it from
/healthonly once the backend can persist and sync the field. - Gate the client UI with
supportsFeature(id). - Strip the field before
POST /syncwhen the id isn’t advertised. - Leave local and sample mode fully capable — there’s no backend there to negotiate with.
