Deployment
Deployment
Dev containers
The repo's infra/docker-compose.yml ships both admin services ready to go:
make admin-core # base layer :3002
make admin # project layer :3001 (extends base)
Both use infra/dockerfiles/Dockerfile.admin — minimal node:20-alpine with pnpm and corepack. First boot runs pnpm install; subsequent boots reuse the named volume for node_modules. Source is bind-mounted so edits hot-reload.
admin-core → binds ../js-admin-nuxt4 → :3002
admin → binds ../your-admin + ../js-admin-nuxt4 (source only) → :3001
Both proxy /api/v1/** to admin-api:8001 via the Nuxt server's routeRules, so in-container requests stay on the Docker network.
Production builds
Use Nuxt's standard nuxt build:
cd your-admin
pnpm build # → .output/
node .output/server/index.mjs
Or containerise with a multi-stage Dockerfile:
FROM node:20-alpine AS build
WORKDIR /app
COPY js-admin-nuxt4/ /app/js-admin-nuxt4/
COPY your-admin/ /app/your-admin/
WORKDIR /app/your-admin
RUN corepack enable && pnpm install --frozen-lockfile
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/your-admin/.output /app/.output
ENV NUXT_API_URL=https://admin-api.yourapp.com
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
Keep the base layer source in the build context (under ../js-admin-nuxt4/) — Nuxt needs its source files at build time, not runtime.
Required env vars in production
| Key | Purpose |
|---|---|
NUXT_API_URL | Admin-api host (public — browser sees it) |
NUXT_API_INTERNAL_URL | Admin-api host for SSR (server-to-server within your cluster) |
NUXT_API_SECRET | Shared secret between admin-api and admin (if your backend enforces one) |
NUXT_PUBLIC_AUTH_PREFIX | Optional — where the backend mounts auth, relative to /api/v1 (default /auth). Set it to match the backend's AUTH_ROUTE_PREFIX when a project moved auth (e.g. /login for a stufio port). A mismatch 404s login. |
NUXT_SESSION_PASSWORD | 32+ char random, for cookie signing |
NUXT_PUBLIC_FLOWDSL_LICENSE_KEY | Optional — unlocks the Flow Studio runner tier (also editable at runtime via flowstudio.license_key admin setting) |
NUXT_PUBLIC_FLOWDSL_UI_PATH | Optional — path the admin-api serves the Flow Studio at (default /flowdsl; must match backend FLOWSTUDIO_UI_PATH) |
Admin-settings overrides
Several deployment-affecting values can now be edited at runtime from the admin UI without a redeploy. The backend re-reads them on the next request so changes take effect immediately.
| Group | Setting | Env fallback |
|---|---|---|
flowstudio.embed | license_key | FLOWDSL_LICENSE_KEY |
flowstudio.embed | embed_url | FLOWSTUDIO_EMBED_URL |
flowstudio.embed | ui_path | FLOWSTUDIO_UI_PATH |
ai-guard.policy | default_on_error | — |
ai-guard.policy | block_flagged | — |
search.defaults | index_prefix | SEARCH_INDEX_PREFIX |
search.defaults | embedding_provider | SEARCH_EMBEDDING_PROVIDER |
search.defaults | embedding_model | SEARCH_EMBEDDING_MODEL |
Env values seed the initial configuration on a fresh deployment; admin edits
override them thereafter and survive restarts (persisted in the _settings
MongoDB collection). Set the admin setting to an empty string to revert to
the env fallback.
LLM pricing — live overrides
The ledger price table ships
with built-in rates for OpenAI, Anthropic, Gemini, and the full OVH catalog
(chat, reasoning, code, vision, embeddings, plus free guard/ASR models).
Admins can PUT/DELETE rows at /admin/llm/pricing/{provider}/{model}
without a code change — the next LLM call picks up the new rate. Admin UI
renders this under System → AI → Pricing.
Never set NUXT_STANDALONE=1 on a layered project admin — that reloads the base layer's CSS and creates duplicate styles.
Reverse proxy
Point your reverse proxy (nginx / Traefik / Caddy) at the admin Nuxt server. A minimal nginx block:
server {
server_name admin.yourapp.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The admin itself proxies /api/v1/** to your admin-api, so you don't need a separate proxy rule for API traffic.
Admin-api gating
The admin UI expects the admin-api binary (backend/cmd/admin-api), which bundles admin-only routes like /admin/users/*, /admin/flows, /admin/settings/*. The public backend/cmd/api omits these by construction — the split is enforced by a CI test (check-admin-leak) that greps the public binary's dependency tree for any admin path.
If your deployment co-locates both on one host, bind admin-api to a separate port (default 8001) and restrict it via your reverse proxy (IP allowlist, Tailscale, basic auth, etc.).
Observability
The admin surfaces system state at /system/status:
- Per-worker heartbeat registry (roles + last-seen) from
go-modules/workers/admin - Live health-check roll-up from
/api/v1/health - Known infrastructure services (mongo, redis, nats, kafka) tagged healthy / unhealthy / not-monitored
Every backend instance publishes worker.heartbeat events on the EventBus; the admin aggregator reads them and renders the list. No additional plumbing needed — just make sure TRANSPORT is set (nats / kafka / redis / memory).