Guides
Deploy to production
Ship the client and server separately. Four environment variables, two hosting rules, one checklist.
A full-stack scaffold is two deployables: a static client (client/dist) and a Node server (server/dist). Host them wherever you like, together or apart.
The short version: four variables, three on the server and one at client build time.
- The server needs
SERVER_PRIVATE_KEY,CLIENT_ORIGINandNODE_ENV=production. - The client needs
VITE_API_URLat build time. - Both URLs must be HTTPS.
1. Deploy the server#
Any host that runs a long-lived Node 22 process and supports WebSockets will do: a VPS, Docker, Railway, Render, Fly.io and so on.
cd server
npm ci
npm run build # tsc → dist/
NODE_ENV=production node dist/index.jsSet these in your host's environment:
NODE_ENV=production
SERVER_PRIVATE_KEY=<64 hex chars, generated once, kept forever>
CLIENT_ORIGIN=https://app.example.com
PORT=3000 # or whatever your host injects
BSV_NETWORK=main # if you're going to mainnetThe server won't start without the first three, and that's on purpose. Generate a key once and store it in your host's secret manager.
Two hosting rules#
- WebSockets must reach
/ws. The mobile wallet's QR pairing uses a WebSocket upgrade on the same server. Serverless function platforms usually can't hold one open, so run the server as a regular process. If you put a proxy in front (nginx, Caddy, a load balancer), let it forwardUpgradeheaders. - One instance, or a shared nonce store.
nonceStore.tskeeps used nonces in memory. With two or more instances, a proof consumed on one isn't known to the others. Move it to Redis or your database before you scale out.
2. Deploy the client#
VITE_API_URL is baked in when you build, so set it before vite build:
cd client
npm ci
VITE_API_URL=https://api.example.com npm run build # → client/distUpload client/dist to any static host: Vercel, Netlify, Cloudflare Pages, S3 + CloudFront, or nginx.
Single-page routing#
The client uses client-side routes (/login, /signed-demo and yours). Configure your host to serve index.html for unknown paths, or a refresh on /login returns a 404:
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }/* /index.html 200location / {
try_files $uri /index.html;
}3. Check it#
curl https://api.example.com/health # {"status":"ok"}
curl https://api.example.com/api/identity # {"identityKey":"03…"}Run the identity check again after a restart. The key must not change. Then open the client, connect a wallet, and run the login demo.
Same origin, if you prefer#
Prefer one domain? Put both behind a reverse proxy, with /api and /ws going to the server and everything else to client/dist. Then:
VITE_API_URL=https://example.com
CLIENT_ORIGIN=https://example.comCORS becomes a no-op, and there's one certificate to manage.
Production checklist#
- Client build fixed (on 1.1.2) and
npm run buildpasses in both apps SERVER_PRIVATE_KEYgenerated once, stored as a secret, and not in gitCLIENT_ORIGINandVITE_API_URLare exacthttps://origins/api/identityreturns the same key after a restart- WebSockets reach
/ws - Nonce store is shared, or you run exactly one instance
- Demo pages (
/login,/signed-demo,/api/echo) removed or kept on purpose BSV_NETWORK/VITE_BSV_NETWORKset tomainif you mean it