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.Persistencebaseline (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 key | Source target | Scope |
|---|---|---|
notifier | /api/v2/notify | Main admin client; replaces its broad legacy alias |
notifierOverrides | /api/v2/overrides | Separate operator-override root; preserves the specific rewrite’s destination |
notify, notifyInbox, notifyAudit | /api/v1/notify, /api/v2/notify, /api/v1/notify/audit | Unchanged core, inbox and audit roots |
| Reporting/assurance/NIS2 keys | Their existing /api/v1/regulatory/* and /api/v1/assurance/* roots | Unchanged |
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
| Current | Target | |
|---|---|---|
| Deployables | notify-web, notifier-worker | notify-web + notify-worker (one family; the whole src/Notifier/ module was frozen at src/__Obsoleted/Notifier/ by NTF-10 on 2026-09-08) |
| Persistence | notify = 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 legacyStellaOps.Notify.Persistence. The legacy assembly’s embedded migrations are discovered without a resource prefix by the deployednotify-webhost (AddNotifyPersistence→AddStartupMigrations) and, viaNotifyMigrationModulePlugin, by platform-web’s central migrator — so any.sqladded there converges the shared platform database on the next rebuild of either live image, and editing the applied001_v1_notify_baseline.sqlin 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 callAddConsolidatedNotifyPersistence(fresh target migrations + canonical own-DB resolver) and thenAddNotifyRuntimeRepositories(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.
| Group | Tables | P13 class | Disposition |
|---|---|---|---|
| Config plane | channels, templates, rules, digests, localization_bundles, anomaly_subscriptions, operator_overrides | operational | carry |
| Delivery plane | deliveries (+ monthly partitions — the estate’s good P13 precedent, keep), inbox, dead_letter_entries | windowed | carry with retention headers |
| On-call/escalation | escalation_policies, escalation_states, on_call_schedules, quiet_hours, maintenance_windows | operational | carry |
| Storm/throttle/fallback | storm_runtime_*, throttle_configs, correlation_runtime_throttle_events, fallback_runtime_* | operational/ephemeral | carry |
| Incidents | incidents, correlation_runtime_incidents, incident_report_timeline_states | operational/audit | carry |
| Webhook security | webhook_security_configs, webhook_validation_nonces | security/ephemeral (nonces) | carry; nonce retention class explicit |
| Retention runtime | retention_policies_runtime, retention_cleanup_executions_runtime | operational | carry — but every cleanup path composes with the SCR-4 obligations (a tenant-deleted cascade handler registers here in NTF-7) |
locks | local lease | ephemeral | reconcile 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_due | default-off SGN-7 NIS2 custody workflow + opaque scheduler index | operational/replay | active 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_handoffs | default-off NTF-9 producer EventId/receipt claim | regulatory source-of-truth | retained for at least the corresponding incident evidence/ledger horizon and never pruned while its producer event can replay; RLS tenant-keyed |
pack_approvals | Task-Pack approval records | operational | split with 012: approval STATE → JobEngine; notification RECORDS stay |
| Tenant machinery | tenant_cross_grants, tenant_isolation_violations, tenant_resource_ownership | operational/audit | ANSWERED (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 |
audit | notify audit | audit | CORRECTED 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)
Row-level security is carried at PARITY for the 001 baseline, not widened. Those 21 tables are
ENABLE/FORCE ROW LEVEL SECURITYwith the same (measured 2026-08-18: baseline 001 forces 21, the LIVE schema forces 22 — the extra one is a live table this design drops, so “parity” holds for the carried set but a window script driven from this list under-covers the live schema by one. Sprint 015 F-NTF9-4.)tenant_id = notify_app.require_current_tenant()policy as the pre-merge schema. Extending it to the tables that never had it (e.g.pack_approvals,dead_letter_entries, the tenant-machinery trio) is a behavioural change that belongs to NTF-4, with the writers that would then have to setapp.tenant_id. SGN-7 migration 002 adds one new FORCE-RLS work table, so the staged schema forced 22 at SGN-7; NTF-9 migration 003 adds the tenant-keyed producer receipt claim, bringing the current staged schema to 23. The opaque due index deliberately has no payload and no RLS policy.RISK for NTF-9, narrowed 2026-08-22:
FORCE ROW LEVEL SECURITYbinds the table owner too, but the old claim that no full-family DAL path setsapp.tenant_idwas an incomplete tree-local search.NotifyDataSourceinherits sharedDataSourceBase.ConfigureSessionAsync, which parameterizes bothapp.current_tenantandapp.tenant_id; ordinary repositories already open tenant connections through it. A real NOBYPASSRLS owner + one-connection A→B→A test now pins channel/delivery write/read and cross-tenant invisibility. The owner therefore remainsNOBYPASSRLS;notify_adminmembership and a standing owner bypass are both rejected.The Notify-owned source prerequisite is now implemented, default-off. One shared
ReplicatedNotifyActiveTenantSourceenrolls through Authority’s approved closedAuth.ServerIntegrationclient seam, refuses a census until the durablecatalog.replica.tenantscheckpoint exists, and returns only activetenant_idslugs. Anomaly dispatch intersects that census with tenant-scoped subscriptions; Delivery dispatch selects pending work through one tenant-scoped connection per catalog tenant. The flag-off branch deliberately preserves the live predecessor’s selectors until the NTF-9 cutover. The concrete replica/checkpoint seam lives in consolidated persistence, which owns the one attributedstellaops_notifypool used by Eventing and CatalogReplication. Its cross-roleINotifyActiveTenantSourcecontract lives in the already-sharedStellaOps.Notify.Modelsleaf, so Delivery does not pull the staged persistence implementation into the rollbacknotifier-worker. The original persistence assembly forwards the public type for binary compatibility.notify-workerregisters the default-off tenants drain and deliberately reaches both the same-family legacy DAL (repository-only againststellaops_notify) and the extracted Delivery library. Its predecessor-equivalent event and dispatch composition root is now source-wired behindNotify:DeliveryPipeline:Enabled, which is absent/default-false in the staged estate. The enabled branch refuses to register either consumer unless the checkpoint-backed tenant census and a realNotify:QueueRedis/NATS connection are also present. It reuses consolidated persistence’s one attributed pool for both the DAL and builtin credential store, with the credential migration and fail-closed KEK probe ordered before either consumer. This source slice does not set those activation values, deploy an image, fence the predecessor, copy data, or claim a pipeline cutover.The Authority-owned source enrollment is now present in both tracked Standard plugin manifests:
stellaops-notifyretains its fournotify.*scopes, adds onlycatalog:replicate, and carriesnotify stellaopsaudiences so the token passes both the service and router-gateway boundaries. Authority was reconciled and restarted on 2026-08-22; the scoped token then read the protected tenants feed with HTTP 200.NOTIFY_TENANTS_REPLICA_ENABLEDremains false for the other NTF-9 gates, and the staged overlay sets no successor delivery activation flag. Five other legacy FORCE-RLS system methods and the pending-escalation scan have no production callers; helper-based retention/dead-letter/tenant-admin operations target tables this baseline does not FORCE. Ack external-ID lookup already receives the signature-validated route tenant and is tenant-scoped in the earlier NTF-9 source slice.pack_approvalsis carried WHOLE pending the 012 split (S1 item 4). The approval-state columns retire in a forward migration once JobEngine and Notify agree the mechanics; dropping columns ahead of that agreement would deviate from an undecided item.DC-26 is deliberately deferred. The recipe’s S3 says to delete the module’s
IMigrationModulePluginfromPlatform.Database/MigrationModulePlugins.csin the same change. Its stated hazard — platform-web racing the new migrator on the same schema — cannot occur here, because the fresh baseline is a different assembly targeting a different database. Removing the plugin now would instead be a live change to platform-web’s convergence of the still-authoritative sharednotifyschema. It is removed with the cutover (NTF-9), where the shared schema is abandoned and grants are revoked. (Done: plugin removed 2026-09-09 by CM-2; theIMigrationModulePluginmechanism was deleted 2026-09-14 by SPRINT_20260722_021 PLT-4.)
3. Build-boundary dispositions (2026-08-03 register)
- The cyclic pair is the headline:
notify-web → notifierandnotifier-worker → notify(the web host compiles the WORKER HOST project and vice versa — not a client seam). The merge replaces both directions with same-family libraries; the web host never references a worker host project again. Both pins die by consolidation. DONE at NTF-4 (2026-08-05). The worker host’s 154-file domain moved verbatim tosrc/Notify/__Libraries/StellaOps.Notify.Delivery;notify-web|notifieris resolved and its register pin deleted.notifier-worker|notifysurvived only while asrc/Notifierhost still exists, and retires with the deployable key at NTF-10 after the NTF-9 successor proof. notify-web → attestor+evidence-locker+signer— re-verified at NTF-4 (2026-08-05); the 2026-08-03 reading was wrong on three counts and is corrected here.- The SIGNER edge is the real one, but it serves the NIS2 CSIRT path, not CSAF:
Nis2CsirtSignerRegistrationcallsSigner.Infrastructure’sAddSignerPipeline()and bindsDsseSignerOptions/SignerEntitlementOptions/SignerReleaseVerificationOptions, andProgram.cscalls it unconditionally.ProductCsafSignerPipelineAdvisorySigneris real but lives inConnectors.Csaf, whichStellaOps.Notify.WebService.csprojdoes not reference — the web host loads onlyConnectors.NCSandConnectors.Shared. - The two halves of the signer pin have different dispositions.
Signer.Coreis a pure contract (interfaces, records, one enum; no persistence in its csproj) and is referenced directly by three Notify connector libraries — it is the natural closed client/contract seam and belongs to 019.Signer.Infrastructureis a composition root re-hosted inside notify-web; a closed-contract seam cannot absorb it, so it must be replaced by the Signer service’s API or by an 019-published client that composes signing without the producer’s DI root. Recorded, not built, at NTF-4. - “Zero direct usings” is REFUTED for attestor:
Connectors.Csaf/ProductCsafAdvisoryDsseService.csandConnectors.DoraInfoSharing/DoraInfoSharingDsseService.csboth useStellaOps.Attestor.Envelopedirectly (the latter through aliased usings, which is how the first sweep missed it) and both carry their ownProjectReference. It is CONFIRMED for evidence-locker:StellaOps.EvidenceLockerappears nowhere undersrc/Notify/**. - The evidence-locker pin is not Signer.Infrastructure baggage — that csproj has no EvidenceLocker reference. It comes from
Findings.Ledger → StellaOps.EvidenceLocker.Core, so killing the NIS2 ledger seam kills the evidence-locker pin; killing the signer seam does not. - EXECUTED by 019 SGN-7 item 19 (2026-08-20): notify-web now calls the existing Signer HTTP API through a Notify-owned wire adapter, compiles zero Signer projects, and no longer seeds the NIS2 custody key locally. Boundary commit
799e18c757c5b56b26391a86b0a14fccf5a7d66cretires the signer pin. The feature remains default-off; NTF-9 still gates the legacy channel, terminal-delivery, and Findings handoff described below.
- The SIGNER edge is the real one, but it serves the NIS2 CSIRT path, not CSAF:
notify-web → findings-ledger— VERIFIED (2026-08-03), and it is worse than a type edge: the NIS2 incident flow (Nis2IncidentTimelineLedgerHandoffServiceviaAddNis2IncidentLedgerEmitter) AppendAsync’s ledger events through in-process Findings.Ledger.Services code with its OWN configured connection string into the ledger database — a cross-database WRITE through foreign persistence (hard §2.7 violation, the write-side sibling of X12). OWNER RULING (2026-08-03, review item A3): the OUTBOX EVENT seam is approved — Notify emits on its own outbox, the ledger consumes and appends (idempotent by event id); durability over immediacy for compliance records. The in-process emitter dies at the merge. NTF-4 addenda (2026-08-05), each verified in source:- The write is active in every real deployment —
Program.csregistersAddNis2IncidentLedgerEmitterunderif (!isTesting)with no feature flag, and compose runs notify-web asProduction. - There is a second half nobody had recorded:
Nis2IncidentLedgerRegistrationalso callsAddStartupMigrations(schemaName: "findings", moduleName: "FindingsLedger.NotifyHost", migrationsAssembly: typeof(LedgerDataSource).Assembly). Notify converges another service’s schema at boot, concurrently with the real findings-ledger host. Removing the emitter must remove this too, or Notify keeps owning a foreign schema. - In the shipped compose it is not literally cross-database: the options fall back
findings:ledger:database:connectionString→Postgres:Notify:ConnectionString, and only the second is set, so both land on the same instance. That indirection is what lets it become a genuine cross-database write under any deployment that sets the first key. - The swap does NOT execute at NTF-4 (D-NTF4-3):
IOutboxWriter.AppendAsyncenlists in the caller’s connection and transaction, so the outbox row cannot commit atomically with the domain write until the web host is repointed atstellaops_notify(NTF-9), and the endpoints’ synchronousLedgerRef/LedgerStatusresponse fields must change shape first (NTF-5). The seam is already one interface wide (INis2IncidentLedgerEventEmitter, fourAppendAsynccall sites), so execution is small once those gates open. - NTF-9 successor source prepared, default-off (2026-08-23):
Nis2IncidentTimelineLedgerHandoffServicenow builds the Notify-ownednis2.incident.ledger-handoff.v1payload. Its stream/constants/DTO shape is isolated in the producer-owned BCL-onlyStellaOps.Notify.Nis2Ledger.Contractsproject; the original namespace is retained andStellaOps.Notify.Modelsforwards all moved public types for prebuilt compatibility. WithNotify:Nis2LedgerOutbox:Enabled=true,PostgresNis2IncidentTimelineOutboxStoreopens one tenant-attributedNotifyDataSourceconnection, upserts the timeline and appends streamnotify.nis2-incident-ledgerunder one transaction, then returnsQueuedonly after commit. Failure rolls both writes back. Migration003_nis2_incident_ledger_handoff_idempotency.sqladds a durable(tenant_id, event_id)producer claim in that same transaction. A replay may tentatively allocate another P6 envelope, but its losing transaction is rolled back and the original receipt is returned; different immutable content under the same EventId fails as a terminal conflict. P6 inbox envelope-id dedupe is not the logical-event guard. - Compatibility/window boundary: the flag is absent/default-false, so the synchronous Findings append and its
Success/Idempotentresponse remain the selected path. The compatibility adapter, Findings ProjectReference andnotify-web|findingspin are deleted only after the owner consumer exists and the NTF-9 window activates/proves the successor. Source preparation is not a liveness claim.
- The write is active in every real deployment —
- Inbound:
scheduler-web → notify(Notify.Models) — the 012 S0 records it; the merged family should publish the models/contract surface the scheduler needs as a closed seam, or 012/015 agree an event contract.
4. What S1 must decide
- Merge confirmed per ADR-039 D14 (no deviation; the empty
notifierschema makes the host fold trivial — the DB move is the real work). - DC-35
notify.locksdisposition (recommendation: P6). - The tenant-machinery answer — ANSWERED (§2: Notify-local, carry).
pack_approvalssplit mechanics with 012.- 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/fixtureswas 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.
- Routes (~10):
/api/v1/notifier/*(three legacy-alias shapes),/api/v2/notify,/api/v1/events/*,/api/v1/notifications,/api/v1/observability,/api/v1/assurance/reporting-timeline-profiles,/api/v1/regulatory/*— all tonotify.stella-ops.local— plus a PLATFORM-fronted subset (/api/v1/notify/{digest-schedules,quiet-hours,throttle-configs,simulate, escalation-policies,localizations,incidents}→ platform-web), the third platform-fronting instance the S0 sweeps have surfaced (after/api/analyticsand/api/v2/evidence) — the merge keeps Platform’s read-model surface distinct from family routes. - Console:
notifications.client.ts+notification-rollup.ts. - Deploy reality: both containers run live (
notify-web,notifier-worker) — live-service consolidation; S7+ needs windows. Compose keys/env consolidate at S8 with the own-database gate. - Pins’ exact types + tenant-machinery sweep: §2/§3.
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_001RAR-5/RAR-7; both gateway route tables now hold 22 routes and none of the eleven rows it replaced survives. Sinceced8a2d286tools/scripts/ntf5-swap-notify-routes.ps1reports the swap as RETIRED and exits 2 on both-Checkand 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 asSPRINT_20260904_001RRS-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
Eleven, not ten.
/api/v1/qa/fixtureswas missing from the S0 list. It is a live Microservice route tonotify-webservingAdvancedAssuranceFixtureImportEndpoints, registered unconditionally atProgram.cs:473— a QA fixture-import surface with a dedicated production gateway route. Carried with a successor because silently dropping a live route is worse, but NTF-7 owes it an environment gate or an explicit ruling; this is the §2.11 concern in HTTP form.The platform-fronted subset is a DEV-ONLY STUB, not a fronting.
MapNotifyCompatibilityEndpoints()is registered only insideif (app.Environment.IsDevelopment() || testingEnvironment)(Platform.WebService/Program.cs:1425-1432), andPlatformProductionCompatibilityGuardTestsasserts production exposes no/api/v1/notifyroute at all. In production those seven Console-facing paths reachplatform-weband it answers none of them; in Development they return fabricated constants (scheduleId: "digest-daily"). The route dies without a successor. Six of the seven names have real Notify implementations at other paths — four already reachable through^/api/v2/notify(.*)(quiet-hours,throttle-configs,escalation-policies,incidents), two not reachable at all today (simulate->/api/v2/simulate,localizations->/api/v2/localization, note the singular) — whiledigest-scheduleshas no implementation anywhere in the estate. So the merge does not “keep Platform’s read-model surface distinct from family routes” here: there is no Platform surface to keep.The largest surface in the family has no route. The whole
/api/v1/notify/**API group —rules,channels,templates,deliveries,delivery/stats,digests,audit,locks, plusanomaly-subscriptions— reachesnotify-webonly through^/api/v1/([^/]+)(.*), which derives the hostname from the first path segment. It works purely because the deployable is namednotify. Route 89’s seven alternatives are interleaved inside that same external prefix but point at a different service:/api/v1/notify/rulesreaches notify,/api/v1/notify/quiet-hoursreaches platform.
5.2 Findings the S0 sweep had not surfaced
Fourteen
/api/v2/*endpoint groups are mapped on the live host and unreachable through the gateway (rules,templates,throttles,incidents,escalation-policies,oncall-schedules,escalations,ack,fallback,localization,quiet-hours,simulate,storm-breaker,overrides). They fall to^/api/v2/([^/]+)(.*), which derives a hostname from the first segment —rules.stella-ops.localand friends do not exist. One exception:/api/v2/overridesis reachable only through the^/api/v1/notifier/overridesrewrite, so removing that alias without the group dark-fires the operator-override surface. This makes NTF-7’s “what should/api/notify/v1serve?” a fourteen-item question, not a two-item one./api/v2/securityis answered by another service. Notify maps it (SecurityEndpoints.cs:16), but route index 103 sends^/api/v2/security(.*)tofindings-security.stella-ops.local, and that route is entry 8 of the FND-5 replaced set. Whichever way FND-9 goes, Notify’s security endpoints stay unreachable. NTF-7 should re-home them under/api/notify/v1/securityrather than contest a shared name.The event fan-in bypasses the gateway. The five in-estate producers (Scanner, Concelier, Integrations, ReleaseOrchestrator, Platform) post direct to
http://notify.stella-ops.local— compose pins the alias inReleaseOrchestrator__DeviationNotify__BaseAddressandSTELLAOPS_NOTIFY_URL— so the^/api/v1/events/(.*)route serves external callers only. The risk therefore belongs to NTF-8’s alias rename, not to this swap.PreserveAuthHeadersis inert here. All eleven replaced routes areIsRegex: true, andProgram.cs:368-371buildsJwtPassthroughPrefixesfromRoutes.Where(r => r.PreserveAuthHeaders && !r.IsRegex). Authorization is stripped on every one; identity already rides the signed envelope. The sprint’s “Bearer/envelope forwarding” trap is real but already resolved in the family’s favour.Half of NTF-7’s host wiring is a config change.
NotifyWebServiceOptions.ApiOptions.BasePathdefaults to/api/v1/notifyandProgram.cs:856maps it, so settingnotify:api:basePathre-homes sixteen endpoints onto the consolidated prefix with no code edit. Every other group hardcodes itsMapGroupliteral.
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:
ledgerRefdoes not come from the ledger.CreateLedgerRefbuilds it locally from request metadata beforeAppendAsyncis called, so it is fully available under an asynchronous append and needs no contract change.ledgerStatusis selected by the active path. The absent/default-false predecessor keeps{Success, Idempotent}. The successor returnsQueuedonly after the timeline/outbox transaction commits; validation, timeline-upsert or outbox-append failure rolls back and fails closed.- The Console will not break on a new value:
nis2-incident-dashboard.client.ts:544-545reads both fields as opaque strings, and no component branches onledgerStatus(the only other occurrence undersrc/app/featuresis a spec fixture).
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.
