Architecture
Architecture
One helm install renders the whole stack into one namespace. Resource names are prefixed with your release name and the chart name, so a release called my-mvno gets my-mvno-platform-api, my-mvno-platform-license-agent, and a Secret called my-mvno-platform-env.
Here is what comes up, and why.
api
The platform binary, as a Deployment and a Service. This is the whole product surface: catalog, orders, SIMs, customers, billing, admin. It is the MVNE or the MVNO binary depending on platform.kind.
It does not start serving immediately. Two initContainers gate it:
wait-for-authwaits for GoTrue to create itsauth.userstable, because the API's own boot migrations have a foreign key onto it.wait-for-leaseblocks until the license agent has written a lease into the platform Secret. An unlicensed platform never serves. This gate is unconditional and cannot be turned off.
Once running, the API watches the lease file at /etc/platform/lease. When the agent renews or rotates the lease, the API picks up the new one without a restart.
auth
GoTrue, the auth realm for your deployment. It owns users, sessions, passkeys, magic links, and any social login providers you enable. It runs against the same Postgres as the API, in its own auth schema.
app
The web tier: an operator console for an MVNE, a customer-facing storefront for an MVNO. The image derives from platform.kind, so you do not name it.
It is off by default (app.enabled: false). Turn it on with --set app.enabled=true. Give it its own hostname with ingress.hosts.app if you want it reachable from a browser, because the app Ingress only renders when both the workload is enabled and a host is set. Without a host it stays reachable inside the cluster only.
ess
The email sender. It consumes email jobs off Kafka, renders the template in-process, and sends through Resend. Order confirmations, eSIM delivery, password resets, and the rest of the transactional mail all flow through it.
Off by default (ess.enabled: false), and it needs Kafka wired up. With no brokers, it does not render at all.
realtime
An SSE sidecar that fans Kafka events out to connected browsers, which is what makes the console and storefront update live instead of on refresh. Off by default (realtime.enabled: false), and like ess it needs Kafka and its own hostname (ingress.hosts.realtime) to be publicly reachable.
Nothing breaks without it. The web tier falls back to normal request/response.
postgres, redis, kafka
The data services. Postgres holds everything durable. Redis is the cache and the job queue. Kafka carries domain events, which is what ess and realtime consume.
Postgres and Kafka default to mode: operator: the chart renders a CloudNativePG Cluster for Postgres and a Strimzi KafkaNodePool plus Kafka for Kafka, and the corresponding operator (a cluster-scoped prerequisite you install once, see Prerequisites) reconciles them into a running, multi-replica-capable instance. Both also take mode: external to point at a managed service instead (RDS, Confluent, whatever you already run), which means you do not need the operator in the cluster at all.
Redis defaults to mode: bundled: a single-node StatefulSet the chart runs itself, no operator involved. It is a cache rather than durable state, so there is no Redis operator worth adding as a prerequisite. mode: external points it at a managed cache if you need the cache itself to survive a node failure.
See Data services for the values that matter for each mode, and High availability for what to run in production.
the license agent
A single-replica Deployment with a Recreate update strategy. On first install it generates the deployment's signing key inside your cluster, enrolls against activation.parentUrl using your activation code, and writes the resulting lease into the platform Secret. That write is what releases the API's wait-for-lease gate.
It does not exit. It stays running for the life of the release, renewing the lease before it expires and rotating the key on schedule. If it stops, the deployment keeps serving until the current lease runs out, and then it stops serving.
Its RBAC is deliberately small: a ServiceAccount, a Role, and a RoleBinding that grant get and patch on exactly one Secret, by name. It cannot read anything else in your namespace and it cannot touch anything in any other namespace.
Do not scale it past one replica. A second replica is not supported: it is a second writer against the same license, and it raises a false alarm on an otherwise healthy deployment. High availability has the detail.
the Secret and the ConfigMap
The Secret carries every generated or derived credential: the JWT secret, the age encryption keypair, the database and cache connection strings, and the licensing keys the agent writes. The ConfigMap carries the non-secret environment.
The chart generates the Secret's values on first install and reuses them on every helm upgrade by reading the live Secret back, so an upgrade never rotates a password a running deployment depends on. If your secrets come from SOPS, sealed-secrets, or External Secrets, point secrets.existingSecret at your own and the chart renders none of its own. See Secrets.
Ingress
One Ingress for the API, always, on platform.host. One each for app and realtime when those are enabled and given their own hostname. All of them are nginx plus cert-manager, with no GKE Gateway and no cloud load-balancer assumptions. DNS and TLS covers what to point where.