YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.












Interface▾


Observability▾usePushNotification(), and two senders on the server: APNs for iOS, FCM for Android and the web. A token sent without its sender's credential is skipped with one log line, so prepare every row that applies to you.register() returns it with the provider that delivers to it.apns on iOS, fcm on Android and the web. The server picks the sender by it.vapidKey..p8 key the server signs its APNs requests with. One key serves both APNs environments.vapidKey, under firebase.native.android.googleServices in akan.config.ts.native in akan.config.ts.env.server.*: the service account the server sends to FCM with.env.server.*: the APNs key, its Key ID, your Team ID and the app's bundle id.push plugin, and permissions: ["push"] in native is all that adds it. There is no package to install. The plugin speaks each platform's own service:push.register() → { provider: "apns" }google-services.json itself, so no Gradle plugin is involved.push.register() → { provider: "fcm" }usePushNotification() hides which is which: it calls the plugin in a native shell and Firebase in a browser, and hands back one PushToken shape either way. Which permission adds which plugin is on Setup.env.client.*, under firebase.vapidKey.env.client.* ships to the browser; the server's service account belongs in env.server.*.apiKey, projectId, messagingSenderId or appId, register() returns undefined on the web.env.client.ts picks env.client.<env>.ts by AKAN_PUBLIC_ENV, so fill in every environment you deploy.firebase in the client env, akan sync writes public/firebase-messaging-sw.js for each environment.native.appId exactly, plus one config file native.android.googleServices names.native.appId.google-services.json.apps/myapp/secrets/google-services.json.native.android in akan.config.ts:appId (a debug build falls back to it too), and a file without that app fails the build with the names it has.secrets/, not public/. Everything in public/ is served to every visitor. secrets keeps the file out of git and carries it with akan upload-env and akan download-env.permissions: ["push"] adds the push plugin and POST_NOTIFICATIONS to the app.

google-services.json is not the server credential. It is the Android app's Firebase config, not the Firebase Admin service account JSON. The server credential goes in env.server.*, as the last section shows.url.native.android.push names a channel ({ id, name, importance? }), a smallIcon (a white-on-transparent PNG in the app folder) and an accent color instead.native.appId, and turn on Push Notifications..p8. Apple lets you download it once; note its Key ID and your Team ID.pushNoti.apns on the server, as the last section shows.permissions: ["push"] to native.GoogleService-Info.plist, no firebase-ios-sdk. The push plugin adds UIBackgroundModes and aps-environment to the app itself.provider: "apns". FCM does not accept it, so the server sends it to APNs itself.xcrun simctl push needs no server. It hands a payload to a simulator, which tests the tap and the routing. Put url at the top level, beside aps, as the server does.

.p8 on the server. It signs pushes to every app of your team. It belongs in env.server.*, never in env.client.* or public/.aps-environment: the push plugin declares development, and a build signed with a provisioning profile takes the profile's value. It decides which APNs environment the device's token belongs to.| Command | aps-environment | Used for |
|---|---|---|
| akan start-ios | development | Simulator and development-signed iPhone runs, through the APNs sandbox. |
| akan build-ios | development | A simulator build. |
| akan release-ios | production | The App Store profile: TestFlight and the App Store. |
| akan release-ios --adHoc | production | An ad hoc profile. |
environment unset, a send goes to production first and, when APNs answers BadDeviceToken (a development build's token), to the sandbox. Set environment to pin one.410, or BadDeviceToken from the last environment tried, removes the token from its owner.libs/shared needs no code of its own. Mount Notification.Zone.Initialize once in a signed-in layout: it registers the device again on every visit and on every token a native shell rotates, and never asks for permission.Notification.Util.PushSetting is that switch. A button of your own calls register() and hands the PushToken to the store:

registerPushToken comes with libs/shared. Without it, hand the PushToken to an endpoint of your own; its fields map one to one onto the DeviceToken shown next.@libs/util/webkit. Most screens need only register().PushToken, or undefined when refused or unsupported.getPermission() first.PushToken. Returns the unsubscribe.token, platform (web | android | ios), provider (apns | fcm) and deviceId, the installation id getPushDeviceId() keeps in the app's storage.libs/shared, st.do.registerPushToken(pushToken) stores it on the signed-in user. The next section shows where.url and a tap opens it through the CSR router. In a native shell the framework routes it from boot, the tap that launched the app included; in a browser the service worker hands it to the open tab. Only a path inside the app is followed.libs/shared keeps every device's token on its owner: user.notiInfo.deviceTokens, one DeviceToken per installation. The field is secret, so it never leaves the server.register() returned, plus when the server stored it:token or the same deviceId replaces that entry, so a rotated token does not pile up.updatedAt is the server's. It is written when the token is registered; the value a client sends is not used.signoutUser sends the installation's deviceId, so a handed-down phone does not get the previous person's notifications.Notification.Zone.Initialize registers the device again on its next visit.User-guarded mutations and queries on the user signal, called through the notification store's registerPushToken, unregisterPushToken and loadPushState:DeviceToken on the caller, replacing its earlier entry.Self supplies the owner, so a client cannot register a token under someone else's account.mcp: false.notificationService.push(userIds, payload) is the one call a domain service makes. It reads each recipient's settings, sends every accepted device through its own provider, and drops the tokens APNs or FCM call gone.firebase is the service account from Firebase Console, under Project settings, then Service accounts. Copy the five fields above from the downloaded JSON. Android and the web need it.apns is the .p8 key. privateKey is the file's text (\n escapes are fine), keyId and teamId come from Apple Developer, and bundleId is the app's native.appId. iOS needs it.google-services.json. That file is the Android app's config; these sign every send.

warn line, such as pushNoti.apns is not configured, and counted as failures.NotificationService.accepts: block and disagree stop everything, fewer lets only actionRequired and essential through, a future pauseUntil stops everything, and a user without tokens is skipped.410 or BadDeviceToken, and FCM messaging/registration-token-not-registered, remove the token from its owner in the same call.push() answers what it reached (targetUserIds, tokenNum, successCount, prunedTokens), and a failed send never fails the caller's own work.type: "all" goes to every active user, 500 at a time, through accepts like any other push.actionRequired, notice, essential, suggestion or advertise. The settings gate reads it.libs/shared, call PushNotificationServer.sendEach(targets, message) from @libs/util/srvkit with { token, provider } targets, and stop storing the invalidTokens it returns.