Deployment
The build is a folder of static files. What you need beyond that is a rewrite rule and four environment variables.
Build
npm run buildIt type-checks and bundles in parallel and writes dist/. Every JavaScript and CSS file is also written as .gz and .br, so a host can serve the compressed copy with no work at request time. There are no source maps, by design.
Check the real build before you ship it:
npm run previewTo see where the weight is:
ANALYZE=true npm run buildThat adds dist/stats.html, a treemap of every chunk with its gzip and brotli size.
Environment variables
Four matter in a deployed build. Set them in your host's environment UI, not in a committed file.
| Variable | Read by | Effect |
|---|---|---|
VITE_API_BASE_URL | The axios instance | The base every request is built on. Defaults to /api. |
VITE_USE_MOCK_AUTH | The auth mock and every demo mock | Only the exact string true turns them on. Leave it unset in production unless you are deploying a public demo. |
VITE_DEMO_MODE | The sign-in page | Pre-fills the demo credentials and does nothing else. Demo deploys only. |
VITE_REALTIME_URL | useRealtimeNotifications | A WebSocket or SSE endpoint. Unset means no realtime channel; the notification center still works locally. |
Two more exist and are development-only. VITE_API_TARGET is the dev proxy's destination in vite.config.ts, and VITE_PRODUCT_REAL_API=true detaches the products module's demo data so that one module talks to a real server while auth stays mocked. Neither does anything in a build.
The zip's .env.development sets VITE_USE_MOCK_AUTH and VITE_DEMO_MODE for npm run dev only. A build does not read that file, so a production build starts with the demo backend off. A public demo sets both variables in the host's environment UI.
Every variable exposed to the browser must start with VITE_. Anything without that prefix stays out of the bundle, which is exactly where a database URL or an API secret belongs.
WARNING
VITE_USE_MOCK_AUTH=true makes the app accept any sign-in and answer every request from browser memory. It must never be on in front of real users.
Any static host
The build is static files, and a single-page app needs one rule: serve index.html for any path that is not a file. Without it a refresh on /invoices/42 returns a 404 from the host before the router ever sees it.
Vercel
vercel.json ships with the one rewrite that does it:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/((?!.*\.).*)", "destination": "/index.html" }
]
}Point the project at the repository, leave the framework preset on Vite, and set your environment variables in the dashboard.
Docker
The Dockerfile builds in Node and serves from nginx:
docker build -t smart-admin .
docker run --rm -p 8080:80 --name smart-admin smart-adminThe app is then on http://localhost:8080. The image carries a health check that pings / every thirty seconds.
nginx.conf is copied in as the only server config. It already handles:
- the SPA fallback, so every unknown path resolves to
index.html; - pre-compressed assets through
gzip_static, with on-the-fly gzip as the fallback; - hashed assets cached for a year (
Cache-Control: max-age=31536000), andindex.htmlsent withCache-Control: no-cache, so a new deploy is reachable on the next navigation; - security headers on every response.
| Header | Value |
|---|---|
X-Frame-Options | SAMEORIGIN |
X-Content-Type-Options | nosniff |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=(), browsing-topics=() |
Strict-Transport-Security | max-age=31536000; includeSubDomains |
Content-Security-Policy | The line below. |
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;Edit connect-src before production. https://api.yourdomain.com is a placeholder. Replace it with your real API origin, or the browser blocks every request to it. If you turn on live notifications, add their wss:// origin to the same list.
The rest of the policy is there for what the app does:
X-Frame-Options: SAMEORIGINandframe-ancestors 'self'let the app frame itself, which the responsive preview needs, and stop every other site from framing it.style-srcallows inline styles because Reka UI writes positioning styles for popovers and dialogs at runtime, andhttps://fonts.googleapis.comfor the stylesheets of the fonts picked in the settings.img-srcallowsblob:for the file-upload previews.
HSTS starts at one year, which belongs only on a domain you serve over HTTPS and intend to keep serving over HTTPS.
The two caching location blocks set caching with expires and nothing else. nginx drops every server-level add_header in a location that has an add_header of its own, so adding one there would strip the security headers from those responses.
A checklist before you ship
VITE_USE_MOCK_AUTHis nottrue.VITE_API_BASE_URLpoints at the real API, or the host proxies/api.- The CSP
connect-srcnames your API origin. - Unknown paths fall back to
index.html. npm run buildis green, andnpm run previewbehaves like the dev server.
Where to go next
- Connect your API — the contract the deployed app expects.
- Getting started — every npm script.