Frontier Admin Console — Deployment Guide
How a raystack/frontier admin console deployment is built, configured, deployed, verified, and rolled back. Applies to any Helm-on-Kubernetes deployment.
Contents
- How it works
- Deploy to an environment
- Configure an environment
- Admin login (bootstrap superuser)
- Verify a deployment
- Roll back
- Cut a release (upstream
frontier) - Gotchas
1. How it works
Every procedure below follows from four facts:
- The console is embedded in the backend. A React/Vite SPA, compiled to static assets and baked
into the Go binary at build time (
//go:embed all:dist,web/apps/admin/embed.go). No Node.js in production. - One image serves every environment. The SPA fetches its config at runtime from
GET /configs; nothing environment-specific is compiled in. - Everything environment-specific lives outside the image — each environment's Frontier config
(the
ui:block,app.admin.bootstrap, ingress, etc.), typically rendered at deploy time from a secrets backend and mounted into the pod as a ConfigMap. - Backend and console are one artifact — they version, deploy, and roll back together. The console cannot roll back alone.
raystack/frontier environment config
───────────────── ──────────────────
web/apps/admin frontier config (ui:, app.admin.bootstrap)
│ vite build → dist/admin │ rendered from secrets at deploy time
│ //go:embed │
▼ ▼
docker.io/raystack/frontier:{tag} ──► helm upgrade → Kubernetes
│
▼
running pod: SPA + /configs + /frontier-connect proxy + APIOne binary runs two HTTP servers (cmd/serve.go), both exposed through the ingress:
| Server | Config key | Default port | Serves | Ingress host |
|---|---|---|---|---|
| UI | ui.port | 8100 | SPA, GET /configs, /frontier-connect/ proxy | <admin-host> |
| Connect/API | app.connect.port | 8002 | ConnectRPC/gRPC, gRPC health, GET /ping | <connect-host> |
Both are deliberately exposed: browsers use the admin host; API clients and automation use the Connect host directly.
UI server routing (pkg/server/server.go): /configs returns the config JSON; /frontier-connect/
reverse-proxies to {app.host}:{app.connect.port} with the prefix stripped; everything else serves
the SPA with an index.html fallback.
Key paths upstream:
| Path | Purpose |
|---|---|
web/apps/admin | Admin SPA (Vite outDir: dist/admin, base: /) |
web/apps/admin/embed.go | //go:embed all:dist → var Assets embed.FS |
web/apps/admin/configs.dev.json | Dev /configs payload |
pkg/server/server.go | ServeUI / ServeConnect, /configs, proxy, /ping, health |
pkg/server/config.go | UIConfig, TerminologyConfig, ConnectConfig |
internal/bootstrap/bootstrapuser.go | Superuser bootstrap |
config/sample.config.yaml | Annotated reference for every config key |
2. Deploy to an environment
A deploy renders the environment's config from the secrets backend, then runs helm upgrade with an
explicitly chosen image tag. The contract, regardless of pipeline tooling:
- Each environment pins an image version — an immutable
vX.Y.Ztag. A pre-production environment may instead track a mutable tag rebuilt from upstreammain(see Roll back for the consequence). - The tag chosen at deploy time always wins. The Helm values file may also carry an
image.tag, but the deploy sets the tag explicitly — never read the values file as the deployed version. Keep it pinned to a real version anyway: it's the fallback for a manualhelm upgrade, and the chart's own default would belatest. - Config is re-rendered on every deploy, reaching the pod as a mounted ConfigMap (e.g. at
/etc/frontier/configs/config.yaml) that the binary is pointed at with-c. Individual keys can also be overridden by env vars using theFRONTIER_SERVICE_prefix (config/config.go).
Chart raystack/frontier from https://raystack.github.io/charts/ (pin a chart version), release
name frontier, in the target namespace.
Steps: run the deploy pipeline for the target environment (overriding the tag only to pin
something other than the environment's configured version), watch the helm upgrade, then
verify.
3. Configure an environment
3.1 Change an existing setting
Console settings live in the environment's Frontier config → the ui: block — not in the Helm
values.
- Edit the
ui:block and merge it. - Redeploy the same image version. No rebuild — config is read at runtime.
Available settings (UIConfig, pkg/server/config.go):
ui:
port: 8100
title: "Frontier Admin"
logo: "data:image/png;base64,…"
app_url: app.example.com # per environment
token_product_id: "" # product id used when adding credits
organization_types: [] # industry list shown in the org form
webhooks:
enable_delete: false
terminology: # optional per-entity rename; no server-side defaults
app_name: "" # serialized to JSON as appName
organization: { singular: "", plural: "" } # likewise project, team, member, userEverything except port is returned by GET /configs (UIConfigApiResponse,
pkg/server/server.go). Most keys are also documented inline in
config/sample.config.yaml,
but that file lags the code — webhooks and terminology.app_name are missing from it — so UIConfig
is the authority. A key on main may not exist in an older tag.
3.2 Add a new setting
A field the backend doesn't know about will not reach the SPA. Change the code upstream first:
- Add the field to
UIConfig(pkg/server/config.go) and document it in theui:block ofconfig/sample.config.yaml— an undocumented key is invisible. - Add it to
UIConfigApiResponseand the/configshandler (pkg/server/server.go). - Consume it in the SPA (
contexts/App.tsx/ theConfigtype). - Test the whole path locally: put the value in
web/apps/admin/configs.dev.jsonand run against a local backend (see the local-setup doc). Vite re-reads that file per request. - Release upstream (Cut a release), set the value in the environment config, bump the environment's image version, and deploy.
For dev-only values, configs.dev.json alone is enough — no backend change.
4. Admin login (bootstrap superuser)
A superuser service account seeded from config at boot, so automation always has a way in.
Requires image v0.108.0+ (raystack/frontier PR #1719).
Wire it once per environment, with both values stored in the secrets backend:
app:
admin:
bootstrap:
client_id: "<from secrets backend>" # a UUID
client_secret: "<from secrets backend>"Log in with Authorization: Basic base64(client_id:client_secret).
Behaviour (internal/bootstrap/bootstrapuser.go):
- Re-seeded on every boot — idempotent, so redeploys never cause a lockout.
- Rotate by changing
client_secretand redeploying. Never changeclient_id— it's the service-account credential id. - Setting only one field is a hard boot failure (
client_id and client_secret must be set together). Emptying both disables seeding — and since granting superuser requires a superuser, none can then be created via the API. - If the
client_idalready belongs to another principal, boot fails loudly rather than rotating the wrong account.
The app.admin.bootstrap block in
config/sample.config.yaml
documents the UUID requirement, uuidgen, rotation, and the cost of disabling it.
5. Verify a deployment
If the hosts are behind an internal load balancer, connect to the VPN first.
-
Liveness
curl http://<connect-host>/ping # → {"status":"SERVING"} -
gRPC health
curl -X POST http://<connect-host>/grpc.health.v1.Health/Check \ -H 'Content-Type: application/json' \ -d '{"service":"raystack.frontier.v1beta1.AdminService"}' # also valid: raystack.frontier.v1beta1.FrontierService -
Configuration — the real test that the right env config was applied:
curl http://<admin-host>/configs # → this environment's title, app_url, terminology, organization_types -
SPA — open
http://<admin-host>/and log in. -
Version — no HTTP endpoint exposes it. The running image is most reliable:
kubectl -n <namespace> get deploy -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[*].image}{"\n"}{end}'Alternatives: the startup log line
frontier starting version=<tag>, orfrontier versionin the pod. A mutable tag is a moving target.
6. Roll back
Rollback = redeploy the previous image version. Versioned tags are immutable and the SPA is embedded, so backend and console roll back together.
- Deploy with the tag set explicitly to the last known-good
vX.Y.Z. - Update the environment's pinned image version to match, so the next deploy doesn't roll forward again.
- Verify.
Caveats
- An environment tracking a mutable tag has no previous version to return to — roll it back by
pinning a real
vX.Y.Z. - The bootstrap superuser is re-seeded on boot, so rollback never causes a lockout — but going below
v0.108.0removes the feature and breaks anything that authenticates with it. - If only config was wrong, don't roll the image back — fix the
ui:block and redeploy the same version (§3.1).
7. Cut a release (upstream frontier)
Only for shipping backend or console code, and only in raystack/frontier. The versioned image
published to docker.io/raystack/frontier is the sole artifact a deployment consumes.
Push a tag matching v*.*.* (or dispatch release.yml). The release pipeline builds the SPA
(make admin-app → dist/admin), embeds it into the Go binary with version ldflags, and publishes
{tag}, latest, and {tag}-amd64 for linux/amd64, plus {tag}-arm64 from a second job. There's no
combined manifest list, so arm64 must be pulled by its own tag. Then bump the target environment's
image version and deploy.
8. Gotchas
| Symptom | Cause / fix |
|---|---|
API works but no UI; log says ui server disabled: no port specified | ui.port is 0 or unset — set it in the environment config and redeploy. |
API works but no UI; log says failed to load ui | Binary built without dist/admin. Rebuild via the release pipeline, or make admin-app before go build. |
| SPA loads but shows wrong title/terminology | Wrong or stale env config. Check GET /configs, fix the ui: block, redeploy the same image. |
New config field never appears in /configs | Field exists only in YAML, not in code — complete every step in §3.2. |
| Deployed version isn't what the values file says | Expected — the deploy-time tag always overrides it (§2). |
| "Rolled back" a mutable tag but nothing changed | Mutable tags have no previous version — pin an explicit vX.Y.Z. |
| Boot fails or superuser login fails | Bootstrap misconfigured: fields set without a redeploy, only one of the two fields populated, client_id colliding with another principal, or image older than v0.108.0 (§4). |
| Browser API calls fail | The SPA proxies /frontier-connect/ to {app.host}:{app.connect.port} inside the pod. Confirm the ingress routes the admin host to the UI port and the connect host to the Connect port. |