# Architecture Notes > Architect's design rationale for the typescript-federation package. > Documents accommodation of Matrix-3 integration requirements while preserving federation invariants. --- ## Design Philosophy This package implements a classic **Anti-Corruption Layer** (Fowler) sitting between: * **Federation / Edge** (MXENV/MXURI semantics + mailbox transport constraints), and * **Local component runtime** (your existing component system / handler contracts). The trick is: **make the integration seam tolerant and observable**, while keeping the core routing/security invariants rigid. This is implemented as a "strangler fig" compatibility layer, not a rewrite. --- ## Invariants (MUST NOT VIOLATE) The invariants live in the **federation boundary**, not in the developer ergonomics: | Invariant | Description | |-----------|-------------| | **Mailbox pattern (no wildcards)** | EdgeGateway subscribes to *exact* ingress topics (`public.ingress.` and/or `mx/in/1//00`) | | **Semantic URI ≠ delivery topic** | MXURI parsing canonicalizes to `toRealm` + `toPath`; delivery goes to mailbox topic | | **Anti-relay** | Block any attempt to turn EdgeGateway into an open relay; enforce `replyTo.realm == from.realm` | | **Replay / rate limit / policy** | Centralized at ingress boundary, not on local bus | **Core principle:** Strict at trust boundaries, tolerant inside the trust domain. --- ## Accommodations Implemented ### P0 - Integration Seam (Must-Have) #### #7 Internal Request Format **Invariant pressure:** None (intra-realm only, local bus). **Pattern:** Bounded context with stable contract + extension points. **Implementation:** 1. Documented internal wire contract: ```typescript interface InternalRequestWire { correlationId: string; replyTo: string; // internal topic (string) params: unknown; // decoded body _mx?: { // optional (config) fromRealm: string; incomingProfile: 'matrix3' | 'mxenv1'; msgId?: string; principalKey?: string; traceId?: string; }; envelope?: unknown; // optional (config) } ``` 2. Config switches on EdgeGateway: | Option | Default | Purpose | |--------|---------|---------| | `includeInternalMeta` | `true` | Set `false` if handlers are strict-schema and choke on extra fields | | `includeInboundEnvelope` | `false` | Set `true` if handlers want full inbound envelope visibility | This is the **Strategy/Policy** pattern at the seam without infecting the core. #### #8 Lenient Internal Replies **Invariant pressure:** None (local bus only). **Pattern:** Tolerant Reader (Postel's Law) + consumer-driven contract. **Implementation:** `normalizeInternalReply(rawPayload, fallbackCorrelationId)` EdgeGateway accepts **any** of these reply shapes: | Shape | Interpretation | |-------|----------------| | `{ correlationId, ok, body, error? }` | Full strict reply | | `{ ok: true, body: ... }` | Semi-strict (no correlationId) | | `{ ok: false, error: { code, message } }` | Error reply | | `{ result: ... }` | Inferred `ok=true`, `body=result` | | Raw JSON value (`"hello"`, `123`, `{...}`) | Inferred `ok=true`, `body=` | | Invalid JSON string | Inferred `ok=true`, `body=` | This allows existing handlers to work without forced schema migration. #### #5 Matrix-3 Compatibility Tests **Invariant pressure:** None (proof harnessing). **Implementation:** `tests/integration/matrix3_compat.test.ts` Covers: - Legacy request envelope with `ttlMs` - Flat string `replyTo` - Legacy replies with `ok:true/false` detection - Legacy URI parsing (no `/-/` delimiter) - Lenient internal reply `{ result: ... }` end-to-end Executable evidence, not hand-wavy claims. --- ### P1 - Observability + Typing #### #3 EdgeGateway Events **Invariant pressure:** None (pure side-effects). **Pattern:** Observer (GoF) / structured tracing hooks. **Implementation:** ```typescript interface EdgeGatewayOptions { onInboundRequest?: (envelope: TaggedEnvelope, internalTopic: string) => void; onInboundReply?: (envelope: TaggedEnvelope) => void; onOutboundReply?: (replyEnvelope, ingressTopic: string) => void; onTimeout?: (correlationId: string, toPath: string) => void; onDrop?: (reason: string, topic: string, rawPayload: string) => void; onSecurityBlock?: (reason: SecurityBlockReason, envelope: TaggedEnvelope) => void; } ``` Makes integration debugging feasible (especially with `nats sub` and correlated logs). #### #6 Export Envelope Type Aliases ```typescript export type { CrossRealmEnvelopeLegacy as CrossRealmEnvelope, ReplyEnvelopeLegacy as ReplyEnvelope, } from './envelope'; ``` Low-friction TypeScript ergonomics: import from package and keep type compatibility. #### #9 TTL Clarification | Field | Semantics | |-------|-----------| | `MxEnvelopeV1.ttl` | Hop limit (routing TTL) | | Legacy `ttlMs` | Expiry duration (milliseconds) | The compat layer **does not coerce** `ttlMs` to `ttl` (different units, different semantics). --- ### P2 - Transport Ergonomics #### #2 Transport Interface Meta ```typescript interface Transport { publish(topic: string, payload: string, meta?: TransportMeta): void; subscribe(topic: string, handler: (topic, payload, meta?) => void): () => void; } interface TransportMeta { qos?: 0 | 1 | 2; retain?: boolean; // extension keys allowed } ``` **Pattern:** Ports-and-adapters. Core stays oblivious, adapters preserve delivery guarantees. ### Metrics + Config Validation **Implementation:** ```typescript // Fail-fast validation const validation = EdgeGateway.validateOptions(opts); if (!validation.valid) throw new Error(validation.errors.join('; ')); // Runtime metrics const metrics = edge.getMetrics(); // { inboundRequests, inboundReplies, outboundReplies, timeouts, securityBlocks, pendingRequests } edge.resetMetrics(); ``` --- ## What Was NOT Implemented (And Why) ### File Naming (PascalCase) Safe but cosmetic. Cross-platform casing hazards (macOS vs Linux CI). Do as a single mechanical commit with consistent import updates if desired. ### Raw Mode With lenient reply normalization + `includeInboundEnvelope`, raw mode is largely redundant. If still needed, formalize an `InternalCodec` Strategy - but that's a bigger API commitment. ### Graceful Shutdown (Drain Pending) Lifecycle semantic decision: fail fast (send negative replies) or wait? Should be pinned to product requirements before implementing. --- ## Integration Recipe Minimal "no churn" bridge wiring: ```typescript import { EdgeGateway, WIRE_PROFILES, } from '@open-matrix/federation'; const edge = new EdgeGateway({ localRealm: 'matrix-3', profile: WIRE_PROFILES.matrix3, // legacy mailbox + legacy envelope backbone: backboneTransport, // NatsTransport instance localBus: localTransport, // NatsTransport or InMemoryBroker // Integration seam knobs: includeInternalMeta: false, // if handlers are strict schema includeInboundEnvelope: true, // if handlers want full envelope visibility // Observability: onInboundRequest: (env, topic) => console.log('IN', env.profile, topic), onOutboundReply: (rep, ingressTopic) => console.log('OUT', ingressTopic), onTimeout: (cid, toPath) => console.warn('TIMEOUT', cid, toPath), onSecurityBlock: (reason) => console.warn('SECURITY', reason), }); edge.start(); ``` On the internal side, your component runtime just needs to: 1. Subscribe to internal topic `mx/1/matrix-3/` 2. Read `replyTo` from the request payload 3. Publish *any* reply shape to `replyTo` (raw value is fine) No forced schema migration. --- ## Future Enhancements (Invariant-Safe) The next "developer happiness" increment is to formalize an `InternalCodec` Strategy (encode/decode) to plug existing internal envelope formats 1:1. But with the changes above, this is likely unnecessary - integration without churn is already achieved while keeping the federation boundary strict.