Problem Statement
Running the portal in dev mode requires manual steps: build the frontend first (cd web && npx vite build), then start the server separately. The server has accumulated conditional guards (IS_LOCAL mocks, dist/web existence checks) that pollute production code paths. This is unlike the standard web dev experience where a single command starts both frontend and backend with hot reload, and the server code stays clean.
Solution
Adopt the standard Vite + backend dev pattern: npm run dev:portal runs both the Vite dev server (frontend with HMR) and the Fastify server (backend with tsx watch) concurrently. The Vite dev server proxies /api requests to the Fastify server, eliminating the need to build the frontend or serve static files in dev mode. Remove all IS_LOCAL mock guards and conditional static-serving logic from the server — the server becomes a pure API server in dev, with static serving only in production.
User Stories
- As a developer, I want to run
npm run dev:portal and have both frontend and backend start with a single command, so that I don't need to manually build or start services separately
- As a developer, I want frontend changes to reflect instantly via HMR, so that I can iterate on UI quickly without rebuilding
- As a developer, I want backend changes to trigger automatic server restart, so that I can iterate on API logic quickly
- As a developer, I want the Vite dev server to proxy API calls to the Fastify backend transparently, so that the frontend works identically to production (same-origin, cookies, etc.)
- As a developer, I want
npm run dev:cli to work as it does today (no frontend), so that CLI development is unaffected
- As a developer, I want the server source code to contain zero dev-mode conditional logic (no
IS_LOCAL, no dist/web existence checks), so that production code paths remain clean and testable
- As a developer, I want external service failures (New API, Sandbox Manager) to degrade gracefully at the route level with proper error responses, so that the portal still starts and serves its UI even without backend infrastructure
- As a developer, I want the server to log clear warnings when external services are unavailable, so that I understand why certain features (sandbox creation, API key management) return errors
Implementation Decisions
1. Vite dev server with API proxy
Add a server block to server/web/vite.config.ts that proxies all /api requests to the Fastify server (default http://localhost:8080). This is the standard Vite proxy pattern. The frontend dev server runs on a separate port (e.g., 5173) and forwards API traffic transparently.
2. Concurrent dev script
Use concurrently (or npm-run-all) to run both processes from a single npm script. The server/package.json dev script changes to run both tsx watch src/index.ts (backend) and vite dev (frontend) in parallel.
The root dev:portal script delegates to the server workspace as before.
3. Frontend dev script in server/web/package.json
Add "dev": "vite" to server/web/package.json scripts. This is currently missing — the web package has no dev/build/start scripts at all.
4. Remove IS_LOCAL mock guards from server source
Remove the IS_LOCAL constant and all early-return mock guards from:
server/src/newapi/index.ts — 6 exported functions
server/src/sandbox-manager/client.ts — 8 exported functions
server/src/routes/sandboxes-ssh.ts — LOCAL-mode WebSocket guard
The startup calls (initNewApi, ensureNewApiChannel) already handle failure gracefully (non-fatal warnings). Route-level callers (sandbox creation, admin approval) already have try/catch with degraded responses. The mock guards are redundant — the existing error handling is sufficient, and returning mock data hides real integration issues.
5. Remove conditional static serving logic from index.ts
Remove the dist/web existence check and the if/else block around @fastify/static registration. The static serving and SPA fallback should always be registered — if dist/web doesn't exist (dev mode without prior build), the @fastify/static warn is acceptable since the Vite dev server handles frontend in dev. In production, dist/web is always populated by the build step.
6. Keep the dotenv loading and DB auto-mkdir
The server/src/env.ts preload module and the fs.mkdirSync in server/src/db/index.ts are legitimate startup infrastructure, not dev-mode hacks. These stay.
7. Environment variable for backend port
The Vite proxy target should read from an env var (e.g., PORTAL_PORT or PORT) with a default of 8080, so developers can override the backend port if needed. The existing .env already defines PORTAL_PORT=3080.
Testing Decisions
- Manual verification: Run
npm run dev:portal and confirm:
- Both Vite and Fastify start in one terminal
- Frontend loads with HMR at the Vite dev server URL
- API calls proxy correctly (login, auth status, admin routes)
- Backend hot-reloads on server file changes
- Frontend hot-reloads on component changes
- No
IS_LOCAL, dist/web, or mock guard logic in server source
- Production build: Run
npm run build:portal and confirm the built server still serves static files from dist/web correctly
- Route-level error handling: With external services unavailable, confirm that sandbox and admin routes return proper error responses (not crashes)
No automated test infrastructure exists in this project, so testing is manual verification.
Out of Scope
- Docker Compose or k3d integration for running external services locally
- Mocking external services (New API, Sandbox Manager) — services are either available or routes degrade gracefully
- WebSocket SSH relay in dev mode — requires a running sandbox-manager, which is out of scope for pure frontend dev
- CLI dev experience changes —
npm run dev:cli already works as expected
- Automated tests
Further Notes
The server/web directory is not listed in the root workspaces array (which only includes server and cli). Since server/web is a subdirectory of server, its dependencies are managed independently. The dev command should be invoked from server/web or via the concurrent script.
The current @fastify/static setup has a known issue: if dist/web doesn't exist at registration time, it logs a warning and fails to serve files even after they're built. This PRD resolves that by making static serving unconditional (production always has the build output) and using Vite dev server in development.
Problem Statement
Running the portal in dev mode requires manual steps: build the frontend first (
cd web && npx vite build), then start the server separately. The server has accumulated conditional guards (IS_LOCALmocks,dist/webexistence checks) that pollute production code paths. This is unlike the standard web dev experience where a single command starts both frontend and backend with hot reload, and the server code stays clean.Solution
Adopt the standard Vite + backend dev pattern:
npm run dev:portalruns both the Vite dev server (frontend with HMR) and the Fastify server (backend withtsx watch) concurrently. The Vite dev server proxies/apirequests to the Fastify server, eliminating the need to build the frontend or serve static files in dev mode. Remove allIS_LOCALmock guards and conditional static-serving logic from the server — the server becomes a pure API server in dev, with static serving only in production.User Stories
npm run dev:portaland have both frontend and backend start with a single command, so that I don't need to manually build or start services separatelynpm run dev:clito work as it does today (no frontend), so that CLI development is unaffectedIS_LOCAL, nodist/webexistence checks), so that production code paths remain clean and testableImplementation Decisions
1. Vite dev server with API proxy
Add a
serverblock toserver/web/vite.config.tsthat proxies all/apirequests to the Fastify server (defaulthttp://localhost:8080). This is the standard Vite proxy pattern. The frontend dev server runs on a separate port (e.g., 5173) and forwards API traffic transparently.2. Concurrent dev script
Use
concurrently(ornpm-run-all) to run both processes from a single npm script. Theserver/package.jsondev script changes to run bothtsx watch src/index.ts(backend) andvite dev(frontend) in parallel.The root
dev:portalscript delegates to the server workspace as before.3. Frontend dev script in server/web/package.json
Add
"dev": "vite"toserver/web/package.jsonscripts. This is currently missing — the web package has no dev/build/start scripts at all.4. Remove IS_LOCAL mock guards from server source
Remove the
IS_LOCALconstant and all early-return mock guards from:server/src/newapi/index.ts— 6 exported functionsserver/src/sandbox-manager/client.ts— 8 exported functionsserver/src/routes/sandboxes-ssh.ts— LOCAL-mode WebSocket guardThe startup calls (
initNewApi,ensureNewApiChannel) already handle failure gracefully (non-fatal warnings). Route-level callers (sandbox creation, admin approval) already have try/catch with degraded responses. The mock guards are redundant — the existing error handling is sufficient, and returning mock data hides real integration issues.5. Remove conditional static serving logic from index.ts
Remove the
dist/webexistence check and theif/elseblock around@fastify/staticregistration. The static serving and SPA fallback should always be registered — ifdist/webdoesn't exist (dev mode without prior build), the@fastify/staticwarn is acceptable since the Vite dev server handles frontend in dev. In production,dist/webis always populated by the build step.6. Keep the dotenv loading and DB auto-mkdir
The
server/src/env.tspreload module and thefs.mkdirSyncinserver/src/db/index.tsare legitimate startup infrastructure, not dev-mode hacks. These stay.7. Environment variable for backend port
The Vite proxy target should read from an env var (e.g.,
PORTAL_PORTorPORT) with a default of8080, so developers can override the backend port if needed. The existing.envalready definesPORTAL_PORT=3080.Testing Decisions
npm run dev:portaland confirm:IS_LOCAL,dist/web, or mock guard logic in server sourcenpm run build:portaland confirm the built server still serves static files fromdist/webcorrectlyNo automated test infrastructure exists in this project, so testing is manual verification.
Out of Scope
npm run dev:clialready works as expectedFurther Notes
The
server/webdirectory is not listed in the rootworkspacesarray (which only includesserverandcli). Sinceserver/webis a subdirectory ofserver, its dependencies are managed independently. The dev command should be invoked fromserver/webor via the concurrent script.The current
@fastify/staticsetup has a known issue: ifdist/webdoesn't exist at registration time, it logs a warning and fails to serve files even after they're built. This PRD resolves that by making static serving unconditional (production always has the build output) and using Vite dev server in development.