app spec: the answers from the app repo, and alpha caught up

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.
This commit is contained in:
2026-08-19 18:59:13 +00:00
parent e57d6cc1cc
commit 98960d2827
+44 -9
View File
@@ -15,6 +15,11 @@ the build order, and it does not repeat the reasoning.
`{"ok":true,"channels":[]}` and every send is a silent no-op that returns success. **A green response
does not mean a push was sent** — check `channels` first, or you will debug the app for a day over a
server with no key.
- **Alpha is up to date as of 2026-08-19 19:xx.** `/api/notify/*` answered `404` there for most of the day
because the server predated this work; it now answers `401` unauthenticated, `push_devices` has its 10
columns, and `officer-notify` is running. If you get a `404`, that server needs `git pull` +
`pm2 restart officer`, and `officer-notify` needs starting — it is a new core process, so an existing
install will not have it until `bun gen:ecosystem` runs.
## Which app
@@ -31,7 +36,7 @@ work, nothing later will, and everything later is harder to debug.
### 1. iOS project setup
- A bundle id for `apps/mobile` (only `dev.officer.music` was ever confirmed to exist — check).
- A bundle id for `apps/mobile` — **confirmed: `dev.officer.mobile`**, plus `dev.officer.mobile.dev`.
- `aps-environment` entitlement. Do **not** copy chat's plugin here; the hub needs the opposite.
- Push capability on the App ID in the developer portal, and a provisioning profile that includes it.
@@ -143,14 +148,15 @@ longer installed and stop.
**One build, ever.** The extension is a generic resolver — it must never `switch` on known codes, or every
new notification kind becomes an app store release, which is the entire thing this design exists to avoid.
Note for Expo: a custom NSE needs a config plugin (`expo-notifications` alone does not give you one). Worth
scoping early — it may be the longest pole in Phase 3.
Expo: `ios/` is generated by prebuild, so the NSE target comes from a config plugin. **One already exists
in this repo** — `packages/bw-vault/plugin/withVaultAutofillTarget.js` creates the AutoFill extension
target. Follow it rather than inventing one.
## Phase 4 — deep linking
`route` from the mapping is the destination. Tapping a notification opens the hub, which routes onward to
the target app or screen. Confirm the scheme with the owner — the platform uses `officer://<hex>` origins
for app surfaces, and I have not verified what `apps/mobile` registers today.
the target app or screen. The scheme is **`officer://`** (`officer-dev://` in the dev variant), from
`app.config.ts`.
## Android — later, but design for it now
@@ -177,8 +183,37 @@ Android build and is not yours to absorb.
- **`appSlug` means the deep-link destination**, not "which app receives this". The name is a leftover from
the first design, where each app had its own token.
## Open questions for the owner
## Answered, from the app repo (2026-08-19)
1. Does `apps/mobile` have a bundle id and a provisioning profile with push enabled?
2. What URL scheme does `apps/mobile` register for deep links?
3. Is the hub in Expo managed or bare? It decides how the NSE gets built.
**Bundle ids exist.** `dev.officer.mobile`, and `dev.officer.mobile.dev` for the side-by-side dev variant.
Two bundle ids is fine and needs nothing special: `push_devices` is unique on `(token, bundleId)`, so the
two variants register as separate rows on the same phone. **Register the dev variant as
`environment: "sandbox"` and production as `"production"`** — that mapping is now concrete, so derive it
from the variant rather than the build config guesswork this document suggested earlier.
**The URL scheme is `officer://`, and `officer-dev://` in the dev variant.** Both come from
`app.config.ts`, and the share extension embeds the same string natively, so they cannot desync. So the
unknown-code fallback deep link is `officer://resync-notification-mapping` as written.
**Expo prebuild (CNG), not bare** — `ios/` is generated and gitignored; everything native comes from config
plugins. This is better news than the document assumed: **a config plugin that creates an iOS app-extension
target already exists in this repo**, `packages/bw-vault/plugin/withVaultAutofillTarget.js`, which builds
the AutoFill extension that prebuild cannot. **The NSE follows that pattern rather than being invented**,
which removes what this document called the longest pole in Phase 3.
**`expo-notifications` is NOT in `apps/mobile`** — only in `apps/chat`, for local turn alerts. Correcting
this document's assumption. Chat's `withoutPushEntitlement.js` still stays exactly as described above; the
hub needs the opposite.
## The one real blocker: the push entitlement
Today's signed build (`2026.08.19.02`) has **no `aps-environment` entitlement at all**. Nothing else in
Phase 1 can be tested until it does.
It has a known shape here. On 2026-08-05 adding a Network Extension failed for the same reason:
`-allowProvisioningUpdates` **cannot add a capability while Xcode has no account signed in**. The fix is a
one-time manual step by the owner — Xcode → Settings → Accounts, team `RGHC4NJC77` — after which it is
automatic again.
**Owner action, and it is the critical path.** Along with the APNs `.p8`, this is the whole of what stands
between here and a banner on a phone.