Product documentation
Delivery-model architecture
How the same artefacts run as an EU-operated managed service or self-managed on the customer's Kubernetes — underwritten by a rebuildable-from-git discipline: Helm for infra, Flyway for schema, config as code.
Delivery-model architecture
Thoryn is delivered two ways — as an EU-operated managed (sovereign SaaS) service and as a self-managed distribution on the customer's own Kubernetes — and the two are co-equal, not a primary product with a footnote. This page explains what makes that possible architecturally: both models run the identical artefacts, and the operational discipline that keeps the managed service reliable is the same discipline the self-managing customer inherits. It is the deep version of white paper §13.
Same artefacts, two responsibility splits
There is no "enterprise build" and "cloud build". The container images, the Helm chart, the Flyway migrations, and the configuration schema are one set. What differs is who operates the stack, not what the stack is:
| Managed (sovereign SaaS) | Self-managed | |
|---|---|---|
| Runs where | EU-operated Thoryn cloud | The customer's Kubernetes |
| Who operates the infra | Thoryn | The customer, via the Helm chart |
| Who configures tenants/clients/policy | The customer (self-service console + API) | The customer (same console + API) |
| The operator plane | Thoryn's internal operator plane | The customer's own operator surface |
| Best for | Fast start, sovereign hosting, no ops burden | Air-gap, full data residency, dedicated infrastructure |
The customer plane — tenant configuration, OAuth-client registration, federation, RBAC/FGA, audit — is self-service in both models. That is what makes Thoryn a product rather than a professional-services engagement: the same console and product API drive configuration whether Thoryn or the customer runs the servers underneath.
The discipline: rebuildable from git, zero manual steps
The property that lets the same artefacts run reliably in both models is a hard rule:
The entire deployed state of any environment must be rebuildable from
gitplus a clean deploy, with zero manual steps.
Concretely, every change ships as code, through a pull request, reaching the cluster via a deploy:
- Infrastructure → Helm chart / templates / values (
deploy/helm/). - Schema → Flyway migrations (
V*__*.sql). - Configuration →
application*.yml. - Behaviour → source.
kubectl edit / patch / apply against live resources, console clicks, and hand-rolled
secrets are not how changes are made — they leave no reproducible trail and drift out of
sync on the next deploy. Emergency incident response is the only exception, and even then the
manual change is a mitigation that must be back-filled into the chart/migration/config in the
same or an immediately-following PR. A fix is "done" only when a clean, from-scratch deploy
reproduces the working state — not when the live cluster was patched into shape.
For a self-managing customer this is not an internal Thoryn nicety — it is the artefact they run. "Rebuildable from git" is simultaneously how Thoryn operates the managed service and what the customer operates themselves, which is exactly why it is treated as a first-class property of the product.
Source: CLAUDE.md (reproducible deployments), deploy/helm/.
The building blocks that make from-scratch deploys trustworthy
Four platform-wide patterns hold the "clean deploy reproduces the working state" promise:
Secrets and cryptography — OpenBao with auto-unseal
The cryptographic custody layer is OpenBao (the Linux Foundation's MPL-2.0 fork of Vault),
run on persistent storage with auto-unseal, using the Transit engine for all signing,
per-tenant envelope encryption, and HMAC. Signing keys never leave the backend
(Token lifecycle → signing). Auto-unseal
plus persistent storage is what makes a backend pod roll a non-event — the earlier -dev-mode
setup wiped keys on every restart and cascaded into sign-in outages, which is part of what the
migration (the vault-to-openbao ADR, adrs/2026-06-07-vault-to-openbao.md)
fixed. Per-service Redis ACL passwords are per-service Kubernetes Secrets rendered into the
ACL at deploy time — never in application.yml.
Schema — forward-patch only, never destructive
Flyway migrations are immutable once applied (a CI gate rejects in-place edits) and
recovery is forward-patch-only. DROP DATABASE / TRUNCATE / drop-and-re-bootstrap is
forbidden in every environment, staging included — a CI guard greps for it — because the
cost of destroying user-created data is unacceptable even in non-production. A schema-drift
incident is resolved by a new V<n+1>__*.sql that brings the schema to the intended state, or
escalated — never by nuking. This means a from-scratch deploy against an existing database
converges rather than diverging or destroying.
Variable-length content columns are TEXT, not VARCHAR(n) — a CI guard enforces it — so a
grown OAuth-scope set or a longer per-tenant-signed token never overflows a column and breaks a
live flow.
Runtime health — tiered readiness
Kubernetes probes are tiered so a cold-start dependency blip cannot crash-loop a healthy pod:
- The startup probe targets the canonical liveness endpoint
(
/actuator/health/liveness) — "is the JVM up and the context initialized?" — a cheap check with no deep dependencies. A CI guard enforces the canonical path. - The readiness group folds in the deep checks (Redis ACL, Vault/OpenBao Transit, JWKS
reachability, R2DBC pool) — "ready to accept traffic?". A
HealthIndicatorgates readiness only when a DOWN state would make callers see 5xx and another replica could plausibly be healthier; purely-observability indicators sit out of the gate.
The separation means a transient Transit warm-up or Redis-ACL render delay flips readiness, not liveness — the pod is given time to become ready instead of being killed.
State — multi-node safe by construction
Any state that must be consistent across requests — rate limits, nonces, session tokens,
idempotency keys — lives in Redis, not in a JVM-local map, because the platform runs as
multiple replicas behind a load balancer. A per-user invariant enforced in-process would
split-brain across pods; the setIfAbsent(key, value, ttl) Redis primitive is the standard
atomic tool. This is what lets a deployment scale horizontally and still enforce a single,
correct rate limit or single-use nonce.
Sources: CLAUDE.md (schema/Flyway rules; tiered readiness; multi-node state; Redis ACLs),
adrs/2026-06-07-vault-to-openbao.md.
Continuous validation — the synthetic monitor
servers/synthetic-monitor is a Tier-3 deep-flow monitor deployed as a Kubernetes CronJob. It
drives real hub/identity OAuth flows against a running environment — not a health-endpoint
ping, but an end-to-end sign-in — so a regression that only shows up in a full protocol
exchange is caught continuously. In both delivery models it is the same artefact: the managed
service runs it against production-adjacent environments; a self-managing customer can run it
against their own.
The platform's operating principle here is that the live deployed system is the thing you validate against — for any change whose correctness depends on Vault/OpenBao policy, Helm/k8s wiring, or a multi-service runtime flow, "it compiles" and "CI is green" are not validation. The synthetic monitor is the standing instrument of that principle.
Source: servers/synthetic-monitor/, CLAUDE.md (validate against the real system).
See also
- Module map — the artefacts that are packaged and deployed.
- Token lifecycle — the signing path that depends on the secrets backend.
- Operations docs — the deploy / upgrade / recovery how-to guides for both delivery models, including the operator plane.