# Deploy to production

> Ship the client and server separately. Four environment variables, two hosting rules, one checklist.

> Agents: search these docs with the `search_docs` tool on the MCP server at https://createbsvapp.vercel.app/mcp, or read everything at https://createbsvapp.vercel.app/llms-full.txt.

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.

::: warning On create-bsv-app 1.1.2? Fix the client build first
The generated client's `npm run build` fails type-checking in 1.1.2, and `useWalletLogin()` throws at runtime. It's a three-line fix. [Apply it here](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails) before you deploy.
:::

## 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.

```bash [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](https://createbsvapp.vercel.app/docs/environment#generate-a-server-key) once and store it in your host's secret manager.

::: danger Rotating `SERVER_PRIVATE_KEY` changes your server's identity
Clients that fetch the identity on load will adapt. Anything that pinned the old key (mobile apps, partner servers, your own config) will reject the new one. Treat the key like a domain name: pick it once.
:::

### 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](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances) before you scale out.

## 2. Deploy the client

`VITE_API_URL` is baked in when you build, so set it **before** `vite build`:

```bash [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.

::: warning Forgot `VITE_API_URL`? You get a blank page
The build still succeeds. The app then throws `VITE_API_URL is required in production` the moment it loads in the browser. If production shows a white screen, check the browser console first.
:::

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

::: code-group
```json [vercel.json]
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
```
```text [netlify _redirects]
/*    /index.html   200
```
```nginx [nginx]
location / {
  try_files $uri /index.html;
}
```
:::

## 3. Check it

```bash
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
