Architecture
Architecture overview
The Matter system as a diagram and three architectural truths. Edge → middleware composition → service packages → domain event log → AuditEntry + CQRS read models + webhook delivery → Postgres + KMS + Rekor + provider ACLs.
Last updated
Matter's runtime is one Next.js application (apps/api) deployed at the edge, with a small set of bounded-context service packages, a canonical domain event log, three projections of that log (AuditEntry chain, CQRS read models, outbound webhooks), and external dependencies (Postgres, KMS, Sigstore Rekor, provider integrations) accessed only through typed wrappers.
The diagram below is the single source of truth. The three "architectural truths" listed underneath are the reasons it is shaped this way.
The diagram
┌─────────────────────────────────────────────────────┐
│ Customer / Agent / SDK / CLI │
│ (Bearer · mTLS · M2M OAuth · session JWT · SCIM) │
└──────────────────────────┬──────────────────────────┘
│
┌──────────────────────────▼──────────────────────────┐
│ Edge (Vercel / Cloudflare WAF) │
│ CSP · DNSSEC · CAA · HSTS · IP/ASN rate-limit · │
│ DDoS · TLS termination · cert pinning │
└──────────────────────────┬──────────────────────────┘
│
┌──────────────────────────────────▼──────────────────────────┐
│ apps/api │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware composition (every handler) │ │
│ │ context → security-headers → CSRF → auth → scope │ │
│ │ → maturity-gate → rate-limit → soft-limit-warn │ │
│ │ → request-validate → cost-track → idempotency │ │
│ │ → spend-guard → dry-run → audit-on-read → service │ │
│ │ → audit → emit-event │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Generated registries (codegen from openapi.yaml): │
│ validators · expand · scope · rate-limit · pii · encrypted │
│ · classification · slo · cost · template · mcp · ids │
│ · maturity · deprecation · i18n-messages │
└────────┬────────────────────────┬──────────────────┬────────┘
│ │ │
┌──────────────▼──────────┐ ┌───────────▼──────────┐ ┌─────▼─────────┐
│ Service packages │ │ Saga orchestrators │ │ Async workers │
│ (pure, DDD-bounded) │ │ formation · round │ │ (leased, │
│ ACL to providers │ │ dissolve · mfn │ │ heart-beat, │
│ no Next, no I/O outside│ │ intent-execute │ │ priority, │
│ Prisma + injected deps │ │ TLA+ verified │ │ shed-aware) │
└──────────────┬──────────┘ └───────────┬──────────┘ └─────┬─────────┘
│ │ │
┌──────────────▼────────────────────────▼──────────────────▼────────┐
│ Domain event log (Event table) │
│ canonical "what happened" — append-only, partitioned │
│ one row per state transition, multi-receiver fanout │
└──────────────┬────────────────────────┬──────────────────┬────────┘
│ │ │
┌────────────▼─────────┐ ┌───────────▼──────────┐ ┌─────▼─────────┐
│ AuditEntry chain │ │ CQRS read models │ │ Outbound │
│ hash-chained │ │ CapTable, Compliance│ │ webhook │
│ JCS-canonical │ │ maintained via CDC │ │ delivery │
│ Rekor + WORM anchor │ │ replay-from-zero │ │ (sharded, │
│ per-(org,mode) lock │ │ safety rebuild │ │ versioned, │
│ m-of-n genesis │ │ weekly full-rebuild │ │ 30-day │
│ │ │ │ │ replay) │
└──────────────────────┘ └───────────────────────┘ └──────────────┘
│ │ │
└────────────────────────┴──────────────────┘
│
┌───────────────────────────────────────▼─────────────────────────┐
│ Persistence (Postgres + Neon serverless) │
│ Per-tenant DEK envelope encryption · partitioned hot tables │
│ Logical replication → CDC consumer · read replicas (P11) │
│ Cold tier (S3 object-lock) · per-tenant DB tier ladder │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ External: KMS · Sigstore Rekor (+ local cosigning fallback) │
│ Provider ACLs (filing, bank, tax, mail, agent) per mode │
│ @repo/storage (Vercel Blob) · OTel collector (vendor-neutral) │
│ Logtail · Status probes (US/EU/APAC) │
└─────────────────────────────────────────────────────────────────┘
Customer-facing surfaces beside the API:
CLI (`matter <command>`) Status page (probes)
In-browser API Console Customer trace/log export
Dashboard request inspector Data export tarball
Webhook receiver mock (npm) SDK (Node, Python, Go)
Quota + soft-limit dashboard SLA + DPA + security packThree architectural truths
Truth 1 — The Event table is the trunk
Every state transition writes one row to the Event table first, inside the same transaction as the resource write. From that single canonical write, three projections are derived:
- AuditEntry chain — hash-chained projection for tamper-evidence. JCS-canonical. Anchored daily into Sigstore Rekor + a WORM bucket. Per-(org, mode) advisory lock during insert.
- CQRS read models —
CapTable,Compliance, and their snapshot tables, maintained via Postgres logical replication consumed by a CDC worker. Replay-from-zero supported. - Outbound webhook delivery — fan-out to registered
WebhookEndpoints. Sharded. Versioned. 30-day replay window per endpoint.
The Event table is canonical; the resource tables are also canonical for current state. The two are kept in lockstep by the service-layer write pattern: every service mutation function writes the resource row + the Event row + the AuditEntry row in one Prisma transaction. The CDC consumer + webhook delivery worker consume the Event row downstream.
Why this matters: a wiped database can be rebuilt deterministically from the Event log + Rekor anchors. The audit trail is not "logs we hope are intact"; it is a verifiable chain externally anchored. The customer view is not "computed once and hoped to be right"; it is a replay-from-zero projection of the canonical log.
Truth 2 — Every read path declares its cache layer
Every read endpoint annotates its cache layer in the spec via x-matter-cache-layer. The runtime middleware enforces:
| Layer | Description | Where it lives |
|---|---|---|
none | Compute on every request. | Default for low-traffic computed reads. |
request | Memoise within the request scope. | apps/api/lib/middleware/cost-tracker.ts keeps a per-request cache. |
redis | Per-token or per-resource warm cache. | Upstash Redis via @repo/rate-limit's connection. TTL declared per operation. Write paths emit invalidation. |
edge | CDN cache. Public read-only endpoints only. | Vercel CDN / Cloudflare. Used for /v1/api-versions, /v1/jurisdictions, /v1/templates, /v1/health. |
snapshot | Pre-materialised in a DB snapshot table. | CapTableSnapshot, ComplianceSnapshot. Fired on signed events; immutable; anchored hashes. |
Per x-matter-cache-target, each cached operation also declares its expected hit ratio. The dashboard surfaces actual vs target; alerts fire on regression.
Write paths emit invalidation hooks; the cache primitive (packages/cache/src/index.ts) routes invalidation by (resource_type, resource_id) plus optionally (token_id) for token-scoped caches.
Truth 3 — Every cross-boundary call is idempotent
Three idempotency layers:
- Request-side (
Idempotency-Keyheader). Token-scoped, 24h TTL. Replay returns the stored response withMatter-Idempotency-Replayed: true(orMatter-Idempotency-Async: truefor 202 replays — the webhook stream is not re-fired). - Internal-call (
apps/api/lib/internal/call.ts). Service-to-service calls auto-mint keys from(originating_request_id, target_operation_id, step_index). Internal retries are safe. - Provider-call (per-saga-step keys). Saga steps that touch external providers (filings, banks, IRS, signing) carry keys derived from
(saga_instance_id, step_index). Re-execution after a crashed worker does not double-file.
Worker leasing + heartbeating (P0.E2) ensures at most one worker holds a job at a time; if a worker crashes, the lease expires and another picks up. Combined with the idempotency layers, every saga step has at-least-once delivery semantics that resolve to exactly-once at the side-effect layer.
Layering
The boundary between apps/api (route handlers + middleware), packages/<resource>-service (pure business logic), and packages/api-providers/<provider>/acl.ts (external integrations) is mechanically defended:
apps/api/app/v1/**— Next route handlers only. ~30 lines each. Compose canonical middleware. No business logic.packages/<resource>-service— pure functions. No Next imports. No I/O outside Prisma + injected provider dependencies. Architecture tests enforce.packages/api-providers/<provider>/acl.ts— anti-corruption layer. Translates provider-shaped responses into canonical Matter domain types. Tested with conformance suites.
Module-boundary lint, per-package allowed-import lists, and architecture tests (under apps/api/__contracts__/architecture/) catch violations at PR time.
Why this shape
Matter's runtime is not a microservice mesh. It is a modular monolith with bounded contexts, deployed at the edge, with strict layering and strict event sourcing. Three reasons:
- The lifecycle is the product. Create / Manage / Exit is a single narrative; pulling it apart into microservices fragments the IA without buying scalability we need at this stage.
- Audit-chain integrity is sacred. A single canonical event log with a hash chain is verifiable by customers. A distributed event log across services is theoretically more scalable but adds attack surface, integration tests, and operational complexity for benefits we do not need at this stage.
- Operational simplicity multiplies with developer velocity. One deploy. One observability story. One on-call rotation. Until horizontal scaling forces us to split, we stay monolithic.
The scaling stages in the capacity plan anticipate when the trade-offs change. Stage 6 — dedicated webhook delivery cluster — is the first foreseen extraction. Stages 5 + 6 do not require a fundamental rearchitecture; they require shipping the work we already plan in P11.
See also
- API design philosophy — the ten principles.
- Bounded contexts — how packages map to domains.
- Capacity plan — how it scales.
- Threat model — the attacks this shape defends against.
- Matter API SLOs — the budgets the shape must meet.