When to use (always in Studio)
Apply this skill whenever you start, fix, or demo a web frontend (or FE+API) in Holix Studio:
- User asks to run the app, open the UI, “подними фронт”, “запусти preview”, “открой в браузере”
- Project has Vite / Next / Nuxt / CRA / static SPA ± API
- You are about to say the app is ready
This skill is platform / required: do not ignore it for Studio web work.
Non‑negotiable rules
- Listen on
0.0.0.0(never only127.0.0.1) for any process that Studio Preview must reach
(Vite/Nuxthost: '0.0.0.0', uvicorn--host 0.0.0.0, nginx published port, etc.). - Always form real preview links in chat after the app listens:
- Call MCP
open_preview_url(port=…)(and for FE+BE, for every public port users need). - Paste into the chat reply:
frame_url/ public origin from the tool result (Markdown link). - Never tell the user to open
localhost/ barehost:portas the main URL.
- Call MCP
- Prefer docker-compose + nginx for “run the app” demos:
- One published port on the host (nginx).
- Frontend and backend only on the compose network (expose, not host-publish unless needed).
- Nginx routes UI + API on the same origin (
/→ FE,/api/→ BE) so the browser does not need a separate API host when possible.
- Do not use Desktop / noVNC for web apps — only Studio → Browser via
open_preview_url. - Do not claim “preview is ready” without a successful tool result that includes the public URL.
Preferred architecture (FE + BE)
Browser → Studio preview (public HTTPS) → host:PORT → nginx container
├─ / → frontend:3000
└─ /api/ → backend:8000
Single public port → one open_preview_url → one link in chat.
docker-compose.yml (template — adapt ports/paths)
Write under the project (e.g. docker-compose.yml or deploy/docker-compose.studio.yml):
services:
frontend:
build: ./frontend # or image + command for node
# CRITICAL: app inside container must listen 0.0.0.0
environment:
- HOST=0.0.0.0
- PORT=3000
# Prefer same-origin /api via nginx — no absolute localhost API
- VITE_API_URL=/api
- NEXT_PUBLIC_API_URL=/api
expose:
- "3000"
# no ports: on host — only nginx publishes
backend:
build: ./backend
environment:
- HOST=0.0.0.0
- PORT=8000
expose:
- "8000"
nginx:
image: nginx:alpine
depends_on:
- frontend
- backend
ports:
# Published port = Studio preview port (pick free project port)
- "8080:80"
volumes:
- ./deploy/nginx.studio.conf:/etc/nginx/conf.d/default.conf:ro
nginx.studio.conf (template)
server {
listen 80;
server_name _;
client_max_body_size 50m;
# Frontend (Vite/Next/Nuxt/static)
location / {
proxy_pass http://frontend:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Backend API (adjust prefix to match the app)
location /api/ {
proxy_pass http://backend:8000/; # trailing slash strips /api prefix if BE has no /api
# use proxy_pass http://backend:8000; # if BE routes already start with /api
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Optional OpenAPI / health
location /docs {
proxy_pass http://backend:8000/docs;
proxy_set_header Host $host;
}
location /health {
proxy_pass http://backend:8000/health;
proxy_set_header Host $host;
}
}
Frontend API base when nginx fronts both
| Stack | Prefer |
|---|---|
| Vite | VITE_API_URL=/api (or empty + relative /api/...) |
| Next | NEXT_PUBLIC_API_URL=/api |
| CRA | REACT_APP_API_URL=/api |
| Axios/fetch | relative /api/... |
Only if the user must call a separate public BE port (no nginx): set API base from MCP backend public origin (not localhost) — see “Split ports” below.
Mandatory agent workflow
A. Default: compose + nginx (propose this first)
Inspect project (package.json, existing Dockerfile/compose, ports).
Propose (and implement if user agrees / task implies run):
docker-composewith frontend, backend (if any), nginx- nginx joins FE+BE as above
- host bind 0.0.0.0 inside services; one published host port on nginx
Start stack from workspace:
- Studio Docker tools /
docker compose up -d --buildin the project dir - Or background process if user forbids Docker — still 0.0.0.0 + preview
- Studio Docker tools /
Wait until nginx port accepts HTTP (health / log /
check_background_process/ docker ps).open_preview_url(port=NGINX_HOST_PORT)(e.g. 8080).In the same assistant message after tools succeed, include Markdown links, e.g.:
**Приложение:** [открыть preview](<frame_url or public origin from tool>) Порт: 8080 · nginx → frontend + `/api` → backendIf FE-only (no BE), still use nginx (or the FE process on
0.0.0.0) and still callopen_preview_url+ link in chat.
B. Dev servers without Docker (fallback)
Only if Docker is unavailable or user forbids it:
- Start processes with host
0.0.0.0:- Vite:
server: { host: '0.0.0.0', port, strictPort: true, allowedHosts: true } - Nuxt:
devServer: { host: '0.0.0.0', port } - uvicorn:
--host 0.0.0.0 --port …
- Vite:
- Prefer a small local nginx (compose one-service nginx + upstream host network) still if possible.
open_preview_urlfor each user-facing port; put all links in chat.- If FE and BE are separate public ports (no shared nginx origin):
open_preview_urlfor FE and BE- Set FE env to backend public origin from
resolve_preview_origin_tool/preview_origins - CORS on BE must allow frontend public origin
C. Split ports (no nginx) — API base
Same as before, but secondary to nginx:
resolve_preview_origin_tool(port=BACKEND)orpreview_origins- FE env = backend origin (H2 or path proxy) — never
http://localhost:PORTfor the browser - Restart FE after env change; re-open previews; link both URLs in chat
Vite / Nuxt HMR (secondary)
HMR WebSocket may fail behind Studio preview. SPA can still work without hot reload.
- Keep listen port consistent with
open_preview_url - Prefer
allowedHosts: true, host0.0.0.0 - If HMR spam:
hmr: falseorclientPort: 443for wss edge — see stack docs - Do not confuse HMR errors with broken REST API
Do NOT
- Start web servers only on
127.0.0.1when Studio preview is required - Advertise localhost as the user URL
- Invent H2 hostnames — only MCP
open_preview_url/preview_origins - Open web apps via Desktop/noVNC
- Install random host nginx/yadisk/rclone for “preview” when Studio Browser + compose is available
- Say “я открыл preview” without a tool result containing the URL
- Skip proposing docker-compose + nginx for FE+BE run tasks
Checklist before “done”
- Services listen on 0.0.0.0 (or via nginx published port)
- docker-compose + nginx proposed (and used when possible) for FE+BE
- Nginx routes
/→ FE and/api/(or project prefix) → BE - FE uses same-origin
/apior backend public origin (not laptop localhost) -
open_preview_urlcalled for the public port(s) - Chat message includes clickable preview link(s) from tool output
- User told: Studio Browser / the link — not localhost
Related tools (Studio)
- MCP
holix_studio:open_preview_url,list_preview_targets,preview_origins,resolve_preview_origin_tool - Docker panel / compose tools for workspace stacks
start_background_process/ terminal only as fallback when compose is impossible
Telegram + Studio (same Holix profile)
Processes started from Telegram for a user profile must appear in Studio for that same profile (Processes panel + Browser targets):
- Holix persists a shared index:
HOLIX_HOME/profiles/<profile>/data/background_processes.json - Always use
start_background_process(not detached raw nohup outside Holix) - Bind 0.0.0.0 and call
open_preview_urlso Studio Browser can attach - Workspace cwd must stay under the profile workspace so Studio can list the app
Slash
/holix-studio-frontend-backend — re-apply this workflow (ports, compose+nginx, preview links).