Skip to content

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.Core knows 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.Config knows no OpAMP vocabulary. Pure YAML-parsing, normalisation, validation, lint and graph-rendering.
  • Ampora.Fleet is the only context that combines the two. It owns inventory, groups, rollouts, governance, GitOps and federation.
  • Ampora.Web consumes 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_ownership table 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:

  1. Soft scopingTenantId on every row, enforced in application queries. Default for small deployments.
  2. 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 DeletedAtUtc rather than hard DELETE, so audit trails and rollout history stay consistent.
  • Optimistic concurrencyRowVersion on 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