Akan.js
Docs
DocsConventionsReferencesCheatsheet
Akan.js
DocsConventionsReferencesCheatsheet
Akan.js

Released under the MIT License

Official Akan.js Consulting onAkansoftCopyright © 2026 Akan.js All rights reserved.System managed bybassman
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.
General▾
AuthorizationOAuth For AgentsSchema DesignText SearchEdge ComputingFile ManagementSingle Sign-OnDataList & Enum
Interface▾
CRUDEndpointMCP ServerAgent ChatForm
Observability▾
LoggingDependency InjectionError HandlingMetrics
Performance▾
CachingImage OptimizationLazy LoadingQueryingMutatingQueueingRealtime
Mobile▾
SetupPush NotificationsDeep LinksUI & KeyboardDesktop Release
Development▾
DocumentationSchema DocsScriptConsoleDockerKubernetesPWATesting
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.
General▾
AuthorizationOAuth For AgentsSchema DesignText SearchEdge ComputingFile ManagementSingle Sign-OnDataList & Enum
Interface▾
CRUDEndpointMCP ServerAgent ChatForm
Observability▾
LoggingDependency InjectionError HandlingMetrics
Performance▾
CachingImage OptimizationLazy LoadingQueryingMutatingQueueingRealtime
Mobile▾
SetupPush NotificationsDeep LinksUI & KeyboardDesktop Release
Development▾
DocumentationSchema DocsScriptConsoleDockerKubernetesPWATesting
PreviousSetupNextDeep Links

Push Setup

The prompt appears, a token comes back, and the server logs a send. Nothing arrives on the phone.
Push is one client API, 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.
Words used on this page
TermDescription
FCM
Firebase Cloud Messaging. Akan sends to Android apps and browsers through it.
APNs
Apple's push service. The server sends to iOS apps through it directly; Firebase is not involved.
push token
The address of one app install. register() returns it with the provider that delivers to it.
provider
apns on iOS, fcm on Android and the web. The server picks the sender by it.
deviceId
A random id the app keeps in its own storage, so a rotated token replaces the old one.
VAPID key
The web push key pair. Its public half goes in the client env as vapidKey.
service account
The Firebase Admin credential the server sends to FCM with. It never reaches the client.
APNs auth key
The .p8 key the server signs its APNs requests with. One key serves both APNs environments.
aps-environment
The iOS entitlement that says whether the app's tokens belong to APNs development or production.
What you prepare
Item
Web
Android
iOS
In the consoles
Firebase app
✓
✓
One Firebase project, with the web app and the Android app registered in it.
VAPID key
✓
A Web Push certificate key pair, generated in Firebase's Cloud Messaging settings.
Push capability
✓
Push Notifications turned on for the App ID in Apple Developer, so its profiles carry the entitlement.
APNs auth key (.p8)
✓
Created under Keys in Apple Developer, with its Key ID and your Team ID. It goes to your server.
In the app folder
env.client.*
✓
The public Firebase web config and vapidKey, under firebase.
google-services.json
✓
The Android Firebase config, named by native.android.googleServices in akan.config.ts.
permissions: ["push"]
✓
✓
Adds the native push plugin; it goes in native in akan.config.ts.
On the server
pushNoti.firebase
✓
✓
In env.server.*: the service account the server sends to FCM with.
pushNoti.apns
✓
In env.server.*: the APNs key, its Key ID, your Team ID and the app's bundle id.
✓NeededNot needed

One Native Plugin

A native app gets push from the runtime's 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:
iOS · APNs
Registers with APNs directly, with no Firebase SDK. A tap, and a message that arrives in front, come through the shell's notification router.
push.register() → { provider: "apns" }
Android · FCM
An FCM module pinned with the runtime. The build reads 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.

Web Push

Web push needs no native project at all. Register a Firebase web app and copy its public config into the client env.
  1. Create or open a web app in Firebase Console.
  2. Copy its public config into env.client.*, under firebase.
  3. Generate a Web Push certificate key pair and put its public key in vapidKey.
The client env file then looks like this:
apps/myapp/env/env.client.local.ts
  • Only public values go here. env.client.* ships to the browser; the server's service account belongs in env.server.*.
  • Four fields are required. Without apiKey, projectId, messagingSenderId or appId, register() returns undefined on the web.
  • One file per environment. env.client.ts picks env.client.<env>.ts by AKAN_PUBLIC_ENV, so fill in every environment you deploy.
  • The service worker is generated. With firebase in the client env, akan sync writes public/firebase-messaging-sw.js for each environment.

Android Push

Android push is a Firebase Android app whose package name matches native.appId exactly, plus one config file native.android.googleServices names.
  1. Open Firebase Console and select the project.
  2. Add an Android app.
  3. Enter the same package name as native.appId.
  4. Download google-services.json.
  5. Place it at apps/myapp/secrets/google-services.json.
Then name it in native.android in akan.config.ts:
apps/myapp/akan.config.ts
  • The build converts the file itself. It picks the client whose package name is the target's 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.
Android Notification Details
How a notification shows depends on whether the app is in front:
  • Foreground. The framework asks the plugin to show a push that arrives while the app is open (banner, list, sound, badge), so it can be tapped like any other.
  • Background. FCM draws the notification itself while the app is not in front. A tap opens the app and routes the push's url.
  • Channel, icon and color. Firebase posts into its default channel with the launcher icon, which the status bar draws as a gray square. 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.

iOS Push

iOS push needs no Firebase at all: the app registers with APNs, and the server sends to APNs itself. What you own is the capability on the App ID and the key the server signs with.
  1. In Apple Developer, open Identifiers, pick the App ID that matches native.appId, and turn on Push Notifications.
  2. Under Keys, create a key with Apple Push Notifications service enabled and download its .p8. Apple lets you download it once; note its Key ID and your Team ID.
  3. Put the three into pushNoti.apns on the server, as the last section shows.
  4. Add permissions: ["push"] to native.
Nothing else goes in the config:
apps/myapp/akan.config.ts
  • No GoogleService-Info.plist, no firebase-ios-sdk. The push plugin adds UIBackgroundModes and aps-environment to the app itself.
  • An iOS token is an APNs device token, with 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.
Keep the .p8 on the server. It signs pushes to every app of your team. It belongs in env.server.*, never in env.client.* or public/.

Which APNs Environment You Built

You never write 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.
Commandaps-environmentUsed for
akan start-iosdevelopmentSimulator and development-signed iPhone runs, through the APNs sandbox.
akan build-iosdevelopmentA simulator build.
akan release-iosproductionThe App Store profile: TestFlight and the App Store.
akan release-ios --adHocproductionAn ad hoc profile.
  • The server tries both. With 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.
  • One key serves both. An APNs auth key is not tied to an environment, so a development run and a TestFlight build need nothing different on the server.
  • A token no environment knows is dropped. A 410, or BadDeviceToken from the last environment tried, removes the token from its owner.

Client Registration

An app that mounts 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.
apps/myapp/page/(user)/_layout.tsx
The permission prompt belongs to a user action, because Chrome ignores a request with no gesture behind it and iOS refuses one. Notification.Util.PushSetting is that switch. A button of your own calls register() and hands the PushToken to the store:
apps/myapp/ui/EnablePush.tsx
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.
What usePushNotification() returns
Import it from @libs/util/webkit. Most screens need only register().
MethodDescription
register()
Asks for permission, then returns a PushToken, or undefined when refused or unsupported.
getToken()
Returns the token without asking. Registering shows no prompt, so check getPermission() first.
getPermission()
Reads the current permission state.
requestPermission()
Shows the permission prompt and returns the answer.
isSupported()
Whether push can work here: the native plugin in a shell, the Firebase web config in a browser.
onTokenChange(listener)
A native token rotates on its own; the listener gets each new PushToken. Returns the unsubscribe.
  • PushToken holds token, platform (web | android | ios), provider (apns | fcm) and deviceId, the installation id getPushDeviceId() keeps in the app's storage.
  • Built-in storage. With libs/shared, st.do.registerPushToken(pushToken) stores it on the signed-in user. The next section shows where.
  • Click routing. Send a 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.

Where Tokens Live

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.
Push token lifecycle
yesyes
Client: register()
Server: addNotiDeviceTokenOfSelf
user.notiInfo.deviceTokens
Server: push(userIds)
Settings accept it?
sendEach by provider
APNs
FCM
User device
Gone?
Server: drop the token
Client: register()
Server: addNotiDeviceTokenOfSelf
user.notiInfo.deviceTokens
Server: push(userIds)
Settings accept it?
yes
sendEach by provider
APNs
FCM
Gone?
yes
User device
Server: drop the token
deviceToken.constant.ts
The scalar holds what register() returned, plus when the server stored it:
libs/shared/lib/__scalar/deviceToken/deviceToken.constant.ts
  • One entry per installation. Registering again with the same 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.
  • Signing out drops this device. signoutUser sends the installation's deviceId, so a handed-down phone does not get the previous person's notifications.
  • Older tokens are skipped. A token stored as a plain string before this shape is not read; Notification.Zone.Initialize registers the device again on its next visit.
The endpoints
All three are User-guarded mutations and queries on the user signal, called through the notification store's registerPushToken, unregisterPushToken and loadPushState:
EndpointDescription
addNotiDeviceTokenOfSelf(deviceToken)
Stores this device's DeviceToken on the caller, replacing its earlier entry.
subNotiDeviceTokenOfSelf(token)
Removes one token from the caller: the push switch turned off.
hasNotiDeviceTokenOfSelf(token)
Whether this device is registered, which is what the switch shows.
  • Self supplies the owner, so a client cannot register a token under someone else's account.
  • Kept off MCP. An agent has no device, so the token endpoints are mcp: false.

Send And Retire Dead Tokens

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.
Server credentials
Put both senders' credentials under pushNoti in each server env file:
apps/myapp/env/env.server.local.ts
  • 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.
  • Neither is google-services.json. That file is the Android app's config; these sign every send.
A sender without credentials sends nothing and throws nothing. Its tokens are skipped with one warn line, such as pushNoti.apns is not configured, and counted as failures.
Sending from a service
Load, save, then notify, with the push fire-and-forget:
apps/myapp/lib/order/order.service.ts
  • The settings gate is one function. 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.
  • Dead tokens go at once. APNs 410 or BadDeviceToken, and FCM messaging/registration-token-not-registered, remove the token from its owner in the same call.
  • It never throws. A push is best effort: push() answers what it reached (targetUserIds, tokenNum, successCount, prunedTokens), and a failed send never fails the caller's own work.
  • A megaphone takes the same gate. An admin notification of type: "all" goes to every active user, 500 at a time, through accepts like any other push.
What push() takes
titlestringrequired
The notification title.
levelcnst.NotiLevelrequired
actionRequired, notice, essential, suggestion or advertise. The settings gate reads it.
contentstring
The notification body.
contentKeystring
A dictionary key for the body instead, resolved in the app's default locale.
urlstring
Where a tap lands: a path inside the app.
tagstring
A collapse key: a second push with the same tag replaces the first.
imageUrlstring
An image shown in the notification.
badgenumber
The app icon's badge count.
No topics. A topic cannot hold an APNs token, cannot ask a person's settings and never reports a dead token, so every send goes to stored tokens. Without libs/shared, call PushNotificationServer.sendEach(targets, message) from @libs/util/srvkit with { token, provider } targets, and stop storing the invalidTokens it returns.

On this page

Push Setup
One Native Plugin
Web Push
Android Push
iOS Push
Which APNs Environment You Built
Client Registration
Where Tokens Live
Send And Retire Dead Tokens
initClickBridge()
Routes the browser's notification clicks. The hook runs it on mount; a native shell needs nothing.