Reaktor Notifications
reaktor-notifications is the Reaktor module and cloud runtime for notification workflows: endpoint registration, permission state, device token lifecycle, durable intake, fanout, provider delivery, receipts, user interactions, typed graph routing, policy, telemetry, and Reaktor Desktop operations.
The central decision is simple:
Cloudflare owns notification operational state and the send path.
reaktorServer mints short-lived Google OAuth access tokens for FCM.
Workers call FCM HTTP v1.
Postgres/Supabase do not store notification endpoints, attempts, receipts, or token lifecycle state.
This replaces the older draft architecture that allowed Postgres as a notification store and placed provider delivery in JVM workers. That shape no longer matches the desired BestBuds/Reaktor platform. Notifications should be edge-native because endpoint registration, fanout, retry, token hygiene, and tap telemetry are operational state, not product business truth.
1. Architecture call
The product-facing API is still a typed workflow:
domain event
-> NotificationIntent
-> D1 transaction: record + idempotency + outbox
-> fanout queue
-> endpoint/policy resolution
-> delivery queue
-> Worker fetch() to FCM HTTP v1
-> attempts/receipts/endpoints/interactions in D1
-> client tap/action
-> NotificationInteractionEvent
-> GraphAction or OpenPath
The Reaktor differentiator is not "send push". It is that a notification is a graph-visible workflow. A tap is the same class of typed graph action as a button press inside the app, not a raw deep-link string hidden inside a provider payload.
2. Current state
The native mobile package slice was implemented and verified through BestBuds dev-screen flows on Android and iOS by 2 June 2026. The table below describes that milestone and remains accurate for the client packages.
| Surface | Current state | Design implication |
|---|---|---|
| Framework module | ../reaktor/reaktor-notification now contains the common NotificationsClient contract, runtime state model, platform capability model, category/action/content/route envelopes, Android actual client, iOS actual client, and debug harnesses. | Treat :reaktor-notification as the app-runtime package. Cloud stores, provider translators, graph facets, and Worker runtime still need to be added around it. |
| BestBuds notification Worker | Superseded by the shipped service. modules/app/src/jsMain/.../cloudflare/notification/ now holds NotificationRoutes.kt (254), NotificationRepository.kt (354), KotlinNotificationDispatchCoordinator.kt (101), NotificationWorkerHttp.kt (101), NotificationWorkerAuth.kt (36) and NotificationWorkerModels.kt (69), deployed as notification-service. | The thin-wrapper rule held: the TypeScript entry point is 26 lines. The runtime is still product-local under bestbuds/modules/app rather than in :reaktor-notification, so the framework module still owns only the client side. |
| Android app | BestBudsActivity installs AndroidNotificationsClient; the package owns permission state, channel/group registration, FCM token fetch/refresh, local scheduling/rendering, synthetic remote envelope handling, tap/action/dismiss/direct-reply bridges, badge/foreground controls, settings navigation, and dev harness state. | Android is ready for endpoint-registration integration and real cloud sends. Later platform depth should add conversation shortcuts, richer grouping, priority conversations, bubbles only if product-approved, and production cold-launch replay hardening. |
| iOS app | BestBudsApplication and BestBudsViewController wire Firebase Messaging, APNs token callbacks, and UNUserNotificationCenter callbacks into IosNotificationsClient; native notification calls are serialized on the main queue. The package owns authorization/settings, category/action/text-input registration, foreground presentation, local scheduling, delivered/pending cleanup, badge state, token bridge runtime, tap/action/dismiss callbacks, and dev harness state. | iOS is ready for endpoint-registration integration and real cloud sends. Later platform depth should add Notification Service Extension, Content Extension, Live Activities/APNs-direct, provisional-auth product strategy, and production cold-launch replay hardening. |
| BestBuds dev verification | tests/bestbudsAndroid/notifications-dev.yaml and tests/bestbudsDarwin/notifications-dev.yaml drive the dev screen end to end. They verify permission/authorization, category/channel registration, SDK probe, token refresh, cloud endpoint registration probe, cloud dry-run probe, local notification, synthetic remote envelope, simulated tap/action, and notification work scheduling. | The SDK can be tested without depending on live FCM/APNs delivery. Real provider sends become a separate cloud milestone instead of a blocker for native package correctness. |
| Cloudflare substrate | reaktor-cloudflare already has D1, Durable Object, service binding, R2, KV, Hono, and Worker context wrappers. | Notifications should use this substrate directly instead of inventing product-local Worker APIs. |
| Server substrate | reaktorServer is a Spring Boot/k3s service with Google/GCP dependencies already in the ecosystem. | Add a narrow internal token broker endpoint there. Do not move the notification store or delivery loop to Spring. |
| Docs and graph model | Unified system graph, cloud pane, analytics/telemetry, work, capability, server, and graph visitor docs have moved forward. | Notifications must project into the Reaktor Engine graph, emit typed signals, and keep runtime work boundaries clear. |
3. Notification domain overview
This section is not Reaktor-specific. It describes the notification domain that any serious mobile/web product has to handle before choosing an implementation architecture.
Mental model
A notification is a user-facing interruption or reminder that appears outside the normal app UI. It is usually caused by a product event, but it is delivered through an operating-system mediation layer controlled by the user and the device. A good notification system therefore has three separate contracts:
| Contract | Question | Examples |
|---|---|---|
| Product contract | Why should the user be interrupted? | Mention, direct message, friend request, event reminder, security alert, campaign reminder. |
| Delivery contract | How does the message reach the device? | FCM, APNs, Web Push, local scheduler, provider retry, token lifecycle. |
| Interaction contract | What happens after the user sees or taps it? | Open the right thread, mark read, direct reply, dismiss, snooze, mute category. |
Push delivery is not a database, a task queue, or a guaranteed real-time command channel. It is a best-effort path into an OS-controlled notification experience. The product system still needs source-of-truth state, idempotency, recovery, and a way to reconcile what was sent with what the user actually did.
How Android notifications work
Android notifications are posted by an app through system notification APIs, usually through NotificationCompat for backward-compatible behavior. Once posted, Android can show the notification as a status-bar icon, notification drawer entry, expanded notification, app icon badge, lock-screen item, wearable item, heads-up alert, conversation item, or bubble depending on API level, channel settings, importance, app state, and user preferences.
Core Android concepts:
| Concept | What it means for implementation |
|---|---|
| Permission | Android 13+ requires the POST_NOTIFICATIONS runtime permission for normal notification posting. The app should request it in context and handle denial as a valid state. |
| Channels | Android 8+ requires notifications to belong to a channel. Channels are user-visible categories with independent importance, sound, vibration, lock-screen, and Do Not Disturb behavior. Once users change channel settings, the app must respect them. |
| Posting | The app builds a notification with icon, title, body, channel, priority/importance, style, content intent, delete intent, actions, and optional grouping/conversation metadata. |
| FCM | Firebase Cloud Messaging gives the app a registration token and delivers remote messages. Notification messages may be displayed by the SDK in the background; data messages are processed by app code. |
| Foreground behavior | If the app is already open, the app should usually update the current UI or show an in-app affordance instead of producing a redundant system interruption. |
| Actions | Android notifications can expose quick actions and direct reply. The action target is an intent, so the app must map that intent back to a typed product action. |
| Conversations | Real-time chat should use MessagingStyle, people metadata, and long-lived shortcuts so Android can rank and display conversations properly. |
| User control | Users can block the whole app, individual channels, bubbles, lock-screen visibility, sounds, vibration, and conversation priority. The system can also group, rank, summarize, or suppress notifications. |
Important Android design consequence: channel design is product design. If all notifications share one broad channel, users can only mute everything or tolerate everything. Good products expose meaningful categories such as messages, social, events, campaigns, and system.
How iOS notifications work
iOS notifications are mediated by the UserNotifications framework and APNs. The app can schedule local notifications on the device or receive remote notifications from APNs. If Firebase Cloud Messaging is used on iOS, FCM still ultimately works through APNs and the iOS notification system.
Core iOS concepts:
| Concept | What it means for implementation |
|---|---|
| Authorization | Apps must request notification authorization before displaying alerts, sounds, or badges. The app must read current settings because users can change them later. |
| Local notifications | The app schedules local notification requests with content and triggers through UNUserNotificationCenter. This is used for reminders and device-local workflows. |
| Remote notifications | A provider server sends a payload to APNs for a device token. APNs handles delivery to the device. The app receives callbacks depending on payload, app state, authorization, and OS policy. |
| Categories | Categories define actionable notification types. Actions, including text input actions, are registered at launch and referenced by payload/category ID. |
| Foreground behavior | UNUserNotificationCenterDelegate decides how to handle a notification while the app is foregrounded: update UI, show banner/list/sound/badge, or suppress. |
| Service extension | A Notification Service Extension can modify eligible remote notifications before display, commonly for attachments or safe decryption/display. It is time-limited and only runs for configured alert payloads. |
| Content extension | A Notification Content Extension can provide custom notification UI when the product experience justifies it. |
| Badges | Badges are explicit app-visible counts and must be kept accurate. They should mean unread or pending notification work, not arbitrary product numbers. |
| User control | Users control whether notifications appear on the Lock Screen, Notification Center, banners, sounds, badges, Focus behavior, summaries, and interruption levels. |
Important iOS design consequence: authorization is not a one-time setup box. The product needs a thoughtful permission moment, an in-app settings path, foreground behavior, category registration, and privacy-safe payloads.
How notification services are designed
A production notification service is usually a workflow system with delivery providers at the edge, not just a "send push" function.
| Layer | Responsibility |
|---|---|
| Event intake | Accept typed product events or notification intents with auth, schema validation, idempotency, correlation IDs, and scheduling metadata. |
| Template and content | Resolve copy, localization, personalization, safe previews, image policy, fallback text, and payload size constraints. |
| Endpoint registry | Store installation IDs, platform, provider tokens, environment, token hashes, encrypted token values, token freshness, and invalidation state. |
| Preferences and policy | Apply category mutes, quiet hours, frequency caps, tenant policy, sensitive-content rules, and product-specific eligibility. |
| Fanout | Resolve target users/installations/segments, dedupe endpoints, split work into provider-specific delivery tasks, and maintain retry cursors. |
| Provider delivery | Translate generic notification intent into FCM/APNs/Web Push payloads, send through a trusted server path, classify provider responses, and invalidate bad endpoints. |
| Receipts and interactions | Record provider acceptance/failure, client received/opened/dismissed/action/direct-reply events, and route-dispatch outcomes. |
| Operations | Expose DLQs, replay, endpoint health, token churn, broker health, provider errors, policy simulation, and safe test-send tooling. |
| Analytics | Attribute notification exposure, open, action, conversion, suppression, opt-in, opt-out, and delivery latency without leaking private payload data. |
There are several non-obvious design rules:
- Provider acceptance is not user visibility. FCM/APNs can accept a message that the user never sees.
- Delivery latency and reliability depend on device power state, network, OS policy, user settings, app state, provider throttling, and payload type.
- Remote push should wake product attention, not carry the whole product state. The app should fetch authoritative state after tap when possible.
- Idempotency matters because retries, DLQ replay, multiple endpoints, and cross-device fanout can otherwise duplicate user-visible notifications.
- Notification copy is part of privacy architecture. Provider-visible title/body fields should not contain sensitive content unless the product explicitly accepts that risk.
User requirements
Users do not want "more notifications"; they want timely information, control, and trust.
| Requirement | Product behavior |
|---|---|
| Relevance | Notify only for events that matter to this user in this context. |
| Consent | Ask permission when the user understands the value, and degrade gracefully when denied. |
| Control | Provide clear categories, mute controls, quiet hours where appropriate, and an obvious settings path. |
| Privacy | Avoid sensitive previews by default for private content; honor lock-screen and preview settings. |
| Actionability | A tap must open the exact relevant object. Actions should complete useful work without forcing an app open. |
| Non-duplication | Do not repeatedly notify for the same unresolved event. Update, collapse, group, or suppress instead. |
| Recovery | If a notification is missed, the user should still find the underlying item in the app or inbox. |
| Accuracy | Badges and unread counts must clear when work is handled. Stale notifications erode trust quickly. |
| Accessibility | Copy, sounds, vibration, actions, and visual treatment should work across device settings and abilities. |
| Localization | Text must be short, localizable, and understandable without relying on app-specific jargon. |
Product manager requirements
Product managers own the notification taxonomy and success criteria. Engineering owns the runtime, but the runtime cannot compensate for vague product rules.
| Requirement | Questions PMs must answer |
|---|---|
| Notification taxonomy | What exact event types exist? Which category does each type belong to? Which are transactional, social, operational, or promotional? |
| User value | Why does this notification help the user now? What should happen if it is suppressed? |
| Priority and urgency | Is this high, normal, low, time-sensitive, or silent? Does it justify sound, vibration, badge, or only inbox? |
| Targeting | Which users/installations receive it? What are the eligibility rules and exclusions? |
| Frequency | How many times can this type notify per hour/day/week? What should collapse or update instead of creating a new notification? |
| Copy and localization | What are title/body/action templates, fallbacks, length limits, pluralization rules, and sensitive-preview alternatives? |
| Routing | What exact screen or graph action should tap/action/direct reply invoke? What should happen if the target object no longer exists? |
| Measurement | What are expected opt-in, delivery, open, action, conversion, mute, and unsubscribe metrics? |
| Experimentation | Is A/B testing allowed? Which copy, timing, or priority variants are safe to test? |
| Compliance | Is this transactional, marketing, safety, child-related, private, regulated, or user-generated content? |
| Operations | Who can send tests, pause a notification type, roll back a campaign, replay DLQ, or invalidate endpoints? |
| Launch criteria | What integration tests, device tests, copy review, privacy review, and analytics dashboards must exist before release? |
Design implications for Reaktor
| Domain reality | Reaktor design implication |
|---|---|
| Android and iOS have different notification primitives. | Model platform capabilities explicitly instead of hiding them behind one weak common API. |
| User settings can block, downgrade, or reroute notifications. | Permission/settings state is part of the graph and must be visible to product and ops tools. |
| Product taxonomy determines useful controls. | Notification category declarations must be graph nodes, not string constants hidden in send code. |
| Provider delivery is only one workflow leg. | Intake, policy, fanout, delivery, receipts, interactions, and operations need separate graph-visible nodes. |
| PM requirements affect runtime correctness. | Templates, priorities, routes, frequency caps, and privacy rules must be declared and testable. |
| Users judge trust by stale badges and duplicate alerts. | Idempotency, collapse/grouping, badge updates, interaction receipts, and inbox recovery are first-class requirements. |
4. Non-negotiables
Cloudflare owns notification state
Notification endpoints, device FCM tokens, preferences, idempotency keys, outbox rows, notification records, attempts, receipts, interactions, token-cache metadata, policy snapshots, and operator audit rows live in D1. They do not live in Supabase Postgres or the Spring server database.
Supabase/Postgres can still be product truth for users, chats, campaigns, events, profiles, and analytics warehouses. It is not the notification control-plane store.
Workers send to FCM
The delivery Worker constructs the FCM HTTP v1 request and calls:
POST https://fcm.googleapis.com/v1/projects/{firebaseProjectId}/messages:send
Authorization: Bearer {shortLivedAccessToken}
Content-Type: application/json
The Worker classifies FCM responses, writes attempts/receipts, invalidates dead endpoints, and routes retries/DLQ through Cloudflare Queues.
reaktorServer is a token broker only
reaktorServer is deployed on GCP Compute Engine/k3s and can use Google Application Default Credentials, Workload Identity, or a least-privilege service account to mint short-lived OAuth 2.0 access tokens scoped for Firebase Cloud Messaging.
It exposes an internal broker endpoint such as:
POST /internal/fcm/access-token
Content-Type: application/json
{
"tenantId": "bestbuds",
"appId": "bestbuds",
"firebaseProjectId": "bestbuds-prod",
"audience": "notification-service"
}
Response:
{
"projectId": "bestbuds-prod",
"accessToken": "ya29...",
"expiresAtEpochMs": 1780380000000,
"scope": "https://www.googleapis.com/auth/firebase.messaging",
"issuer": "reaktorServer"
}
Cloudflare stores and rotates notification-related operational state. The broker never receives device tokens, never stores notification rows, and never sends pushes. It mints access tokens and emits audit/telemetry.
No long-lived Google key in Workers
Workers should not carry a Firebase service-account JSON file. The Worker authenticates to the broker with a Cloudflare secret, service binding, Cloudflare Access, mTLS tunnel, or equivalent internal channel. The broker authenticates to Google. The Worker caches only short-lived OAuth access tokens with expiry/skew.
Typed routes, not strings
Provider payloads can carry compact JSON, but the Reaktor API surface is:
sealed class NotificationRoute {
data class OpenPath(val path: String) : NotificationRoute()
data class GraphAction(val type: String, val payloadJson: String) : NotificationRoute()
data object None : NotificationRoute()
}
A client tap becomes NotificationInteractionEvent. The graph dispatcher validates and resolves it. Deep-link parsing is an internal implementation detail, not the product contract.
Platform depth is explicit
Android channels, channel groups, conversation shortcuts, direct reply, grouping, foreground presentation, and FirebaseMessagingService callbacks are typed Android surfaces. iOS categories, actions, provisional auth, Focus/interruption level, foreground presentation, APNs token bridging, service extensions, and optional Live Activities are typed iOS surfaces. The common API should not flatten these into a lowest-common-denominator push helper.
Implementation order follows that principle. The native Android and iOS packages are the first concrete build milestone after design alignment. Cloudflare delivery, D1 storage, and provider fanout should depend on the package contracts, not the other way around, so the graph can describe the real OS capabilities that BestBuds and future Reaktor apps actually use.
Reaktor Desktop can operate it
The unified graph should show notification resources, bindings, D1 schemas, Durable Object namespaces, queues, token broker health, endpoint hygiene, DLQ depth, provider errors, policy coverage, categories, routes, and graph actions. Humans and agents should mutate configuration through typed commands with auth and audit.
5. System decomposition
The runtime has five graph-visible subgraphs.
| Subgraph | Runs on | Owns | Primary state |
|---|---|---|---|
| ClientGraph | Android, iOS, later web | permission, token acquisition, local scheduling, foreground handling, tap/action events | device runtime + local pending interaction queue |
| IntakeGraph | Cloudflare Worker + optional DO | validate intent, auth, idempotency, record/outbox transaction | D1 |
| FanoutGraph | Cloudflare Queue consumer + DO coordination | resolve target, load endpoints, apply policy, enqueue endpoint tasks | D1 + Queues |
| DeliveryGraph | Cloudflare Queue consumer | get/cached FCM access token, build FCM payload, send HTTP v1, classify provider response | D1 + token cache |
| ReceiptInteractionGraph | Cloudflare Worker/Queue consumer + clients | write attempts/receipts/interactions, invalidate endpoints, replay/DLQ | D1 |
End-to-end flow
ChatRoom Durable Object / domain ServiceNode
|
| NotificationIntent(chat.mention, target=User, route=GraphAction)
v
NotificationIntakeWorker
| validate AuthContext + schema + idempotency
| write notification_records + notification_idempotency + notification_outbox
v
notifications.fanout queue
|
v
NotificationFanoutWorker
| resolve target -> active notification_endpoints
| apply preferences, quiet hours, frequency caps
| enqueue one delivery task per endpoint
v
notifications.delivery queue
|
v
FcmDeliveryWorker
| load endpoint + record
| get short-lived OAuth token from cache or reaktorServer broker
| fetch FCM HTTP v1
| write notification_attempts + notification_receipts
| invalidate endpoint on terminal token errors
v
Device
| platform callback / system tray / local renderer
| tap or action
v
NotificationInteractionEvent
|
v
GraphNotificationRouteDispatcher -> GraphAction or OpenPath
6. Data authority and D1 schema
D1 is the notification operational database. It is the source of truth for state that the notification runtime needs to be correct and observable.
Initial cloud tables
| Table | Purpose | Retention |
|---|---|---|
notification_endpoints | Installation/provider token registry, status, platform, environment, last seen, coarse device metadata. | Until unregister, terminal invalidation TTL, or user deletion. |
notification_preferences | Per-user/per-installation category settings, quiet hours, frequency cap overrides. | Until replaced or account deletion. |
notification_records | One row per accepted NotificationIntent. | 30-180 days by tenant policy. |
notification_idempotency | Deduplicate logical sends by idempotency key. | Greater than max retry/DLQ replay window. |
notification_outbox | Durable transaction-to-queue handoff. | Until published or dead-lettered. |
notification_attempts | One provider attempt per endpoint. | 14-30 days by default. |
notification_receipts | Provider acceptance/failure plus client open/dismiss/action receipts where available. | Analytics/audit window. |
notification_interactions | Tap, action, direct reply, dismiss, and route dispatch facts. | Analytics/audit window. |
notification_token_cache | Optional short-lived FCM OAuth access-token cache metadata and encrypted value if we choose D1 cache over DO memory. | Minutes, never long-term. |
notification_operator_audit | Workbench/agent mutations such as replay, invalidate, policy update. | Security/audit policy. |
Initial D1 schema sketch
CREATE TABLE notification_endpoints (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
user_id TEXT,
installation_id TEXT NOT NULL,
platform TEXT NOT NULL, -- android | ios | web
provider TEXT NOT NULL, -- fcm | apns_direct | web_push
provider_project_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_ciphertext TEXT NOT NULL,
environment TEXT NOT NULL, -- dev | stage | prod
locale TEXT,
timezone TEXT,
device_info_json TEXT NOT NULL,
status TEXT NOT NULL, -- active | stale | invalid | unregistered
invalid_reason TEXT,
last_seen_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(tenant_id, app_id, installation_id, provider, provider_project_id)
);
CREATE INDEX idx_notification_endpoints_user
ON notification_endpoints(tenant_id, app_id, user_id, status);
CREATE INDEX idx_notification_endpoints_installation
ON notification_endpoints(tenant_id, app_id, installation_id);
CREATE TABLE notification_preferences (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
scope_type TEXT NOT NULL, -- user | installation | tenant
scope_id TEXT NOT NULL,
category TEXT NOT NULL,
enabled INTEGER NOT NULL,
quiet_hours_json TEXT,
frequency_cap_json TEXT,
updated_at INTEGER NOT NULL,
UNIQUE(tenant_id, app_id, scope_type, scope_id, category)
);
CREATE TABLE notification_records (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
type TEXT NOT NULL,
category TEXT,
priority TEXT NOT NULL,
target_type TEXT NOT NULL,
target_value TEXT NOT NULL,
content_json TEXT NOT NULL,
route_json TEXT NOT NULL,
status TEXT NOT NULL,
correlation_id TEXT,
causation_id TEXT,
created_by_principal TEXT NOT NULL,
created_at INTEGER NOT NULL,
scheduled_at INTEGER,
fanout_started_at INTEGER,
completed_at INTEGER
);
CREATE INDEX idx_notification_records_tenant_created
ON notification_records(tenant_id, app_id, created_at DESC);
CREATE INDEX idx_notification_records_correlation
ON notification_records(correlation_id);
CREATE TABLE notification_idempotency (
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
notification_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
PRIMARY KEY(tenant_id, app_id, idempotency_key)
);
CREATE TABLE notification_outbox (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
kind TEXT NOT NULL, -- fanout | receipt | interaction
payload_json TEXT NOT NULL,
status TEXT NOT NULL, -- pending | published | failed
attempts INTEGER NOT NULL DEFAULT 0,
available_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
published_at INTEGER,
last_error TEXT
);
CREATE INDEX idx_notification_outbox_ready
ON notification_outbox(status, available_at);
CREATE TABLE notification_attempts (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
notification_id TEXT NOT NULL,
endpoint_id TEXT NOT NULL,
provider TEXT NOT NULL,
attempt_number INTEGER NOT NULL,
status TEXT NOT NULL, -- queued | accepted | retryable | terminal
provider_message_id TEXT,
provider_error_code TEXT,
provider_error_json TEXT,
latency_ms INTEGER,
created_at INTEGER NOT NULL,
completed_at INTEGER
);
CREATE INDEX idx_notification_attempts_notification
ON notification_attempts(notification_id, endpoint_id);
CREATE TABLE notification_receipts (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
notification_id TEXT NOT NULL,
endpoint_id TEXT,
provider TEXT,
receipt_type TEXT NOT NULL, -- accepted | failed | opened | dismissed | action
receipt_json TEXT NOT NULL,
occurred_at INTEGER NOT NULL,
recorded_at INTEGER NOT NULL
);
CREATE TABLE notification_interactions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
notification_id TEXT NOT NULL,
endpoint_id TEXT,
interaction_type TEXT NOT NULL, -- tap | action | reply | dismiss
action_id TEXT,
route_json TEXT,
dispatch_status TEXT NOT NULL, -- pending | dispatched | rejected | expired
occurred_at INTEGER NOT NULL,
recorded_at INTEGER NOT NULL
);
CREATE TABLE notification_token_cache (
cache_key TEXT PRIMARY KEY, -- tenant/app/firebaseProjectId/scope
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
firebase_project_id TEXT NOT NULL,
access_token_ciphertext TEXT,
scope TEXT NOT NULL,
expires_at INTEGER NOT NULL,
refreshed_at INTEGER NOT NULL,
broker_instance TEXT,
status TEXT NOT NULL
);
CREATE TABLE notification_operator_audit (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
app_id TEXT NOT NULL,
actor_principal TEXT NOT NULL,
operation TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_id TEXT NOT NULL,
reason TEXT,
request_json TEXT NOT NULL,
result_json TEXT,
created_at INTEGER NOT NULL
);
7. Durable Objects
Durable Objects are coordination atoms, not a replacement for the D1 operational store. Model them around keys that need ordered mutation.
| Durable Object | Key | Responsibility |
|---|---|---|
NotificationTenantDO | {tenantId}:{appId} | Serialize tenant-level maintenance: schema readiness checks, outbox drain cursor, policy cache invalidation, DLQ replay locks. |
NotificationInstallationDO | {tenantId}:{appId}:{installationId} | Serialize endpoint token replacement, unregister/register races, and last-seen updates for one installation. |
NotificationRecordDO | {tenantId}:{appId}:{notificationId} | Optional coordination for fanout progress, retry cursors, and replay state on very large sends. |
FcmTokenBrokerCacheDO | {tenantId}:{appId}:{firebaseProjectId} | Cache short-lived OAuth access tokens in memory/DO storage, refresh before expiry, avoid broker stampedes. |
ProviderCircuitDO | {tenantId}:{appId}:{provider}:{projectId} | Shared provider breaker state so retries do not pile onto a failing FCM project. |
Rules:
- One DO per coordination atom; never one global notification DO.
- Persist critical cursor/state before updating in-memory cache.
- Use DO alarms for refresh/drain/probe loops. Remember one alarm per DO.
- Use D1 for canonical queryable records. DO storage can hold local coordinator metadata and hot caches.
- Expose DOs as graph resource nodes in Reaktor Engine, but not as app-runtime graph dependencies unless an app actually talks to the actor/interface.
8. Queues and retry model
Use Cloudflare Queues for async hops. The queue surface should be visible as graph nodes with retry policy, DLQ, bindings, and consumers.
| Queue | Producer | Consumer | Payload |
|---|---|---|---|
notifications.fanout | Intake Worker / outbox drain | Fanout Worker | FanoutRequest(notificationId) |
notifications.delivery | Fanout Worker | FCM Delivery Worker | DeliveryTask(notificationId, endpointId, attempt) |
notifications.receipts | Delivery Worker / client receipt API | Receipt Worker | ProviderReceipt or ClientReceipt |
notifications.interactions | Client interaction API | Interaction Worker | NotificationInteractionEvent |
notifications.dlq | Queue retry exhaustion | Replay Worker / Workbench | original envelope plus failure state |
Each envelope carries:
data class NotificationEnvelope<T>(
val id: String,
val kind: String,
val tenantId: String,
val appId: String,
val payload: T,
val correlationId: String,
val causationId: String?,
val attempt: Int,
val traceparent: String?,
val createdAtEpochMs: Long,
)
Retry classification:
| Result | Action |
|---|---|
| FCM accepted | Write attempt/receipt, ack queue message. |
UNREGISTERED, invalid registration token, sender mismatch | Mark endpoint invalid, write terminal receipt, ack. |
| Quota/rate exceeded | Retry with delay and jitter; breaker may open at project/provider level. |
| FCM unavailable/internal/network timeout | Retry with exponential backoff and bounded attempts. |
| Broker auth failure | Open circuit, write provider health event, retry only if token refresh is likely recoverable. |
| Payload/schema bug | Terminal failure, DLQ with validation finding. |
DLQ replay is a typed workbench command, not a manual queue poke.
9. FCM access-token broker
Firebase Cloud Messaging HTTP v1 requests require an OAuth 2.0 bearer token. Google documents ADC for services running on Compute Engine/GKE and short-lived access tokens generated from service account credentials or Workload Identity. That maps cleanly to the BestBuds shape: reaktorServer runs in GCP and can mint tokens without putting Google private keys in Cloudflare Workers.
Broker responsibilities
reaktorServer:
- Authenticates the calling Worker/service.
- Authorizes
(tenantId, appId, firebaseProjectId, scope)against a Kotlin platform model or config table. - Mints a short-lived access token for
https://www.googleapis.com/auth/firebase.messaging. - Returns expiry, issuer, and project metadata.
- Emits audit and OTel spans.
- Does not receive endpoint tokens or notification payloads.
- Does not call FCM.
- Does not own notification retry/fanout/receipt state.
Cloudflare notification runtime:
- Stores tenant/app/firebase project mapping in D1 or generated config.
- Stores device endpoint FCM tokens in D1.
- Caches short-lived OAuth access tokens in DO memory, DO storage, or
notification_token_cachewith expiry and skew. - Refreshes before expiry and prevents stampedes through
FcmTokenBrokerCacheDO. - Calls FCM HTTP v1 directly.
- Writes token-broker events to notification audit/telemetry.
Internal API contract
@Serializable
data class FcmAccessTokenRequest(
val tenantId: String,
val appId: String,
val firebaseProjectId: String,
val scope: String = "https://www.googleapis.com/auth/firebase.messaging",
val audience: String = "notification-service",
val correlationId: String,
)
@Serializable
data class FcmAccessTokenResponse(
val projectId: String,
val accessToken: String,
val expiresAtEpochMs: Long,
val scope: String,
val issuer: String,
)
Suggested Worker-side token flow:
delivery task starts
-> key = tenant/app/firebaseProjectId/scope
-> FcmTokenBrokerCacheDO.getValidToken(key)
-> if token exists and expiresAt > now + skew: return
-> otherwise call reaktorServer /internal/fcm/access-token
-> store short-lived token with expiresAt
-> Worker calls FCM HTTP v1
Security rules:
- The broker endpoint is private. Prefer service binding/tunnel/private ingress over public HTTP. If public HTTP is unavoidable, protect it with signed requests, strict audience, timestamp, nonce, and secret rotation.
- Cloudflare secrets hold only broker auth material and optional token-encryption keys. They do not hold Firebase service-account JSON.
- Access tokens are short-lived and cached with an expiry skew. Never cache beyond provider expiry.
- D1 token-cache rows are optional; DO memory is enough for the first cloud delivery slice. If D1 is used, encrypt token values and include audit metadata.
- Every token mint event carries
tenantId,appId,firebaseProjectId,scope,principal, andcorrelationId.
10. Provider strategy
First provider: FCM only
Use FCM HTTP v1 for Android and iOS in the first real implementation:
- Android devices register FCM tokens directly.
- iOS registers for APNs, bridges the APNs token into Firebase Messaging, and uploads the FCM registration token.
- Provider payloads use a compact Reaktor data envelope plus notification fields where the OS should display in the background.
- Client code records opens/actions/dismissals because provider "accepted" is not "seen".
Later providers
| Provider | When | Why |
|---|---|---|
| APNs direct | Later provider slice | Live Activities, critical alerts, direct APNs headers, iOS features not exposed well through FCM. |
| Web Push | Later provider slice | Browser/PWA notifications through service workers and VAPID. |
| Expo bridge | Compatibility only | Useful for tenants already on Expo, not the core Reaktor path. |
| In-app inbox | Later engagement slice | Product surface backed by D1 records/interactions, not a push provider. |
FCM message policy
Use a Reaktor envelope in data for graph routing and receipts:
{
"message": {
"token": "device_fcm_registration_token",
"notification": {
"title": "Alice mentioned you",
"body": "Open BestBuds to reply"
},
"data": {
"reaktor_notification_id": "n_123",
"reaktor_endpoint_id": "e_456",
"reaktor_route_type": "graph_action",
"reaktor_route": "{\"type\":\"bestbuds.chat.open\",\"payloadJson\":\"{\\\"chatId\\\":\\\"c_1\\\"}\"}",
"reaktor_correlation_id": "msg_789"
},
"android": {
"priority": "HIGH",
"notification": {
"channel_id": "messages",
"tag": "chat:c_1"
}
},
"apns": {
"headers": {
"apns-priority": "10"
},
"payload": {
"aps": {
"thread-id": "chat:c_1",
"category": "messages"
}
}
}
}
}
Sensitive content policy:
- Provider payloads are not end-to-end encrypted by default. Sensitive chat content must not be put in provider-visible fields.
- For MLS/E2EE conversations, default body is a safe preview such as "New mention in BestBuds". The app fetches/decrypts after tap.
- Rich/decrypted display can be added through an iOS Notification Service Extension later, but it must not give the server plaintext.
11. Common model and contracts
The common API stays provider-neutral and graph-native.
var Feature.Notifications by CreateSlot<NotificationsClient>()
@Serializable
data class NotificationIntent(
val tenantId: String,
val appId: String,
val type: String,
val target: NotificationTarget,
val content: NotificationContent,
val route: NotificationRoute,
val priority: NotificationPriority = NotificationPriority.Normal,
val category: String? = null,
val scheduledAt: Instant? = null,
val idempotencyKey: String? = null,
val correlationId: String? = null,
)
@Serializable
sealed class NotificationTarget {
data class User(val userId: String) : NotificationTarget()
data class Users(val userIds: List<String>) : NotificationTarget()
data class Installation(val installationId: String) : NotificationTarget()
data class Installations(val installationIds: List<String>) : NotificationTarget()
data class Segment(val segmentId: String) : NotificationTarget() // later
data class Topic(val topic: String) : NotificationTarget() // later
}
@Serializable
data class NotificationContent(
val title: String,
val subtitle: String? = null,
val body: String,
val imageUrl: String? = null,
val categoryId: String? = null,
val threadId: String? = null,
val person: NotificationPerson? = null,
val badge: Int? = null,
val sound: NotificationSound = NotificationSound.Default,
val interruptionLevel: InterruptionLevel = InterruptionLevel.Active,
val data: Map<String, String> = emptyMap(),
)
Client contract
interface NotificationsClient {
val devHarness: NotificationDevHarness?
suspend fun getPermissions(): NotificationPermissionStatus
suspend fun requestPermissions(
options: NotificationPermissionOptions = NotificationPermissionOptions(),
): NotificationPermissionStatus
suspend fun getPlatformCapabilities(): NotificationPlatformCapabilities
suspend fun registerCategories(categories: List<NotificationCategorySpec>)
suspend fun getDeviceToken(): DevicePushToken?
suspend fun registerRemoteEndpoint(userId: String? = null): RegisterEndpointResult
suspend fun unregisterRemoteEndpoint()
suspend fun updatePreferences(command: UpdateNotificationPreferences)
suspend fun scheduleLocal(request: LocalNotificationRequest): LocalNotificationId
suspend fun getState(): NotificationRuntimeState
suspend fun setForegroundPresentation(policy: ForegroundPresentationPolicy)
suspend fun setBadge(count: Int?)
suspend fun clearDelivered(ids: List<String> = emptyList())
suspend fun cancelScheduled(ids: List<String> = emptyList())
suspend fun openSettings(target: NotificationSettingsTarget = NotificationSettingsTarget.App)
fun addReceivedListener(listener: suspend (NotificationEnvelope) -> Unit): ListenerHandle
fun addResponseListener(listener: suspend (NotificationResponseEvent) -> Unit): ListenerHandle
}
The interface intentionally includes operations that are not provider-send concerns: open OS settings, change foreground presentation, clear delivered notifications, cancel scheduled notifications, and inspect runtime state. Those are what make the package useful to Reaktor Desktop as a control surface instead of just a push-token helper.
Platform capability model
The common API exposes OS depth through typed capabilities. A platform implementation should return supported, unsupported, and unavailable states explicitly instead of hiding native differences behind a weak common abstraction.
@Serializable
data class NotificationPlatformCapabilities(
val platform: NotificationPlatform,
val authorizationModes: Set<NotificationAuthorizationMode>,
val presentationFeatures: Set<NotificationPresentationFeature>,
val actionFeatures: Set<NotificationActionFeature>,
val schedulingFeatures: Set<NotificationSchedulingFeature>,
val extensionFeatures: Set<NotificationExtensionFeature>,
)
@Serializable
data class NotificationCategorySpec(
val id: String,
val displayName: String,
val actions: List<NotificationActionSpec> = emptyList(),
val android: AndroidCategoryOptions? = null,
val ios: IosCategoryOptions? = null,
)
@Serializable
data class NotificationActionSpec(
val id: String,
val title: String,
val kind: NotificationActionKind,
val route: NotificationRoute,
val options: Set<NotificationActionOption> = emptySet(),
)
This is how Reaktor keeps full Android/iOS access without pretending both OSs are identical. The graph can show exactly which app/environment supports channels, categories, text input actions, conversation notifications, foreground presentation, service extensions, or future Live Activity/APNs-direct features.
Cloud service contract
interface NotificationsService {
suspend fun registerEndpoint(command: RegisterEndpointCommand): RegisterEndpointResult
suspend fun unregisterEndpoint(command: UnregisterEndpointCommand)
suspend fun updatePreferences(command: UpdateNotificationPreferences)
suspend fun createNotification(intent: NotificationIntent): NotificationAccepted
suspend fun getNotification(id: String): NotificationRecord?
suspend fun recordInteraction(event: NotificationInteractionEvent)
}
Implementation boundaries:
commonMainowns domain types, service contracts, policy, routes, graph facets, capability descriptors, and telemetry names.androidMainowns Android permission, token, channel, renderer, direct-reply, andFirebaseMessagingServiceintegration.iosMainowns APNs registration, Firebase Messaging delegate, notification center delegate, categories, service-extension hooks, and foreground/tap handling.jsMainowns Cloudflare D1 stores, Workers, DO adapters, Queue consumers, FCM HTTP v1 provider, token broker client, and workbench-safe operations.jvmMainowns only client/server helpers that truly need JVM. For this architecture, provider delivery does not move to JVM.
12. Android integration
The Android package is a native OS notification abstraction, not only an FCM helper. It exposes Android surfaces through typed capabilities, with gates for API level, permission state, channel settings, OEM behavior, app role, and user settings.
Implemented mobile SDK slice
The current implementation lives in ../reaktor/reaktor-notification/src/androidMain/.../AndroidNotifications.kt and is installed in BestBuds through BestBudsActivity.
Android implementation pieces:
| Piece | Current responsibility |
|---|---|
AndroidNotificationsClient | Implements NotificationsClient, capability reporting, local endpoint probe, local scheduling/rendering, runtime state, badge state, foreground policy, delivered/scheduled cleanup, settings navigation, and listeners. |
AndroidNotificationPermissionController | Handles Android 13+ POST_NOTIFICATIONS, app-wide notification enabled state, channel blocked state, and request/open-settings flow. |
AndroidNotificationChannelRegistry | Creates channel groups and channels from NotificationCategorySpec, including importance, sound, badge, vibration, DND, lock-screen visibility, and bubble options. |
AndroidNotificationRenderer | Renders local and synthetic remote envelopes through one path, including title/body/subtitle, category/channel, grouping, progress, image URI, actions, direct reply inputs, dismiss intents, and route metadata. |
AndroidNotificationScheduler | Schedules delayed/calendar local notifications through AlarmManager and cancels scheduled notifications. Immediate local notifications render directly. |
ReaktorNotificationAlarmReceiver | Converts scheduled alarms back into package-local render calls. |
ReaktorNotificationActionReceiver | Converts tap, action, dismiss, and direct-reply intents into NotificationResponseEvent. |
ReaktorFirebaseMessagingService | Routes onMessageReceived, onNewToken, and onDeletedMessages into the installed package runtime. |
AndroidNotificationsRuntime | Holds the installed Android client so manifest receivers/services can delegate without product-local notification logic. |
AndroidNotificationDevHarness | Debug-only facade used by BestBuds dev tools and Maestro to refresh tokens, inspect state, inject remote envelopes, send local notifications, simulate taps/actions/direct reply, and clear state. |
Implemented behavior:
- Requests
POST_NOTIFICATIONSin context and treats denial as a valid runtime state. - Reports app-wide notification state, channel blocked state, OS capabilities, category/channel registration, pending local IDs, delivered IDs, badge, preferences, and foreground presentation state.
- Registers stable BestBuds categories/channels such as messages, social, events, campaigns, and system through the common category catalog.
- Fetches FCM tokens through Firebase Messaging and records token refreshes from
ReaktorFirebaseMessagingService.onNewToken. - Parses Reaktor envelopes from FCM data maps and synthetic dev harness payloads.
- Renders local and remote-like notifications through the same renderer path.
- Emits tap, action, dismiss, and direct reply events through the package listener model.
- Opens app or channel notification settings through typed
NotificationSettingsTarget. - Provides a dev harness that works before live Cloudflare, FCM, APNs, or token-broker delivery exists.
Android verification:
- The Reaktor Android notification package compiles as part of the Android debug build.
- The BestBuds Android debug app installs the package and routes product lifecycle hooks through it.
tests/bestbudsAndroid/notifications-dev.yamlpasses on the attached Android device. The flow verifies permission state, channel registration, SDK probe, token refresh, endpoint registration probe, cloud dry-run probe, local notification, synthetic remote envelope, simulated tap, and notification work scheduling.- Future Android test expansion should stay integration/e2e focused: instrumented device tests for notification channels, renderer output, receiver dispatch, and FCM service delegation; Maestro flows for product-visible behavior.
Remaining Android depth:
- Direct reply wired to product chat send once route auth and chat send contracts are stable.
- Conversation shortcuts and priority conversations for real chats.
- Rich images with privacy policy checks.
- Production cold-launch pending-interaction replay.
- Full grouping and badge policy.
- Bubbles only if the product explicitly opts in.
- Foreground-service notification affordances where relevant.
- OEM/device-state diagnostics surfaced into Reaktor Desktop.
13. iOS integration
The iOS package mirrors the same principle: expose UserNotifications, APNs, and Firebase Messaging through typed Reaktor contracts instead of flattening iOS into "send push". OS-specific features are represented as capabilities, category options, runtime state, or extension hooks.
Implemented mobile SDK slice
The current implementation lives in ../reaktor/reaktor-notification/src/iosMain/.../IosNotifications.kt and is wired from the BestBuds iOS app lifecycle. Native notification calls are serialized on the main queue because UNUserNotificationCenter, authorization prompts, remote registration, and settings callbacks are UIKit/UserNotifications-facing APIs.
iOS implementation pieces:
| Piece | Current responsibility |
|---|---|
IosNotificationsClient | Implements NotificationsClient, capability reporting, authorization/settings, category registration, local endpoint probe, local scheduling, runtime state, badge state, foreground policy, delivered/scheduled cleanup, settings navigation, and listeners. |
IosNotificationsRuntime | Holds the installed iOS client and receives app lifecycle callbacks for APNs token, FCM token, foreground notification, response handling, and remote-registration start. |
IosNotificationDevHarness | Debug-only facade used by BestBuds dev tools and Maestro to request authorization, refresh tokens, inspect state, inject remote envelopes, send local notifications, simulate taps/actions/text input, and clear state. |
| Planned service/content extension bridges | Explicit future extension points for rich attachments, safe decryption/display, custom notification UI, and Live Activities/APNs-direct behavior. |
Implemented behavior:
- Requests standard authorization for alert, badge, and sound in context, with provisional and critical-alert options represented in the common request type.
- Reads current notification settings through
UNUserNotificationCenter. - Registers categories and actions from
NotificationCategorySpec, including text input actions and custom dismiss behavior. - Bridges APNs token observation and Firebase Messaging FCM token observation through
IosNotificationsRuntime. - Schedules immediate, delayed, and calendar local notifications with content, sound, thread IDs, target content IDs, launch image names, relevance, attachments, and route metadata.
- Applies typed foreground presentation policy for banner/list/sound/badge handling.
- Converts foreground receive, tap, action, dismiss, and text input callbacks into package response events.
- Supports badge state, delivered cleanup, scheduled cancellation, and open-app-settings behavior.
- Provides a dev harness that works before live Cloudflare, FCM, APNs, or token-broker delivery exists.
iOS verification:
- The Reaktor iOS notification package compiles for simulator and device targets.
- The BestBuds iOS debug app installs the package and routes product lifecycle hooks through it.
tests/bestbudsDarwin/notifications-dev.yamlpasses on the attached iPhone. The flow verifies authorization/settings state, category registration, SDK probe, token refresh, endpoint registration probe, cloud dry-run probe, local notification, synthetic remote envelope, simulated tap/action, and notification work scheduling.- Future iOS test expansion should stay integration/e2e focused: simulator/device tests for authorization mapping, category registration, local scheduling, foreground delegate behavior, response callbacks, and Maestro flows for product-visible behavior.
Remaining iOS depth:
- Provisional authorization strategy and in-app settings surfacing.
- Text input actions/direct reply wired to product chat send once routing is stable.
- Notification Service Extension for rich media and E2EE-safe display.
- Notification Content Extension for custom display only where product UX needs it.
- APNs direct for Live Activities, time-sensitive/critical-alert-class features, and headers that should not be forced through FCM.
14. Policy engine
Policy runs during fanout, before delivery. It is a pure function of intent, endpoint, preference state, and time.
interface DeliveryPolicy {
fun permits(
intent: NotificationIntent,
endpoint: NotificationEndpoint,
preferences: NotificationPreferences,
now: Instant,
): PolicyDecision
}
sealed class PolicyDecision {
data object Send : PolicyDecision()
data class Suppress(val reason: String) : PolicyDecision()
data class DelayUntil(val at: Instant, val reason: String) : PolicyDecision()
}
Default policy fragments:
| Policy | Behavior |
|---|---|
| Category mute | Suppress disabled categories per user/installation. |
| Quiet hours | Delay or suppress low/normal priority sends in endpoint timezone. |
| Frequency cap | Suppress or delay categories over rolling windows. |
| Endpoint health | Suppress stale, invalid, environment-mismatched, or wrong-provider endpoints. |
| Sensitive payload | Reject payloads that place forbidden fields into provider-visible notification body/data. |
| Tenant override | Allow tenant-specific policy functions through DI/config, but require graph visibility and tests. |
Policy decisions are written to attempts/receipts or policy audit rows when useful for debugging. Suppressed notifications are first-class outcomes, not silent drops.
15. Graph integration
Notifications are graph-visible capabilities, facets, routes, resources, and operations.
Facets
| Facet | Carried by | Consumed by |
|---|---|---|
NotificationFacet | Domain nodes that emit notification intents. | Docs, policy pass, workbench catalog. |
NotificationCategoryFacet | Category declarations. | Android/iOS registries, preference UI, policy pass. |
NotificationRouteFacet | OpenPath/GraphAction declarations. | Route validator, action registry, deep-link integration. |
NotificationProviderFacet | FCM/APNs/Web Push provider nodes. | Deployment planner, token broker config, provider health panel. |
NotificationStoreFacet | D1 database/table resources. | Schema migration, drift checks, workbench queries. |
NotificationQueueFacet | Cloudflare Queue nodes. | Retry/DLQ operations, telemetry, backlog panels. |
NotificationTokenBrokerFacet | reaktorServer broker endpoint/resource. | Auth validation, broker health, token-mint audit. |
TelemetryFacet | Every workflow leg. | OTel/Grafana/Reaktor Insights. |
Visitors and passes
| Pass | What it checks |
|---|---|
NotificationCoveragePass | Every NotificationRoute.OpenPath matches a real route and every GraphAction resolves to a registered action. |
NotificationCategoryPass | Android channels and iOS categories are declared for every notification category. |
NotificationPolicyPass | Every category has default mute/quiet/frequency policy and preference copy. |
NotificationProviderPass | FCM provider/project config exists for every app/environment that declares remote push. |
NotificationSchemaPass | D1 migrations match the expected table/index contract. |
NotificationSecretPass | Workers do not hold Firebase service account JSON; broker auth secret is present and scoped. |
NotificationPrivacyPass | Provider payload fields do not include forbidden content keys for E2EE/sensitive domains. |
EndpointHygienePass | Flags invalidation spikes, stale tokens, environment mismatches, and high churn. |
Dispatcher
class GraphNotificationRouteDispatcher(
private val graph: Graph,
private val actionRegistry: TactileActionRegistry,
) : NotificationRouteDispatcher {
override suspend fun dispatch(event: NotificationInteractionEvent) {
when (val route = event.route) {
is NotificationRoute.OpenPath -> {
val routeNode = graph.resolveRoute(route.path) ?: return
graph.dispatch(Push(routeNode.edge(), routeNode.parsePayload(route.path)))
}
is NotificationRoute.GraphAction -> {
val actionRef = ActionRef(route.type, payloadSchema = route.type)
val payload = TactileValue.fromJson(route.payloadJson)
actionRegistry.dispatch(actionRef, payload)
}
NotificationRoute.None -> Unit
}
}
}
Cold-launch behavior:
- Platform bridge writes tap/action into a pending interaction queue.
- Auth/session restoration runs.
- Graph route/action registry becomes ready.
- Dispatcher replays pending interactions with TTL and dedup.
- Interaction result is recorded back to Cloudflare.
This should reuse the deep-link integration queue/gating model where possible.
16. Reaktor Desktop and MCP operations
Reaktor Desktop should make notifications operable, not just documented.
Graph nodes
| Node | Example |
|---|---|
| Runtime code | NotificationsNode, ChatMentionNotificationEmitter, GraphNotificationRouteDispatcher |
| Runtime interface | NotificationsClient, NotificationsService, FcmTokenBrokerClient |
| Cloud resource | D1 database, Queue, Durable Object namespace, Worker, broker service endpoint |
| Binding | notification-service -> NOTIFICATIONS_DB, notification-service -> FCM_TOKEN_BROKER_SECRET |
| Infra | Pulumi/Dagger/wrangler materializers for D1, Queues, DOs, Worker bindings |
| Operation | replay DLQ, invalidate endpoint, rotate broker secret, preview migration, send test notification |
| Signal | delivery latency, invalidation rate, DLQ depth, token broker failures, route dispatch failures |
Commands
| Command | Effect | Guard |
|---|---|---|
RegisterNotificationCategory | Adds/updates category model for Android/iOS/preferences. | Category id unique; preference copy present. |
UpdateNotificationPolicy | Changes quiet hours, mutes, or frequency caps. | Tenant admin; policy simulation passes. |
SendTestNotification | Sends a synthetic notification to an installation. | Explicit target; safe payload; rate limit. |
InvalidateEndpoint | Marks an endpoint invalid or stale. | Operator reason required. |
ReplayNotificationDlq | Replays DLQ envelope through the right queue. | Idempotency key present; duplicate-send warning. |
RefreshFcmAccessToken | Forces cache refresh via broker. | Service principal only; audit. |
RotateBrokerCredential | Updates Worker-to-broker auth material. | High-risk operation; approval and deploy plan. |
PreviewNotificationMigration | Runs D1 migration preview/drift check. | No destructive schema changes without approval. |
OpenProviderDashboard | Opens Cloudflare/Firebase/Grafana view. | Read permission. |
MCP tools
Agents should be able to query and operate the notification system through permissioned tools:
notifications.topology()notifications.endpointHealth(tenantId, appId)notifications.findNotification(notificationId)notifications.findByCorrelation(correlationId)notifications.dlqDepth()notifications.replayDlq(envelopeId, reason)notifications.sendTest(installationId, payload)notifications.policyCheck(type, target, category)notifications.routeCheck(type)notifications.tokenBrokerHealth(firebaseProjectId)
Every mutating tool writes notification_operator_audit and emits an operational signal.
17. Telemetry and analytics
Notification signals use the Reaktor analytics/telemetry model. Product analytics, runtime telemetry, and operational audit remain distinct but correlated by IDs.
Spans
| Span | Attributes |
|---|---|
notification.intake | tenant.id, app.id, notification.type, notification.id, idempotency.hit, graph.node.id |
notification.fanout | notification.id, target.type, endpoint.count, policy.suppressed.count, queue.name |
notification.delivery.fcm | notification.id, endpoint.id, provider=fcm, firebase.project_id, attempt, http.status_code, fcm.error_code |
notification.token_broker | firebase.project_id, cache.hit, broker.status, expires_in_ms |
notification.receipt | receipt.type, provider.message_id, endpoint.status |
notification.interaction | interaction.type, route.type, dispatch.status, graph.action.type |
Metrics
Use low-cardinality labels. Do not put raw user IDs, full endpoint IDs, or full notification IDs in metric labels.
| Metric | Labels |
|---|---|
notification_intake_total | tenant, app, type, status |
notification_fanout_endpoints_total | tenant, app, type |
notification_policy_suppressed_total | tenant, app, category, reason |
notification_delivery_attempt_total | tenant, app, provider, status, error_class |
notification_delivery_latency_ms | tenant, app, provider |
notification_endpoint_invalidated_total | tenant, app, platform, reason |
notification_dlq_depth | tenant, app, queue |
notification_token_broker_request_total | tenant, app, project, status, cache |
notification_interaction_total | tenant, app, type, route_type, dispatch_status |
Product analytics
Product events should be emitted through app-owned analytics catalogs, not ad hoc strings:
notification_receivednotification_openednotification_action_clickednotification_direct_reply_sentnotification_dismissednotification_permission_requestednotification_permission_grantednotification_permission_denied
Join fields:
notification_idcorrelation_idtypecategoryroute_typegraph_node_idrelease_idsession_id
Do not emit message bodies, raw FCM tokens, raw APNs tokens, or high-cardinality route payloads.
18. Security and privacy
Notifications are privacy-sensitive because provider infrastructure can see payload metadata and, for notification messages, user-visible text.
Rules:
- FCM device tokens are secrets. Store token hash for lookup/dedup and encrypted token value for delivery.
- Never log raw endpoint tokens, OAuth tokens, APNs tokens, or full provider responses containing tokens.
- Workers do not contain Firebase service-account JSON.
- The token broker has a least-privilege Google identity for FCM send.
- The token broker endpoint is private and audience-scoped.
- Provider payloads carry only safe previews and opaque IDs for sensitive domains.
- Tenant/app/environment are part of every key and uniqueness constraint.
- Operator actions require explicit principal, scope, reason, and audit row.
- D1 queries exposed through the workbench are permissioned and redacted.
- Deletion/export flows must delete or redact endpoint rows, preferences, interactions, and token cache for a user/installation.
Suggested scopes:
| Scope | Allows |
|---|---|
notifications:read | Inspect topology, records, endpoint health, receipts. |
notifications:endpoint:write | Register/unregister/update endpoint preferences. |
notifications:send | Create notification intents. |
notifications:test | Send synthetic notifications to owned/dev endpoints. |
notifications:replay | Replay DLQ/receipt/interaction events. |
notifications:admin | Policy, category, provider, migration operations. |
fcm-token:mint | Service-only scope for token broker requests. |
19. Relationship to reaktor-work
reaktor-work is for runtime background tasks on app/device/JVM/JS targets. Notification cloud delivery is not a mobile background task.
Use reaktor-work for:
- Client-side token refresh checks.
- Opportunistic preparation or reconciliation around local notifications; the notification adapter owns the native schedule.
- Pending interaction upload retry.
- Analytics/receipt flush on app background.
- Periodic endpoint health ping where platform schedulers permit it.
Use Cloudflare Queues, Durable Object alarms, and later Cloudflare Workflows for:
- Intake outbox drain.
- Fanout.
- Delivery retries.
- Receipt processing.
- DLQ replay.
- Token broker cache refresh.
- Campaign/segment fanout workflows.
Reaktor Engine can display all of these in one graph, but their runtime semantics are different.
20. Module layout
reaktor-notification/
src/commonMain/kotlin/dev/shibasis/reaktor/notification/
domain/
NotificationIntent.kt
NotificationTarget.kt
NotificationContent.kt
NotificationRoute.kt
NotificationEndpoint.kt
NotificationRecord.kt
DeliveryTask.kt
ProviderReceipt.kt
NotificationInteractionEvent.kt
client/
NotificationsClient.kt
NotificationPermissionStatus.kt
NotificationPlatformCapabilities.kt
NotificationCategorySpec.kt
NotificationActionSpec.kt
LocalNotificationRequest.kt
service/
NotificationsService.kt
NotificationEndpointStore.kt
NotificationStore.kt
NotificationRouteDispatcher.kt
policy/
DeliveryPolicy.kt
QuietHours.kt
FrequencyCap.kt
graph/
NotificationsNode.kt
NotificationFacet.kt
NotificationCoveragePass.kt
GraphNotificationRouteDispatcher.kt
telemetry/
NotificationSignals.kt
src/androidMain/
AndroidNotificationsClient.kt
AndroidNotificationPermissionController.kt
FcmTokenProvider.kt
ReaktorFirebaseMessagingService.kt
AndroidNotificationChannelRegistry.kt
AndroidNotificationRenderer.kt
AndroidConversationNotificationBridge.kt
AndroidNotificationActionRegistry.kt
AndroidNotificationInteractionBridge.kt
AndroidLocalNotificationScheduler.kt
AndroidNotificationDevHarness.kt
src/iosMain/
IosNotificationsClient.kt
IosNotificationPermissionController.kt
ApnsRegistrationProvider.kt
FirebaseMessagingTokenProvider.kt
AppleCategoryRegistry.kt
IosNotificationForegroundPresenter.kt
IosNotificationInteractionBridge.kt
IosLocalNotificationScheduler.kt
IosNotificationServiceExtensionBridge.kt
IosNotificationContentExtensionBridge.kt
IosLiveActivityBridge.kt
IosNotificationDevHarness.kt
src/jsMain/
worker/
NotificationIntakeWorker.kt
NotificationFanoutWorker.kt
FcmDeliveryWorker.kt
NotificationReceiptWorker.kt
NotificationInteractionWorker.kt
durable/
NotificationTenantDurableObject.kt
NotificationInstallationDurableObject.kt
FcmTokenBrokerCacheDurableObject.kt
ProviderCircuitDurableObject.kt
store/
D1NotificationEndpointStore.kt
D1NotificationStore.kt
D1NotificationMigration.kt
provider/
FcmHttpV1Provider.kt
FcmErrorClassifier.kt
FcmTokenBrokerClient.kt
src/jvmMain/
broker/
FcmAccessTokenBrokerClient.kt # optional test/client helper
Dependencies should pivot from the current :reaktor-ui-only placeholder toward:
reaktor-corereaktor-graphreaktor-graph-portreaktor-ioreaktor-authreaktor-capabilityreaktor-cloudflarereaktor-telemetryreaktor-workonly for client background task integration
21. BestBuds first slice
First consumer: chat mention.
Notification types
| Type | Trigger | Target | Route | Category | Priority |
|---|---|---|---|---|---|
chat.mention | Mention in a chat room | mentioned user | GraphAction("bestbuds.chat.open", {"chatId"}) | messages | High |
chat.dm | Direct message | recipient user | GraphAction("bestbuds.chat.open", {"chatId"}) | messages | High |
friend.request | Incoming friend request | recipient user | OpenPath("/friends") | social | Normal |
event.invite | Event invite | invited user | OpenPath("/events/{id}") | events | Normal |
campaign.reminder | Active campaign reminder | user | OpenPath("/campaigns/current") | campaigns | Low |
Migration steps
| Area | Current status / next change |
|---|---|
BestBuds.kt / app startup | Mobile package is installed through platform lifecycle wiring. Next: add graph-visible notification nodes/routes once cloud contracts land. |
| BestBuds dev screen | Implemented. The debug panel is backed by AndroidNotificationDevHarness and IosNotificationDevHarness and exposes permission/settings state, channel/category registration, token status, local notification send, synthetic remote envelope injection, tap/action route result, cloud endpoint probe, cloud dry-run probe, and notification work scheduling. |
| Android | Implemented for the mobile package slice. BestBudsActivity installs AndroidNotificationsClient; manifest receivers/services delegate to the package runtime. |
| iOS | Implemented for the mobile package slice. App lifecycle callbacks bridge Firebase Messaging, APNs token observation, and UNUserNotificationCenter callbacks into IosNotificationsClient. |
| Android Maestro | Implemented and passing: tests/bestbudsAndroid/notifications-dev.yaml. |
| iOS Maestro | Implemented and passing: tests/bestbudsDarwin/notifications-dev.yaml. |
notification-service Worker | Next. Replace dev/probe-only behavior with Reaktor intake/fanout/delivery/receipt routes. |
| D1 | Next. Create NOTIFICATIONS_DB binding and migrations. |
| Queues | Next. Add fanout, delivery, receipts, interactions, and DLQ queues. |
| Durable Objects | Next. Add tenant/install/token-cache/provider-circuit namespaces. |
reaktorServer | Next. Add private /internal/fcm/access-token broker endpoint after worker-server service-token auth is ready. |
| Reaktor Engine | Next. Add notification topology, endpoint health, DLQ, broker health, policy, and send-test panels. |
| Dagger/Pulumi/wrangler | Next. Materialize D1, Queues, DOs, bindings, secrets, and deploy order from the Kotlin platform model. |
Native package verification before real send
This milestone is complete for the mobile package slice. It deliberately verifies native SDK behavior before making live provider delivery the gating condition.
- BestBuds debug builds open the notification dev screen on Android and iOS.
- Android flow asserts app notification permission state and channel registration state.
- iOS flow asserts authorization/settings state and category registration state.
- Each platform registers categories/channels through the Reaktor package.
- Each platform refreshes/probes device token state through the Reaktor package.
- Each platform probes endpoint registration and cloud dry-run without requiring real provider delivery.
- Each platform schedules or renders a local notification through the Reaktor package.
- Each platform injects a synthetic Reaktor remote envelope through the dev harness.
- Tap/action events dispatch through the package listener model and update the dev-screen route/status state.
- Maestro verifies the flow on Android and iOS without depending on live Cloudflare delivery, FCM delivery, APNs delivery, or the token broker.
Verification artifacts:
| Platform | Flow | Result |
|---|---|---|
| Android | tests/bestbudsAndroid/notifications-dev.yaml | Passed on attached Android device after the mobile package integration. |
| iOS | tests/bestbudsDarwin/notifications-dev.yaml | Passed on attached iPhone after native iOS calls were serialized on the main queue and the authorization wait was hardened. |
Send path for first real cloud test
- Android or iOS app registers endpoint after login.
- Cloudflare D1 shows active endpoint with token hash and encrypted token.
- Chat mention creates
NotificationIntentwith idempotency key. - Intake writes D1 record/outbox and enqueues fanout.
- Fanout resolves active endpoints and enqueues delivery.
- Delivery Worker requests/uses FCM access token from broker cache.
- Delivery Worker sends FCM HTTP v1 request.
- Target device receives push and tap dispatches
bestbuds.chat.open. - D1 has record, attempt, receipt, and interaction rows.
- Reaktor Desktop shows the whole flow by correlation ID.
22. Implementation plan
Phase 0 - design alignment (complete)
- Replace stale docs and duplicate draft pages.
- Confirm D1/Worker/DO/Queue/FCM/broker architecture.
- Confirm that native package work is the first build milestone and cloud delivery follows it.
- Keep graph nodes/resources aligned with the unified system graph model as the cloud phase lands.
Phase 1 - native notification packages and BestBuds dev verification (complete)
This phase is complete for the mobile package slice. It intentionally made native Android/iOS behavior testable before live provider delivery.
Completed objectives:
:reaktor-notificationis now the shared KMP notification package for app runtimes.- Native OS features are represented through typed capabilities instead of a lowest-common-denominator push helper.
- Android and iOS expose permission/settings state, token lifecycle probes, local notifications, remote envelope handling, categories/channels, actions, direct reply/text input where supported, foreground presentation, tap/dismiss handling, delivered/scheduled cleanup, settings navigation, runtime state, and diagnostics.
- Each platform has a debug harness that can run without live Cloudflare delivery, FCM delivery, APNs delivery, or the token broker.
- The BestBuds dev screen is the first human and Maestro control surface for package verification.
Completed common deliverables:
- Added
NotificationsClient,NotificationRuntimeState,NotificationPlatformCapabilities,NotificationCategorySpec,NotificationActionSpec,ForegroundPresentationPolicy,NotificationEnvelope,NotificationRoute,LocalNotificationRequest,NotificationResponseEvent,NotificationDevState, andNotificationDevHarness. - Added stable serialization for notification envelopes, routes, category specs, action specs, local notification requests, platform options, and response events.
- Added capability reporting so Reaktor Desktop can see supported, unsupported, unavailable, and entitlement-gated features by platform.
- Added an in-memory
NotificationsClientfor non-platform examples and local graph/runtime probes.
Completed Android deliverables:
- Implemented
AndroidNotificationsClient. - Implemented permission/settings handling for
POST_NOTIFICATIONS, app-wide notification settings, channel block state, and settings intents. - Implemented channel and channel-group registration from common category specs.
- Implemented Firebase token fetch and token-refresh recording through
ReaktorFirebaseMessagingService. - Implemented local notification rendering, delayed/calendar scheduling, cancellation, delivered cleanup, badge state, foreground policy, route metadata, action buttons, dismiss handling, and direct reply extraction.
- Implemented manifest receiver/service delegation through
AndroidNotificationsRuntime. - Implemented
AndroidNotificationDevHarness.
Completed iOS deliverables:
- Implemented
IosNotificationsClient. - Implemented authorization/settings handling with main-queue native calls and a hardened authorization wait.
- Implemented APNs/FCM token observation through
IosNotificationsRuntime. - Implemented category registration, actions, text input actions, custom dismiss behavior, foreground presentation, local scheduling, cancellation, delivered cleanup, badge state, settings navigation, and response event conversion.
- Kept Notification Service Extension, Notification Content Extension, and Live Activity bridges as planned platform-depth extension points.
- Implemented
IosNotificationDevHarness.
Completed BestBuds dev screen:
- Added a debug-only notification panel in BestBuds.
- Shows permission/settings state, platform capabilities, channel/category registration state, token state, endpoint registration probe, cloud dry-run probe, local notification result, synthetic remote envelope result, simulated tap/action route result, and notification work scheduling state.
- Commands call the Reaktor notification package rather than product-local platform APIs.
Completed verification:
- Android and iOS packages run their notification dev-screen flows without live provider delivery.
- Maestro tests pass on BestBuds debug builds for Android and iOS.
- Product code no longer owns raw notification behavior outside lifecycle wiring and app-specific installation.
Remaining Phase 1 follow-up:
- Project platform capabilities, categories/channels, package health, and dev harness state into the Reaktor Engine graph instead of only the app dev screen.
- Harden production cold-launch pending-interaction replay.
- Add deeper integration/e2e coverage for OS-specific surfaces as they become product-enabled. Do not add unit-test-only coverage as the primary confidence mechanism for this package.
Phase 2 - cloud contracts, D1 storage, and service-token broker auth (mostly complete)
Done: cloud domain types and typed Service contracts, D1 migrations and typed store wrappers for endpoints, preferences and deliveries, wrangler bindings for D1 and the dispatcher DO, worker-side bearer auth (NotificationWorkerAuth.kt), and a Playwright end-to-end test.
Not done, and now overdue: Queue bindings; D1 tables for attempts, receipts, interactions, idempotency, outbox, token-cache audit and DLQ; the reaktorServer broker endpoint. The Worker holds GOOGLE_SERVICE_ACCOUNT_JSON instead.
- Complete the cloud-facing domain types and service contracts that depend on the Phase 1 package model.
- Add D1 migration files and typed store wrappers for endpoints, preferences, notifications, attempts, receipts, interactions, idempotency, outbox, token-cache audit, and DLQ state.
- Add wrangler bindings for D1, Queues, DOs, and broker auth secret.
- Add the broker endpoint contract to
reaktorServer:reaktorServermints short-lived Google OAuth access tokens for FCM and returns no endpoint tokens, notification payloads, attempts, receipts, or preference state. - Wire worker-server service-token auth before exposing the broker path. Do not ship the token broker without service-token auth, route allow-listing, audit logging, and redacted logs.
- Add Cloudflare Worker integration tests for idempotency, endpoint upsert, endpoint encryption/token hashing, preference writes, route serialization, and policy checks.
- Add Reaktor Desktop graph nodes for D1 health, Worker routes, Queue lag, DO namespaces, broker health, and endpoint registration status.
Phase 3 - endpoint registration (complete)
Register, unregister and preferences APIs are live and exercised by the e2e test. Endpoint token hashing is enforced by a UNIQUE constraint on token_hash. Endpoint encryption at rest and the Reaktor Desktop hygiene view are not built.
- Connect the Phase 1 Android token provider to Cloudflare endpoint registration.
- Connect the Phase 1 iOS Firebase token provider/APNs bridge to Cloudflare endpoint registration.
- Cloudflare register/unregister/preferences APIs.
- Endpoint encryption, token hashing, installation identity, and environment separation.
- Receipt/interaction upload API used by the package.
- Endpoint hygiene view in Reaktor Desktop.
Phase 4 - FCM delivery from Cloudflare (partial)
Real FCM HTTP v1 sends work from the dispatcher DO, with error classification and dry_run recorded per delivery. The four-Worker split, FcmTokenBrokerCacheDO, DLQ, replay, rate limiting and provider circuit state are not built, and the token broker was replaced by a Worker-held service account.
- Intake Worker, fanout Worker, delivery Worker, receipt Worker.
FcmTokenBrokerCacheDO.FcmHttpV1ProviderandFcmErrorClassifier.- Real FCM send to Android and iOS dev endpoints after package verification.
- Attempts/receipts/invalidation in D1.
- DLQ, replay, rate limiting, provider circuit state, and token-broker cache tests.
Phase 5 - graph routing, operations, and telemetry
NotificationsNode.GraphNotificationRouteDispatcher.- Pending interaction queue for cold launch.
- Product analytics and OTel spans/metrics.
- Workbench notification flow panel.
- MCP/workbench operations for topology, endpoint health, send test, DLQ replay, policy check, route check, and token broker health.
Phase 6 - platform depth and OS expansion
- Android priority conversations, bubbles only if product opts in, richer grouping, richer badges, foreground-service notification diagnostics, and OEM/device-state diagnostics.
- iOS provisional auth, text input actions wired to product chat send, Notification Service Extension, Notification Content Extension, Live Activities, and APNs-direct features.
- Rich media policy.
- Permission rationale UI.
- Expanded Maestro and integration tests for the OS-specific features that are enabled.
Phase 7 - broader providers and engagement
- APNs direct for iOS-specific features.
- Web Push.
- In-app inbox.
- Campaign/segment fanout.
- A/B and conversion reporting.
23. Risks and open questions
| Risk | Mitigation |
|---|---|
| Token broker becomes a hidden delivery service | Keep endpoint/payload data out of broker API; test contract; expose only access-token mint. |
| D1 schema becomes product-specific | Keep tenantId/appId/type/category/route generic; product-specific data stays in typed content_json/route_json with schema validation. |
| FCM token cache leaks OAuth token | Prefer DO memory for the first cloud delivery slice; if persisted, encrypt, short TTL, redact logs, audit access. |
| Duplicate sends on replay | Idempotency key required for domain-triggered notifications; DLQ replay checks idempotency before send. |
| Provider payload leaks sensitive chat content | Privacy pass rejects forbidden keys; E2EE domains default to safe preview. |
| Queue retries overload FCM during outage | Provider circuit DO, exponential backoff, DLQ, and tenant/provider rate limits. |
| iOS via FCM misses Apple-specific features | Accept for the first provider slice; APNs direct is planned for Live Activities/critical/direct APNs headers. |
| Reaktor Engine graph drifts from infra | Generate Pulumi/wrangler/Dagger graph from Kotlin platform model and reconcile with live Cloudflare/GCP APIs. |
Open questions:
- Should
notification_token_cachepersist encrypted OAuth tokens in D1, or should the first cloud delivery slice use only DO memory and audit rows? - Should the token broker be reachable through Cloudflare Tunnel/private hostname, Cloudflare Access, or a service-to-service mTLS path?
- Do we create one D1 database per tenant/app or one shared D1 with strict tenant partitioning for the first cloud delivery slice?
- Should endpoint token encryption use Workers Secrets, Cloudflare Secrets Store, or a key from GCP Secret Manager delivered to Cloudflare?
- Which Reaktor module should own the generic
QueueBindingwrapper for Cloudflare Queues:reaktor-cloudflare,reaktor-server, or a newreaktor-busslice?
24. References
Internal docs:
- Reaktor Unified System Graph
- Reaktor Cloud Pane
- Reaktor Server
- Reaktor Work
- Reaktor Analytics and Telemetry
- Deep Link Integration
- Capability Runtime
External platform references:
- Android notifications overview
- Android create a notification
- Android notification runtime permission
- Android notification channels
- Android people and conversations notifications
- Firebase Cloud Messaging message types
- Firebase Cloud Messaging: receive messages in Android apps
- Apple Notifications overview
- Apple Human Interface Guidelines: Notifications
- Apple UserNotifications: request authorization
- Apple UserNotifications: declaring actionable notification types
- Apple UserNotifications: modifying content in newly delivered notifications
- Apple UserNotifications: setting up a remote notification server
- Maestro documentation
- Maestro assertVisible
- Maestro tapOn
Current code:
../reaktor/reaktor-notification/src/commonMain/kotlin/dev/shibasis/reaktor/notification/NotificationAdapter.ktmodules/app/src/jsMain/kotlin/ai/bestbuds/app/cloudflare/notification/NotificationWorker.ktmodules/app/src/androidMain/kotlin/ai/bestbuds/app/AndroidNotificationService.ktmodules/app/src/androidMain/kotlin/ai/bestbuds/app/BestBudsActivity.ktmodules/app/src/iosMain/kotlin/ai/bestbuds/app/BestBudsApplication.kt../reaktor/reaktor-cloudflare/src/jsMain/kotlin/dev/shibasis/reaktor/cloudflare/D1.kt../reaktor/reaktor-cloudflare/src/jsMain/kotlin/dev/shibasis/reaktor/cloudflare/DurableObjects.kttargets/reaktorServer/src/main/kotlin/ai/bestbuds/server/ServerApplication.kt
External docs:
- Firebase Cloud Messaging HTTP v1 send API
- Firebase Cloud Messaging error codes
- Firebase Cloud Messaging message types
- Cloudflare D1
- Cloudflare Durable Objects
- Cloudflare Queues
- Cloudflare Queues dead letter queues
- Cloudflare Workers service bindings
- Cloudflare Workers secrets
Related docs
- Service Layer - Use before adding service APIs, splitting reaktor-service out of reaktor-graph, wiring handlers to Spring/Workers, or making modules graph-projectable without hard graph dependencies.
- Work - Use before changing background scheduling, worker contracts, WorkRequest/WorkConstraints, BestBuds workers, edge/web scheduling, work telemetry, or graph projection for work.
- Reaktor Bus - Use before designing durable async events, jobs, fan-out, queues, Pub/Sub, outbox, DLQ, replay, or graph-visible delivery.
- Auth Architecture - Use before changing reaktor-auth or request authorization boundaries.
- Perf and Observability - Use before optimizing or adding instrumentation.
- Server - Use before adding any production backend primitive (resilience, messaging, data patterns) or designing the Spring/Workers server runtime.