Frontier Admin Console — Deployment Guide
How a raystack/frontier admin console deployment is built, configured, deployed, verified, and rolled back.
About this doc
| Documents | A deployment of raystack/frontier — the console itself is open source |
| Maintained in | The deployment repo (referred to below as the deploy repo) |
| Config lives in | A separate configs repo → frontier/ — not in the deploy repo |
| Endpoints & values | Examples use frontier-admin.<env>.example.com / frontier-connect.<env>.example.com; substitute the deployment's actual hosts. Hosts behind an internal load balancer require a VPN connection |
| Local setup | See the deployment's local-setup doc (referenced below as local setup) |
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
- Ports & config reference
1. How it works
- The console is embedded in the backend. A React 19 / 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. - Three repos, three jobs.
raystack/frontierbuilds and publishes the image; the configs repo holds each environment's config (e.g. as consul-template files rendered from a secrets backend such as Vault); the deploy repo holds the base Helm values and the workflows that render config and deploy. - Backend and console are one artifact — they version, deploy, and roll back together. The console cannot roll back alone.
raystack/frontier configs repo deploy repo
───────────────── ──────────── ───────────
web/apps/admin frontier/<env>_config.* frontier/values.yaml
│ vite build → dist/admin │ (ui:, app.admin.bootstrap) │ (base k8s resources)
│ //go:embed │ │
▼ ▼ ▼
frontier binary config renderer + secrets deploy workflow
│ └──────────┬───────────────────┘
▼ ▼
docker.io/raystack/frontier:{tag} ────► helm upgrade → Kubernetes (target namespace)
│
▼
running pod: SPA + /configs + /frontier-connect proxy + APIOne binary runs two HTTP servers (cmd/serve.go), both exposed through the ingress /
load balancer:
| Server | Config key | Example deployed port | Serves | Example ingress host (dev) |
|---|---|---|---|---|
| UI | ui.port | 3000 | SPA, GET /configs, /frontier-connect/ proxy | frontier-admin.dev.example.com |
| Connect/API | app.connect.port | 8082 | ConnectRPC/gRPC, gRPC health, GET /ping | frontier-connect.dev.example.com |
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.
2. Deploy to an environment
Helm-on-Kubernetes via a deploy workflow (e.g. .github/workflows/frontier_deploy.yaml): it renders
every config file from the secrets backend, then runs helm upgrade.
Triggers (example scheme)
| Environment | Trigger | Cluster |
|---|---|---|
| dev | Push to main, or any non-draft PR against main | the dev cluster |
| staging | Push of a tag (runs after dev succeeds/skips) | the staging cluster |
| production | A GitHub release being created (runs after staging) | the production cluster |
| any one env | workflow_dispatch — pick environment, optionally tag | — |
Adapt tag and release conventions to the repo's release process.
Which image version gets deployed
values: frontier-app.image.tag=${{ github.event.inputs.tag || vars.FRONTIER_IMAGE_VERSION }}
- The
taginput is optional; blank means the environment'sFRONTIER_IMAGE_VERSIONvariable (Settings → Environments → env → Variables). frontier/values.yamlmay also carry animage.tag, but the line above always overrides it — never read it 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, so the key shouldn't simply be dropped.
Example variable values (always re-check the actual values before relying on them):
| Environment | FRONTIER_IMAGE_VERSION |
|---|---|
| dev | dev — a mutable tag rebuilt from upstream main; see Roll back |
| staging | vX.Y.Z (a pinned, immutable tag) |
| production | vX.Y.Z (typically one behind staging) |
Steps
- Actions → the deploy workflow → Run workflow (or let a trigger above fire).
- Pick the environment. Set tag only to pin something other than the environment variable.
- Watch the
helm upgradestep; a chat notification (e.g. Slack) can be wired to post either way. - Verify a deployment.
3. Configure an environment
3.1 Change an existing setting
Console settings live in the configs repo, not the deploy repo:
frontier/<development|staging|production>_config.* → the ui: block.
- Edit the
ui:block in that file and merge it. - Re-run the deploy workflow (Deploy to an environment) for that environment on the same image version. No rebuild — config is read at runtime, and the ConfigMap is re-rendered on every deploy.
Available settings (UIConfig, pkg/server/config.go), with example values:
ui:
port: 3000 # example deployed value; 8100 is the upstream sample
title: "Admin Console"
logo: "data:image/png;base64,…"
app_url: app.example.com # per environment, e.g. app.staging.example.com
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: Workspace, plural: Workspaces } # example rename
project: { singular: Project, plural: Projects }
team: { singular: Team, plural: Teams }
member: { singular: Member, plural: Members }
user: { singular: User, plural: Users }Everything except port is returned by GET /configs (UIConfigApiResponse, pkg/server/server.go).
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— that file is upstream's reference for the config surface, so 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 local setup — run the backend, then the frontend admin app). Vite re-reads that file per request, so no restart between edits. - Release upstream (Cut a release), set the value in the
configs repo, bump
FRONTIER_IMAGE_VERSION, and deploy.
For dev-only values, configs.dev.json alone is enough — no backend change. API URLs go in .env
instead — see the env-file reference in local setup.
4. Admin login (bootstrap superuser)
A superuser service account seeded from config at boot, so automation always has a way in. This is typically what an automated reconcile workflow authenticates as.
Requires image v0.108.0+ — the feature doesn't exist in earlier tags (raystack/frontier
PR #1719).
Where it's configured
Wire it once per environment; nothing to do for a normal deploy afterwards:
# configs repo → frontier/<env>_config.*
app:
admin:
bootstrap:
client_id: "<rendered from the secrets backend>"
client_secret: "<rendered from the secrets backend>"Store both in the secrets backend (e.g. Vault at a per-environment path). If automation needs the same credentials, render them as an env file for that workflow too.
Logging in
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 the password by changing
client_secretin the secrets backend and redeploying. Never changeclient_id— it's the service-account credential id, and must be a UUID. - Setting only one field is a hard boot failure (
client_id and client_secret must be set together), not a silent skip. 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.
Platform admins and members beyond this account are best managed via a GitOps reconcile flow (permissions, roles, users declared in the repo and applied by a workflow).
5. Verify a deployment
Swap the host per environment. If the hosts are internal, connect to the VPN first.
-
Liveness
curl http://frontier-connect.dev.example.com/ping # → {"status":"SERVING"} -
gRPC health
curl -X POST http://frontier-connect.dev.example.com/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://frontier-admin.dev.example.com/configs # → this environment's title, app_url, terminology, organization_types -
SPA — open
http://frontier-admin.dev.example.com/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 (prints Version, Build date, Commit). If dev tracks a mutable tag likedev, remember it's 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.
- Actions → the deploy workflow → Run workflow.
- Select the environment and set tag to the last known-good version explicitly —
FRONTIER_IMAGE_VERSIONstill points at the bad version until someone edits it. - Update that variable to match, so the next trigger-driven deploy doesn't roll forward again.
- Verify a deployment.
Caveats
- If dev tracks a mutable tag (e.g.
dev), it has no previous version to return to — roll dev back by pinning a realvX.Y.Zin thetaginput. - The bootstrap superuser is re-seeded on boot, so rollback never causes a lockout — but going below
v0.108.0removes the feature and breaks any automation that authenticates with it (Admin login). - If only config was wrong, don't roll the image back — fix the
ui:block in the configs repo and redeploy the same version (Change an existing setting).
7. Cut a release (upstream frontier)
Only for shipping backend or console code, and only in raystack/frontier. The versioned image is
the sole artifact the deploy repo consumes.
- Push a tag matching
v*.*.*(or runrelease.ymlviaworkflow_dispatch). - GoReleaser's
beforehook runsmake admin-app→web/Makefile build-admin(pnpm install && pnpm exec turbo run build --filter=admin) →dist/admin. go buildembeds the SPA and injectsVersion,BuildCommit,BuildDatevia ldflags. (A localmake buildinjectsVersiononly.)Dockerfilepackages the binary on Alpine as the non-rootfrontieruser.- Two jobs publish to
docker.io/raystack/frontier:release-fast(.goreleaser-fast.yml) — linux/amd64: tagslatest,{tag},{tag}-amd64release-full(.goreleaser.yml,mode: append) —{tag}-arm64plus darwin/windows binaries
Then bump FRONTIER_IMAGE_VERSION for the target environment and deploy —
Deploy to an environment.
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 in the configs repo, redeploy the same image. |
New config field never appears in /configs | Field exists only in YAML, not in code — complete every step in Add a new setting. |
Deployed version isn't what values.yaml says | Expected. values.yaml's image.tag is always overridden by FRONTIER_IMAGE_VERSION (or the tag input). |
| Dev "rolled back" but nothing changed | Dev tracks a mutable tag. Pin an explicit vX.Y.Z in the tag input. |
| Cannot log in as superuser | Secret changed without a redeploy, bootstrap disabled (both fields empty), or image older than v0.108.0. |
Boot fails: client_id and client_secret must be set together | Only one bootstrap field is populated — set both, or clear both. |
| Boot fails mentioning the bootstrap credential | client_id collides with a different principal. Restore the original UUID; never reuse another account's. |
| Automation can't authenticate | Bootstrap creds in the secrets backend don't match the deployed config, or the image predates v0.108.0. |
| 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. |
| Any host unreachable | If the ingress hosts are on an internal load balancer, connect to the VPN. |
9. Ports & config reference
Example deployed ports (set in the environment config and frontier/values.yaml):
| Port | Purpose | Config key |
|---|---|---|
3000 | Admin console / SPA + /configs | ui.port |
8082 | ConnectRPC / gRPC + /ping + health | app.connect.port |
9000 | Metrics (Prometheus scrape) | app.metrics_port — code default if not set in config |
50051 | SpiceDB gRPC | spicedb.port |
Where each file lives:
| File | Repo | Purpose |
|---|---|---|
frontier/values.yaml | deploy repo | Base K8s resources — container ports, volumes, service, autoscaling |
frontier/<env>_config.* | configs repo | Frontier app config, incl. ui: and app.admin.bootstrap |
frontier/k8s_<env>.* | configs repo | Env K8s overrides — replicas, PDB, ingress |
frontier/<env>_bootstrap_credentials.* | configs repo | Bootstrap creds as an env file for automation |
.github/workflows/frontier_deploy.yaml | deploy repo | Render config + helm upgrade per environment |
.github/workflows/frontier_reconcile.yaml | deploy repo | GitOps reconcile (permissions, roles, users, …) |
Key paths upstream in raystack/frontier:
| 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 |
web/sdk | npm SDK package (admin surface under web/sdk/admin) |
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, incl. ui: and app.admin.bootstrap. Sample values, not deployed ones — compare against the running tag |