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.

SurfaceCurrent stateDesign 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 WorkerSuperseded 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 appBestBudsActivity 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 appBestBudsApplication 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 verificationtests/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 substratereaktor-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 substratereaktorServer 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 modelUnified 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:

ContractQuestionExamples
Product contractWhy should the user be interrupted?Mention, direct message, friend request, event reminder, security alert, campaign reminder.
Delivery contractHow does the message reach the device?FCM, APNs, Web Push, local scheduler, provider retry, token lifecycle.
Interaction contractWhat 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:

ConceptWhat it means for implementation
PermissionAndroid 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.
ChannelsAndroid 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.
PostingThe app builds a notification with icon, title, body, channel, priority/importance, style, content intent, delete intent, actions, and optional grouping/conversation metadata.
FCMFirebase 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 behaviorIf 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.
ActionsAndroid 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.
ConversationsReal-time chat should use MessagingStyle, people metadata, and long-lived shortcuts so Android can rank and display conversations properly.
User controlUsers 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:

ConceptWhat it means for implementation
AuthorizationApps must request notification authorization before displaying alerts, sounds, or badges. The app must read current settings because users can change them later.
Local notificationsThe app schedules local notification requests with content and triggers through UNUserNotificationCenter. This is used for reminders and device-local workflows.
Remote notificationsA 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.
CategoriesCategories define actionable notification types. Actions, including text input actions, are registered at launch and referenced by payload/category ID.
Foreground behaviorUNUserNotificationCenterDelegate decides how to handle a notification while the app is foregrounded: update UI, show banner/list/sound/badge, or suppress.
Service extensionA 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 extensionA Notification Content Extension can provide custom notification UI when the product experience justifies it.
BadgesBadges are explicit app-visible counts and must be kept accurate. They should mean unread or pending notification work, not arbitrary product numbers.
User controlUsers 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.

LayerResponsibility
Event intakeAccept typed product events or notification intents with auth, schema validation, idempotency, correlation IDs, and scheduling metadata.
Template and contentResolve copy, localization, personalization, safe previews, image policy, fallback text, and payload size constraints.
Endpoint registryStore installation IDs, platform, provider tokens, environment, token hashes, encrypted token values, token freshness, and invalidation state.
Preferences and policyApply category mutes, quiet hours, frequency caps, tenant policy, sensitive-content rules, and product-specific eligibility.
FanoutResolve target users/installations/segments, dedupe endpoints, split work into provider-specific delivery tasks, and maintain retry cursors.
Provider deliveryTranslate generic notification intent into FCM/APNs/Web Push payloads, send through a trusted server path, classify provider responses, and invalidate bad endpoints.
Receipts and interactionsRecord provider acceptance/failure, client received/opened/dismissed/action/direct-reply events, and route-dispatch outcomes.
OperationsExpose DLQs, replay, endpoint health, token churn, broker health, provider errors, policy simulation, and safe test-send tooling.
AnalyticsAttribute 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.

RequirementProduct behavior
RelevanceNotify only for events that matter to this user in this context.
ConsentAsk permission when the user understands the value, and degrade gracefully when denied.
ControlProvide clear categories, mute controls, quiet hours where appropriate, and an obvious settings path.
PrivacyAvoid sensitive previews by default for private content; honor lock-screen and preview settings.
ActionabilityA tap must open the exact relevant object. Actions should complete useful work without forcing an app open.
Non-duplicationDo not repeatedly notify for the same unresolved event. Update, collapse, group, or suppress instead.
RecoveryIf a notification is missed, the user should still find the underlying item in the app or inbox.
AccuracyBadges and unread counts must clear when work is handled. Stale notifications erode trust quickly.
AccessibilityCopy, sounds, vibration, actions, and visual treatment should work across device settings and abilities.
LocalizationText 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.

RequirementQuestions PMs must answer
Notification taxonomyWhat exact event types exist? Which category does each type belong to? Which are transactional, social, operational, or promotional?
User valueWhy does this notification help the user now? What should happen if it is suppressed?
Priority and urgencyIs this high, normal, low, time-sensitive, or silent? Does it justify sound, vibration, badge, or only inbox?
TargetingWhich users/installations receive it? What are the eligibility rules and exclusions?
FrequencyHow many times can this type notify per hour/day/week? What should collapse or update instead of creating a new notification?
Copy and localizationWhat are title/body/action templates, fallbacks, length limits, pluralization rules, and sensitive-preview alternatives?
RoutingWhat exact screen or graph action should tap/action/direct reply invoke? What should happen if the target object no longer exists?
MeasurementWhat are expected opt-in, delivery, open, action, conversion, mute, and unsubscribe metrics?
ExperimentationIs A/B testing allowed? Which copy, timing, or priority variants are safe to test?
ComplianceIs this transactional, marketing, safety, child-related, private, regulated, or user-generated content?
OperationsWho can send tests, pause a notification type, roll back a campaign, replay DLQ, or invalidate endpoints?
Launch criteriaWhat integration tests, device tests, copy review, privacy review, and analytics dashboards must exist before release?

Design implications for Reaktor

Domain realityReaktor 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.

SubgraphRuns onOwnsPrimary state
ClientGraphAndroid, iOS, later webpermission, token acquisition, local scheduling, foreground handling, tap/action eventsdevice runtime + local pending interaction queue
IntakeGraphCloudflare Worker + optional DOvalidate intent, auth, idempotency, record/outbox transactionD1
FanoutGraphCloudflare Queue consumer + DO coordinationresolve target, load endpoints, apply policy, enqueue endpoint tasksD1 + Queues
DeliveryGraphCloudflare Queue consumerget/cached FCM access token, build FCM payload, send HTTP v1, classify provider responseD1 + token cache
ReceiptInteractionGraphCloudflare Worker/Queue consumer + clientswrite attempts/receipts/interactions, invalidate endpoints, replay/DLQD1

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

TablePurposeRetention
notification_endpointsInstallation/provider token registry, status, platform, environment, last seen, coarse device metadata.Until unregister, terminal invalidation TTL, or user deletion.
notification_preferencesPer-user/per-installation category settings, quiet hours, frequency cap overrides.Until replaced or account deletion.
notification_recordsOne row per accepted NotificationIntent.30-180 days by tenant policy.
notification_idempotencyDeduplicate logical sends by idempotency key.Greater than max retry/DLQ replay window.
notification_outboxDurable transaction-to-queue handoff.Until published or dead-lettered.
notification_attemptsOne provider attempt per endpoint.14-30 days by default.
notification_receiptsProvider acceptance/failure plus client open/dismiss/action receipts where available.Analytics/audit window.
notification_interactionsTap, action, direct reply, dismiss, and route dispatch facts.Analytics/audit window.
notification_token_cacheOptional 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_auditWorkbench/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 ObjectKeyResponsibility
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.

QueueProducerConsumerPayload
notifications.fanoutIntake Worker / outbox drainFanout WorkerFanoutRequest(notificationId)
notifications.deliveryFanout WorkerFCM Delivery WorkerDeliveryTask(notificationId, endpointId, attempt)
notifications.receiptsDelivery Worker / client receipt APIReceipt WorkerProviderReceipt or ClientReceipt
notifications.interactionsClient interaction APIInteraction WorkerNotificationInteractionEvent
notifications.dlqQueue retry exhaustionReplay Worker / Workbenchoriginal 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:

ResultAction
FCM acceptedWrite attempt/receipt, ack queue message.
UNREGISTERED, invalid registration token, sender mismatchMark endpoint invalid, write terminal receipt, ack.
Quota/rate exceededRetry with delay and jitter; breaker may open at project/provider level.
FCM unavailable/internal/network timeoutRetry with exponential backoff and bounded attempts.
Broker auth failureOpen circuit, write provider health event, retry only if token refresh is likely recoverable.
Payload/schema bugTerminal 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_cache with 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, and correlationId.

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

ProviderWhenWhy
APNs directLater provider sliceLive Activities, critical alerts, direct APNs headers, iOS features not exposed well through FCM.
Web PushLater provider sliceBrowser/PWA notifications through service workers and VAPID.
Expo bridgeCompatibility onlyUseful for tenants already on Expo, not the core Reaktor path.
In-app inboxLater engagement sliceProduct 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:

  • commonMain owns domain types, service contracts, policy, routes, graph facets, capability descriptors, and telemetry names.
  • androidMain owns Android permission, token, channel, renderer, direct-reply, and FirebaseMessagingService integration.
  • iosMain owns APNs registration, Firebase Messaging delegate, notification center delegate, categories, service-extension hooks, and foreground/tap handling.
  • jsMain owns Cloudflare D1 stores, Workers, DO adapters, Queue consumers, FCM HTTP v1 provider, token broker client, and workbench-safe operations.
  • jvmMain owns 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:

PieceCurrent responsibility
AndroidNotificationsClientImplements NotificationsClient, capability reporting, local endpoint probe, local scheduling/rendering, runtime state, badge state, foreground policy, delivered/scheduled cleanup, settings navigation, and listeners.
AndroidNotificationPermissionControllerHandles Android 13+ POST_NOTIFICATIONS, app-wide notification enabled state, channel blocked state, and request/open-settings flow.
AndroidNotificationChannelRegistryCreates channel groups and channels from NotificationCategorySpec, including importance, sound, badge, vibration, DND, lock-screen visibility, and bubble options.
AndroidNotificationRendererRenders 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.
AndroidNotificationSchedulerSchedules delayed/calendar local notifications through AlarmManager and cancels scheduled notifications. Immediate local notifications render directly.
ReaktorNotificationAlarmReceiverConverts scheduled alarms back into package-local render calls.
ReaktorNotificationActionReceiverConverts tap, action, dismiss, and direct-reply intents into NotificationResponseEvent.
ReaktorFirebaseMessagingServiceRoutes onMessageReceived, onNewToken, and onDeletedMessages into the installed package runtime.
AndroidNotificationsRuntimeHolds the installed Android client so manifest receivers/services can delegate without product-local notification logic.
AndroidNotificationDevHarnessDebug-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_NOTIFICATIONS in 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.yaml passes 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:

PieceCurrent responsibility
IosNotificationsClientImplements 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.
IosNotificationsRuntimeHolds the installed iOS client and receives app lifecycle callbacks for APNs token, FCM token, foreground notification, response handling, and remote-registration start.
IosNotificationDevHarnessDebug-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 bridgesExplicit 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.yaml passes 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:

PolicyBehavior
Category muteSuppress disabled categories per user/installation.
Quiet hoursDelay or suppress low/normal priority sends in endpoint timezone.
Frequency capSuppress or delay categories over rolling windows.
Endpoint healthSuppress stale, invalid, environment-mismatched, or wrong-provider endpoints.
Sensitive payloadReject payloads that place forbidden fields into provider-visible notification body/data.
Tenant overrideAllow 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

FacetCarried byConsumed by
NotificationFacetDomain nodes that emit notification intents.Docs, policy pass, workbench catalog.
NotificationCategoryFacetCategory declarations.Android/iOS registries, preference UI, policy pass.
NotificationRouteFacetOpenPath/GraphAction declarations.Route validator, action registry, deep-link integration.
NotificationProviderFacetFCM/APNs/Web Push provider nodes.Deployment planner, token broker config, provider health panel.
NotificationStoreFacetD1 database/table resources.Schema migration, drift checks, workbench queries.
NotificationQueueFacetCloudflare Queue nodes.Retry/DLQ operations, telemetry, backlog panels.
NotificationTokenBrokerFacetreaktorServer broker endpoint/resource.Auth validation, broker health, token-mint audit.
TelemetryFacetEvery workflow leg.OTel/Grafana/Reaktor Insights.

Visitors and passes

PassWhat it checks
NotificationCoveragePassEvery NotificationRoute.OpenPath matches a real route and every GraphAction resolves to a registered action.
NotificationCategoryPassAndroid channels and iOS categories are declared for every notification category.
NotificationPolicyPassEvery category has default mute/quiet/frequency policy and preference copy.
NotificationProviderPassFCM provider/project config exists for every app/environment that declares remote push.
NotificationSchemaPassD1 migrations match the expected table/index contract.
NotificationSecretPassWorkers do not hold Firebase service account JSON; broker auth secret is present and scoped.
NotificationPrivacyPassProvider payload fields do not include forbidden content keys for E2EE/sensitive domains.
EndpointHygienePassFlags 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

NodeExample
Runtime codeNotificationsNode, ChatMentionNotificationEmitter, GraphNotificationRouteDispatcher
Runtime interfaceNotificationsClient, NotificationsService, FcmTokenBrokerClient
Cloud resourceD1 database, Queue, Durable Object namespace, Worker, broker service endpoint
Bindingnotification-service -> NOTIFICATIONS_DB, notification-service -> FCM_TOKEN_BROKER_SECRET
InfraPulumi/Dagger/wrangler materializers for D1, Queues, DOs, Worker bindings
Operationreplay DLQ, invalidate endpoint, rotate broker secret, preview migration, send test notification
Signaldelivery latency, invalidation rate, DLQ depth, token broker failures, route dispatch failures

Commands

CommandEffectGuard
RegisterNotificationCategoryAdds/updates category model for Android/iOS/preferences.Category id unique; preference copy present.
UpdateNotificationPolicyChanges quiet hours, mutes, or frequency caps.Tenant admin; policy simulation passes.
SendTestNotificationSends a synthetic notification to an installation.Explicit target; safe payload; rate limit.
InvalidateEndpointMarks an endpoint invalid or stale.Operator reason required.
ReplayNotificationDlqReplays DLQ envelope through the right queue.Idempotency key present; duplicate-send warning.
RefreshFcmAccessTokenForces cache refresh via broker.Service principal only; audit.
RotateBrokerCredentialUpdates Worker-to-broker auth material.High-risk operation; approval and deploy plan.
PreviewNotificationMigrationRuns D1 migration preview/drift check.No destructive schema changes without approval.
OpenProviderDashboardOpens 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

SpanAttributes
notification.intaketenant.id, app.id, notification.type, notification.id, idempotency.hit, graph.node.id
notification.fanoutnotification.id, target.type, endpoint.count, policy.suppressed.count, queue.name
notification.delivery.fcmnotification.id, endpoint.id, provider=fcm, firebase.project_id, attempt, http.status_code, fcm.error_code
notification.token_brokerfirebase.project_id, cache.hit, broker.status, expires_in_ms
notification.receiptreceipt.type, provider.message_id, endpoint.status
notification.interactioninteraction.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.

MetricLabels
notification_intake_totaltenant, app, type, status
notification_fanout_endpoints_totaltenant, app, type
notification_policy_suppressed_totaltenant, app, category, reason
notification_delivery_attempt_totaltenant, app, provider, status, error_class
notification_delivery_latency_mstenant, app, provider
notification_endpoint_invalidated_totaltenant, app, platform, reason
notification_dlq_depthtenant, app, queue
notification_token_broker_request_totaltenant, app, project, status, cache
notification_interaction_totaltenant, app, type, route_type, dispatch_status

Product analytics

Product events should be emitted through app-owned analytics catalogs, not ad hoc strings:

  • notification_received
  • notification_opened
  • notification_action_clicked
  • notification_direct_reply_sent
  • notification_dismissed
  • notification_permission_requested
  • notification_permission_granted
  • notification_permission_denied

Join fields:

  • notification_id
  • correlation_id
  • type
  • category
  • route_type
  • graph_node_id
  • release_id
  • session_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:

ScopeAllows
notifications:readInspect topology, records, endpoint health, receipts.
notifications:endpoint:writeRegister/unregister/update endpoint preferences.
notifications:sendCreate notification intents.
notifications:testSend synthetic notifications to owned/dev endpoints.
notifications:replayReplay DLQ/receipt/interaction events.
notifications:adminPolicy, category, provider, migration operations.
fcm-token:mintService-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-core
  • reaktor-graph
  • reaktor-graph-port
  • reaktor-io
  • reaktor-auth
  • reaktor-capability
  • reaktor-cloudflare
  • reaktor-telemetry
  • reaktor-work only for client background task integration

21. BestBuds first slice

First consumer: chat mention.

Notification types

TypeTriggerTargetRouteCategoryPriority
chat.mentionMention in a chat roommentioned userGraphAction("bestbuds.chat.open", {"chatId"})messagesHigh
chat.dmDirect messagerecipient userGraphAction("bestbuds.chat.open", {"chatId"})messagesHigh
friend.requestIncoming friend requestrecipient userOpenPath("/friends")socialNormal
event.inviteEvent inviteinvited userOpenPath("/events/{id}")eventsNormal
campaign.reminderActive campaign reminderuserOpenPath("/campaigns/current")campaignsLow

Migration steps

AreaCurrent status / next change
BestBuds.kt / app startupMobile package is installed through platform lifecycle wiring. Next: add graph-visible notification nodes/routes once cloud contracts land.
BestBuds dev screenImplemented. 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.
AndroidImplemented for the mobile package slice. BestBudsActivity installs AndroidNotificationsClient; manifest receivers/services delegate to the package runtime.
iOSImplemented for the mobile package slice. App lifecycle callbacks bridge Firebase Messaging, APNs token observation, and UNUserNotificationCenter callbacks into IosNotificationsClient.
Android MaestroImplemented and passing: tests/bestbudsAndroid/notifications-dev.yaml.
iOS MaestroImplemented and passing: tests/bestbudsDarwin/notifications-dev.yaml.
notification-service WorkerNext. Replace dev/probe-only behavior with Reaktor intake/fanout/delivery/receipt routes.
D1Next. Create NOTIFICATIONS_DB binding and migrations.
QueuesNext. Add fanout, delivery, receipts, interactions, and DLQ queues.
Durable ObjectsNext. Add tenant/install/token-cache/provider-circuit namespaces.
reaktorServerNext. Add private /internal/fcm/access-token broker endpoint after worker-server service-token auth is ready.
Reaktor EngineNext. Add notification topology, endpoint health, DLQ, broker health, policy, and send-test panels.
Dagger/Pulumi/wranglerNext. 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.

  1. BestBuds debug builds open the notification dev screen on Android and iOS.
  2. Android flow asserts app notification permission state and channel registration state.
  3. iOS flow asserts authorization/settings state and category registration state.
  4. Each platform registers categories/channels through the Reaktor package.
  5. Each platform refreshes/probes device token state through the Reaktor package.
  6. Each platform probes endpoint registration and cloud dry-run without requiring real provider delivery.
  7. Each platform schedules or renders a local notification through the Reaktor package.
  8. Each platform injects a synthetic Reaktor remote envelope through the dev harness.
  9. Tap/action events dispatch through the package listener model and update the dev-screen route/status state.
  10. Maestro verifies the flow on Android and iOS without depending on live Cloudflare delivery, FCM delivery, APNs delivery, or the token broker.

Verification artifacts:

PlatformFlowResult
Androidtests/bestbudsAndroid/notifications-dev.yamlPassed on attached Android device after the mobile package integration.
iOStests/bestbudsDarwin/notifications-dev.yamlPassed 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

  1. Android or iOS app registers endpoint after login.
  2. Cloudflare D1 shows active endpoint with token hash and encrypted token.
  3. Chat mention creates NotificationIntent with idempotency key.
  4. Intake writes D1 record/outbox and enqueues fanout.
  5. Fanout resolves active endpoints and enqueues delivery.
  6. Delivery Worker requests/uses FCM access token from broker cache.
  7. Delivery Worker sends FCM HTTP v1 request.
  8. Target device receives push and tap dispatches bestbuds.chat.open.
  9. D1 has record, attempt, receipt, and interaction rows.
  10. 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-notification is 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, and NotificationDevHarness.
  • 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 NotificationsClient for 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: reaktorServer mints 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.
  • FcmHttpV1Provider and FcmErrorClassifier.
  • 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

RiskMitigation
Token broker becomes a hidden delivery serviceKeep endpoint/payload data out of broker API; test contract; expose only access-token mint.
D1 schema becomes product-specificKeep tenantId/appId/type/category/route generic; product-specific data stays in typed content_json/route_json with schema validation.
FCM token cache leaks OAuth tokenPrefer DO memory for the first cloud delivery slice; if persisted, encrypt, short TTL, redact logs, audit access.
Duplicate sends on replayIdempotency key required for domain-triggered notifications; DLQ replay checks idempotency before send.
Provider payload leaks sensitive chat contentPrivacy pass rejects forbidden keys; E2EE domains default to safe preview.
Queue retries overload FCM during outageProvider circuit DO, exponential backoff, DLQ, and tenant/provider rate limits.
iOS via FCM misses Apple-specific featuresAccept for the first provider slice; APNs direct is planned for Live Activities/critical/direct APNs headers.
Reaktor Engine graph drifts from infraGenerate Pulumi/wrangler/Dagger graph from Kotlin platform model and reconcile with live Cloudflare/GCP APIs.

Open questions:

  • Should notification_token_cache persist 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 QueueBinding wrapper for Cloudflare Queues: reaktor-cloudflare, reaktor-server, or a new reaktor-bus slice?

24. References

Internal docs:

External platform references:

Current code:

  • ../reaktor/reaktor-notification/src/commonMain/kotlin/dev/shibasis/reaktor/notification/NotificationAdapter.kt
  • modules/app/src/jsMain/kotlin/ai/bestbuds/app/cloudflare/notification/NotificationWorker.kt
  • modules/app/src/androidMain/kotlin/ai/bestbuds/app/AndroidNotificationService.kt
  • modules/app/src/androidMain/kotlin/ai/bestbuds/app/BestBudsActivity.kt
  • modules/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.kt
  • targets/reaktorServer/src/main/kotlin/ai/bestbuds/server/ServerApplication.kt

External 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.