Panel Deployment¶
Build, deploy, and serve the QS-Bridge admin panel.
Build Pipeline¶
The panel is a static single-page application. One command produces a deployable bundle:
Build Output¶
dist/
├── index.html # SPA entry point
├── assets/
│ ├── index-[hash].js # Application bundle (~180 KB gzipped)
│ ├── index-[hash].css # Compiled Tailwind styles (~28 KB gzipped)
│ ├── vendor-[hash].js # React + dependencies (~65 KB gzipped)
│ └── stdb-[hash].js # SpacetimeDB SDK chunk (~22 KB gzipped)
├── favicon.ico
└── logo.svg
Code Splitting¶
Vite's chunk strategy keeps the initial payload under 300 KB:
| Chunk | Contents | Size (gzip) |
|---|---|---|
index |
App shell, router, pages | ~180 KB |
vendor |
React 19, React Router, clsx | ~65 KB |
stdb |
SpacetimeDB TypeScript SDK | ~22 KB |
| CSS | Tailwind utilities | ~28 KB |
Environment Variables¶
Build-time variables (embedded via import.meta.env):
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
/api |
API gateway base URL |
VITE_STDB_HOST |
ws://localhost:3000 |
SpacetimeDB WebSocket endpoint |
VITE_STDB_MODULE |
qs-bridge |
SpacetimeDB module name |
VITE_APP_TITLE |
QS-Bridge |
Browser tab title |
Runtime variables (read from window config):
| Variable | Description |
|---|---|
PANEL_SESSION_TIMEOUT |
JWT expiry (default 24 hours) |
Deployment Options¶
Option 1 — Bundled with API Gateway (Default)¶
The API gateway serves the panel's static files directly:
gateway/
├── server.js # Express + API routes
├── dist/ # ← Panel build output copied here
│ ├── index.html
│ └── assets/
└── Dockerfile
// gateway/server.js
app.use(express.static('dist'));
app.get('*', (req, res) => res.sendFile('dist/index.html'));
Advantages: Single container, single port, no CORS configuration needed.
Option 2 — Separate Static Server¶
Serve the panel from nginx or any static file server:
server {
listen 443 ssl;
server_name panel.example.com;
root /var/www/panel/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://gateway:3100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Option 3 — Docker¶
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Serve stage
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
SPA Routing¶
The panel uses client-side routing with React Router. The server must return index.html for all non-asset paths:
GET /dashboard → 200 index.html (React Router handles route)
GET /players → 200 index.html
GET /assets/index.js → 200 (actual static file)
GET /api/servers → proxy to API gateway
Without the try_files fallback (nginx) or wildcard route (Express), refreshing any page other than / returns 404.
Health Checks¶
| Endpoint | Source | Response |
|---|---|---|
GET / |
Panel (index.html) | 200 OK |
GET /api/health |
API Gateway | { status: 'ok', uptime: ... } |
ws://host:3000 |
SpacetimeDB | WebSocket upgrade |
Production Checklist¶
- Build with
NODE_ENV=production - Set
VITE_API_URLto production gateway URL - Set
VITE_STDB_HOSTto production SpacetimeDB host - Enable gzip/brotli compression on the serving layer
- Set
Cache-Control: max-age=31536000for/assets/(files are content-hashed) - Set
Cache-Control: no-cacheforindex.html - Configure HTTPS (TLS 1.2+ required for WebSocket)
- Verify SPA fallback routing works (refresh on
/playersreturns 200)
Related Pages¶
- Panel Overview — technology stack and design system
- API Gateway — backend routes and configuration
- Installation — full platform deployment guide