Skip to content

Hosting the UI Separately

By default the Sparrow server serves the dashboard itself (SPARROW_SERVE_UI=true), on the same origin as the API. That needs no extra configuration and is what the Docker images and Compose files do.

This page covers the other layout: the dashboard is a static site (nginx, a CDN, object storage) on one origin, e.g. https://sparrow.example.com, and the Sparrow server runs on another, e.g. https://sparrow-api.example.com.

ConcernEmbedded UI (SPARROW_SERVE_UI=true)Separately hosted UI
Where the UI sends API callsSame originapiUrl in /config.js, or PUBLIC_API_URL at build time
API key (SPARROW_API_KEY)Sign-in prompt on first 401 (key never written into the page)Sign-in prompt on first 401, or set in /config.js
CORSNot neededCORS_ALLOWED_ORIGINS must list the UI origin
Security headers (CSP, framing)Set by SparrowSet by your static host
Terminal window
cd web
npm ci
npm run build # output: ../internal/ui/dist

Upload the contents of internal/ui/dist/ to your static host. The same build works for any server: the API URL is set at deploy time in config.js (next step), so you don’t need to rebuild for each environment.

If you’d rather bake the URL in at build time, set PUBLIC_API_URL for the build:

Terminal window
PUBLIC_API_URL=https://sparrow-api.example.com npm run build

PUBLIC_API_URL is read only by vite build. Changing it later means rebuilding. An apiUrl in config.js overrides it.

The build ships a config.js next to index.html. It is loaded before the app starts. Edit it on the static host:

window.__SPARROW_CONFIG__ = window.__SPARROW_CONFIG__ || {
apiUrl: "https://sparrow-api.example.com",
// apiKey: "", // optional, see "Authentication" below
};
  • apiUrl: the absolute URL of the Sparrow server. A path prefix is fine (https://gw.example.com/sparrow) if a reverse proxy mounts Sparrow there. Trailing slashes are ignored.
  • apiKey: optional. Leave it out and the UI asks for the key when it needs it.

Serve config.js and index.html with Cache-Control: no-cache so edits take effect straight away.

Every unknown path must return index.html, because the UI routes on the client. The UI must be served at the root of its origin (https://sparrow.example.com/), not under a sub-path, since assets are referenced as /_app/... and /config.js.

nginx example:

server {
listen 443 ssl;
server_name sparrow.example.com;
root /srv/sparrow-ui;
location /_app/immutable/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /config.js {
add_header Cache-Control "no-cache";
}
location / {
add_header Cache-Control "no-cache";
try_files $uri /index.html;
}
}
Terminal window
# Exact origin of the UI: scheme + host + port, no path.
CORS_ALLOWED_ORIGINS=https://sparrow.example.com
SPARROW_API_KEY=<long random string>
ENVIRONMENT=production
# Optional: turn off the server's own copy of the UI.
SPARROW_SERVE_UI=false
  • CORS_ALLOWED_ORIGINS is required. With ENVIRONMENT=production and no allowlist, the server rejects every cross-origin request. Without ENVIRONMENT=production and no allowlist, it accepts any origin, which is only meant for local development. List more origins separated by commas. A trailing slash is ignored.
  • The UI sends the key in the X-API-Key header, never as a cookie, so no credentialed CORS is involved.

When the server has SPARROW_API_KEY set, the UI needs a credential. Pick one of these options (1 and 3 combine well):

  1. Prompt (default, recommended). Leave apiKey out of config.js. On the first 401 the UI shows a Sign in to Sparrow prompt. You can paste the master key or an access token. A pasted master key is exchanged for a browser token behind the scenes, so the master key is never stored in the browser. The sidebar shows “Signed in as <name>” with a Sign out button. If the stored credential stops working (revoked, expired, or key rotated), the prompt opens again with a clear message.
  2. apiKey in config.js. Nobody has to type anything, but anyone who can load the UI can read the key (see Security). apiKey may be the master key or a tenant-wide access token. Don’t use it if you expose the consumer portal from the same host, because portal visitors can download config.js too.
  3. One-time invite. Run sparrow invite alice --ui-url https://sparrow.example.com (or POST /v1/invites) and send the printed link. Opening it redeems the invite and creates a named access token for that browser — the recipient never sees the master key. Each link works once, expires after its TTL (default 24 hours, max 7 days), and can be cancelled with sparrow invites cancel. Consumer invites (--consumer acme) open the portal instead of the console. See Access: Tokens and Invites for the full guide.
  4. Authenticating proxy. Put the UI and API behind a proxy that logs users in and adds X-API-Key itself (see Security → auth proxy). Leave apiKey empty.

The portal (/portal) works from a separately hosted UI. Its API calls go to <apiUrl>/portal/api/... with the consumer’s bearer token, never the admin key. When you mint a link with POST /v1/tokens and a consumer, the response’s portal_path (/portal#token=...) is relative. Prepend the UI’s base URL (https://sparrow.example.com/portal#token=...), not the API’s.

make run (server on :8080) plus make run-web (vite on :5173) is also a split deployment. It works without configuration because the server allows any origin when ENVIRONMENT isn’t production, and the dev UI defaults to http://localhost:8080. If you run the server with ENVIRONMENT=production, also set CORS_ALLOWED_ORIGINS=http://localhost:5173.

SymptomLikely cause
Browser console shows blocked by CORS policyUI origin missing from CORS_ALLOWED_ORIGINS (check scheme and port), or the server is in production mode with no allowlist
API calls go to the UI host and return HTML or 404No apiUrl in config.js and the build had no PUBLIC_API_URL
API key required dialog keeps coming backThe key you entered doesn’t match the server’s SPARROW_API_KEY
Reloading a deep link like /webhooks/abc returns 404Static host has no SPA fallback to index.html
Blank page, /_app/... requests return 404UI served under a sub-path; serve it at the origin root