# 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` 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` / `record` / `record` / `record` 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` | | 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.