| 6 | Edge cloud zones | *(none)* | ❌ No | SRM authoritative. FM reads via `GET /internal/zones`; stores only zone UUIDs when catalogued. |
| 7 | Service specifications / manifests | *(none)* | ❌ No | SRM owns the catalog. FM stores only the `service_specification_id` reference inside an agreement. |
| 8 | Service instance detail | *(none)* | ❌ No | SRM owns the instance record. FM keeps only `operation_id`/result summary in a transaction. |
| 9 | Capacity / reservations | *(none)* | ❌ No | SRM holds no stored capacity (ADR-0002); FM's `POST /internal/capacity-check` is best-effort, not persisted. |
| 7 | Edge cloud zones | *(none)* | ❌ No | SRM authoritative. FM reads via `GET /internal/zones`; stores only zone UUIDs when catalogued. |
| 8 | Service specifications / manifests | *(none)* | ❌ No | SRM owns the catalog. FM stores only the `service_specification_id` reference inside an agreement. |
| 9 | Service instance detail | *(none)* | ❌ No | SRM owns the instance record. FM keeps only `operation_id`/result summary in a transaction. |
| 10 | Capacity / reservations | *(none)* | ❌ No | SRM holds no stored capacity (ADR-0002); FM's `POST /internal/capacity-check` is best-effort, not persisted. |
### What is intentionally not persisted
@@ -143,12 +152,28 @@ anything owned by SRM is held only as a UUID reference.
| `supported_apis` | JSONB | NOT NULL, DEFAULT '[]' | API families the partner supports |
| `direction` | VARCHAR(10) | NOT NULL | `inbound` or `outbound` |
| `federation_operation_id` | UUID | | Opaque handle returned to partner as EWBI `operationId`; minted independently of `operation_id`. NULL for sync queries |
| `operation_id` | UUID | | SRM correlation when an async command was dispatched; NULL for sync queries. Never returned to partners |
| `correlation_id` | UUID | | Internal trace ID; mapped to/from `federation_correlation_id` at the boundary. Never returned to partners |
| `federation_correlation_id` | VARCHAR(255) | NOT NULL | External EWBI correlation ID |
| `api_type` | VARCHAR(100) | NOT NULL | e.g. `edge-cloud-deploy`, `qod-session` |
Indexes: `idx_fed_tx_partner`, `UNIQUE (federation_operation_id) WHERE federation_operation_id IS NOT NULL` (poll lookup key for `GET /ewbi/v1/service/operations/{operationId}`), `idx_fed_tx_operation`, `idx_fed_tx_status`, `idx_fed_tx_started`, `idx_fed_tx_fed_correlation`.
Indexes: `idx_fed_tx_partner`, `UNIQUE (federation_operation_id) WHERE federation_operation_id IS NOT NULL`, `UNIQUE (partner_op_id, api_type, idempotency_key) WHERE idempotency_key IS NOT NULL`, `idx_fed_tx_context`, `idx_fed_tx_operation`, `idx_fed_tx_status`, `idx_fed_tx_started`, `idx_fed_tx_fed_correlation`.
Retention: configurable per operator policy; consider partitioning by `started_at` quarter.
### J.2 NATS (DataBus)
FM publishes commands and consumes SRM events; the client-visible source of truth for federation
FM publishes SRM commands, consumes FM federation commands and SRM events; the client-visible source of truth for federation
status remains `federation_transactions` in PostgreSQL.
| Stream | Subjects | Role |
@@ -232,6 +264,11 @@ status remains `federation_transactions` in PostgreSQL.
| `OOP_TASKS` | `command.srm.service.{deploy,scale,terminate}`, `command.srm.network.capability.{activate,update,deactivate}` | FM publishes; SRM consumes. Every command carries `CommandEnvelopeV1` with `source=federation`, `federation_partner_ref`, `app_provider_id=partner.id` (ADR-0001, ADR-0018) |
| `OOP_EVENTS` | `event.srm.operation.status`, `event.srm.operation.completed`, `event.srm.catalog.updated` | FM consumes via durable `fm-event-worker` (explicit ack, max-deliver 3) |
OEG-proxied federation establishment is a separate FM command input under
`command.fm.federation.*` (ADR-0043), including `command.fm.federation.create` and
`command.fm.federation.zone_subscribe`. The command schema/stream binding remains a platform
integration item; edge-cloud deployment API types are not dispatched through this hand-off.
**Delivery guarantee:** FM writes the `federation_transactions` row (`pending`) **before** publishing;
on successful publish it sets `in_progress` locally. Duplicates are absorbed by SRM's
│ │ ├── errors.py # federation errors (no HTTP detail)
│ │ ├── topology_hiding.py # sanitize rules
@@ -278,15 +315,15 @@ federation-manager/
| Topic | Approach |
|---|---|
| Inbound AuthN/Z | OAuth2 Client-Credentials + signed JWT via Keycloak `federation` realm (`fed-mgmt` scope); FM validates against Keycloak's JWKS (ADR-0038) |
| Inbound AuthN/Z | OAuth2 Client-Credentials + signed JWT via Keycloak `federation` realm (`fed-mgmt` scope); FM validates against Keycloak's JWKS (ADR-0042) |
| Admin AuthN/Z | Keycloak JWT, `operator` role, on `/internal/*` admin routes only — same centralized Keycloak, `oop` realm |
| Outbound AuthN | OAuth2 Client-Credentials — FM fetches a token from the partner's realm using our registered client credentials (ADR-0038) |
| Future: mTLS defense-in-depth | Deferred until real Nginx mTLS termination exists (ADR-0038). No certificate columns are carried today; the trust material they would need is defined when that layer is actually built. |
| Outbound AuthN | OAuth2 Client-Credentials — FM fetches a token from the partner's realm using our registered client credentials (ADR-0042) |
| Future: mTLS defense-in-depth | Deferred until real Nginx mTLS termination exists (ADR-0042). `partner_ca_cert_path` supports ordinary HTTPS verification; it is not an mTLS identity field. |
| Topology hiding | mandatory `TopologyHider` on every EWBI response (REQ-FED-02); strips internal IPs, cluster/namespace/node names, pod UIDs, trace IDs |
| Header trust | FM must be reachable only via Nginx. FM trusts no caller-supplied identity header — partner identity comes solely from the validated JWT. |
| `federation_contexts` | Not yet | — | Required to resolve opaque OPG.04 `federationContextId` by partner and direction |
| `federation_agreements` | Full | `PostgresAgreementRepo.find_active_for_partner` (prefers the agreement valid now, else newest) | Used by inbound authorizer and outbound hand-off (`AgreementChecker`) |
| `routing_rules` | Full (incl. `UNIQUE(identifier_type, value_range, priority)`, partial active index) | `PostgresRoutingRuleRepo.list_active` | Matching lives in `domain/routing.py` (`RoutingResolver`): most specific match wins, `priority` breaks ties |
| `federation_transactions` | Full (incl. partial unique index on `federation_operation_id`) | `PostgresTransactionRepo.add`, `.record_outcome` | Written `pending` before the outbound EWBI call by `POST /internal/federation/outbound`; inbound `command.srm.*` path not wired yet |
| `federation_transactions` | Partial (current code lacks context, idempotency, request fingerprint, external resource, and callback columns) | `PostgresTransactionRepo.add`, `.record_outcome` | Written `pending` before the outbound hand-off; inbound `command.srm.*` path not wired yet |
## Revision History
| Version | Date | Summary |
|---------|------|---------|
| 2.0-draft | 2026-08-06 | Initial FM persistence model, promoted from requirements-and-design §J–§K. |
| 2.1-draft | 2026-09-08 | Align entities, context routing, transaction audit fields, ADR references, and DataBus roles with current FM architecture. |