Server
What the chroma dev server serves, on which ports, with which MIME types and status codes, and how to deploy dist/ to a static host.
chroma dev serves your project with a small local HTTP server, the engine binary built from Chroma-Server. It only sends files: index.html, JavaScript, .chr modules, CSS, and chroma_bg.wasm. It does not run your app logic or API route handlers; those run in the browser after the files load.
The dev server is the usual Portable (Web) entry during development (chroma dev --no-desktop). Executable (Chroma Desktop) develop mode still uses this server in the background; Desktop only provides the window and host bridges (Runtime modes).
Who serves what
| Situation | Server | Port | Bind address |
|---|---|---|---|
chroma dev | Installed engine binary | dev.port from chroma.json, --port, else 4173 | 127.0.0.1 |
engine run by hand | Same binary | --port, else 4173 | 127.0.0.1 |
| Chroma Desktop library (imported apps) | Server built into Desktop, a separate implementation of the same design | 47823, or a free port if that one is busy | 127.0.0.1 |
| Production | Your static host, not engine | Host-defined | Usually public |
Ports
- The dev server and Desktop's built-in server are different programs on different ports.
chroma devwithdev.desktop: truestarts the dev server on the dev port and opens Desktop pointed athttp://127.0.0.1:<dev port>/<entry>. Apps you import into Desktop run from port47823. - Desktop keeps
47823fixed so the origin, and therefore IndexedDB data, survives restarts. If another program holds the port, Desktop falls back to a random free port and storage will not persist across restarts. - The dev server binds the loopback interface only. There is no option to listen on
0.0.0.0, so other machines on your network cannot reach it. If the port is taken, the server logs an error and exits with status1.
Contract
| Topic | Behavior |
|---|---|
| Methods | GET and HEAD only. Anything else returns 405. This includes a POST to /api/...: API routes run in the browser through @chroma/router, not here. |
| TLS | None. |
| Directory listing | None. / and any directory path serve that folder's index.html, else 404. |
| Query string and fragment | Stripped before the file lookup, so /app.js?v=1 serves /app.js. |
| SPA fallback | None. Unknown paths return 404; they are not rewritten to index.html. |
| Percent-encoding | Paths are decoded (%20 becomes a space) before lookup. |
| Connections | One thread per connection, no keep-alive. Every response sends Connection: close. |
| Request size | One 8 KB read. Request bodies are never read, and headers beyond 8 KB are cut off. |
| Headers sent | Only Content-Type, Content-Length, and Connection. No caching, CORS, Date, or Server headers. |
Status codes
| Status | When |
|---|---|
200 | The file was found and read. HEAD returns the same headers with no body. |
403 | The resolved path (after .. segments, encoded traversal, and symlinks) is outside the served folder. |
404 | The path does not exist, or a directory has no index.html. |
405 | The method is not GET or HEAD. |
Error bodies are plain text (Not Found, Forbidden, Method Not Allowed). Path safety does not rely on string checks: the server resolves the real path of every request and verifies it still lies inside the root.
MIME types
The type comes from the file extension only:
| Extension | Content-Type |
|---|---|
html | text/html; charset=utf-8 |
js, mjs, chr | text/javascript; charset=utf-8 |
css | text/css; charset=utf-8 |
json, map | application/json; charset=utf-8 |
wasm | application/wasm |
svg | image/svg+xml |
png | image/png |
jpg, jpeg | image/jpeg |
ico | image/x-icon |
txt | text/plain; charset=utf-8 |
| anything else | application/octet-stream |
Run it by hand
You rarely need to, but you can serve any folder, for example to smoke-test a build:
cd my-app
chroma build
engine --port 4173 --dir ./dist # open http://127.0.0.1:4173/| Flag | Default | Meaning |
|---|---|---|
--port <n> | 4173 | Port to listen on. |
--dir <path> | . | Folder to serve. If it does not exist, the server exits with status 1. |
--reload, --no-reload | reload off | Live reload switches; see Live reload. |
CHROMA_LOG_LEVEL filters the server's log output; the values are in the CLI log levels table.
Dependencies and live reload
Static serving uses only the Rust standard library. The live-reload feature adds the sha1 crate for the WebSocket handshake. It watches files by polling modification times every 200 ms, and it injects /__chroma/dev-reload.js into served HTML only while reload is enabled. The crate manifest also lists notify, which the current source does not use. The endpoints, watched files, and every switch are documented in Live reload.
Deploying dist/
After chroma build, upload the contents of outDir (default dist/) to any static host. You do not deploy engine.
| Asset | Requirement |
|---|---|
index.html | The entry point, served at /. |
bundle.js (dynamic) or minified scripts (static) | Served as JavaScript. |
chroma_bg.wasm (dynamic) | Served as application/wasm, in the same folder as the bundle. |
plugins/ and chroma.json | Fetched at runtime by plugins/loader.js; keep them. |
Mirror the table above on your host: .wasm as application/wasm and .js/.mjs/.chr as JavaScript. Browsers cannot load .wasm from file://, so always use HTTP or HTTPS.
Plugins in dist/plugins/ load the same way on a static host as in chroma dev, but side effects (IndexedDB partitions, Chroma-Fs backend choice, live reload) depend on the runtime mode. The API router still runs only in the page; this server never executes api/ handlers.
For the Executable path, upload is not required: users import the zip from chroma build into Chroma Desktop, which serves the same files from loopback HTTP on a different port (Who serves what).