the piece that makes a new notification kind a server change rather than an
app store release. a push carries a code and integers; this is what the code
means, synced to the device instead of compiled into it.
deliberately the same shape as affordances rather than a second mechanism: a
`notifications/` directory of yaml in the plugin repository, copied to
capabilities/notifications/<publisher>/<appName>/ on install, removed on
uninstall. publish.ts mirrors affordances/publish.ts closely enough to diff.
the namespace is DERIVED from where the file lands, never from what it
declares — same rule as mountPrefix. a plugin writing `officer/core:test` as a
key gets `<publisher>/<plugin>:officer/core:test` and the real core entry is
untouched. that is the security property of the whole scheme and it is pinned
by test.
version is a content hash, not a counter. a counter needs somewhere to live
and is then wrong after a restore, a rollback, or two servers built from the
same install; a hash needs no storage and is right in all three. so the client
compares mv !== version, and both docs now say so — i had written "higher".
core entries ship in code rather than on disk, because capabilities/ is
installed rather than committed and `test` has to exist on a machine that has
installed nothing — it is what first light is proved with.
read from disk per request, not memoised: installing a plugin has to change
the answer without a restart, or it presents as "the new plugin's
notifications don't work", which is the hardest kind of bug to place.
verified live: 401 through the platform, plugin catalogue appearing and
disappearing with no restart, version returning to its prior value when the
plugin is removed. 10 catalogue tests, 100% coverage of catalogue.ts.
three questions answered from monorepo-mobile, and two of my assumptions were
wrong.
bundle ids exist — dev.officer.mobile and dev.officer.mobile.dev. two of them
needs nothing special: push_devices is unique on (token, bundleId), so the
variants are separate rows on one phone, and the dev variant is exactly the
sandbox/production distinction the doc warned about. concrete now rather than
derived from build config by guesswork.
scheme is officer:// and officer-dev://, both from app.config.ts.
expo prebuild, not bare — and a config plugin that creates an ios app
extension target ALREADY EXISTS there, withVaultAutofillTarget.js for the
autofill extension. the nse follows it. that removes what i had called the
longest pole in phase 3.
corrected: expo-notifications is not in apps/mobile at all, only apps/chat.
the real blocker is narrower than the doc implied and is not code. today's
signed build has no aps-environment entitlement, and adding one fails while
xcode has no account signed in — same failure as the network extension on
2026-08-05. one manual sign-in, team RGHC4NJC77, then automatic again.
alpha: /api/notify was 404 all day because that server predated the work.
pulled, db:push, gen:ecosystem, officer-notify started, officer restarted.
verified 401 unauthenticated and push_devices with its 10 columns.
the 2026-07-31 design promised this document to `officer-suite/COMMS/`, a repo
path that has never existed. writing it where the work is instead, to be
handed to whoever builds the client.
exact contracts rather than prose: the three device routes with real request
and response shapes read off devices.ts, what is required versus defaulted,
and that the server takes the user from the session and ignores a userId in
the body.
most of it is the failure modes that cost a day each and are invisible from
the client — a send with no credentials configured returns success and
delivers nothing; a sandbox token registered as production fails at apple with
no user-visible symptom; the notification extension must not touch the network
or it will pass every test at a desk and fail on cellular with the vpn off.
leaves three questions for the owner: whether apps/mobile has a bundle id and
a provisioning profile at all, its url scheme, and expo managed versus bare —
that last one decides how the extension gets built and is probably the longest
pole in phase 3.