Architecture¶
This page is a tour through Ampora's architecture at three levels of zoom: the bounded contexts in the codebase, the runtime topology of a deployed server, and the scale-out story from single instance to multi-region federation.
Bounded contexts¶
Ampora is a layered .NET 10 solution. The boundaries are deliberate — the OpAMP protocol layer must not know about Collector concepts, and vice versa — so the same OpAMP server can manage non-Collector agents in the future.
┌────────────────────────────────────────────────────────────────────┐
│ Ampora.Web (Blazor Server UI + REST API + endpoints) │
└─────────────┬──────────────────────────────────────────────────────┘
│ Application services
┌─────────────┴──────────────────────────────────────────────────────┐
│ Ampora.Fleet (inventory, groups, rollouts, governance) │
└──┬──────────────┬─────────────────┬───────────────┬────────────────┘
│ │ │ │
┌──┴──┐ ┌────────┴───────┐ ┌──────┴─────┐ ┌─────┴────┐
│Auth │ │ Ampora.OpAmp │ │ Ampora. │ │ Ampora. │
│OIDC │ │ .Core (proto- │ │ Collector. │ │ Persist- │
│ │ │ col, sessions) │ │ Config │ │ ence (EF)│
└─────┘ └────────────────┘ └────────────┘ └────┬─────┘
│
┌────────────────────┴────┐
│ PostgreSQL (or SQLite │
│ in development) │
└──────────────────────────┘
Strict ownership rules:
Ampora.OpAmp.Coreknows no Collector vocabulary. A configuration is an opaque blob plus a hash. This is what lets Ampora manage any OpAMP-capable agent — not only the OpenTelemetry Collector.Ampora.Collector.Configknows no OpAMP vocabulary. Pure YAML-parsing, normalisation, validation, lint and graph-rendering.Ampora.Fleetis the only context that combines the two. It owns inventory, groups, rollouts, governance, GitOps and federation.Ampora.Webconsumes application services, never repositories directly.
This isolation means an unrelated change to the Collector config schema cannot break OpAMP wire-format handling, and vice versa.
Runtime components¶
A standard production deployment has four moving pieces:
| Component | Purpose | Notes |
|---|---|---|
| Ampora server | Blazor UI + REST + OpAMP WebSocket + background services | One container; stateless w.r.t. session ownership when paired with the dispatch backplane |
| PostgreSQL | Primary persistence | 16+. Required in production. SQLite is dev-only. |
| Reverse proxy / Ingress | TLS termination, sticky sessions | NGINX, Traefik, k8s Ingress, Application Gateway — all supported |
| OpenTelemetry Collector | Self-observability sink | Ampora dogfoods OTel; metrics + traces are exported via OTLP |
Optional components depending on the feature surface you enable:
- Redis — cross-instance event bus when you do not want to use Postgres LISTEN/NOTIFY (see Dispatch backplane).
- MinIO / S3 — package artefact storage for binary updates.
- HSM / KMS — at-rest signing key protection (AWS KMS, Azure Key Vault, GCP KMS, PKCS#11, HashiCorp Vault Transit).
Single-instance topology¶
The simplest, supported, production-shape topology:
flowchart LR
Client[Operator browser] -- HTTPS --> LB[Reverse proxy]
Agent[OpAMP agents] -- WSS --> LB
LB --> Ampora1[Ampora]
Ampora1 --> PG[(PostgreSQL)]
Ampora1 --> OTel[(OTel collector)] Suitable for fleets of a few hundred agents. Single-instance means a planned restart drops all WebSockets — agents reconnect automatically with exponential backoff.
High-availability topology¶
Multiple Ampora instances behind a sticky-session load balancer share a single PostgreSQL cluster:
flowchart LR
Client[Operator browsers] --> LB[Sticky LB / Ingress]
Agent[OpAMP agents] --> LB
LB --> A1[Ampora #1]
LB --> A2[Ampora #2]
LB --> A3[Ampora #3]
A1 --> PG[(Primary PostgreSQL)]
A2 --> PG
A3 --> PG
A1 <-->|Backplane| A2
A2 <-->|Backplane| A3
A1 <-->|Backplane| A3 Important properties:
- Session ownership — every connected agent's session is "owned" by the instance it talks to. The
agent_session_ownershiptable records that ownership with a fencing token. - Cross-instance dispatch — when an operator clicks "Push config" on instance #1 but the target agent's session lives on instance #3, the request is routed through the backplane (Postgres LISTEN/NOTIFY or Redis pub/sub) to the owning instance.
- Leader-elected work — periodic jobs (drift reconciler, GitOps poller, audit retention, certificate roll) run on a single elected leader at a time, with a fencing token to defeat split-brain.
- Sticky sessions — Blazor Server requires affinity to a single instance for the SignalR circuit. The reverse proxy must hash on the appropriate cookie or header. See Scaling out for proxy-specific configuration.
Federated topology¶
Two or more Ampora servers run independently per region/cluster and expose a read-only federation API to each other:
flowchart LR
subgraph Region_EU
U_EU[Operator EU] --> A_EU[Ampora EU]
A_EU --> Agents_EU[Agents EU]
end
subgraph Region_US
U_US[Operator US] --> A_US[Ampora US]
A_US --> Agents_US[Agents US]
end
A_EU <-->|Federation API mTLS + token| A_US Federation is bilateral and manual. Each side knows the other's mTLS thumbprint and a shared secret. There is no global control plane. Reads aggregate fleets across peers; writes (federated rollouts, cross-cluster handover) require explicit operator intent and have their own audit trail. See Federation tutorial.
Multi-tenant topology¶
Ampora supports two multi-tenant modes, configurable per deployment:
- Soft scoping —
TenantIdon every row, enforced in application queries. Default for small deployments. - Hard isolation — PostgreSQL Row-Level Security (ADR-036) plus per-tenant connection roles. The database itself rejects cross-tenant queries even if the application layer is wrong. Default for SaaS-style deployments.
In both modes the OIDC subject's tenant claim chooses the active tenant on every request; tenant theming (ADR-037) lets each tenant get its own brand, palette and login wall message without recompiling.
Persistence model¶
PostgreSQL is the only production-supported store. Key shape decisions:
- JSONB for semi-structured fields (
AgentDescription, capability bitfields, lint rule bodies) with GIN indexes on the high-cardinality ones. - Snapshot + history split — agent state is stored as a current snapshot and an append-only event history. Reads on the Fleet page hit the snapshot, audits and time-travel use the history.
- Immutable published versions — once a config is published, the row is effectively read-only. New changes are new versions.
- Soft delete — groups, agents, and configurations support
DeletedAtUtcrather than hardDELETE, so audit trails and rollout history stay consistent. - Optimistic concurrency —
RowVersionon draft configs and rollouts prevents lost updates on concurrent edits.
Where to dive deeper¶
| Topic | Page |
|---|---|
| Bounded-context rules and packaging | Contributor → Bounded contexts |
| HA wire-up | Operator → High availability |
| Dispatch backplane | Operator → Dispatch backplane |
| Multi-tenant hard isolation | ADR-036 |
| Federation protocol | ADR-050 |