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.
    initClickBridge()
    Routes the browser's notification clicks. The hook runs it on mount; a native shell needs nothing.
    • 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