Files
2026-05-27 23:29:53 +01:00

14 KiB

add-api-plugin-notifications

Package: @suaveplan/api-plugin-notifications Status: PROPOSED Type: new-package Design: (none — straightforward migration; see this proposal + spec delta) Spec delta: ./specs/api-plugin-notifications/spec.md

Why

Genesis ships the foundational pieces of @suaveplan/api (database, search, events, auth, site, etc.) but is missing the notifications plugin — in-app notification CRUD, device-token registration, and APN push delivery. Without this plugin, downstream apps that need lock-screen pushes or in-app notification feeds have to hand-roll the surface. Upstream enterprise/api-plugin-notifications.md documents the audit (4 *.test.ts files, two .spec.ts Playwright integration files, the APN mock smell, the mock middleware no-op, and the autoCleanOldDays abandoned config flag).

This change brings the package across at full functional parity, adapted to the genesis @suaveplan/api plugin shape (PluginDescriptor, ctx.getService(...)), the genesis DatabaseAdapter positional-param surface, and the ctx.eventBus cross-plugin emission pattern. The local-emitter wart and the APN mock are both replaced — the APN service now shapes payloads against a pluggable transport interface (ApnTransport) so the plugin never depends on a concrete APN client library. Production hosts inject a node-apn adapter; the plugin ships a deterministic MemoryApnTransport that records every send for hermetic tests.

What changes

  • ADDED: createNotificationsPlugin(options?) — factory returning a PluginDescriptor<NotificationsPluginConfig> for @suaveplan/api.
  • ADDED: NotificationsPluginOptionsSchema / NotificationsPluginConfig / NotificationsPluginConfigInput — Zod-validated config (basePath, pushBasePath, enablePush, apn, autoCleanOldDays, maxBulkRecipients).
  • ADDED: ApnConfigSchema / ApnConfig — Zod validation of the APN config bundle (cert/key/pfx/token + topic + production + network knobs).
  • ADDED: NotificationRepository + INotificationRepository — SurrealDB-backed notification CRUD repository over the genesis SurrealAdapter service slot.
  • ADDED: DeviceTokenRepository + IDeviceTokenRepository — SurrealDB-backed device-token CRUD repository.
  • ADDED: createNotificationsRoutes — Hono OpenAPI route module with 11 endpoints (list / unread-count / create / get / patch / read / read-many / read-all / dismiss / delete / grouped collapses into list).
  • ADDED: createPushRoutes — Hono OpenAPI route module with 5 endpoints (register / unregister / list tokens / send / send-bulk).
  • ADDED: ApnService + createApnService — APN delivery service with a pluggable transport.
  • ADDED: MemoryApnTransport + createMemoryApnTransport + ApnTransport / ApnTransportNotification / ApnTransportResponse / ApnTransportFactory — the transport contract and a deterministic default implementation.
  • ADDED: NOTIFICATIONS_PLUGIN_CODES registry + NotificationsPluginError class — 18 stable codes spanning configuration, lifecycle, repository, device-token, route, and APN failure modes.
  • ADDED: OTel instruments — 6 counters (notificationsCreatedTotal, notificationsReadTotal, notificationsDeletedTotal, pushSentTotal{platform}, pushFailedTotal{reason}, deviceTokensRegisteredTotal{platform}).
  • ADDED: ServiceRegistry augmentation — notificationRepository, deviceTokenRepository, apnService slots become available to downstream plugins via typed ctx.getService(...).
  • ADDED: Local Zod schemas for notification and push surfaces (NotificationSchema, CreateNotificationSchema, UpdateNotificationSchema, NotificationQuerySchema, NotificationListResponseSchema, MarkManyAsReadSchema, DeviceTokenSchema, RegisterDeviceTokenSchema, SendPushNotificationSchema, SendBulkPushNotificationSchema, etc.) — @suaveplan/api-schemas on genesis does not yet ship a notification subpath, so the plugin owns the boundary schemas.
  • ADDED: surql/tables/notification.surql + surql/tables/device_token.surql — slimmed migrations covering only the columns the repositories read/write. The upstream cross-plugin permission rules, stored helper functions (fn::notification::*, fn::device::token::*, fn::push::queue::*, fn::push::history::*), bitwise-typed notification-type constants, and push_notification_queue / push_notification_history tables are excluded (see below).
  • ADDED: NotificationsPluginEvents event-map keys — notifications:initialized, notifications:ready, notification:created, notification:read, notification:dismissed, notification:deleted, notification:push:sent, notification:push:failed, notification:device:registered, notification:device:unregistered.

Excluded from this migration

These items from enterprise/api-plugin-notifications.md "Required enterprise-quality improvements" and "Feature-rich enhancements" are deferred because each requires cross-package deps that don't yet exist on genesis (the same scope-out pattern that add-api-plugin-events used for fn::notification, fn::bitwise, fn::group references):

  • Real @parse/node-apn integration in the production codepath — the catalog deliberately omits @parse/node-apn. The migration ships an ApnTransport injection contract instead; production hosts add the library to their application dependencies and inject a node-apn-backed transport. The deterministic MemoryApnTransport ships with the plugin and is the default in tests + dev.
  • FCM / Web Push / Email / SMS / Slack channels — depend on @suaveplan/notification-channel, @suaveplan/emailer, @suaveplan/sms-providers, @suaveplan/slack-client which are not yet on genesis. Deferred to add-api-plugin-notifications-channels.
  • autoCleanOldDays scheduled cleanup — deleteOldRead(daysOld) is shipped on the repository and is callable; wiring it through @suaveplan/job-scheduler on onReady waits for the job-scheduler plugin (concurrent migration). Deferred to add-api-plugin-notifications-cleanup-job.
  • Preference center, quiet hours, templates, suppression lists, delivery tracking, batching+backoff queue, rich media, digests, in-app SSE/WS — every one of these requires a sibling plugin that doesn't exist on genesis yet (@suaveplan/templates, @suaveplan/suppression, @suaveplan/delivery-tracking, @suaveplan/queue, @suaveplan/api-plugin-sse, @suaveplan/websocket-gateway, @suaveplan/datetime, @suaveplan/i18n). Each lands in its own follow-up change.
  • Auth gating via @suaveplan/api-plugin-auth — this plugin reads c.get("user") (set by the host's auth middleware) and raises UNAUTHENTICATED when missing; explicit requireAuth() composition is a host-level concern.
  • Audit-log emission, idempotency, rate-limit — depend on @suaveplan/api-plugin-audit, @suaveplan/idempotency, @suaveplan/api-plugin-rate-limit (concurrent migrations); will be composed at the host level once they land.
  • Upstream push_notification_queue / push_notification_history tables, fn::push::queue::*, fn::push::history::*, fn::notification::create / should_create / friend_request / post_like / comment SurrealQL helpers — all reference record<site> / record<post> / record<comment> / record<group> types and call fn::bitwise::has_bits / fn::settings::get / fn::session::is_root which only exist inside sibling plugins (api-plugin-site, api-plugin-bitwise, api-plugin-groups). To avoid hand-rolling four downstream plugins as a side-quest, the migration ships a slimmer schema covering only the columns the repositories read / write; queue + history + helper functions migrate when their dependent plugins land.

Source mapping

This change derives from ../suaveplan/enterprise/api-plugin-notifications.md and the upstream source at ../suaveplan/packages/server/api-plugin-notifications/.

Requirement Source lines Notes
Plugin factory createNotificationsPlugin(config?) enterprise.md L17-L29; src/index.ts L64-L130 reworded — genesis uses PluginDescriptor not Plugin<NotificationConfig>
Config schema (basePath, pushBasePath, enablePush, apn, autoCleanOldDays) enterprise.md L13, L27-L56; src/index.ts L27-L56 extended with pushBasePath and maxBulkRecipients for genesis route-mount + bulk cap
Plugin dependencies: ["database"] enterprise.md L18; src/index.ts L75 verbatim
migrationsPath removed in genesis enterprise.md L86; src/index.ts L86 the database plugin owns migration application in genesis
NotificationRepository: create/findById/findByRecipient/getUnreadCount/update/markAsRead/markManyAsRead/markAllAsRead/dismiss/delete/findGroupedByType/deleteOldRead src/repository/notification.repository/notification.repository.ts reworded — typed db.create/select/update/delete replaced by positional SurQL (api-plugin-events Path-A pattern); upstream getById → findById, getByRecipient → findByRecipient, getGroupedByType → findGroupedByType, deleteOldNotifications → deleteOldRead
DeviceTokenRepository: register/findById/findByToken/findActiveForUser/deactivate/deactivateByToken/touchLastUsed/cleanup upstream surql/tables/device_token.surql helpers new — upstream put device-token CRUD in stored functions; genesis pulls them up into a TS repository so the plugin can run without the surql helpers
createNotificationsRoutes (11 endpoints) src/routes/notifications/notifications.ts verbatim — ctx.eventBus replaces local EventEmitter; grouped collapses into list filter
createPushRoutes (5 endpoints) src/routes/push/push.ts reworded — device_registration table folded into device_token; bulk send capped by maxBulkRecipients schema bound
APNService (initialize/sendNotification/sendBulkNotifications/shutdown/isInitialized) src/services/apn/apn.service.ts reworded — apn.Provider swapped for an injectable ApnTransport; same payload shape, same validateConfig rules, same mapResponse semantics
MemoryApnTransport (deterministic in-memory delivery) n/a (genesis-original) required because the genesis catalog omits @parse/node-apn
notification SurrealQL table upstream surql/tables/notification.surql slimmed — see ## Excluded from this migration
device_token SurrealQL table upstream surql/tables/device_token.surql slimmed

Requirements with no source line

Requirement Justification
100% test / branch / function / line coverage Genesis-wide policy (CLAUDE.md "Quality", AGENTS.md §7).
defineCodes("NOTIFICATIONS_PLUGIN", [...]) namespace Genesis convention; avoids collision with EVENTS (@suaveplan/events) and EVENTS_PLUGIN (@suaveplan/api-plugin-events).
OTel instruments via metrics.getMeter("@suaveplan/api-plugin-notifications") Genesis-wide observability policy; emitted from production paths and asserted with a real in-memory MeterProvider (AGENTS.md §13).
Use ctx.eventBus from @suaveplan/api-plugin context, not a local EventEmitter Fixes the upstream smell where notifications fired into a private EventEmitter invisible to siblings.
Subpath exports per feature module AGENTS.md §6 — required so consumers can tree-shake.
Schemas owned locally @suaveplan/api-schemas on genesis lacks a notification subpath; the plugin's schemas live inside src/schemas/ until upstream is augmented.
Positional $p0-style params instead of named $id Genesis DatabaseAdapter.query(sql, params: readonly unknown[]) wraps params as {p0: …, p1: …}. Path-A escape valve documented in CLAUDE.md.
Injectable ApnTransport Genesis catalog omits @parse/node-apn (network-touching libraries are injected by the host). Matches the genesis convention for transport adapters and means the plugin's test suite is hermetic.

Verification approach

  • src/error-codes/error-codes.test.ts — pins every code is reachable from a real call site.
  • src/otel/instruments.test.ts — real MeterProvider, asserts instrument shape + emission.
  • src/schemas/notification-schemas.test.ts + src/schemas/push-schemas.test.ts + src/schemas/options.test.ts — Zod boundary tests.
  • src/repositories/notification-repository/notification-repository.test.ts — full repository surface against a stub SurrealAdapter.
  • src/repositories/device-token-repository/device-token-repository.test.ts — same coverage shape for device-tokens.
  • src/transport/apn-transport/apn-transport.test.ts — MemoryApnTransport behaviour including failure plans.
  • src/services/apn-service/apn-service.test.ts — full APN service surface, every error branch, transport-injection and factory pathways.
  • src/routes/notifications-routes/notifications-routes.test.ts — every route, every status code, ownership check, auth guard, event-bus emission, OTel emission.
  • src/routes/push-routes/push-routes.test.ts — register / unregister / list / send / send-bulk against an in-memory APN service.
  • src/plugin/plugin.test.ts — lifecycle: onInit registers repos + APN, double-mount guard, missing-dep failure, emits initialized/ready.
  • src/api-plugin-notifications.spec-conformance.test.ts — pins every public symbol re-exported by the package root.
  • tests/integration/full-boot.test.ts — real SurrealDB testcontainer, applies the two surql migrations, then full plugin boot + CRUD roundtrip on both repositories.

Migration impact

None — additive only. This is a brand-new package on genesis. Consumers will opt-in via createNotificationsPlugin() once they need notification routes.

Impact

Affected dependents

None on genesis today — package is additive. Downstream apps that previously had no notifications capability gain access.