Skip to content
create-bsv-app

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.

  1. The server needs SERVER_PRIVATE_KEY, CLIENT_ORIGIN and NODE_ENV=production.
  2. The client needs VITE_API_URL at build time.
  3. 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.

build and start
cd server
npm ci
npm run build                       # tsc → dist/
NODE_ENV=production node dist/index.js

Set these in your host's environment:

dotenv
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 mainnet

The 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 forward Upgrade headers.
  • One instance, or a shared nonce store. nonceStore.ts keeps 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:

build
cd client
npm ci
VITE_API_URL=https://api.example.com npm run build   # → client/dist

Upload 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" }] }

3. Check it#

Terminal
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:

dotenv
VITE_API_URL=https://example.com
CLIENT_ORIGIN=https://example.com

CORS becomes a no-op, and there's one certificate to manage.

Production checklist#

  • Client build fixed (on 1.1.2) and npm run build passes in both apps
  • SERVER_PRIVATE_KEY generated once, stored as a secret, and not in git
  • CLIENT_ORIGIN and VITE_API_URL are exact https:// origins
  • /api/identity returns 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_NETWORK set to main if you mean it