Notify consolidation — S0 target design (SPRINT_20260722_015 NTF-1)

Status: Historical S0 design (2026-08-03), with the current routing disposition in section 0. Verification base for the historical design: the sprint’s recorded dig rounds (17 + the 2026-07-23 empty-shell finding), the live Notify.Persistence baseline (re-enumerated this pass), and the 2026-08-03 build-boundary register. ⚠ marks remaining verification.

0. Current routing disposition (RAR-6, 2026-08-30)

NTF-9’s database move is recorded DONE in SPRINT_20260722_015. It did not install the proposed /api/notify/v1 route group. The current source package repoints consumers to the host’s published native paths; it does not perform a future consolidated-host cutover.

Console registry keySource targetScope
notifier/api/v2/notifyMain admin client; replaces its broad legacy alias
notifierOverrides/api/v2/overridesSeparate operator-override root; preserves the specific rewrite’s destination
notify, notifyInbox, notifyAudit/api/v1/notify, /api/v2/notify, /api/v1/notify/auditUnchanged core, inbox and audit roots
Reporting/assurance/NIS2 keysTheir existing /api/v1/regulatory/* and /api/v1/assurance/* rootsUnchanged

HTTP verbs, request bodies and response adapters remain unchanged. The existing admin override model is not the native suppression-bypass model: the latter requires an actor, reason and positive duration, and revokes by POST /{id}/revoke rather than PUT/DELETE. Sending the admin model to /api/v2/notify/overrides would instead reach an explicit unsupported-model response. Template updates and delivery pagination also retain their pre-existing contract gaps.

No Console/CLI source caller uses the singular /api/v1/notifier/delivery* spelling: the client uses plural /deliveries, which does not match that regex. /api/v1/notifications* also has no Console/CLI source caller; the authored route-map probe now uses GET /api/v2/notify/inbox. The main/specific aliases remain until the served Console and authenticated native reads are verified. Source preparation is not a deployment or route-removal receipt.

Backend routes verified against c62a2aacfa453345d80802f0c35a4df79e37405e in NotifyApiEndpoints.cs, NotifyAdminCompatEndpoints.cs and OperatorOverrideEndpoints.cs. Re-verify with GET /api/openapi/aggregate and the Console registry/HTTP client specs. The routing runbook defines the current verification boundary. Sections 1-4 and 5.1-5.2 below retain historical design observations; their route indexes, wildcard assumptions and proposed group are not current execution guidance.

1. Historical current → target shape

CurrentTarget
Deployablesnotify-web, notifier-workernotify-web + notify-worker (one family; the whole src/Notifier/ module was frozen at src/__Obsoleted/Notifier/ by NTF-10 on 2026-09-08)
Persistencenotify = a 35±table notification platform; notifier = ZERO tables (empty shell — Notifier wires no migrations; drops trivially)connection-repointed existing DAL + separate fresh migration authority, database stellaops_notify, env STELLAOPS_POSTGRES_NOTIFY_CONNECTION fail-closed

NTF-3 staging (2026-08-04). The fresh baseline ships as a SEPARATE assembly, StellaOps.Notify.Persistence.Consolidated, not inside the legacy StellaOps.Notify.Persistence. The legacy assembly’s embedded migrations are discovered without a resource prefix by the deployed notify-web host (AddNotifyPersistenceAddStartupMigrations) and, via NotifyMigrationModulePlugin, by platform-web’s central migrator — so any .sql added there converges the shared platform database on the next rebuild of either live image, and editing the applied 001_v1_notify_baseline.sql in place would fail live startup on checksum. (2026-09-14, SPRINT_20260722_021 PLT-4: the plugin was removed by CM-2 on 2026-09-09 and the central migrator mechanism itself is now deleted; only the host path remains.)

F-NTF9-1 owner decision (2026-08-23): option 1, CONNECTION repoint. The 74-file repository layer is proven runtime code, not the defect, so it remains in StellaOps.Notify.Persistence. Both target roles now call AddConsolidatedNotifyPersistence (fresh target migrations + canonical own-DB resolver) and then AddNotifyRuntimeRepositories (the existing DAL without its legacy migration host). This was the pre-NTF-9 state; the database move is now recorded DONE, separately from routing and worker activation. Because the D12 name stays occupied by the surviving DAL, NTF-10 no longer promises to delete it or rename the consolidated assembly onto that name; the documented two-assembly split and D12 exception remain explicit.

2. Table inventory + dispositions (P13)

Re-enumerated this pass (35 CREATEs + partitioned deliveries twins).

Built at NTF-3: 32 tables; current staged schema after NTF-9 source preparation: 35. The arithmetic from the 35 CREATEs, verified against the shipped baseline by ConsolidatedNotifySchemaTests.Baseline_CreatesTheDesignSection2TableSet: 35 − deliveries_default (a partition, not a table) − audit (dropped by v1, see below) − locks (retired per DC-35) = 32. Forward migration 002_nis2_csirt_signing_work.sql adds the tenant-scoped durable work table and an opaque due table; 003_nis2_incident_ledger_handoff_idempotency.sql adds the tenant/EventId receipt claim, bringing the current fresh convergence to 35. Column shapes are the CONVERGED post-004 shape; the v1 data repairs (003/004 group-key backfills, 009 external_id backfill, 010 metadata prune) are deliberately not carried — a fresh database has nothing to repair.

GroupTablesP13 classDisposition
Config planechannels, templates, rules, digests, localization_bundles, anomaly_subscriptions, operator_overridesoperationalcarry
Delivery planedeliveries (+ monthly partitions — the estate’s good P13 precedent, keep), inbox, dead_letter_entrieswindowedcarry with retention headers
On-call/escalationescalation_policies, escalation_states, on_call_schedules, quiet_hours, maintenance_windowsoperationalcarry
Storm/throttle/fallbackstorm_runtime_*, throttle_configs, correlation_runtime_throttle_events, fallback_runtime_*operational/ephemeralcarry
Incidentsincidents, correlation_runtime_incidents, incident_report_timeline_statesoperational/auditcarry
Webhook securitywebhook_security_configs, webhook_validation_noncessecurity/ephemeral (nonces)carry; nonce retention class explicit
Retention runtimeretention_policies_runtime, retention_cleanup_executions_runtimeoperationalcarry — but every cleanup path composes with the SCR-4 obligations (a tenant-deleted cascade handler registers here in NTF-7)
lockslocal leaseephemeralreconcile per DC-35; no independent lease lineage. APPLIED: the fresh baseline creates no locks table; AddConsolidatedNotifyPersistence converges eventing.leases + eventing.fencing_token_seq; both target hosts then select UseConsolidatedNotifyLeaseBackedLocks after the retained repository DAL. The adapter preserves tenant/resource identity, owner, TTL, contention, expiry and exact-owner release over P6. Because ILockRepository returns only bool, it exposes mutual exclusion, not a fencing handle/transaction; a transactional writer must use ILeaseManager.EnsureFencedAsync directly. The predecessor keeps LockRepository/notify.locks unchanged for rollback. The NTF-9 data-only copy excludes the table permanently.
nis2_csirt_signing_work, nis2_csirt_signing_duedefault-off SGN-7 NIS2 custody workflow + opaque scheduler indexoperational/replayactive work is never pruned; terminal/refused rows receive a P13 retention disposition. The due row contains identity/time only and cascades from work. Eventing fenced leases provide ownership; no notify.locks revival
nis2_incident_ledger_handoffsdefault-off NTF-9 producer EventId/receipt claimregulatory source-of-truthretained for at least the corresponding incident evidence/ledger horizon and never pruned while its producer event can replay; RLS tenant-keyed
pack_approvalsTask-Pack approval recordsoperationalsplit with 012: approval STATE → JobEngine; notification RECORDS stay
Tenant machinerytenant_cross_grants, tenant_isolation_violations, tenant_resource_ownershipoperational/auditANSWERED (2026-08-03): Notify-local isolation machinery, carry. The only consumers are the family’s own PostgresTenantIsolationValidator (Notifier.Worker delivery-path guard) + SecurityEndpoints — no estate-level tenancy resolution happens here (violations are an AUDIT record, grants a local ACL). Any future estate-tenancy read rides the 027 replica, never these tables
auditnotify auditauditCORRECTED at NTF-3 (2026-08-04): already gone — do NOT recreate. notify.audit was dropped by v1 DEPRECATE-003 (DROP TABLE IF EXISTS notify.audit CASCADE, folded into 001_v1_notify_baseline.sql) in favour of timeline.unified_audit_events; NotifyAuditRepository is a deprecated facade whose readers are no-ops and whose writer emits to Timeline. The “carry” above was a re-enumeration slip — the collapsed baseline’s CREATE was counted without its later DROP. The fresh baseline omits it

2.1 Baseline decisions taken at NTF-3 (2026-08-04)

3. Build-boundary dispositions (2026-08-03 register)

4. What S1 must decide

  1. Merge confirmed per ADR-039 D14 (no deviation; the empty notifier schema makes the host fold trivial — the DB move is the real work).
  2. DC-35 notify.locks disposition (recommendation: P6).
  3. The tenant-machinery answer — ANSWERED (§2: Notify-local, carry).
  4. pack_approvals split mechanics with 012.
  5. NEW (from the §3 verification): the NIS2 ledger-handoff seam — owner API vs event; the cross-DB in-process write cannot survive the split.

Remaining verification CLOSED (2026-08-03):

CORRECTED at NTF-5 (2026-08-05) — read §5 before using the route bullet below. The count is eleven, not ten (/api/v1/qa/fixtures was missed); the PLATFORM-fronted subset is a Development/Testing-only stub, not a fronting, so the closing clause of that bullet describes a Platform surface that does not exist in production; and the largest surface in the family (/api/v1/notify/**) has no route at all — it rides the ^/api/v1/([^/]+)(.*) wildcard. The bullet is kept as the S0 record; §5.1 is the verified inventory.

5. Historical route proposal — NTF-5 verification (2026-08-05)

Sections 5.1-5.2 describe the route table at that date, before published-native routing and RAR-5 retirement. The proposed group below is not the current RAR-6 target. Section 5.3 records the current consumer disposition; section 0 takes precedence over historical claims.

The S0 line above said “Routes (~10) … plus a PLATFORM-fronted subset”. NTF-5 verified that against the live route table and the hosts. The count is nearly right; three of the characterisations were not, and each one would have cost the NTF-9 window a surface. Full evidence lives in tools/scripts/ntf5-notify-route-swap.json — the manifest that both ntf5-swap-notify-routes.ps1 and NotifyConsolidatedRouteConfigTests read. Window procedure: docs/runbooks/notify/notify-cutover-ntf5-route-swap.md.

Currency (2026-09-04) — this swap is RETIRED; the script refuses. The 2026-08-28 owner ruling (“all microservices needs to autopublish no router configuration for microservices endpoints”) was executed by SPRINT_20260809_001 RAR-5/RAR-7; both gateway route tables now hold 22 routes and none of the eleven rows it replaced survives. Since ced8a2d286 tools/scripts/ntf5-swap-notify-routes.ps1 reports the swap as RETIRED and exits 2 on both -Check and a bare run. Notify is served by the auto-published layer. Read the runbook banner before acting on anything in this section; remediation is tracked as SPRINT_20260904_001 RRS-1.

Consolidated group: ^/api/notify/v1(.*) -> http://notify.stella-ops.local/api/notify/v1$1 (Microservice, regex), replacing eleven routes across seven external namespaces. Nothing is installed live at this stage; the deliverable is the manifest, the script, the guard and the runbook.

5.1 Corrections to the S0 route inventory

5.2 Findings the S0 sweep had not surfaced

5.3 Current Console source (RAR-6)

The Notify family remains centralised in api-path-prefixes.ts. RAR-6 changes notifier to /api/v2/notify and adds notifierOverrides at /api/v2/overrides; the existing notify, notifyInbox, notifyAudit, assuranceReportingTimelineProfiles, regulatoryReportingTimelines and nis2IncidentWrites values remain unchanged. check-console-notify-prefix-literals.sh covers both native roots and keeps literals out of non-spec application code. Its self-test includes the separate overrides root and a similarly named non-matching namespace.

notifications.client.ts derives the inbox base from both registry values, not from a hardcoded regex replacement. This still protects a future prefix change, but does not mean that all values should now converge on /api/notify/v1. That earlier namespace proposal requires separate host/publication evidence. The source/deployment split and unchanged semantic gaps are specified in section 0.

5.4 NIS2 ledger response contract (D-NTF4-3 -> D-NTF5-1)

Historical NTF-5 preparation record, separate from the RAR-6 prefix-only change. The reshape was prepared at source for NTF-9 behind the default-off activation gate:

Notify:Nis2LedgerOutbox:Enabled is not set by repository defaults or the staged overlay. Therefore a pre-window source deployment retains the live wire values; the response changes to Queued only with the deliberate NTF-9 activation. Findings consumption/checkpoint proof and removal of the compatibility source edge remain window gates. Recorded as D-NTF5-1.