Key-manager UI development
A one-command local environment for working on the key-manager web UI, with a populated model list and hot reload. You do not need to understand the operator, the gateway, or Kubernetes to use it.
What you get
Section titled “What you get”The UI is a React 19 + TypeScript app (Vite, Tailwind, shadcn/ui) that lives in
frontend/ at the repo root. It is a standalone single-page app: in production
it is built to a static bundle and served by its own nginx container image, and
it talks to the key-manager only over /api/*.
make run-dev (or cd dev && ./run-dev.sh) brings up a local
kind cluster with the operator, the key-manager (in
dev mode, so no Keycloak login), and three OpenRouter passthrough models, then:
- port-forwards the key-manager API to
http://localhost:8080, and - starts the Vite dev server at
http://localhost:5173with hot module reload, proxying/api/*to the port-forwarded key-manager.
You develop against http://localhost:5173.
Prerequisites
Section titled “Prerequisites”- Docker, kind, kubectl, helm, and Go 1.25+.
- Node.js 22+ and npm (for the Vite dev server).
- An OpenRouter API key.
cd devcp .env.example .env# edit .env and set OPENROUTER_API_KEY=sk-or-v1-..../run-dev.sh # or: make run-dev (from dev/) · make ui (from repo root)First run creates the cluster and builds images, so it takes a few minutes. Later runs reuse the cluster and start in seconds. When it is ready you will see:
key-manager API: http://localhost:8080Vite dev server: http://localhost:5173 <-- develop hereOpen http://localhost:5173. The dev server runs with VITE_DEV_NO_AUTH=true,
so the Keycloak PKCE login is bypassed and you are signed in automatically as
user dev (group llm). The model list shows claude-sonnet-45,
gemini-25-flash, and llama-33-70b. Mint and revoke keys against them as a
real user would.
Press Ctrl-C to stop; the port-forward is cleaned up for you. The cluster keeps
running for the next make run-dev.
Editing the UI
Section titled “Editing the UI”The React source lives under frontend/src/. Edit any file there and the Vite
dev server at :5173 hot-reloads the browser. The dev server proxies /api/*
to the port-forwarded key-manager, so the UI talks to a real backend (real
models, real key minting) while you iterate on components, styles, and state.
Auth in dev mode
Section titled “Auth in dev mode”In production the SPA owns login: keycloak-js performs a login-required PKCE
redirect to Keycloak, obtains an access token, and the app sends
Authorization: Bearer <token> on every /api call. The dev loop shortcuts
this on both ends:
- The Vite dev server sets
VITE_DEV_NO_AUTH=true, which skips the keycloak-js redirect so you never hit a login screen. - The in-cluster key-manager runs with
LLM_DEV_MODE=true, which bypasses auth and injects a fixed identity (dev, groupllm). So/api/mereturns that identity and every/api/*call works without a token.
Never enable dev mode in a real deployment.
Frontend quality gate
Section titled “Frontend quality gate”Before opening a PR, run the same checks CI runs, from frontend/:
cd frontendnpm run build # tsc -b && vite buildnpm test # vitestnpm run check # biome lint + formatShipping a change
Section titled “Shipping a change”The UI ships as its own image
(ghcr.io/nebari-dev/llm-serving-pack/frontend, nginx serving the built
bundle) - it is no longer embedded in the Go key-manager binary. Committing your
edits to frontend/ is all that is needed for them to ship: CI builds and
publishes the frontend image on merge, and a chart upgrade rolls it out.
During local development you iterate against the Vite dev server (make ui in
dev/), which hot-reloads on save and proxies /api to the port-forwarded
key-manager - there is no in-cluster frontend pod in the dev stack to rebuild or
restart. (make build-images builds only the operator, key-manager, and
mock-vllm images.)
API reference (what the UI calls)
Section titled “API reference (what the UI calls)”All under /api, all gated by auth (bypassed in dev mode):
| Method | Path | Purpose |
|---|---|---|
| GET | /api/me | Current user identity (username, groups). |
| GET | /api/models | Models the user may mint keys for. |
| GET | /api/keys | The user’s existing keys. |
| POST | /api/keys | Mint a key. Body: {"modelName": "<model>"}. |
| DELETE | /api/keys/{namespace}/{model}/{clientID} | Revoke a key. |
Troubleshooting
Section titled “Troubleshooting”SSL_ERROR_RX_RECORD_TOO_LONG/ “Secure Connection Failed” - the dev server is plain HTTP. Usehttp://localhost:5173(or:8080), nothttps://. If your browser force-upgrades localhost to HTTPS, use a private window or disable HTTPS-Only mode for the site.- Model list is empty - the key-manager resyncs models every 30s; give it a
moment after
make run-dev, or checkkubectl -n llm-operator-system get passthroughmodel. make run-devsaysOPENROUTER_API_KEY is not set- createdev/.envfromdev/.env.exampleand set the key.- Start over -
make teardowndeletes the cluster;make run-devrebuilds it.
Related
Section titled “Related”- Local Development - the full local dev path (operator, passthrough models, inference).
- Installation - real-cluster deployment with Keycloak and the gateway.