The Nuxt client in apps/client/ runs as a SPA (ssr: false). In production
it is built to static assets that are embedded in the Rust binary and served
same-origin — no proxy is involved.
In development (pnpm dev:client) the Nuxt dev server runs on
http://localhost:3001 and proxies API + tile + admin requests to the Rust
backend running on a separate port. The proxy table is dev-only.
Default ports
| Concern | Default | Source |
|---|---|---|
| Nuxt dev server | :3001 | pnpm dev:client script |
| Public Rust backend | :8080 | [server].port in config.toml |
| Admin Rust backend | :8081 | [server].admin_bind in config.toml |
The Nuxt dev proxy hardcodes :8080 and :8081 as its default targets.
Overriding the backend ports
If you run tileserver-rs on non-default ports — for example because 8080 is
already in use, or because you set custom [server].port and
[server].admin_bind values in your config.toml — export two env vars
before starting the dev server:
TILESERVER_BACKEND_PORT=9000 \
TILESERVER_ADMIN_PORT=9001 \
pnpm dev:client
The Vite dev proxy reads these once at config load and rewrites every proxied target accordingly.
These env vars are deliberately not prefixed with NUXT_. The NUXT_*
prefix is reserved by Nuxt
for env vars that auto-map into runtimeConfig. The proxy block in
nuxt.config.ts runs at config-load time (before any runtime exists), so
it reads process.env directly and uses the unprefixed name.
This only affects local development. Production builds embed the SPA inside
the Rust binary, so the browser talks to whatever port the operator configures
in config.toml without any proxy layer.
Concrete example
Run the backend on :9000 (public) and :9001 (admin):
# my-config.toml
[server]
host = "127.0.0.1"
port = 9000
admin_bind = "127.0.0.1:9001"
# Terminal 1 — backend
tileserver-rs --config my-config.toml
# Terminal 2 — frontend
TILESERVER_BACKEND_PORT=9000 \
TILESERVER_ADMIN_PORT=9001 \
pnpm dev:client
The frontend at http://localhost:3001 will now proxy /data/*,
/styles/*, /__admin/*, etc. to the right backend ports.
Why two ports?
The admin server runs on a separate bind so it can be firewalled to localhost
(or a private subnet) while the public tile server stays internet-facing.
The MCP admin UI (/admin/mcp/connected-apps, /admin/mcp/devices) talks to
/__admin/oauth/* endpoints that only exist on the admin port. See the
MCP guide for the security model.