Platform architectureread it before you trust it

There is no black box in here.

Twenty-two services behind a single gateway, one database per service, and an event mesh between them. Every boundary on this page is one your reviewer can inspect on your own hardware.

Verified, not certified — every claim below names the check that produces it.

CLIENTS KONG · :8000 · ONE DOOR 22 SERVICES · ONE DATABASE EACH APACHE PULSAR EVENT MESH Operator console Next.js · browser · WebSocket Aircraft & docks MQTT · MAVLink · SRT Authority & partners REST · federated USS X-USER-ID · X-TENANT-ID · X-ROLES STRIPPED AT THE DOOR BEARER TOKEN PASSED THROUGH VERIFIED INSIDE EACH SERVICE auth-iam verifies every token · derives tenant flight-ops operations · conformance monitoring deconfliction F3548-21 · DSS client remote-id F3411 · SP + DP roles geofence ED-269 zone lifecycle evidence hash-linked audit trail no cross-service database access EventEnvelope one contract · canonical topics Asynchronous only publish and subscribe · never a table read Dead-letter topic bounded retries · the envelope survives 20 app services + 2 headless workers · Python 3.12 · FastAPI · async SQLAlchemy · one Alembic history each
Identity is derived, never received Kong strips X-User-ID, X-Tenant-ID and X-Roles. Each service verifies the bearer token itself.
No shared database Each service owns its schema and migrations. Integration is events, not table reads.

The shape of the system

22 Services behind one gateway 20 app services · 2 headless workers
1:1 Service to database separate schema · separate migrations
0 Identity headers trusted stripped at the boundary, always
RLS Tenant isolation in Postgres enforced under a non-owner role
The boundary

One door — and it does not believe what you tell it.

Every client reaches the platform through Kong on port 8000. Kong strips the identity headers a client might set, and it does not verify the token either. That second part is deliberate: verification happens inside each service, against the token, on every request.

As sent by the client
POST /api/v1/operations
Host: utm.example.gov:8000
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6…
X-User-ID: 3f2a-9c11-… stripped
X-Tenant-ID: ministry-ops stripped
X-Roles: platform_admin stripped
As the service receives it
POST /api/v1/operations
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6…
— identity headers are gone before routing
Identity, after the service verifies the token
sub · 3f2a-9c11 tenant · corniche-air roles · operator

The header claimed platform_admin and a different tenant. The verified token says operator at corniche-air. The service uses the token, because the header never arrives.

Removed, not merely ignored

X-User-ID, X-Tenant-ID and X-Roles are stripped at the gateway, so no service can receive them from outside. There is no configuration in which a forged header becomes an identity — the header simply is not there.

The gateway is not the authority

Kong routes, rate-limits and terminates TLS. It does not decide who you are. A gateway that both routes and authenticates becomes a single point of failure and a single point of trust at the same time.

Each service verifies the token itself

HS256 and RS256 with JWKS, in one shared library rather than twenty-two implementations. User, tenant and roles are derived from verified claims — never from transport, never from a trusted-network assumption.

Default deny is the backstop

Shared middleware refuses an unauthenticated request on any route that has not explicitly opted out. A newly added route is closed until somebody deliberately opens it, which is the opposite of the usual accident.

One honest caveat. The Docker Compose stack also publishes each service on a direct host port for development smoke tests. Those ports bypass the gateway, and they must not be exposed in production — the deployment guide says so, the production preflight checks it, and we would rather you read it here than find it yourself.


Tenancy

Row-level security — and the condition under which it does nothing.

Every tenant-scoped table carries a PostgreSQL row-level security policy. That is the easy half. The half most vendors leave out of the datasheet is that RLS does not apply to the role that owns the table.

Isolation off the failure mode

Connected as the database owner

The owner bypasses every policy. A tenant-scoped query returns every tenant's rows and nothing anywhere warns you. This is documented PostgreSQL behaviour, not a defect in the platform — which is precisely why it has to be designed against rather than assumed away.

SELECT … FROM operations2 431 rows
tenants represented9
Isolation enforced the required configuration

Connected as the application role

A non-owner role, provisioned by make db-setup after the migrations run. Policies apply on every read and every write. Cross-tenant reads and writes are covered behaviourally, so the isolation is a tested property rather than a diagram.

SELECT … FROM operations287 rows
tenants represented1
So the runtime refuses to start in the wrong one.

In production, if the application user is not set to the non-owner role, the preflight fails and the stack does not come up. A misconfiguration becomes an outage you notice at deploy time instead of a data leak you notice later.

scripts/production_preflight.py

POSTGRES_APP_USER must be the app role · fails closed when ENV is production

Why publish our own worst failure mode. Because a control you cannot inspect is a promise, not a control. If this page only said "multi-tenant with row-level security", you would have no way to tell whether the deployment in front of you actually enforces it. Now you know exactly which line to check.


Integration

Services do not read each other's databases.

One database per service, each with its own Alembic history. Services integrate by publishing onto an Apache Pulsar mesh under a shared EventEnvelope contract — never by reaching into another service's tables. It costs a little latency and buys the ability to change one service without a coordinated release of the other twenty-one.

flight-ops publishes operations.intent.v1 EventEnvelope · canonical topic deconfliction re-checks the volume evidence appends to the chain notifications tells the control room dead-letter topic bounded retries, then parked — replayable one publisher, any number of subscribers
One envelope

Every message carries event id, type, version, tenant and correlation id around its payload. A consumer can reject what it does not understand instead of guessing at it.

Canonical topics

Topic names come from a shared module, never a string literal in a handler. A typo becomes an import error at start-up rather than a subscription that is silently empty.

Failure is parked

Retries are bounded; what still fails lands on a dead-letter topic with the envelope intact, so it can be replayed once the consumer is fixed. Nothing is dropped quietly.

Migrations stay local

A schema change in one service cannot break another service's reads, because no other service reads it. That is the whole reason the databases are separate.


Deployment

Three modes. One codebase. The values file changes, not the platform.

An operator programme, a regulator and a national network run the same services with different surfaces enabled. There is no separate build, no "enterprise fork", and nothing you would have to migrate off if your remit grows.

Operator

Enterprise drone programmes
  • Plan → fly → prove, with SORA 2.5 and live conformance
  • Edge ingestion, video wall, fleet and crew management
  • Runs on your Kubernetes, or one Docker Compose host
PROFILE=operator

Authority

CAAs and ANSP innovation units
  • Approval workbench with jurisdiction rules and SLAs
  • ED-269 zone authoring and cross-operator supervision
  • Hash-linked audit trail with tenant-scoped export
PROFILE=authority

National

Country-scale programmes
  • DSS federation, Network Remote ID and CISP interfaces
  • Multi-authority delegation and cross-border workflows
  • Multi-region deployment with geo-replication values
PROFILE=national

Air-gapped is a first-class path, not a workaround.

The disconnected install is the same stack as the connected one. Images come from your private registry, licences are signed and verified locally, and nothing calls home to stay running — so a vendor outage cannot switch a control room off. What you give up is listed plainly in the air-gapped playbook, because there is a real list.

Container imagesPrivate registry mirror
Licence verificationSigned · offline · no phone-home
Map and terrain dataSeeded tiles, held locally
Local AI modelsWeights shipped in the bundle
UpdatesStaged bundles with rollback

Conformance

What is implemented, and how you check it yourself.

No independent conformance scheme for UTM platforms exists in most jurisdictions today. Be wary of anyone who implies otherwise. What we publish instead is the scope of each implementation, the maturity label it honestly carries, and the command that regenerates its evidence on your own stack.

Standards ledger — dated runs, or an explicit cadence. Never a vague claim.
Standard What is implemented How it is evidenced Status
ASTM F3548-21 Strategic deconfliction: operational-intent sharing, conflict detection and constraint awareness against a real DSS. InterUSS qualifier suites pass with zero findings above Low severity. make uss-qualifier · 2026-06-10 GA
ASTM F3411 v19 / v22a Network Remote ID in both the Service Provider and Display Provider roles, on one deployment. Verified by the same qualifier run against the reference DSS. make uss-qualifier · 2026-06-10 GA
JARUS SORA 2.5 Full ground and air-risk methodology with the complete SAIL table. The go/no-go it produces is advisory — a human still approves. Behaviourally pinned, not a lookup stub. SAIL matrix suite · every CI run GA
EUROCAE ED-269 Geographical-zone lifecycle: authoring, versioning, activation windows and publication, validated against the ED-269 model. Schema contract tests; the official schema gate is a licensed capability. ED-269 schema suite · every CI run GA
EU 2021/664 U-space Mandatory U-space service coverage across the platform capability set, including traffic information. Capability contract tests. Field experience is still narrow — hence the label. capability suite · every CI run Beta
ASTM F3548-21 ASTM F3411 JARUS SORA 2.5 EUROCAE ED-269 EU U-space Verified, not certified.

Day two

The part that decides whether it survives a year.

Anyone can demonstrate a platform on a good day. These are the properties your infrastructure team will ask about in the second meeting, and the reason they usually say yes.

Observability, in the chart

Prometheus, Grafana, Loki, Jaeger and SLO alerts ship inside the Helm chart rather than as a separate project you have to assemble. A correlation id issued at the gateway follows the request through every service log and every span.

/healthz liveness · /readyz readiness · uniform error envelope

Backups you have actually restored

Scheduled backups are unremarkable; a restore nobody has rehearsed is a story you tell an inquiry. Restore drills run in CI, and CloudNativePG with point-in-time recovery is available on the high-availability path.

restore drill in CI · PITR on the HA path

Your identity provider, not ours

OIDC federation to the IdP you already run, with roles mapped by allowlist rather than by whatever a token happens to assert. There is no vendor-hosted account directory and no shadow set of users to deprovision separately.

OIDC SSO · allowlist role mapping

Licences that verify offline

Licences are signed and validated locally. The platform never calls a licence server to keep running, which is what makes air-gapped operation ordinary rather than exceptional — and what stops a vendor outage from becoming your outage.

signed offline licences · no phone-home

Give this page to your security reviewer.

The evaluation pack is the installer we ship to production, the architecture notes behind everything above, and the conformance suites you can re-run without asking us for permission.

No vendor account. No phone-home. Thirty days of the full tier on your own hardware.