# create-bsv-app documentation (CLI v1.1.2) Every page of https://createbsvapp.vercel.app/docs in one file, in reading order. --- # Quick start > Go from an empty folder to a running BSV app with a connected wallet, passwordless login and signed API calls. > 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. One command gives you a React client, an Express server, a Connect wallet button and two working demos, with providers, routes and CORS already wired. You won't write any key-handling or transaction code to get there. ::: tip TL;DR Install [BSV Browser](https://browser.bsvb.tech/), then: ```bash npx create-bsv-app@latest my-app \ --starter full-stack \ --capabilities wallet-login,signed-requests --yes cd my-app && npm run dev ``` Open http://localhost:5173 and click **Connect wallet**. The rest of this page explains each step. ::: ## Before you start You need two things: - **Node.js 22 or newer.** Run `node -v` to check. Older versions are unsupported and may fail in odd ways. - **A BRC-100 wallet.** We recommend [BSV Browser](https://browser.bsvb.tech/). It's free and runs on macOS, Windows, iOS and Android. It holds your keys, so your app never does. ::: tip Nothing here costs money Logging in and signing requests are *signatures*, not transactions. No coins move, and new projects target testnet by default. ::: ## Create your app ::::: steps ### Scaffold the project Pick your package manager: ::: code-group ```bash [npm] npx create-bsv-app@latest my-app \ --starter full-stack \ --capabilities wallet-login,signed-requests \ --yes ``` ```bash [pnpm] pnpm create bsv-app my-app \ --starter full-stack \ --capabilities wallet-login,signed-requests \ --package-manager pnpm --yes ``` ```bash [yarn] yarn create bsv-app my-app \ --starter full-stack \ --capabilities wallet-login,signed-requests \ --package-manager yarn --yes ``` ```bash [bun] bun create bsv-app my-app \ --starter full-stack \ --capabilities wallet-login,signed-requests \ --package-manager bun --yes ``` ::: That's a React + Express app with all three [capabilities](https://createbsvapp.vercel.app/docs/capabilities). `wallet-connect` is always included. `--yes` skips the prompts, and dependencies install before the command exits. ::: warning Using pnpm, yarn or bun? Pass `--package-manager` The CLI doesn't detect which package manager launched it. Without the flag it installs with npm, whatever command you ran. ::: ### Start both apps ```bash cd my-app npm run dev ``` The root `dev` script starts the Vite client on **http://localhost:5173** and the Express server on **http://localhost:3000** together. Press Ctrl + C once to stop both. ### Connect your wallet Open **http://localhost:5173**. You'll see *BSV app* and a **Connect wallet** button. - **Desktop wallet open?** Approve the request in BSV Browser. The page switches to `Connected: 02ab…`. That hex string is your identity key. - **No desktop wallet found?** A dialog offers **Connect with a mobile wallet**. Scan the QR code with BSV Browser on your phone and you're paired. ### Try the demos Once you're connected, a **Demos** list appears: - **Wallet login →** signs a `login` proof and sends it to `POST /api/login`. The server verifies it and replies with your identity key. - **Signed request demo →** signs a single API call to `POST /api/echo`, bound to its exact body. Both pages log every step, so you can watch the full exchange between wallet, client and server. ::::: ::: tip It worked if… You see `✓ Logged in as 02…` on the login page. If you don't, check [Troubleshooting](https://createbsvapp.vercel.app/docs/troubleshooting#wallet). ::: ## Other ways to start **Answer prompts instead of passing flags.** Run the command with no arguments and the CLI asks for the mode, starter, name, stack, capabilities, package manager and network: ```bash npx create-bsv-app@latest ``` ::: warning The `custom` starter needs a stack `custom` is the default starter, and it defaults to no frontend and no backend. Choose at least one, or pick `full-stack`. Otherwise the CLI stops with `Invalid config: a new project needs at least a frontend or a backend`. ::: **Use a form in your browser.** `--ui` opens a local, single-use configurator on `127.0.0.1` that walks through the same questions: ```bash npx create-bsv-app@latest --ui --dir my-app ``` **Copy a finished example app.** Clone one of the [complete examples](https://createbsvapp.vercel.app/docs/starters#complete-examples) and read its README: ```bash npx create-bsv-app@latest my-meter-app --starter meter --yes ``` ::: note You'll see two sets of "next steps". Follow the last one Halfway through, create-vite prints its own *Done. Now run: cd client / npm install / npm run dev*. Ignore it. The final **Next:** block is the one written for your project. ::: ## Where to next ::: cards [**Try it in your browser** The interactive tutorial: edit the real scaffold, log in, sign a request, break it. Nothing to install.](https://createbsvapp.vercel.app/learn/welcome) [**Build your first feature** A 15-minute tutorial: a guestbook where every entry is signed by a wallet.](https://createbsvapp.vercel.app/docs/tutorial) [**Tour the project** Every generated file, and which ones you should edit.](https://createbsvapp.vercel.app/docs/project-structure) [**BSV for web devs** Wallets, identity keys and BRCs in plain English.](https://createbsvapp.vercel.app/docs/bsv-primer) [**Ship it** The three environment variables production needs, and why.](https://createbsvapp.vercel.app/docs/deploy) ::: --- # Tutorial: a signed guestbook > Build a real feature on top of your scaffold. Every guestbook entry is signed by a wallet and verified by your server, with no accounts or passwords. > 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. You'll add one API route, one React page and one test script. By the end, your server will know *exactly* who wrote each entry, cryptographically, without storing a single password. **You'll learn how to:** - protect an Express route with `verifySignedRequest()` - call it from React with `signedFetch()` - prove from the terminal that replayed and tampered requests are rejected Allow about 15 minutes. The code on this page was compiled and run against create-bsv-app 1.1.2. ::: tip New to signed requests? Try them in your browser first The [interactive tutorial](https://createbsvapp.vercel.app/learn/welcome) runs the scaffold's real code in the page, with a tutorial wallet: log in, send a signed request, then replay it and watch the server refuse. Nine short lessons, nothing to install. This page then builds the same ideas into your own project. ::: ## What you're building ```text [the flow] Browser Wallet Your server ─────── ────── ─────────── type "gm", click Sign ──▶ signs { action, body } with your identity key ──▶ POST /api/guestbook { proof, body } verifySignedRequest() ✓ signature valid for this body ✓ nonce never seen before ✓ proof not expired ◀─────────────────────────────────────────────────────── 201 { identityKey, message } ``` The server never sees a password or a session cookie. It gets a signature it can check, and the identity key that made it. ## 1. Scaffold the app Skip this step if you already followed the [quick start](https://createbsvapp.vercel.app/docs). Otherwise: ::: code-group ```bash [npm] npx create-bsv-app@latest guestbook --starter full-stack --capabilities signed-requests --yes cd guestbook ``` ```bash [pnpm] pnpm create bsv-app guestbook --starter full-stack --capabilities signed-requests --package-manager pnpm --yes cd guestbook ``` ```bash [yarn] yarn create bsv-app guestbook --starter full-stack --capabilities signed-requests --package-manager yarn --yes cd guestbook ``` ```bash [bun] bun create bsv-app guestbook --starter full-stack --capabilities signed-requests --package-manager bun --yes cd guestbook ``` ::: Run `npm run dev` and keep it running. The server restarts itself when you save, and the client hot-reloads. ## 2. Add the server route Create a new file next to the server entry. Anyone can read the guestbook. Writing needs a valid signed request. ::: code-group ```ts [server/src/guestbook.ts] twoslash // A tiny guestbook: anyone can read, only a wallet-signed request can write. import type { Request, Response } from 'express' import { verifySignedRequest } from './bsv/verifySignedRequest.js' import { consumeNonce } from './bsv/nonceStore.js' type ServerWallet = Parameters[0] interface Entry { identityKey: string, message: string, at: string } const entries: Entry[] = [] // in memory for now; swap for a database later export function listEntries (_req: Request, res: Response): void { res.json(entries.slice(-50).reverse()) } export function signEntry (serverWallet: ServerWallet) { return async (req: Request, res: Response): Promise => { const { proof, body } = req.body ?? {} const message = typeof body?.message === 'string' ? body.message.trim() : '' if (message.length === 0 || message.length > 280) { res.status(400).json({ error: 'message must be 1–280 characters' }) return } // The proof is bound to this exact action + body. Change either and it fails. const result = await verifySignedRequest(serverWallet, proof, { action: 'sign-guestbook', body }, consumeNonce) if (!result.valid || result.identityKey == null) { res.status(401).json({ error: 'invalid proof' }) return } const entry = { identityKey: result.identityKey, message, at: new Date().toISOString() } entries.push(entry) res.status(201).json(entry) } } ``` ```ts [bsv/verifySignedRequest.ts (generated)] // Framework-agnostic verification of a signed request. Works in Express, Next API // routes, Fastify — it's a plain function. Pass your own single-use nonce store. import { verifyAuthProof, type AuthProof, type RequestBody } from './auth.js' export async function verifySignedRequest ( serverWallet: { verifySignature: (args: any) => Promise<{ valid: boolean }> }, proof: AuthProof, opts: { action: string, body?: RequestBody }, consumeNonce: (nonce: string, expiresAt: Date) => boolean | Promise ): Promise<{ valid: boolean, identityKey?: string, error?: string }> { return await verifyAuthProof(serverWallet, proof, { action: opts.action, body: opts.body }, consumeNonce) } ``` ::: Then mount both handlers in `server/src/index.ts`, next to the existing `/api/echo` route: ```ts [server/src/index.ts] import { consumeNonce } from './bsv/nonceStore.js' import { listEntries, signEntry } from './guestbook.js' // ← add this line // … app.post('/api/echo', async (req, res) => { /* … */ }) app.get('/api/guestbook', listEntries) // ← add this line app.post('/api/guestbook', signEntry(serverWallet)) // ← add this line ``` ::: deep Why `.js` in a TypeScript import? The server compiles to Node ES modules (`"module": "NodeNext"`), and Node needs the real file extension at runtime. TypeScript resolves `./guestbook.js` to `guestbook.ts` while type-checking. The generated files follow the same rule. ::: ## 3. Build the page `useSignedRequest()` gives you `signedFetch()`. It fetches the server's identity, asks the wallet to sign `{ action, body }`, and posts `{ proof, body }` through the bounded API client. ::: code-group ```tsx [client/src/Guestbook.tsx] twoslash import { useEffect, useState, type FormEvent } from 'react' import { Link } from 'react-router-dom' import { ConnectWallet } from './bsv/ConnectWallet' import { useSignedRequest } from './bsv/useSignedRequest' import { apiFetch, readApiJson } from './bsv/apiClient' interface Entry { identityKey: string, message: string, at: string } async function fetchEntries (): Promise { const res = await apiFetch('/api/guestbook') return res.ok ? await readApiJson(res) as Entry[] : [] } export function Guestbook () { const { signedFetch, connected } = useSignedRequest() const [entries, setEntries] = useState([]) const [message, setMessage] = useState('') const [status, setStatus] = useState(null) useEffect(() => { let live = true fetchEntries().then((list) => { if (live) setEntries(list) }).catch(() => {}) return () => { live = false } }, []) const sign = async (e: FormEvent) => { e.preventDefault() setStatus('Approve the request in your wallet…') try { const res = await signedFetch('/api/guestbook', { action: 'sign-guestbook', body: { message } }) if (!res.ok) { setStatus(`Server rejected it (${res.status})`); return } setMessage('') setStatus(null) setEntries(await fetchEntries()) } catch (err) { setStatus(String(err)) } } return (
← Back to home

Guestbook

Sign with your wallet. No account, no password.

{connected && (
{ void sign(e) }}> setMessage(e.target.value)} maxLength={280} placeholder="Say gm" />
)} {status != null &&

{status}

}
    {entries.map((entry) => (
  • {entry.identityKey.slice(0, 10)}… {entry.message}
  • ))}
) } ``` ```ts [bsv/useSignedRequest.ts (generated)] // Hook: signedFetch attaches a proof bound to the route + JSON body. import { useCallback } from 'react' import { useWallet } from './WalletContext.js' import { createSignedRequest } from './signedRequest.js' import { getServerIdentity, requireIdentityKey } from './serverIdentity.js' import { apiFetch } from './apiClient.js' import type { RequestBody } from './auth.js' // serverIdentityKey is optional: when omitted it's fetched from GET /api/identity. export function useSignedRequest (serverIdentityKey?: string) { const { wallet } = useWallet() const signedFetch = useCallback(async (url: string, opts: { action: string, body?: RequestBody }): Promise => { if (wallet === null) throw new Error('connect a wallet first') const counterparty = requireIdentityKey(serverIdentityKey ?? await getServerIdentity()) const proof = await createSignedRequest(wallet, { serverIdentityKey: counterparty, action: opts.action, body: opts.body }) return await apiFetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ proof, body: opts.body }) }) }, [wallet, serverIdentityKey]) return { signedFetch, connected: wallet !== null } } ``` ::: Register the route and link to it from the home hub: ::: code-group ```tsx [client/src/App.tsx] import { SignedRequestDemo } from './bsv/SignedRequestDemo' import { Guestbook } from './Guestbook' // ← add this line // …inside } /> } /> {/* ← add this line */} ``` ```tsx [client/src/bsv/Home.tsx] Signed request demo → Guestbook → {/* ← add this line */} ``` ::: ::: warning `action` and `body` must match on both sides The client signs `action: 'sign-guestbook'` with `{ message }`. The server must verify the same action and the same body object it received. Rename one side only and every request comes back `401`. ::: ## 4. Try it Open **http://localhost:5173**, connect your wallet, then click **Guestbook →**. Type a message and press **Sign guestbook**. Your wallet asks you to approve, and then your entry appears with the first characters of your identity key. Open a second browser profile with a different wallet and sign again. Two keys, two authors, and no user table anywhere. ## 5. Break it on purpose A guestbook is only as good as its forgery protection, so let's attack it. This script signs entries with a brand-new throwaway key, then tries a **replay** (sending the same proof twice) and a **tamper** (a valid proof with a different body): ```ts [server/scripts/try-guestbook.ts] twoslash // @filename: scripts/try-guestbook.ts // ---cut--- // Sign guestbook entries from Node with a throwaway key, then try to cheat. import { PrivateKey, ProtoWallet } from '@bsv/sdk' import { createAuthProof } from '../src/bsv/auth.js' const API = 'http://localhost:3000' const { identityKey: server } = await (await fetch(`${API}/api/identity`)).json() const wallet = new ProtoWallet(PrivateKey.fromRandom()) // a brand-new identity async function post (label: string, payload: unknown) { const res = await fetch(`${API}/api/guestbook`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) }) console.log(label.padEnd(10), res.status, await res.text()) } const body = { message: 'gm from Node' } const proof = await createAuthProof(wallet, { counterparty: server, action: 'sign-guestbook', body }) await post('signed', { proof, body }) await post('replayed', { proof, body }) await post('tampered', { proof: await createAuthProof(wallet, { counterparty: server, action: 'sign-guestbook', body }), body: { message: 'gm from Mallory' } }) ``` Run it from the `server` folder while `npm run dev` is still going. You should see: ```console $ cd server $ npx tsx scripts/try-guestbook.ts signed 201 {"identityKey":"020d26…","message":"gm from Node","at":"…"} replayed 401 {"error":"invalid proof"} tampered 401 {"error":"invalid proof"} ``` The replay fails because the server already consumed that proof's nonce. The tamper fails because the signature covers the exact body, and `gm from Mallory` isn't what was signed. ::: deep What exactly is in a proof? The wallet signs `{ action, identityKey, expiresAt, nonce }` with the body's exact JSON bytes appended, using a key derived between *your* identity key and the *server's* identity key (that's why the client fetches `GET /api/identity` first). The server checks the signature, checks that `expiresAt` hasn't passed (proofs live for 2 minutes by default), and records the nonce so it can never be used again. See [Security model](https://createbsvapp.vercel.app/docs/security) for the full list of guarantees. ::: ## Recap You built a feature where: - **identity comes from the wallet.** `result.identityKey` is the only "user id" you need. - **every write is authenticated on its own.** No session to steal, no cookie to fixate. - **forgery fails closed.** Replays, tampering and expired proofs all get `401`. ## Where to take it next - **Persist it.** Swap the `entries` array for a database, keyed by `identityKey`. - **Harden replay protection.** The generated `nonceStore.ts` is in-memory and single-process. Before you run more than one server instance, back it with Redis or your database. [Here's how](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances). - **Add sessions if you want them.** Use [wallet login](https://createbsvapp.vercel.app/docs/capabilities#wallet-login) once, then issue your own session or JWT. - **Ship it.** [Deploy](https://createbsvapp.vercel.app/docs/deploy) the client and server, with the three environment variables production needs. --- # Project structure > Every file a full-stack scaffold creates, what it does, and which ones are yours to change. > 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. This is what `--starter full-stack --capabilities wallet-login,signed-requests` writes, minus create-vite's own assets and lint config. Other starters produce a subset: `react` is the `client/` half at the project root, and `express` is the `server/` half. ## The tree ```tree my-app/ ├── AGENTS.md # how each capability works, for humans and AI agents ├── bsv-scaffold.json # what was generated (read by later `add` runs) ├── package.json # root runner: dev, build, install:apps ├── scripts/ │ └── run-apps.mjs # starts client + server together ├── client/ # Vite + React + TypeScript (from create-vite) │ ├── package.json │ ├── vite.config.ts │ └── src/ │ ├── main.tsx # wraps in │ ├── App.tsx # routes: /, /login, /signed-demo │ └── bsv/ │ ├── config.ts # API_BASE_URL, BSV_NETWORK (from VITE_* env) │ ├── apiClient.ts # the one fetch wrapper: bounded, no redirects │ ├── auth.ts # createAuthProof / verifyAuthProof │ ├── serverIdentity.ts # getServerIdentity() → GET /api/identity │ ├── walletAcquisition.ts # desktop wallet via WalletClient('auto') │ ├── WalletConnectionContext.tsx # mobile QR relay session │ ├── WalletContext.tsx # useWallet(): status, wallet, identityKey │ ├── WalletProviders.tsx # both providers in one component │ ├── ConnectWallet.tsx # the button + "no wallet" dialog │ ├── Home.tsx # demo hub │ ├── WalletLogin.tsx # /login demo (wallet-login) │ ├── useWalletLogin.tsx # login() hook (wallet-login) │ ├── SignedRequestDemo.tsx # /signed-demo demo (signed-requests) │ ├── signedRequest.ts # createSignedRequest (signed-requests) │ ├── useSignedRequest.ts # signedFetch() hook (signed-requests) │ └── bsv.css # minimal demo styles └── server/ # Express 5 + TypeScript, run with tsx ├── package.json └── src/ ├── index.ts # routes, CORS, wallet relay, listen() └── bsv/ ├── config.ts # SERVER_PRIVATE_KEY, PORT, CLIENT_ORIGIN, BSV_NETWORK ├── auth.ts # same proof helpers as the client ├── nonceStore.ts # single-use nonces (in memory) ├── loginRoute.ts # POST /api/login (wallet-login) └── verifySignedRequest.ts # framework-agnostic (signed-requests) ``` ## What runs where | What | Client | Server | | --- | --- | --- | | Dev command | `vite` | `tsx watch src/index.ts` | | Dev URL | http://localhost:5173 | http://localhost:3000 | | Build | `tsc -b && vite build` → `client/dist` | `tsc` → `server/dist` | | Start in production | any static host | `node dist/index.js` | From the project root, `npm run dev` runs both dev commands, `npm run build` builds both, and `npm run install:apps` reinstalls both. Each app also works on its own: `cd client && npm run dev` is fine. Client and server are **separate packages** with their own `package.json`, lockfile and `node_modules`. You can deploy them to different hosts, and add a database driver to the server without touching the client. ## Server routes | Route | From | What it does | | --- | --- | --- | | `GET /health` | base | `{ "status": "ok" }` for load balancers | | `GET /api/identity` | wallet-connect | the server's public identity key | | `GET /api/session`, `/ws` | wallet-connect | mobile wallet pairing (QR relay) | | `POST /api/login` | wallet-login | verifies a `login` proof, returns `{ identityKey }` | | `POST /api/echo` | signed-requests | verifies a signed request, echoes the signer | ## Which files are yours? **All of them.** Nothing is hidden in a package or framework. That said, they fall into three groups: - **Edit freely:** `App.tsx`, `main.tsx`, `server/src/index.ts`, `Home.tsx`, the demo pages and `bsv.css`. These are starting points, and the demo pages are there to delete. - **Read before you edit:** `config.ts`, `apiClient.ts`, `auth.ts`, `nonceStore.ts`, `serverIdentity.ts`. They're small but security-sensitive. See [Security model](https://createbsvapp.vercel.app/docs/security) for what each one guarantees. - **Don't hand-edit:** `bsv-scaffold.json`. Later `add` runs read it to decide what's already installed. ::: tip Re-running is safe Run `npx create-bsv-app@latest add --capabilities --yes` later and existing helper files are left alone unless you pass `--force`. Your `App.tsx`, `main.tsx` and server entry are never touched in `add` mode. `AGENTS.md` is regenerated with the wiring snippets to paste. See [Add to an existing project](https://createbsvapp.vercel.app/docs/add-to-existing). ::: ::: deep Why are client and server `auth.ts` identical? The proof format is the same on both sides. Shipping one tiny file to each package keeps them independently deployable, with no shared workspace package and no build step. Both wrap [`@bsv/auth`](https://www.npmjs.com/package/@bsv/auth). ::: ## Read `AGENTS.md` next Every scaffold writes an `AGENTS.md` at the project root. For each installed capability it covers *how it works*, *how it's used* (exact function signatures and files) and *future integrations*. It's written for coding agents, and it's the best quick reference for humans too. --- # Starters > Every project starts from a starter. Pick a clean generated scaffold you compose with capabilities, or copy a complete example app. > 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. Choose a starter with `--starter `, or pick one from the list in the interactive prompts. There are two kinds, and they behave very differently: | What | Generated starters | Complete examples | | --- | --- | --- | | What you get | A fresh Vite and/or Express app, plus BSV helper files | A full app, cloned from its GitHub repo | | Takes capabilities | Yes | No. Passing any is an error | | Runs with | `npm run dev` | whatever its README says | | `AGENTS.md` | Yes | Only if the repo ships one | | Needs `git` installed | No | Yes | ## Generated starters Clean, minimal and wired. New projects always get `wallet-connect`. Add the others with `--capabilities`. | Starter | Stack | What you get | | --- | --- | --- | | `custom` | You pick | Choose a frontend, backend, or both, then add BSV capabilities. | | `react` | React | A Vite React app with wallet connection and optional authentication capabilities. | | `express` | Express | A lean TypeScript Express API with optional BSV authentication capabilities. | | `full-stack` | React + Express | Independent React and Express apps with one root command and an end-to-end wallet flow. | ::: code-group ```bash [full-stack] npx create-bsv-app@latest my-app --starter full-stack --yes ``` ```bash [react] npx create-bsv-app@latest my-app --starter react --capabilities wallet-login --yes ``` ```bash [express] npx create-bsv-app@latest my-api --starter express --capabilities signed-requests --yes ``` ```bash [custom] npx create-bsv-app@latest my-app --starter custom --frontend react --backend express --yes ``` ::: ### Which one? - **`full-stack`**: start here. You get the complete wallet flow, including mobile QR pairing, which needs a server. - **`react`**: the frontend only, at the project root. Desktop wallet connect works. Mobile pairing, login and signed requests need a server to talk to, so point `VITE_API_URL` at one. - **`express`**: an API only, at the project root. Use it to add BSV verification to a backend whose frontend lives elsewhere. - **`custom`**: choose the stack yourself with `--frontend` and `--backend`. With both it's the same layout as `full-stack`. With one, that app sits at the project root. ::: deep What "generated" means The CLI runs the official generators, then layers BSV files on top. React comes from `create-vite` (template `react-ts`, ESLint enabled). The Express app is a lean TypeScript skeleton. On top of those it writes the capability files into `src/bsv/`, wires providers and routes into the base app (unless `--no-glue`), adds dependencies to each `package.json`, writes `AGENTS.md` and `bsv-scaffold.json`, and installs. ::: ## Complete examples Full, maintained apps from the BSV ecosystem. The CLI clones the latest commit of the listed branch (`git clone --depth 1`), deletes `.git` so the project starts with no history, records the exact commit in `bsv-scaffold.json`, and installs dependencies. | Starter | Stack | What it is | Source | | --- | --- | --- | --- | | `brc102-frontend` | React · BRC-102 | **BRC-102 frontend project template**: The established frontend project template with deployment-info.json support. | [p2ppsr/frontend-project-template](https://github.com/p2ppsr/frontend-project-template) | | `brc102-backend` | Express · BRC-102 | **BRC-102 overlay backend template**: The established overlay-service backend template with deployment-info.json support. | [p2ppsr/backend-project-template](https://github.com/p2ppsr/backend-project-template) | | `pollr` | React + Express · BRC-102 | **Pollr**: Blockchain polls backed by overlay networks. | [p2ppsr/Pollr](https://github.com/p2ppsr/Pollr) | | `meter` | React + Express · BRC-102 | **Meter**: An introduction to wallets, sCrypt contracts, and overlays. | [p2ppsr/meter](https://github.com/p2ppsr/meter) | | `metamarket` | React + Express · BRC-102 | **MetaMarket**: A marketplace for 3D objects. | [p2ppsr/MetaMarket](https://github.com/p2ppsr/MetaMarket) | | `todo` | React · BRC-102 | **ToDo List**: A simple demonstration of wallet baskets and encryption. | [p2ppsr/todo-ts](https://github.com/p2ppsr/todo-ts) | | `marscast` | React | **MarsCast**: Micropayment-monetized weather data from Mars. | [p2ppsr/mars-cast](https://github.com/p2ppsr/mars-cast) | | `coinflip` | React + Express · BRC-102 | **Coinflip**: Trustless, provably fair peer-to-peer interactions. | [p2ppsr/coinflip](https://github.com/p2ppsr/coinflip) | | `postboard` | React + Express · BRC-102 | **Postboard**: A public town square of messages built on an overlay. | [p2ppsr/hello-overlay](https://github.com/p2ppsr/hello-overlay) | | `locksmith` | React + Express · BRC-102 | **Locksmith**: Lock coins with a message and unlock them through a wallet. | [p2ppsr/locksmith](https://github.com/p2ppsr/locksmith) | | `peerpay` | React · BRC-102 | **PeerPay**: Peer-to-peer BSV payments backed by identity. | [p2ppsr/peerpay](https://github.com/p2ppsr/peerpay) | | `atfinder` | React | **AtFinder**: An alternative PeerPay interface using the same protocols. | [p2ppsr/atfinder-ui](https://github.com/p2ppsr/atfinder-ui) | ```bash npx create-bsv-app@latest my-meter-app --starter meter --yes cd my-meter-app # now follow the README that came with it ``` ::: warning Examples ignore stack flags and reject capabilities A complete example ships its own structure, so `--frontend`, `--backend` and `--variant` are ignored. `--capabilities` fails with `starter meter is a complete example and does not accept generated capabilities`. Want capabilities? Use a generated starter. ::: ::: note What's BRC-102? Starters tagged **BRC-102** use a `deployment-info.json` file describing how the app and its overlay services are deployed. You only need it if you're using that deployment tooling. Read more in the [BSV primer](https://createbsvapp.vercel.app/docs/bsv-primer#brcs-you-will-meet). ::: ### Knowing exactly what you cloned Because examples track a branch, two people cloning on different days can get different code. The manifest makes it reproducible: ```json [bsv-scaffold.json] { "starter": { "id": "meter", "kind": "repository", "repository": "https://github.com/p2ppsr/meter.git", "ref": "master", "commit": "4f1c…" } } ``` To get the same code later, check out that `commit` from that `repository`. ## Suggest a starter Built something others should start from? Starters live in the CLI's catalogue in [`src/starters.ts`](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/helpers/create-bsv-app). Open a pull request there. A repository starter only needs a public repo, a branch and a README that explains how to run it. --- # Capabilities > The three BSV building blocks create-bsv-app wires into your app, the APIs they give you, and the one proof mechanism behind all of them. > 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 capability is a small set of readable TypeScript files dropped into `src/bsv/`, plus the dependencies, routes and providers they need. Pick them with `--capabilities a,b` or in the prompts. | Capability | Requires | Client | Server | | --- | --- | --- | --- | | `wallet-connect` (always on) | nothing | `useWallet()` React context
`` button and fallback dialog
`apiClient.ts`: bounded, redirect-free fetch
`getServerIdentity()` | `GET /api/identity`
`WalletRelayService`: `/api/session` and `/ws` for mobile QR pairing | | `wallet-login` | `wallet-connect` | `/login` page
`useWalletLogin()` hook | `POST /api/login` via `loginRoute(serverWallet)` | | `signed-requests` | `wallet-connect` | `/signed-demo` page
`useSignedRequest()` and `signedFetch()` | `POST /api/echo`
`verifySignedRequest()`, framework-agnostic | Dependencies are resolved for you: asking for `wallet-login` pulls in `wallet-connect`. ## wallet-connect **Connect any BRC-100 wallet and use it anywhere in your React tree.** It's always installed in new projects. ```tsx import { useWallet } from './bsv/WalletContext' const { status, connected, wallet, identityKey, connect, connectMobile, cancel } = useWallet() ``` | Field | Type | What it is | | --- | --- | --- | | `status` | `'disconnected' \| 'connecting' \| 'choosing' \| 'pairing' \| 'connected'` | where the connect flow is | | `connected` | `boolean` | `wallet !== null` | | `wallet` | `WalletInterface \| null` | the full BRC-100 wallet from `@bsv/sdk` | | `identityKey` | `string \| null` | the user's public identity key (hex) | | `connect()` | `() => Promise` | try the desktop wallet; on failure go to `choosing` | | `connectMobile()` | `() => Promise` | start QR pairing (`pairing`) | | `cancel()` | `() => void` | back to `disconnected` | The flow is a small state machine: ```text [connect flow] disconnected ──connect()──▶ connecting ──desktop wallet found──▶ connected │ └─ none found ─▶ choosing ──connectMobile()──▶ pairing ──QR scanned──▶ connected ``` `` renders all of this for you: the button, the *No desktop wallet found* dialog and the QR code. Use it as is, restyle it, or build your own UI on `useWallet()`. ::: note Mobile pairing needs the server The QR path runs over a relay that the server hosts (`WalletRelayService` from `@bsv/wallet-relay`, on `/api/session` and `/ws`). With a frontend-only project, only desktop connect works. ::: ::: warning The connection doesn't survive a reload The generated context keeps the wallet in memory, so users click **Connect wallet** again after a refresh. Restoring it on load (re-probing the desktop wallet, or resuming the relay session) is up to you, and `AGENTS.md` lists it under future integrations. Calling `connect()` on load isn't enough on its own, because it opens the *No desktop wallet found* dialog when there isn't one. ::: ## wallet-login **Passwordless login.** The wallet signs a proof with `action: 'login'`, the server verifies it, and you get an `identityKey` you can trust. ::: code-group ```tsx [client] import { useWalletLogin } from './bsv/useWalletLogin' const { login } = useWalletLogin() const { identityKey } = await login() ``` ```ts [server] import { loginRoute } from './bsv/loginRoute.js' app.post('/api/login', loginRoute(serverWallet)) // → { identityKey } or 401 ``` ::: ::: warning On create-bsv-app 1.1.2, fix one import first This hook throws `ReferenceError: requireIdentityKey is not defined` until you apply the [three-line fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails). The `/login` demo page doesn't use the hook, which is why the demo still works. ::: `useWalletLogin()` takes optional `{ serverIdentityKey, loginEndpoint }`. Pass `serverIdentityKey` to pin the server's key instead of fetching it. [Here's why you might](https://createbsvapp.vercel.app/docs/security#server-identity-is-trusted-via-your-api-origin). ### Turning login into a session The scaffold stops at "this identity key is proven". It doesn't pick a session strategy for you. Here's a pattern we've tested end to end: exchange the login proof for a short-lived JWT, then send it as a bearer header. ::: code-group ```ts [server/src/session.ts] twoslash // Turn a verified wallet login into a short-lived bearer token. import type { NextFunction, Request, Response } from 'express' import { SignJWT, jwtVerify } from 'jose' import { verifyAuthProof } from './bsv/auth.js' import { consumeNonce } from './bsv/nonceStore.js' type ServerWallet = Parameters[0] const secretText = process.env.JWT_SECRET if (secretText == null || new TextEncoder().encode(secretText).byteLength < 32) { throw new Error('JWT_SECRET must contain at least 32 bytes') } const secret = new TextEncoder().encode(secretText) /** POST /api/session-login: verify a `login` proof, return { token, identityKey }. */ export function sessionLogin (serverWallet: ServerWallet) { return async (req: Request, res: Response): Promise => { const result = await verifyAuthProof(serverWallet, req.body, { action: 'login' }, consumeNonce) if (!result.valid || result.identityKey == null) { res.status(401).json({ error: 'invalid proof' }) return } const token = await new SignJWT({ sub: result.identityKey }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('1h') .sign(secret) res.json({ token, identityKey: result.identityKey }) } } /** Middleware: require `Authorization: Bearer `; exposes res.locals.identityKey. */ export async function requireSession (req: Request, res: Response, next: NextFunction): Promise { const token = req.get('authorization')?.replace(/^Bearer /, '') try { const { payload } = await jwtVerify(token ?? '', secret, { algorithms: ['HS256'] }) res.locals.identityKey = payload.sub next() } catch { res.status(401).json({ error: 'not logged in' }) } } ``` ```ts [server/src/index.ts] import { requireSession, sessionLogin } from './session.js' // ← add this line app.post('/api/session-login', sessionLogin(serverWallet)) // ← add this line app.get('/api/me', requireSession, (_req, res) => { // ← add this line res.json({ identityKey: res.locals.identityKey }) // ← add this line }) // ← add this line ``` ```ts [client/src/session.ts] twoslash // Exchange a wallet login proof for a bearer token, then call protected routes. import type { WalletInterface } from '@bsv/sdk' import { createAuthProof } from './bsv/auth' import { getServerIdentity } from './bsv/serverIdentity' import { apiFetch, readApiJson } from './bsv/apiClient' let token: string | null = null export async function loginForSession (wallet: WalletInterface): Promise { const counterparty = await getServerIdentity() const proof = await createAuthProof(wallet, { counterparty, action: 'login' }) const res = await apiFetch('/api/session-login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(proof) }) if (!res.ok) throw new Error(`login failed: ${res.status}`) token = (await readApiJson(res) as { token: string }).token } export async function authedFetch (path: string, init: RequestInit = {}): Promise { if (token == null) throw new Error('log in first') return await apiFetch(path, { ...init, headers: { ...init.headers, authorization: `Bearer ${token}` } }) } ``` ::: Install `jose` in the server (`npm i jose`), and set `JWT_SECRET` to 32+ random bytes. `node -e "console.log(crypto.randomBytes(32).toString('base64url'))"` makes one. ::: warning Two traps if you go your own way - **Cookies won't be sent.** The generated `apiFetch` uses `credentials: 'omit'` on purpose. A `Set-Cookie` session needs you to change that *and* enable `credentials: true` in CORS. Bearer headers avoid both. - **Don't reuse `useWalletLogin` for tokens.** Its response check accepts exactly `{ identityKey }` and rejects anything else, so a `token` field makes it throw `server returned an invalid identity response`. Use a separate route like the one above. ::: ## signed-requests **Authenticate a single API call**, with no session at all. The proof is bound to an `action` name and the exact request body. ::: code-group ```tsx [client] import { useSignedRequest } from './bsv/useSignedRequest' const { signedFetch } = useSignedRequest() const res = await signedFetch('/api/notes', { action: 'create-note', body: { text: 'gm' } }) ``` ```ts [server] /r.identityKey/ import { verifySignedRequest } from './bsv/verifySignedRequest.js' import { consumeNonce } from './bsv/nonceStore.js' app.post('/api/notes', async (req, res) => { const { proof, body } = req.body const r = await verifySignedRequest(serverWallet, proof, { action: 'create-note', body }, consumeNonce) if (!r.valid) { res.status(401).json({ error: 'invalid proof' }); return } // r.identityKey is the signer. Authorize them, then do the work. res.json({ ok: true, by: r.identityKey }) }) ``` ::: `signedFetch(path, { action, body })` always sends `POST` with `{ proof, body }` as JSON. `verifySignedRequest` is a plain function, so it works the same in Express, Fastify, Hono or a Next.js route handler. **When to use which?** Use login plus a session for an app people stay in. Use signed requests for individual high-value actions (posting, paying, voting, admin operations) and for machine-to-machine calls where there's no browser to hold a session. ## How the proof works All three capabilities share one primitive from [`@bsv/auth`](https://www.npmjs.com/package/@bsv/auth) (BRC-103 style mutual authentication, simplified to a single message): 1. The client fetches the server's public identity key from `GET /api/identity`. It becomes the proof's **counterparty**. 2. The wallet signs `{ action, identityKey, expiresAt, nonce }`, with the request body's exact JSON bytes appended when there is one. The signing key is derived between the user's identity and the server's, so a proof made for one server can't be replayed against another. 3. The server checks the signature, checks `expiresAt` (proofs live **2 minutes** by default, with 30 seconds allowed for clock skew), and calls `consumeNonce`, which refuses any nonce it has already seen. 4. If all three checks pass, `identityKey` is cryptographically proven. If any check fails, you get `{ valid: false }`. ::: deep Why "exact JSON bytes" matters The client signs `JSON.stringify(body)`, and the server verifies `JSON.stringify` of the body it parsed. These match as long as nothing rewrites the object in between. Express's JSON parser keeps key order, so it just works. If you add middleware that normalizes, sorts or coerces request bodies, run it *after* verification, not before. ::: ## Adding a capability later ```bash npx create-bsv-app@latest add --capabilities signed-requests --yes ``` Run this inside the project. The new files are placed, dependencies are added and installed, and `bsv-scaffold.json` is updated. Your own files aren't rewritten. [More on add mode](https://createbsvapp.vercel.app/docs/add-to-existing). --- # Wallet recipes > Encrypt user data, message another identity, sign things anyone can verify, and derive per-app keys. Four BRC-100 calls you can use the moment a wallet connects. > 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. Login and signed requests are just the start. The connected `wallet` from `useWallet()` is a full [BRC-100](https://createbsvapp.vercel.app/docs/bsv-primer#wallets) wallet, so your app can ask it for cryptography without ever touching a key. Depending on the protocol's security level, the wallet may ask the user to approve the first call. Every recipe on this page was run against real wallet instances (`ProtoWallet` from `@bsv/sdk`, which implements the same calls) with the SDK version the scaffold installs. ```tsx const { wallet } = useWallet() // WalletInterface from @bsv/sdk, null until connected ``` ## Protocol IDs in 30 seconds Every call names a **protocol** and a **key ID**. The wallet derives a fresh key from them ([BRC-43](https://createbsvapp.vercel.app/docs/bsv-primer#key-derivation-briefly)), so your app's keys never collide with another app's. ```ts const protocolID: [1, string] = [1, 'my app notes'] // [security level, name] const keyID = 'note-1' // any string, per item or per purpose ``` | Level | Wallet asks the user | Use for | | --- | --- | --- | | `0` | never | low-stakes, high-frequency operations | | `1` | once per app | most app features | | `2` | once per app *and* counterparty | messages and signatures involving other identities | Names use lowercase letters, numbers and spaces. Pick one per feature and never reuse it for something else. ## Encrypt data only the user can read Store private notes, drafts or settings on your server without being able to read them. Use counterparty `'self'`: ```ts [client/src/notes.ts] twoslash import { Utils, type WalletInterface } from '@bsv/sdk' const notes = { protocolID: [1, 'my app notes'] as [1, string], keyID: 'note-1' } export async function seal (wallet: WalletInterface, text: string): Promise { const { ciphertext } = await wallet.encrypt({ ...notes, plaintext: Utils.toArray(text, 'utf8'), counterparty: 'self' }) return ciphertext // store this; it's useless without the user's wallet } export async function unseal (wallet: WalletInterface, ciphertext: number[]): Promise { const { plaintext } = await wallet.decrypt({ ...notes, ciphertext, counterparty: 'self' }) return Utils.toUTF8(plaintext) } ``` ::: tip Your database becomes boring, in a good way A breach leaks ciphertext. Only the user's wallet, with the same protocol and key ID, can decrypt it. ::: ## Send a message only one identity can read Encrypt *to* another user's identity key. Only their wallet can open it, and it proves it came from you: ```ts twoslash import { Utils, type WalletInterface } from '@bsv/sdk' declare const aliceWallet: WalletInterface, bobWallet: WalletInterface declare const aliceIdentityKey: string, bobIdentityKey: string // ---cut--- const dm = { protocolID: [2, 'my app messages'] as [2, string], keyID: 'msg-1' } // Alice, to Bob: const { ciphertext } = await aliceWallet.encrypt({ ...dm, plaintext: Utils.toArray('hi bob', 'utf8'), counterparty: bobIdentityKey }) // Bob, from Alice: const { plaintext } = await bobWallet.decrypt({ ...dm, ciphertext, counterparty: aliceIdentityKey }) ``` The key is derived from *both* identities, so the pair is what matters. Use the same `protocolID` and `keyID` on both sides, and swap the counterparty. ## Sign something anyone can verify Public votes, attestations, "I wrote this" stamps. Sign with counterparty `'anyone'`, and verify with an `'anyone'` wallet, which needs no secret at all: ```ts twoslash import type { WalletInterface } from '@bsv/sdk' declare const wallet: WalletInterface declare const signerIdentityKey: string // ---cut--- import { ProtoWallet, Utils } from '@bsv/sdk' const votes = { protocolID: [2, 'my app votes'] as [2, string], keyID: 'poll-42' } const data = Utils.toArray(JSON.stringify({ vote: 'yes', poll: 42 }), 'utf8') // In the browser, with the user's wallet: const { signature } = await wallet.createSignature({ ...votes, data, counterparty: 'anyone' }) // Anywhere, later (your server, another app, a script): const verifier = new ProtoWallet('anyone') const ok = await verifier .verifySignature({ ...votes, data, signature, counterparty: signerIdentityKey }) .then(() => true, () => false) ``` ::: warning `verifySignature` throws when the signature is bad It doesn't return `{ valid: false }`. It throws `ERR_INVALID_SIGNATURE`. Catch it as above, or let it become a 401. ::: ## A public key per app or feature Show users a key for your app without exposing (or correlating) their identity key: ```ts const { publicKey } = await wallet.getPublicKey({ protocolID: [1, 'my app'], keyID: '1', counterparty: 'self' }) ``` Different protocol or key ID, different key. Your app sees a stable key per user, and two apps can't link their users by it. ## Pay someone Creating transactions needs a funded wallet, so start on testnet. The shape of the call is on the [BSV primer](https://createbsvapp.vercel.app/docs/bsv-primer#your-first-payment). ## Test these without a wallet Every call above works on a `ProtoWallet` with a random key, which is how this page was verified. Copy the pattern from [Testing without a wallet](https://createbsvapp.vercel.app/docs/testing): ```ts import { PrivateKey, ProtoWallet } from '@bsv/sdk' const alice = new ProtoWallet(PrivateKey.fromRandom()) ``` --- # Add to an existing project > Drop wallet connect, login or signed requests into a React or Express app you already have, or add more capabilities to a create-bsv-app project later. > 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. `add` mode installs capability files into a project that already exists. It never runs a base generator and never edits your `App.tsx`, `main.tsx` or server entry. Instead, it writes the exact snippets to paste into `AGENTS.md`. ```bash cd my-existing-app npx create-bsv-app@latest add --capabilities wallet-connect,wallet-login --yes ``` ::: danger In `add` mode, list `wallet-connect` yourself New projects pull in required capabilities automatically. `add` mode doesn't. On an app that doesn't have `wallet-connect` yet, `--capabilities wallet-login` alone writes `loginRoute.ts` without the `auth.ts` and `nonceStore.ts` it imports, and the build breaks. Always include `wallet-connect` the first time: ```bash npx create-bsv-app@latest add --capabilities wallet-login --yes # [!code error] npx create-bsv-app@latest add --capabilities wallet-connect,wallet-login --yes ``` ::: ## How the CLI finds your app You don't usually need to pass `--mode add`. When the target folder isn't empty, the CLI looks for, in order: 1. **`bsv-scaffold.json`.** Reuses the recorded stack, folders and network, and only offers capabilities you don't have yet. 2. **A root package with `react` or `express`** in its dependencies. Files go into that package's `src/bsv/`. 3. **`client/` + `server/`**, or **`frontend/` + `backend/`**. The first is treated as the React app, the second as the Express app. If none of those match, the CLI assumes you meant `new`, and a non-empty folder stops it with `target directory is not empty`. ::: warning One package with both React and Express? The CLI can't tell where client and server files belong, so it stops with `cannot infer separate client/server targets from a single package containing both react and express; use --file with explicit targets`. Do exactly that: ```json [add.json] { "mode": "add", "name": "my-app", "stack": { "frontend": { "framework": "react" }, "backend": { "framework": "express" } }, "targets": { "client": "web", "server": "api" }, "capabilities": ["wallet-connect", "wallet-login"] } ``` ```bash npx create-bsv-app@latest --file add.json ``` `name` is required even in add mode. `targets` are paths relative to the project root. ::: ## Then wire it up Open the regenerated `AGENTS.md` and find **Wiring (manual)**. It has one block per file. Here's the client side: ```tsx [src/main.tsx] import { WalletProviders } from './bsv/WalletProviders' // ← add this line createRoot(document.getElementById('root')!).render( {/* ← add this line */} {/* ← add this line */} ) ``` ### The server needs a little more A generated server already has a server wallet, an identity route and CORS. An existing Express app has none of them, and the `AGENTS.md` snippet assumes they're there. Here's a complete minimal setup. We compiled and ran this against an `add`-mode project: ```ts [server/src/index.ts] import http from 'node:http' import express from 'express' import cors from 'cors' import { PrivateKey, ProtoWallet } from '@bsv/sdk' import { WalletRelayService } from '@bsv/wallet-relay' import { loginRoute } from './bsv/loginRoute.js' const CLIENT_ORIGIN = process.env.CLIENT_ORIGIN ?? 'http://localhost:5173' const key = process.env.SERVER_PRIVATE_KEY if (key == null && process.env.NODE_ENV === 'production') throw new Error('SERVER_PRIVATE_KEY is required in production') // The server's own identity. Keep the key stable, or clients see a new server on every restart. const serverWallet = new ProtoWallet(key != null ? PrivateKey.fromString(key) : PrivateKey.fromRandom()) const app = express() app.use(cors({ origin: CLIENT_ORIGIN })) app.use(express.json({ limit: '64kb' })) // Clients fetch this first: it's the counterparty every proof is made for. app.get('/api/identity', async (_req, res) => { const { publicKey } = await serverWallet.getPublicKey({ identityKey: true }) res.json({ identityKey: publicKey }) }) app.post('/api/login', loginRoute(serverWallet)) // The mobile QR relay attaches to the raw HTTP server (it needs WebSocket upgrades). const server = http.createServer(app) new WalletRelayService({ app, server, wallet: serverWallet, origin: CLIENT_ORIGIN }) server.listen(Number(process.env.PORT ?? 3000)) ``` `add` mode puts the BSV packages in your `package.json` but not `cors`, so add it yourself: ```bash npm i cors && npm i -D @types/cors ``` ::: tip Let your agent do the pasting "Apply the *Wiring (manual)* section of AGENTS.md" is a perfectly good prompt. The snippets are exact. See [Agents](https://createbsvapp.vercel.app/docs/agents). ::: ## Re-running on a create-bsv-app project Run it again any time to add what you skipped: ```bash npx create-bsv-app@latest add --capabilities signed-requests --yes ``` | What | Happens | | --- | --- | | New capability files | written | | Existing helper files in `src/bsv/` | **kept**, unless you pass `--force` | | `AGENTS.md` | rewritten, with manual wiring for the new capability | | `bsv-scaffold.json` | capabilities merged in | | `package.json` | new dependencies added, then installed (skip with `--skip-install`) | | Your `App.tsx`, `main.tsx`, `server/src/index.ts` | **never touched** | ::: danger `--force` overwrites your edits `--force` replaces every existing capability helper file with a fresh copy, which is handy after a CLI upgrade. Commit first, then read the diff. ::: ## Frameworks other than Vite and Express The helpers are plain TypeScript. `verifySignedRequest()` and `verifyAuthProof()` run in any Node server, and the React hooks run in any React app. The *wiring* snippets assume a Vite-style `src/main.tsx` and an Express server entry, so in Next.js, Remix, Fastify or Hono, use them as a guide rather than pasting them as is. --- # Environment variables > The six variables a generated app reads, their dev defaults, what production insists on, and how to load them. > 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. All configuration lives in one file per app, `client/src/bsv/config.ts` and `server/src/bsv/config.ts`. Each reads the environment once, validates it, and exports typed constants. The rest of the code imports those constants instead of reading `process.env` directly. ## The variables ### Client (Vite) | Variable | Default in dev | Production | What it does | | --- | --- | --- | --- | | `VITE_API_URL` | `http://localhost:3000` | **required**, must be `https://` | where the API lives; every `apiFetch` goes here | | `VITE_BSV_NETWORK` | `test` | optional | `main`, `test` or `ttn` | ### Server (Node) | Variable | Default in dev | Production | What it does | | --- | --- | --- | --- | | `SERVER_PRIVATE_KEY` | random on every start | **required** | the server's identity key, used to verify proofs | | `CLIENT_ORIGIN` | `http://localhost:5173` | **required**, must be `https://` | the one browser origin CORS allows | | `PORT` | `3000` | optional | 1–65535 | | `BSV_NETWORK` | `test` | optional | `main`, `test` or `ttn` | "Production" means `NODE_ENV=production` on the server, and `vite build` on the client (Vite sets `import.meta.env.PROD`). In production the app **refuses to start** with a missing or invalid value, instead of quietly falling back to a dev default: ```text [what you'll see] Error: VITE_API_URL is required in production Error: SERVER_PRIVATE_KEY is required in production Error: CLIENT_ORIGIN is required in production Error: CLIENT_ORIGIN must be credential-free HTTPS (or exact HTTP localhost development) origin ``` That's deliberate. A server that silently picks a random identity, or a client that silently calls `localhost`, is a bug you'd rather find at deploy time than from your users. ::: warning `VITE_*` values are public Vite inlines `VITE_` variables into the JavaScript bundle at build time, and anyone can read them. They're fine for URLs and network names. Never put a secret in one. `SERVER_PRIVATE_KEY` belongs on the server only. ::: ## Generate a server key Run this from the `server/` folder, where `@bsv/sdk` is installed: ```bash node --input-type=module -e "import { PrivateKey } from '@bsv/sdk'; console.log(PrivateKey.fromRandom().toString())" ``` It prints 64 hex characters. Treat the key like a password: it *is* your server's identity. If it changes, clients see a different server identity. If it leaks, someone else can impersonate your server. ::: deep Why does a dev restart change the server's identity? With no `SERVER_PRIVATE_KEY`, the server makes a fresh random key on each start. Clients fetch the identity again (`GET /api/identity`), so dev keeps working. Anything that *pinned* the old key, or proofs signed for it in the last two minutes, stops matching. Set a key in dev too once you start [pinning](https://createbsvapp.vercel.app/docs/security#server-identity-is-trusted-via-your-api-origin). ::: ## Load them **Client:** Vite loads `client/.env`, `client/.env.local` and `client/.env.production` for you. Only `VITE_` variables reach the browser. ```dotenv [client/.env.production] VITE_API_URL=https://api.example.com VITE_BSV_NETWORK=main ``` **Server:** nothing loads `server/.env` automatically, and that catches people out. Either export the variables in your shell or host, or tell Node to read the file: ::: code-group ```bash [dev] # server/package.json → "dev": "tsx watch --env-file=.env src/index.ts" npm run dev ``` ```bash [production] npm run build NODE_ENV=production node --env-file=.env dist/index.js ``` ::: ```dotenv [server/.env] SERVER_PRIVATE_KEY=7b28…c8 CLIENT_ORIGIN=https://app.example.com PORT=3000 BSV_NETWORK=main ``` ::: danger Keep `.env` out of git The client's `.gitignore` comes from create-vite. It ignores `*.local` files but not `.env`, and the server and project root have no `.gitignore` at all. Add one at the project root before your first commit: ```bash printf 'node_modules\n.env\n.env.*\n!.env.example\ndist\n' > .gitignore ``` ::: ## Networks `--network` sets the default baked into both configs, and the env variables override it per deployment. | Value | Network | Use it for | | --- | --- | --- | | `test` | testnet | development. Coins are free and worthless | | `ttn` | Teratestnet | testing against the newer Teranode network | | `main` | mainnet | production. Real money | Login and signed requests are pure signatures, so they work identically on every network. The network matters once you start creating transactions. See [BSV for web devs](https://createbsvapp.vercel.app/docs/bsv-primer#networks). --- # Testing without a wallet > Unit-test wallet-authenticated routes with throwaway keys. No wallet app, no browser, no network, just node:test. > 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. You can't click "Approve" in a wallet from CI. You don't have to. A wallet's job in a proof is to *sign*, and `ProtoWallet` from `@bsv/sdk` signs with a key held in memory. Give each test its own random key and you have as many users as you like. ## A complete test file Five tests covering the guarantees you actually rely on. Copy it into `server/test/auth.test.ts`. It uses only what a scaffold already installs. ```ts [server/test/auth.test.ts] twoslash // @filename: test/auth.test.ts // ---cut--- import { test } from 'node:test' import assert from 'node:assert/strict' import { PrivateKey, ProtoWallet } from '@bsv/sdk' import { createAuthProof, type RequestBody } from '../src/bsv/auth.js' import { verifySignedRequest } from '../src/bsv/verifySignedRequest.js' // Two throwaway identities: no wallet app, no network, no browser. const server = new ProtoWallet(PrivateKey.fromRandom()) const alice = new ProtoWallet(PrivateKey.fromRandom()) const { publicKey: serverKey } = await server.getPublicKey({ identityKey: true }) const { publicKey: aliceKey } = await alice.getPublicKey({ identityKey: true }) // A fresh in-memory nonce store per test keeps tests independent. const nonceStore = () => { const seen = new Set() return (nonce: string) => (seen.has(nonce) ? false : (seen.add(nonce), true)) } const sign = (body: RequestBody) => createAuthProof(alice, { counterparty: serverKey, action: 'create-note', body }) test('a signed request proves who sent it', async () => { const body = { text: 'gm' } const r = await verifySignedRequest(server, await sign(body), { action: 'create-note', body }, nonceStore()) assert.equal(r.valid, true) assert.equal(r.identityKey, aliceKey) }) test('a changed body is rejected', async () => { const proof = await sign({ text: 'gm' }) const r = await verifySignedRequest(server, proof, { action: 'create-note', body: { text: 'gn' } }, nonceStore()) assert.equal(r.valid, false) }) test('a different action is rejected', async () => { const body = { text: 'gm' } const r = await verifySignedRequest(server, await sign(body), { action: 'delete-note', body }, nonceStore()) assert.equal(r.valid, false) }) test('a replayed proof is rejected', async () => { const body = { text: 'gm' } const proof = await sign(body) const consume = nonceStore() assert.equal((await verifySignedRequest(server, proof, { action: 'create-note', body }, consume)).valid, true) assert.equal((await verifySignedRequest(server, proof, { action: 'create-note', body }, consume)).valid, false) }) test('a proof made for another server is rejected', async () => { const other = new ProtoWallet(PrivateKey.fromRandom()) const body = { text: 'gm' } const r = await verifySignedRequest(other, await sign(body), { action: 'create-note', body }, nonceStore()) assert.equal(r.valid, false) }) ``` Run it from `server/`: ```console $ npx tsx --test test/auth.test.ts ✔ a signed request proves who sent it ✔ a changed body is rejected ✔ a different action is rejected ✔ a replayed proof is rejected ✔ a proof made for another server is rejected ℹ tests 5 ℹ pass 5 ℹ fail 0 ``` Add `"test": "tsx --test test/*.test.ts"` to `server/package.json` and it's `npm test` from then on. ::: tip Why the server is a `ProtoWallet` too That's exactly what the generated server does: `new ProtoWallet(PrivateKey.fromString(SERVER_PRIVATE_KEY))`. A `ProtoWallet` does the key work (deriving keys, signing, verifying) with no coin storage behind it, which is all a verifier needs. ::: ## Testing over HTTP To exercise your real routes, middleware included, start the server and post proofs at it. The [tutorial's attack script](https://createbsvapp.vercel.app/docs/tutorial#5-break-it-on-purpose) does exactly that. The only extra step is fetching the server's identity first, because it's the proof's counterparty: ```ts const { identityKey: server } = await (await fetch('http://localhost:3000/api/identity')).json() const proof = await createAuthProof(wallet, { counterparty: server, action: 'login' }) await fetch('http://localhost:3000/api/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(proof), }) ``` ::: warning Set `SERVER_PRIVATE_KEY` in long test runs Without it, the server picks a new identity each time it restarts. In watch mode that means mid-run, and any proof signed before the restart fails. Put a fixed test key in `server/.env.test` and start the server with `tsx watch --env-file=.env.test src/index.ts`. ::: ## Testing the React side The client hooks call a real wallet through `WalletClient('auto')`. For component tests, mock `useWallet()` rather than the wallet. With [Vitest](https://vitest.dev) (not installed by the scaffold): ```tsx vi.mock('./bsv/WalletContext', () => ({ useWallet: () => ({ connected: true, identityKey: '02ab…', status: 'connected', wallet: null }), })) ``` For a true end-to-end run (Playwright clicking **Connect wallet**), you need a wallet that approves automatically. That's out of scope for the scaffold. Most teams cover the crypto in server tests like the ones above, and the UI with mocks. --- # 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 --- # Use it with AI agents > Let Claude Code, Cursor or any coding agent scaffold and extend BSV apps. One JSON file in, an AGENTS.md out, and docs every agent can read. > 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. create-bsv-app was built to be driven by agents as well as people. Three things make that work: a **deterministic input** (`--file`), a **generated guide** in every project (`AGENTS.md`), and **docs agents can read** (Markdown at stable URLs). ## Give your agent the skill The fastest setup is a skill: a short instruction file that teaches an agent the commands, flags and gotchas. We publish one. ::: code-group ```bash [Claude Code] mkdir -p .claude/skills/create-bsv-app curl -fsSL https://createbsvapp.vercel.app/.well-known/agent-skills/create-bsv-app/SKILL.md \ -o .claude/skills/create-bsv-app/SKILL.md ``` ```bash [any agent] curl -fsSL https://createbsvapp.vercel.app/.well-known/agent-skills/create-bsv-app/SKILL.md # paste it into your agent's rules or system prompt ``` ::: The CLI package also ships a smaller skill (`scaffolding-bsv-apps`) inside `node_modules/create-bsv-app/.claude/skills/`. Ours adds the troubleshooting knowledge from these docs. ## Connect the docs over MCP These docs are also a read-only [MCP](https://modelcontextprotocol.io) server, so your agent can search and read them mid-task instead of guessing: ::: code-group ```bash [Claude Code] claude mcp add --transport http create-bsv-app-docs https://createbsvapp.vercel.app/mcp ``` ```json [any MCP client] { "mcpServers": { "create-bsv-app-docs": { "type": "http", "url": "https://createbsvapp.vercel.app/mcp" } } } ``` ::: | Tool | What it does | | --- | --- | | `search_docs` | full-text search over every section; returns page slugs and anchors | | `get_page` | one page as Markdown, by slug (`tutorial`, `cli`, `troubleshooting`…) | | `list_pages` | every page with its one-line summary | No auth, no state, and it only reads the published docs. ## Prompts that work ```text [scaffold] Scaffold a full-stack BSV app called "tipjar" with wallet login and signed requests using create-bsv-app. Run it non-interactively, then read AGENTS.md and summarise the routes and hooks I can use. ``` ```text [add a feature] Using the signed-requests capability, add POST /api/tips that records { identityKey, amount, note } and a React form that calls it with signedFetch. Follow the patterns in AGENTS.md and add a node:test test like https://createbsvapp.vercel.app/docs/testing.md ``` ```text [existing app] Add wallet-connect and wallet-login to this repo with create-bsv-app in add mode, then apply the "Wiring (manual)" section of the generated AGENTS.md. ``` ## Rules for agents ::: danger Never run it interactively `npx create-bsv-app` with no flags waits on prompts that an agent can't answer. Always pass `--yes` or `--file`. ::: - **New project:** `npx create-bsv-app@latest --starter full-stack --capabilities wallet-login,signed-requests --yes` - **Existing project:** `npx create-bsv-app@latest add --capabilities wallet-connect, --yes`. List `wallet-connect` explicitly the first time, because add mode doesn't expand dependencies. - **Non-npm package managers:** add `--package-manager pnpm|yarn|bun`. It isn't auto-detected. - **Errors** go to stderr, the exit code is `1`, and config problems start with `Invalid config:`. The message says what to change. - **After scaffolding**, read `AGENTS.md` before writing code. It has the exact function signatures, file paths and wiring for every installed capability. ## The deterministic door: `--file` Everything the prompts ask, as JSON. Same pipeline, same output, no prompts: ```json [config.json] { "mode": "new", "name": "tipjar", "starter": "full-stack", "capabilities": ["wallet-login", "signed-requests"], "packageManager": "npm", "network": "test", "install": true } ``` ```bash npx create-bsv-app@latest --dir tipjar --file config.json ``` Every field is in the [ProjectConfig reference](https://createbsvapp.vercel.app/docs/config). Check the file into your repo and the scaffold is reproducible. ## What's in AGENTS.md Every generated project gets one at its root, rebuilt on every run: - **Install:** the exact dependency ranges added to each package. - **Wiring:** "wired automatically", or in add mode / `--no-glue`, the exact snippets to paste into `main.tsx`, `App.tsx` and the server entry. - **Per capability:** *How it works*, *How it's used* (files and function signatures) and *Future integrations* (sessions, persistence, Redis nonces and more). ## Docs built for context windows | URL | What you get | | --- | --- | | [`/llms.txt`](https://createbsvapp.vercel.app/llms.txt) | an index of every page, with one-line summaries | | [`/llms-full.txt`](https://createbsvapp.vercel.app/llms-full.txt) | the entire docs site as one Markdown file | | `/docs/.md` | any page as Markdown, e.g. [`/docs/cli.md`](https://createbsvapp.vercel.app/docs/cli.md) | | any page + `Accept: text/markdown` | the same Markdown via content negotiation | | [`/.well-known/agent-skills/index.json`](https://createbsvapp.vercel.app/.well-known/agent-skills/index.json) | discoverable skills, with SHA-256 digests | | `/mcp` | the MCP server above (card at [`/.well-known/mcp/server-card.json`](https://createbsvapp.vercel.app/.well-known/mcp/server-card.json)) | | `/a2a` | the same tools as an [A2A](https://a2a-protocol.org) agent: `message/send` a question or a page slug (card at [`/.well-known/agent-card.json`](https://createbsvapp.vercel.app/.well-known/agent-card.json)) | | [`/openapi.json`](https://createbsvapp.vercel.app/openapi.json) | OpenAPI 3.1 description of everything above | | [`/.well-known/agents.json`](https://createbsvapp.vercel.app/.well-known/agents.json) | every agent endpoint in one directory | ```bash curl -H "Accept: text/markdown" https://createbsvapp.vercel.app/docs/capabilities ``` Every page also has **Copy page** and **Ask Claude / ChatGPT** buttons, top right. --- # Migrate from @bsv/app > @bsv/app is deprecated. Here's how every old template and flag maps to create-bsv-app, and what you gain by switching. > 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. **Should you still use `npx @bsv/app`? No.** It's deprecated, and its last release (1.1.0) only prints a notice and forwards your arguments to `create-bsv-app`. Everything it offered now lives in create-bsv-app, plus generated starters, capabilities, add mode and a config-file mode. ## Already have an @bsv/app project? Nothing breaks. `@bsv/app` cloned a template repository and got out of the way, so your project doesn't depend on it at runtime. Keep building. You only need this page the next time you start a project. ## Command mapping | You used to run | Run this instead | | --- | --- | | `npx @bsv/app` | `npx create-bsv-app@latest` | | `npx @bsv/app --yes` (cloned Meter as `bsv-app`) | `npx create-bsv-app@latest bsv-app --starter meter --yes` | | `--skip-install` / `-s` | `--skip-install` | | `--git-init` / `-g` | no flag. Run `git init` yourself afterwards | | `--yes` / `-y` | `--yes` (now skips every prompt, using your flags) | | author prompt | gone. Edit `package.json` | ## Template mapping Every template from the old menu is a starter id now: | Old menu entry | `--starter` | | --- | --- | | Frontend Template | `brc102-frontend` | | Backend Template | `brc102-backend` | | Full Stack Template | `full-stack` (generated), or `brc102-frontend` + `brc102-backend` | | Pollr | `pollr` | | Meter | `meter` | | MetaMarket | `metamarket` | | ToDo List | `todo` | | MarsCast | `marscast` | | Coinflip | `coinflip` | | Postboard | `postboard` | | Locksmith | `locksmith` | | PeerPay | `peerpay` | | AtFinder | `atfinder` | | Convo | retired, no replacement | ::: note "Full Stack Template" used to clone the frontend template In `@bsv/app` it pointed at the same repository as the Frontend Template. Today's `full-stack` is a real generated React + Express app with wallet connect, login and signed requests. If you want the overlay-based setup the old name hinted at, combine `brc102-frontend` and `brc102-backend`. ::: ## What's different - **Two kinds of starter.** The old templates are all here as *complete examples*. They're cloned from their repos, now with the exact commit recorded in `bsv-scaffold.json`. The new *generated* starters are clean, minimal and composable with [capabilities](https://createbsvapp.vercel.app/docs/capabilities). - **Add mode.** Bring wallet connect, login or signed requests into an app you already have. [Guide](https://createbsvapp.vercel.app/docs/add-to-existing). - **Reproducible.** `--file config.json` describes a whole scaffold in one file. [Reference](https://createbsvapp.vercel.app/docs/config). - **Agent-ready.** Generated projects ship an `AGENTS.md`, and these docs are available as [Markdown](https://createbsvapp.vercel.app/docs/agents#docs-built-for-context-windows). - **History-free clones.** Complete examples arrive without the template's git history, so `git init` gives you a clean first commit. --- # Troubleshooting > Every error message we know of, quoted exactly, with what causes it and how to fix it. Search the page for the text you're seeing. > 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. **Tip:** press ⌘ F (or Ctrl F) and paste your error. Headings below are the literal messages. ## Scaffolding ### `Invalid config: a new project needs at least a frontend or a backend` You used the `custom` starter, its default, without choosing a stack. `custom` starts with no frontend and no backend. Either pick a named starter or choose one: ```bash npx create-bsv-app@latest my-app --starter full-stack --yes # or npx create-bsv-app@latest my-app --starter custom --frontend react --backend express --yes ``` ### `target directory is not empty: … new projects scaffold into an empty directory` `new` mode only writes into an empty folder (a lone `.git` or `bsv-scaffold.json` is allowed). Pick a new folder name, or, to add capabilities to the project that's there, use [add mode](https://createbsvapp.vercel.app/docs/add-to-existing): ```bash npx create-bsv-app@latest add --capabilities wallet-login --yes ``` ### `Invalid config: unknown starter: …` Starter ids are lowercase and exact: `custom`, `react`, `express`, `full-stack`, `brc102-frontend`, `brc102-backend`, `pollr`, `meter`, `metamarket`, `todo`, `marscast`, `coinflip`, `postboard`, `locksmith`, `peerpay`, `atfinder` See [Starters](https://createbsvapp.vercel.app/docs/starters) for what each one is. ### `Invalid config: unknown capability: …` Valid ids are `wallet-connect`, `wallet-login` and `signed-requests`. Separate them with commas and no spaces: `--capabilities wallet-login,signed-requests`. ### `Invalid config: starter meter is a complete example and does not accept generated capabilities` Complete examples are cloned as they are, so drop `--capabilities`. If you want capabilities, use a [generated starter](https://createbsvapp.vercel.app/docs/starters#generated-starters). ### `cannot infer separate client/server targets from a single package containing both react and express` Add mode found one `package.json` with both React and Express, and can't guess where client and server files go. Pass a config with explicit `targets`: [here's the exact file](https://createbsvapp.vercel.app/docs/add-to-existing#how-the-cli-finds-your-app). ### `Invalid config: name is required` A `--file` config needs `"name"`, even in add mode. ### `Invalid config: --network must be main, test, or ttn` The same pattern applies to `--mode`, `--frontend`, `--backend` and `--package-manager`: the message lists the allowed values. `… requires a value` means a flag is missing its argument, and `unknown option: …` means a typo. `npx create-bsv-app --help` prints every flag. ### `command failed (…): git clone …` Complete-example starters are cloned with `git`. Install git, check that you can reach github.com, and run again into an empty folder. ### The output says "cd client, npm install, npm run dev". Is that for me? No. That's create-vite's own message, printed halfway through. Follow the final **Next:** block, which says `cd my-app` and `npm run dev`. Dependencies are already installed. ### It installed with npm but I use pnpm The CLI doesn't detect the package manager that launched it. Pass `--package-manager pnpm` (or `yarn`, or `bun`). In an existing project with a lockfile, installs follow the lockfile. ### Odd failures on Node.js 20 or older create-bsv-app requires Node.js 22 or newer (npm may print an `EBADENGINE` warning). Check with `node -v`. Upgrade with `nvm install 22`, `fnm install 22`, or from nodejs.org. Older versions may half-work and then fail in confusing ways. ## Build ### Client build fails with `TS2304: Cannot find name 'requireIdentityKey'` {#client-build-fails} A known issue in create-bsv-app 1.1.2. The generated client has three type errors that `npm run dev` doesn't catch (Vite doesn't type-check) but `npm run build` does: ```text src/bsv/apiClient.ts(84,25): error TS2345: Argument of type 'Uint8Array | null' is not assignable to parameter of type 'BodyInit | null | undefined'. src/bsv/useWalletLogin.tsx(15,26): error TS2304: Cannot find name 'requireIdentityKey'. src/bsv/WalletLogin.tsx(6,54): error TS6133: 'requireIdentityKey' is declared but its value is never read. ``` The second one is also a **runtime** bug: `useWalletLogin().login()` throws `ReferenceError: requireIdentityKey is not defined`. The demo login page doesn't use the hook, which is why the demo still works. Three one-line fixes: ::: code-group ```ts [client/src/bsv/useWalletLogin.tsx] import { getServerIdentity, readIdentityKeyResponse } from './serverIdentity.js' // ← remove this line import { getServerIdentity, readIdentityKeyResponse, requireIdentityKey } from './serverIdentity.js' // ← add this line ``` ```ts [client/src/bsv/WalletLogin.tsx] import { getServerIdentity, readIdentityKeyResponse, requireIdentityKey } from './serverIdentity.js' // ← remove this line import { getServerIdentity, readIdentityKeyResponse } from './serverIdentity.js' // ← add this line ``` ```ts [client/src/bsv/apiClient.ts] async function readBoundedBody (response: Response): Promise { // ← remove this line async function readBoundedBody (response: Response): Promise> { // ← add this line ``` ::: With those three changes, `npm run build` passes. We verified it on a fresh 1.1.2 full-stack scaffold. ## Wallet & connection {#wallet} ### "No desktop wallet found" even though I installed one `WalletClient('auto')` looks for a wallet running on this machine. Make sure [BSV Browser](https://browser.bsvb.tech/) is **open and unlocked**, then click **Connect wallet** again. Still nothing? Choose **Connect with a mobile wallet** and scan the QR code with BSV Browser on your phone. ### `failed to fetch server identity: …` or `TypeError: Failed to fetch` The client can't reach the server, or CORS blocked it. Check in this order: 1. **Is the server running?** `curl http://localhost:3000/health` should print `{"status":"ok"}`. 2. **Are you on exactly `http://localhost:5173`?** The server only allows `CLIENT_ORIGIN`, which defaults to `http://localhost:5173`. Opening `127.0.0.1:5173`, or getting moved to `:5174` because 5173 was busy, is a different origin, and CORS blocks it. Free the port, or set `CLIENT_ORIGIN` to match. 3. **Is `VITE_API_URL` right?** Restart `npm run dev` after changing `.env`. Vite reads it at startup. ### The QR code never appears ("Generating code…") The mobile relay lives on the server (`/api/session`, `/ws`). Same checks as above. In production, make sure your host forwards WebSocket upgrades: see [Deploy](https://createbsvapp.vercel.app/docs/deploy#two-hosting-rules). ### Login or signed request returns `401 {"error":"invalid proof"}` The server rejected the proof. In order of likelihood: - **The server restarted** between fetching its identity and receiving the proof. Without `SERVER_PRIVATE_KEY` it gets a new identity on every start. Refresh the page, or [set a key](https://createbsvapp.vercel.app/docs/environment#generate-a-server-key). - **`action` or `body` differ** between client and server. They must match exactly. See [capabilities](https://createbsvapp.vercel.app/docs/capabilities#signed-requests). - **The proof was reused or is stale.** Each proof is single-use and expires after 2 minutes. Sign a fresh one per request. - **Your clock is off** by more than 30 seconds. Sync your system time. - **The nonce store is full.** The in-memory store holds 10,000 recent nonces and refuses new ones until they expire. Under sustained load, [move it to Redis](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances). ### `server returned another wallet identity` The identity the server verified isn't the wallet you connected, usually because you switched accounts in the wallet mid-session. Reload and connect again. ### `API endpoint must be a safe absolute path` `apiFetch` only accepts paths made of letters, numbers, `/`, `_` and `-`. **Query strings aren't allowed.** Put parameters in the path (`/api/notes/42`), or in a POST body. ### `API request body must be a string` `apiFetch` takes a string body. Wrap objects with `JSON.stringify(...)` and set `content-type: application/json`. ### `API response exceeds the byte limit` `apiFetch` caps responses at 1 MiB and requests at 1 MiB, and times out after 10 seconds. Paginate large responses, or change the limits at the top of `apiClient.ts` if you really need to. ## Production ### White screen in production, console says `VITE_API_URL is required in production` `VITE_API_URL` wasn't set when you ran `vite build`. It's baked in at build time. Rebuild with it set. It also has to be `https://`, or you'll see `VITE_API_URL must be credential-free HTTPS (or exact HTTP localhost development)`. ### `SERVER_PRIVATE_KEY is required in production` / `CLIENT_ORIGIN is required in production` Set them in the server's environment. See [Deploy](https://createbsvapp.vercel.app/docs/deploy#1-deploy-the-server). `CLIENT_ORIGIN` must be a bare `https://` origin: no path, no trailing slash, no credentials. ### `SERVER_PRIVATE_KEY must use its canonical encoding` The key has to be in the exact format `PrivateKey.toString()` prints: lowercase hex. Uppercase hex or extra whitespace fail. Regenerate with the [one-liner](https://createbsvapp.vercel.app/docs/environment#generate-a-server-key), or re-print your existing key through it. ### `PORT must be an integer from 1 to 65535` `PORT` has to be a plain number with no leading zeros. ### A refresh on `/login` returns 404 Your static host needs a single-page-app fallback to `index.html`. [Config for Vercel, Netlify and nginx](https://createbsvapp.vercel.app/docs/deploy#single-page-routing). ## Still stuck? - Read your project's `AGENTS.md`. It documents every generated function. - Ask an assistant with the full docs: copy [/llms-full.txt](https://createbsvapp.vercel.app/llms-full.txt) into it. - Open an issue on [bsv-blockchain/ts-stack](https://github.com/bsv-blockchain/ts-stack/issues) with your `bsv-scaffold.json`, your Node version and the full error. --- # FAQ > Straight answers to the questions people ask before and after they scaffold their first BSV app. > 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. **TL;DR:** it's free to run, users need a BRC-100 wallet, and the generated code is yours to read and change. Before real users arrive, work through the [deploy](https://createbsvapp.vercel.app/docs/deploy#production-checklist) and [security](https://createbsvapp.vercel.app/docs/security#before-you-launch) checklists. ## Does it cost money to run? Not for authentication. Wallet login and signed requests are signatures, not transactions, so no coins move and nothing touches the blockchain. New projects target testnet by default. Costs only start when *your* app creates [transactions](https://createbsvapp.vercel.app/docs/bsv-primer#signatures-vs-transactions). ## Do my users need a wallet? Yes, any [BRC-100](https://createbsvapp.vercel.app/docs/glossary#brc-100) wallet. We recommend [BSV Browser](https://browser.bsvb.tech/), free on macOS, Windows, iOS and Android. Desktop users connect directly, and phone users scan a QR code. ## Is it production-ready? The generated code is built with production in mind: production config refuses to start without HTTPS and a stable server key, proofs are single-use and expire, and the API client is locked down. Whether *your app* is ready depends on what you add. In particular: - apply the [1.1.2 build fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails) - move the nonce store to [Redis or your database](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances) before running more than one instance - add rate limits and authorization, and remove the demo routes The [security model](https://createbsvapp.vercel.app/docs/security) lists exactly what is and isn't guaranteed. ## Why signatures instead of passwords or sessions? A signature proves who sent a request without a shared secret that can leak, be phished or be reused. Signed requests are stateless: no session store, nothing to steal from a cookie jar. If you want "stay logged in", issue your own session after [wallet login](https://createbsvapp.vercel.app/docs/capabilities#turning-login-into-a-session). ## Why generate code instead of shipping a library? So you can read, change and own the code that decides who's logged in. See [Why create-bsv-app](https://createbsvapp.vercel.app/docs/why#the-positions-we-take). ## Can I use Next.js, Vue, Fastify or Hono? The generated *starters* are Vite + React and Express. The *helpers* are plain TypeScript: `verifySignedRequest` runs in any Node server, and the React hooks run in any React app. Use [add mode](https://createbsvapp.vercel.app/docs/add-to-existing#frameworks-other-than-vite-and-express) and treat the wiring snippets as a guide. ## Can I add this to an app I already have? Yes, with add mode: `npx create-bsv-app@latest add --capabilities wallet-connect,wallet-login --yes`. See [Add to an existing project](https://createbsvapp.vercel.app/docs/add-to-existing). ## Does it work without a server? Partly. A frontend-only app (the `react` starter) can connect a desktop wallet. Phone pairing, login and signed requests need a server to verify proofs and host the relay. ## Where is user data stored? Nowhere, until you decide. The scaffold has no database: the only "user record" is the identity key in a verified proof. Store whatever you need, keyed by it. ## Where do I get testnet coins? From the [BSV Faucet](https://bsvfaucet.com/), run by the BSV Association: sign in, paste a testnet address from your wallet, and request up to 10 million satoshis every 24 hours. You only need coins once your app creates [transactions](https://createbsvapp.vercel.app/docs/bsv-primer#your-first-payment); login and signed requests are free. ## How do I go to mainnet? Pass `--network main` when scaffolding, or set `BSV_NETWORK` and `VITE_BSV_NETWORK` to `main`. Signatures work the same on every network. See [Networks](https://createbsvapp.vercel.app/docs/environment#networks). ## How do I get the latest helper files after a CLI update? Commit first, then run `npx create-bsv-app@latest add --capabilities --force --yes` and review the diff. `--force` overwrites the helper files. ## Is @bsv/app still supported? No. It's deprecated and forwards to create-bsv-app. See [Migrate from @bsv/app](https://createbsvapp.vercel.app/docs/migrate-from-bsv-app). ## Can my AI agent use this? Yes. Use `--file` or `--yes`, read the generated `AGENTS.md`, and connect the docs over [MCP](https://createbsvapp.vercel.app/docs/agents#connect-the-docs-over-mcp). --- # Why create-bsv-app > The problems with wiring BSV wallet auth by hand, and the positions create-bsv-app takes to solve them. > 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. ## The problems Adding "log in with a wallet" to a web app sounds like one feature. Done properly, it's a dozen. **Connecting is a state machine, not a button.** Users have a desktop wallet, a phone wallet or neither. Desktop needs a local probe; phone needs a relay server, a session, a QR code and a WebSocket. Every app reinvents this, and most only ship the happy path. **Signatures are easy; secure signatures are not.** A proof must name *which server* it's for, *what action* it authorizes, *which exact body* it covers, *when* it expires, and be refused the *second* time it arrives. Miss one and you've built a replayable password. These are the bugs that never show up in a demo. **The boring parts are where the holes are.** An API client that follows redirects can carry a proof to another host. A server that falls back to a random key in production changes identity on every deploy. A CORS rule mistaken for auth protects nothing. None of these are BSV problems, and all of them break BSV auth. **Then there's the glue.** Providers in `main.tsx`, routes in `App.tsx`, CORS and the relay in the server, environment variables on both sides, and a README nobody keeps current. ## The positions we take **Generate code, don't wrap it in a library.** The code that decides who's logged in is the code you most need to read, change and own. create-bsv-app writes small, plain TypeScript files into your project. No runtime package of ours sits in your dependency tree, and nothing is hidden. The cryptography itself comes from maintained packages (`@bsv/sdk`, `@bsv/auth`, `@bsv/wallet-relay`). **Identity comes from the wallet.** No passwords, no emails, no reset flows. The user's [identity key](https://createbsvapp.vercel.app/docs/glossary#identity-key) *is* the account, proven by a signature on every request that matters. **Fail closed.** Proofs expire in two minutes and work once. The API client refuses redirects and caps sizes. Production config refuses to start without HTTPS and a stable key. When something's wrong, you get an error at deploy time instead of a breach later. **One pipeline, four ways in.** Prompts, flags, a JSON file and a browser form all produce the same config and the same result. Humans and [agents](https://createbsvapp.vercel.app/docs/agents) drive the same tool. **Working beats blank.** `npm run dev` gives you a running app with a connected wallet, a login and a signed request, so you start from something that works and change it. ## What it isn't - **Not a wallet.** Users bring their own BRC-100 wallet, such as [BSV Browser](https://browser.bsvb.tech/). - **Not a framework.** After scaffolding, it's out of the way. Re-run it only to [add capabilities](https://createbsvapp.vercel.app/docs/add-to-existing). - **Not (yet) payments.** The scaffold covers identity and authentication. Transactions are your next step: see [your first payment](https://createbsvapp.vercel.app/docs/bsv-primer#your-first-payment). - **Not a backend-as-a-service.** No accounts, no hosted database, no lock-in. You deploy two ordinary apps. ## Read next ::: cards [**Quick start** From an empty folder to a connected wallet.](https://createbsvapp.vercel.app/docs) [**Security model** What a verified proof guarantees, and what it doesn't.](https://createbsvapp.vercel.app/docs/security) ::: --- # BSV for web developers > Wallets, identity keys, signatures, BRCs and networks, explained by mapping each one to something you already know from web development. > 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. You don't need to know how a blockchain works to use create-bsv-app. You *do* need five ideas, because they replace things you'd normally build yourself: user accounts, passwords and sessions. ## The cheat sheet | You know | In a BSV app | Where it lives | | --- | --- | --- | | User ID | **identity key**, a public key like `02ab…` | the user's wallet | | Password | a **signature** from the user's wallet | never sent, never stored | | "Sign in with Google" | "Connect wallet" | `useWallet()` | | Session cookie | a **proof** per request, or your own JWT after login | `signedFetch()`, `login()` | | Server API key | the server's own identity key | `SERVER_PRIVATE_KEY` | | Stripe checkout | a **transaction** the wallet creates | `wallet.createAction()` (not in the scaffold yet) | ## Wallets A wallet is an app the user installs, such as [BSV Browser](https://browser.bsvb.tech/). It holds their private keys and **asks them before doing anything** with those keys. Your app never sees a private key. It asks the wallet to sign, encrypt or pay, and the wallet decides whether to. **BRC-100** is the standard interface between apps and wallets, and `@bsv/sdk` exposes it as `WalletInterface`. Any BRC-100 wallet works with your app, on desktop or on a phone (paired by QR code). ## Identity keys When a user connects, you get their **identity key**: a public key, 66 hex characters starting with `02` or `03`. It's stable for that wallet, it's unique, and nobody else can produce signatures for it. In practice it's your user ID. ::: warning There's no "forgot password" If a user loses their wallet and has no backup, their identity is gone. Recovery is the wallet's job (backups, recovery phrases), not your app's. If your app needs account recovery, link identity keys to something you can verify another way, such as an email address. ::: ## Signatures vs transactions This one trips people up: - **A signature** proves "the owner of key X approved this exact message". It's free, instant, works offline and touches no blockchain. **Wallet login and signed requests are signatures.** That's why the scaffold costs nothing to run. - **A transaction** moves coins and gets broadcast to the network. It costs a fee (on BSV, a tiny fraction of a cent) and is permanent. The scaffold doesn't create transactions *yet*. That's your next step. ### Your first payment When you're ready, the connected `wallet` can create transactions. The user approves each one in their wallet: ```ts [client/src/pay.ts] twoslash import { P2PKH, type WalletInterface } from '@bsv/sdk' /** Ask the user's wallet to send `satoshis` to a BSV address. The wallet shows an approval prompt. */ export async function pay (wallet: WalletInterface, address: string, satoshis: number): Promise { const { txid } = await wallet.createAction({ description: 'Tip the author', outputs: [{ lockingScript: new P2PKH().lock(address).toHex(), satoshis, outputDescription: 'Tip for the author' }] }) return txid } ``` ```tsx const { wallet } = useWallet() if (wallet) await pay(wallet, recipientAddress, 1000) // 1000 satoshis ``` This type-checks against the `@bsv/sdk` version the scaffold installs. Try it on **testnet** first, with a testnet address and test coins from the [BSV Faucet](https://bsvfaucet.com/) (sign in, paste a testnet address, up to 10 million satoshis a day). Descriptions must be 5–50 characters, or the wallet rejects the action. ## Key derivation, briefly A wallet doesn't sign everything with one key. For each purpose it derives a fresh key pair from the identity key, a **protocol**, a **key ID** and a **counterparty** (BRC-42 and BRC-43). That's how a login proof made *for your server* can't be replayed *against another server*: the counterparty is part of the key. The [testing guide](https://createbsvapp.vercel.app/docs/testing) proves it with a test. ::: note Privacy The identity key itself is the same in every app the user connects to, so two apps can tell they're talking to the same wallet. Derived keys are different per protocol and counterparty, and that's where the privacy comes from. ::: ## Networks | Network | `--network` | Coins | Use it for | | --- | --- | --- | --- | | Testnet | `test` (default) | free, worthless, from the [BSV Faucet](https://bsvfaucet.com/) | building and testing | | Teratestnet | `ttn` | free, worthless | testing against Teranode, BSV's new node software | | Mainnet | `main` | real | production | Signatures work the same everywhere. The network only matters once you create transactions. ## BRCs you will meet BRCs ("BSV Request for Comments") are BSV's open standards, published in the [BRCs repository](https://github.com/bitcoin-sv/BRCs). | BRC | What it is | Where you'll see it | | --- | --- | --- | | BRC-100 | the app ↔ wallet interface | `WalletInterface`, `WalletClient` | | BRC-103 | mutual authentication between peers | `@bsv/auth` proofs | | BRC-42 / 43 | key derivation, protocol IDs and counterparties | every signature | | BRC-52 | identity certificates (verified attributes on top of a key) | a future step for KYC-style needs | | BRC-102 | deployment info (`deployment-info.json`) | the BRC-102 example starters | ## Overlays Several [complete examples](https://createbsvapp.vercel.app/docs/starters#complete-examples) (Pollr, Postboard, MetaMarket and others) use **overlay services**: small servers that watch for transactions matching a topic and index them so apps can look them up. You don't need overlays for login or signed requests. They become relevant when your app's data lives *on chain*. ## Read next ::: cards [**How the proof works** The four checks the server makes on every request.](https://createbsvapp.vercel.app/docs/capabilities#how-the-proof-works) [**Security model** What's guaranteed, what isn't, and what to harden before launch.](https://createbsvapp.vercel.app/docs/security) ::: --- # How it works > One config, one pipeline. How your flags, prompts or JSON become a ProjectConfig, and what the CLI does with it in new and add mode. > 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. Under the hood, create-bsv-app is small and predictable. However you call it, it builds one `ProjectConfig` object and hands it to one function. Knowing that makes every flag, prompt and error easier to reason about. ```text [the pipeline] flags ─┐ prompts ─┼──▶ ProjectConfig ──▶ applyConfig() --file ─┤ (validated) │ --ui ─┘ ├─ mode "new" ─▶ starter ─▶ base app ─▶ capability files ─▶ wiring ─▶ manifest ─▶ install └─ mode "add" ─────────────────────────▶ capability files ─────────▶ manifest ─▶ install ``` ## Four ways in All four produce the same `ProjectConfig`, so the same config always takes the same path through the pipeline. They only differ in how you supply it. | Way in | Command | Best for | | --- | --- | --- | | Prompts | `npx create-bsv-app@latest` | exploring, first run | | Flags | `… --starter full-stack --capabilities wallet-login --yes` | scripts, docs, muscle memory | | Config file | `… --dir my-app --file config.json` | CI, AI agents, reproducible setups | | Browser UI | `… --ui --dir my-app` | point and click | You can mix them. Flags fill in their answers and the prompts only ask about the rest. With `--yes` there are no prompts at all, and anything unspecified uses its default. ::: deep What `--ui` actually starts A tiny HTTP server bound to `127.0.0.1` (never reachable from your network) serves a one-page form, generated from the same schema the terminal prompts use. Press **Generate** and it runs the same pipeline, then shuts itself down. It's single-use by design. ::: ## Modes ### `new` Creates a project in an **empty** directory (a `.git` folder or a `bsv-scaffold.json` is allowed). In order, it: 1. **Clones or generates the base.** Complete examples are `git clone`d, and that's nearly the end: the manifest is written and dependencies installed. Generated starters run `create-vite` (React) and/or write a lean Express app. 2. **Writes capability files** into `src/bsv/` of each target. 3. **Wires the base app** (unless `--no-glue`). It wraps `` in ``, adds routes to `App.tsx`, and mounts routes, CORS and the wallet relay in the server. 4. **Writes the root runner** when there's both a client and a server (`npm run dev` for both). 5. **Writes `AGENTS.md` and `bsv-scaffold.json`.** 6. **Adds dependencies** to each `package.json` and installs them (unless `--skip-install`). ### `add` Adds capabilities to an existing project. No base generator runs, and **your own files are never edited**. It writes capability files, `AGENTS.md` (with manual wiring snippets) and the manifest, adds dependencies, and installs. Existing helper files are kept unless you pass `--force`. [Full guide](https://createbsvapp.vercel.app/docs/add-to-existing). ### How the mode is chosen ```text [mode inference] --mode new|add given? → use it bsv-scaffold.json in the target? → add React/Express project detected? → add otherwise → new ``` ## Defaults Everything has one, so `--yes` with nothing else is valid as long as the starter determines a stack: | Field | Default | | --- | --- | | directory | `.` | | name | the directory name | | starter | `custom` | | capabilities | `wallet-connect` (always included in `new`) | | bsvDir | `src/bsv` | | packageManager | `npm` | | network | `test` | | glue | on | | install | on | ## Why generate, rather than ship a library? A library hides the code that decides who's logged in, and that's the code you most need to read and own. Generated files are yours: readable, editable and deletable, with no version lock-in and no magic. The heavy cryptography still comes from maintained packages (`@bsv/sdk`, `@bsv/auth`, `@bsv/wallet-relay`). The scaffold is the thin, visible layer between them and your app. --- # Security model > What a verified proof guarantees, what it doesn't, and the four things to harden before real users show up. > 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. The generated code is small on purpose, so you can audit it in one sitting. This page tells you what to look for. ## What a verified proof guarantees When `verifySignedRequest()` or `loginRoute()` says `valid: true`, you know: | Guarantee | Because | Tested | | --- | --- | --- | | **Who:** the holder of `identityKey` signed it | an ECDSA signature only that wallet can make | ✔ | | **What:** this exact `action` and body | both are inside the signed bytes | ✔ changed body and changed action are rejected | | **For whom:** your server, and only yours | the signing key is derived with your server's identity as counterparty | ✔ another server rejects it | | **When:** within the last ~2 minutes | `expiresAt` is signed, with a 2-minute window and 30 s of clock skew allowed | by `@bsv/auth` | | **Once:** it was never accepted before | `consumeNonce` refuses repeats | ✔ replay rejected | The ✔ rows are covered by the test file on the [Testing](https://createbsvapp.vercel.app/docs/testing) page. Run it yourself. ## What it does *not* give you - **Authorization.** A valid proof tells you *who*, not *whether they're allowed*. Check `identityKey` against your own rules (owners, roles, allow-lists) before doing anything. - **Secrecy.** Proofs aren't encrypted. Use HTTPS, which production config enforces. - **Sessions.** Every signed request stands alone. If you want "logged in for an hour", [add a session after login](https://createbsvapp.vercel.app/docs/capabilities#turning-login-into-a-session). - **CORS is not security.** `CLIENT_ORIGIN` controls which *browser pages* can read responses. Scripts, servers and `curl` ignore it. Only the proof check authenticates anyone. ## Server identity is trusted via your API origin Before signing, the client fetches the server's identity key from `GET /api/identity`. It trusts whatever comes back from `VITE_API_URL` over HTTPS, so the trust is exactly as strong as your DNS and TLS. For most apps that's fine. When it isn't (a mobile app that must keep talking to the *same* server identity across domain moves, proxies or CDNs), **pin** the key: ```ts const { login } = useWalletLogin({ serverIdentityKey: '03d234…' }) const { signedFetch } = useSignedRequest('03d234…') ``` Pinned keys skip the fetch entirely. Pinning only works if `SERVER_PRIVATE_KEY` never changes, so [set it](https://createbsvapp.vercel.app/docs/environment#generate-a-server-key) and keep it. ## Replay protection across instances The generated `nonceStore.ts` is a `Map` in process memory. That's correct for one process, but: - **Two or more instances:** a proof consumed on instance A is unknown to instance B, so it can be replayed there within its 2-minute window. - **Restarts:** the memory is wiped. Proofs expire after 2 minutes anyway, so the window is small. - **Capacity:** it holds 10,000 nonces. When it's full it **rejects new proofs** until old ones expire. That fails safe, but it's a cheap denial-of-service: anyone can generate keys and valid proofs. Put a rate limit in front of `/api/login` and your signed routes. For anything beyond one instance, use an atomic shared store. With Redis it's one command, `SET … NX PXAT`: ```ts [server/src/bsv/redisNonceStore.ts] // Replay protection shared by every server instance. SET NX is atomic: only the first caller wins. import { createClient } from 'redis' const redis = createClient({ url: process.env.REDIS_URL }) await redis.connect() export async function consumeNonce (nonce: string, expiresAt: Date): Promise { if (expiresAt.getTime() <= Date.now()) return false const ok = await redis.set(`bsv-nonce:${nonce}`, '1', { NX: true, PXAT: expiresAt.getTime() }) return ok === 'OK' } ``` Then import `consumeNonce` from `./bsv/redisNonceStore.js` instead of `./bsv/nonceStore.js` in `loginRoute.ts` and your signed routes. Redis deletes each key when its proof expires, so the store never grows. (Type-checked against `redis` 6. Any store with an atomic "insert if absent" works: a unique index in Postgres or Mongo, for example.) ## The API client is deliberately strict Every generated request goes through `client/src/bsv/apiClient.ts`, which: - only calls `VITE_API_URL`. Paths must be plain (`/api/x`), with no query strings and no `..`. - **refuses redirects**, so a proof can never be forwarded to another host. - sends no cookies or credentials (`credentials: 'omit'`) and no referrer. - times out after **10 s** and caps requests and responses at **1 MiB**. - rejects responses that aren't strict UTF-8 JSON. The server side matches: `express.json({ limit: '64kb' })`. Loosen any of these deliberately, not accidentally. ## Before you launch - [ ] Every route that changes data checks a proof **and** authorizes the `identityKey` - [ ] Nonce store is shared, or you run exactly one instance - [ ] Rate limits on `/api/login` and signed routes - [ ] `SERVER_PRIVATE_KEY` is stable, secret and backed up - [ ] Demo routes (`/api/echo`, demo pages) removed - [ ] HTTPS everywhere (production config refuses anything else) - [ ] You've read `auth.ts`, `nonceStore.ts`, `config.ts` and `apiClient.ts`. Together they're a few hundred lines, and they're the whole trust boundary. --- # Glossary > The BSV and create-bsv-app terms these docs use, each in one or two plain sentences. > 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. ## Action {#action} The name a [proof](#proof) is signed for, such as `login` or `create-note`. Client and server must use the same one, so a proof for one route can't be used on another. ## BRC {#brc} "BSV Request for Comments": BSV's open standards, published at [github.com/bitcoin-sv/BRCs](https://github.com/bitcoin-sv/BRCs). ## BRC-100 {#brc-100} The standard interface between apps and wallets. `@bsv/sdk` exposes it as `WalletInterface`. Any BRC-100 wallet works with a generated app. ## BRC-102 {#brc-102} "The deployment-info.json Specification": one file, `deployment-info.json`, that tells local and cloud tools how to build, run and deploy a BSV app. The two BRC-102 [complete examples](https://createbsvapp.vercel.app/docs/starters#complete-examples) ship with it. ## BRC-103 {#brc-103} Mutual authentication between peers. `@bsv/auth` implements the single-message proof the capabilities use. ## BRC-42 / BRC-43 {#brc-42} How wallets derive a fresh key per purpose from an identity key, a [protocol ID](#protocol-id), a key ID and a [counterparty](#counterparty). ## Basket {#basket} A named group a wallet uses to track the outputs that belong to one app or purpose, so the app can list and spend just its own. ## Capability {#capability} A feature the CLI wires into your project: `wallet-connect`, `wallet-login` or `signed-requests`. See [Capabilities](https://createbsvapp.vercel.app/docs/capabilities). ## Complete example {#complete-example} A starter that clones a maintained app from GitHub, as opposed to a [generated starter](#generated-starter). ## Counterparty {#counterparty} The other identity a key is derived with. For proofs it's the server's identity key, which is why a proof made for one server fails on another. `'self'` and `'anyone'` are special values. ## Generated starter {#generated-starter} `custom`, `react`, `express` or `full-stack`: a fresh app the CLI builds and wires, ready for capabilities. ## Identity key {#identity-key} A user's public key, 66 hex characters starting `02` or `03`. Stable per wallet and unforgeable. Use it as the user ID. ## Manifest {#manifest} `bsv-scaffold.json`, the CLI's record of what it generated. See [bsv-scaffold.json](https://createbsvapp.vercel.app/docs/manifest). ## Micropayment {#micropayment} A payment of a few satoshis, small enough to charge per request, per item or per second. BSV's low fees are what make them practical. ## Nonce {#nonce} A random value inside every proof. The server records it and refuses to accept it twice. That's replay protection. ## Overlay {#overlay} An overlay network or service: an index that watches the chain for just the transactions one app cares about, grouped under a topic, and answers lookups about them. ## Proof {#proof} The signed message a wallet produces: `{ action, identityKey, expiresAt, nonce }`, plus the body's bytes when there is one. Valid for 2 minutes and accepted once. ## Protocol ID {#protocol-id} `[securityLevel, 'name']`, which namespaces the keys a wallet derives for your app. Level `0` never prompts, `1` prompts once per app, `2` once per app and counterparty. ## Relay {#relay} The server-side service (`@bsv/wallet-relay`) that pairs a phone wallet with the web app over a QR code and a WebSocket. ## Satoshi {#satoshi} The smallest unit of BSV: one hundred-millionth of a coin. ## sCrypt {#scrypt} A TypeScript framework for writing BSV smart contracts: scripts that lock coins until their conditions are met. ## Server identity {#server-identity} The server's own key pair, from `SERVER_PRIVATE_KEY`, published at `GET /api/identity`. It must stay stable in production. ## Signed request {#signed-request} An API call that carries its own proof, bound to its action and exact body. No session needed. See [signed-requests](https://createbsvapp.vercel.app/docs/capabilities#signed-requests). ## Teratestnet {#teratestnet} `--network ttn`: a free test network for Teranode, BSV's newer node software. ## Testnet {#testnet} `--network test`, the default: a network with free, worthless coins for building and testing. Get some from the [BSV Faucet](https://bsvfaucet.com/). ## Wallet connect {#wallet-connect} The `wallet-connect` [capability](#capability), in every generated starter: a Connect wallet button that finds a desktop wallet directly, or pairs a phone by QR code through the [relay](#relay). It gives your app the wallet and the user's [identity key](#identity-key). ## Wallet login {#wallet-login} Passwordless login, the `wallet-login` capability: the wallet signs a one-time [proof](#proof) for the action `login`, and the server verifies it and learns the user's identity key. No password, no email, and no coins move. --- # CLI reference > Every command, flag and default in create-bsv-app 1.1.2, plus copy-paste recipes for the common jobs. > 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. ## Usage ```text create-bsv-app [new|add] [directory] [options] ``` Run it with `npx create-bsv-app@latest`, `pnpm create bsv-app`, `yarn create bsv-app` or `bun create bsv-app`. Every argument is optional. Anything you don't pass is asked interactively, or with `--yes`, takes its default. ## Flags | Flag | Default | Mode | Description | | --- | --- | --- | --- | | `new \| add` | inferred | both | Mode as a positional subcommand. Omit it and the CLI infers: a valid `bsv-scaffold.json` or a detected React/Express project means `add`, anything else means `new`. | | `[directory]` | `.` | both | Target directory as a positional argument. Same as `--dir`. | | `--dir ` | `.` | both | Target directory. In `new` mode it must be empty (a lone `.git` or `bsv-scaffold.json` is allowed). | | `--starter ` | `custom` | new | Starter from the [catalogue](https://createbsvapp.vercel.app/docs/starters). Repository starters ignore stack and capability flags. | | `--name ` | directory name | new | Project name passed to the generators and recorded in the manifest. | | `--frontend ` | `none` | new | Frontend for the `custom` starter. `react` runs create-vite under the hood. | | `--backend ` | `none` | new | Backend for the `custom` starter. `express` writes a lean TypeScript Express app. | | `--variant ` | `react-ts` | new | create-vite template variant for the frontend. | | `--capabilities ` | `wallet-connect` | both | Comma-separated [capability](https://createbsvapp.vercel.app/docs/capabilities) ids. `new` mode always adds `wallet-connect`; dependencies are expanded for you. | | `--bsv-dir ` | `src/bsv` | both | Where capability helper files go, relative to each target (`client/`, `server/`). | | `--package-manager ` | `npm` | new | Used for the generators, the install, and the root runner. Not auto-detected: pass it if you don't use npm. | | `--network ` | `test` | new | Default BSV network baked into the generated config. `ttn` is Teratestnet. | | `--yes` | off | both | Non-interactive. Resolves everything from flags (plus an existing manifest) and never prompts. | | `--file ` | none | both | Read a complete [ProjectConfig](https://createbsvapp.vercel.app/docs/config) from JSON and skip prompts. `--mode` overrides the file's `mode`. | | `--mode ` | inferred | both | Force the mode instead of inferring it. | | `--ui` | off | both | Open the browser configurator on `127.0.0.1`. Single use: it shuts down after you press Generate. | | `--glue / --no-glue` | glue on | new | Auto-wire providers into `main.tsx`, routes into `App.tsx`, and routes into the server. With `--no-glue`, files are still written and `AGENTS.md` prints the snippets to paste. | | `--install / --skip-install` | install on | both | Install dependencies before exiting. Skip when CI or another tool installs. | | `--force` | off | add | Overwrite existing capability helper files with fresh copies. `AGENTS.md` and the manifest are always rewritten. | | `-h, --help` | none | both | Print usage and exit. | Notation: `` is required after the flag, and `a|b` means one of. Flags can appear in any order, before or after the directory. ## Recipes ::: code-group ```bash [full-stack, everything] npx create-bsv-app@latest my-app --starter full-stack \ --capabilities wallet-login,signed-requests --yes ``` ```bash [frontend only] npx create-bsv-app@latest my-app --starter react --capabilities wallet-login --yes ``` ```bash [API only] npx create-bsv-app@latest my-api --starter express --capabilities signed-requests --yes ``` ```bash [mainnet, pnpm] npx create-bsv-app@latest my-app --starter full-stack \ --network main --package-manager pnpm --yes ``` ```bash [CI, no install] npx create-bsv-app@latest my-app --starter full-stack --skip-install --yes ``` ```bash [wire it yourself] npx create-bsv-app@latest my-app --starter full-stack --no-glue --yes # then follow "Wiring (manual)" in AGENTS.md ``` ::: ::: code-group ```bash [add to existing] cd existing-app npx create-bsv-app@latest add --capabilities wallet-connect,wallet-login --yes ``` ```bash [add more later] npx create-bsv-app@latest add --capabilities signed-requests --yes ``` ```bash [refresh helper files] npx create-bsv-app@latest add --capabilities wallet-connect --force --yes ``` ```bash [from a file] npx create-bsv-app@latest --dir my-app --file config.json ``` ```bash [browser UI] npx create-bsv-app@latest --ui --dir my-app ``` ```bash [complete example] npx create-bsv-app@latest my-app --starter pollr --yes ``` ::: ## Output On success it prints a summary and the next commands, then exits `0`: ```console $ npx create-bsv-app@latest my-app --starter full-stack --capabilities wallet-login,signed-requests --yes Scaffolded my-app (28 file(s) written). Dependencies installed. Next: cd my-app npm run dev See the generated README/AGENTS.md when present and bsv-scaffold.json for exact provenance. ``` When some existing files were kept (typical in `add` mode), the first line reads `Updated … (n file(s) written).` instead. With `--skip-install` you'll see `Dependencies were not installed. Run your package manager install command before starting.` ## Errors and exit codes | Exit code | Meaning | | --- | --- | | `0` | done | | `1` | something failed. The message is on stderr | Config problems are prefixed with `Invalid config:` and name the field. Every message is listed with its fix in [Troubleshooting](https://createbsvapp.vercel.app/docs/troubleshooting). ## Versions These docs describe create-bsv-app **1.1.2**. `npx create-bsv-app@latest` always runs the newest release. To pin one, for reproducible CI, use `npx create-bsv-app@1.1.2`. Check what's current with `npm view create-bsv-app version`. --- # ProjectConfig reference > The JSON accepted by --file. Every field, its type, its default, and how it maps to the CLI flags. > 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. `--file ` reads a complete config and skips the prompts. Every way into the CLI (flags, prompts, `--ui`) builds this same object, so this page is also the precise meaning of every flag. ```json [config.json] { "mode": "new", "name": "my-app", "starter": "custom", "stack": { "frontend": { "framework": "react", "variant": "react-ts" }, "backend": { "framework": "express" } }, "targets": { "client": "client", "server": "server" }, "bsvDir": "src/bsv", "capabilities": ["wallet-login", "signed-requests"], "glue": true, "install": true, "packageManager": "npm", "network": "test" } ``` ```bash npx create-bsv-app@latest --dir my-app --file config.json ``` Only `name` is always required. A `new` config also needs a stack, either from a named starter or from `stack` on `custom`. ## Fields | Field | Type | Default | Flag | | --- | --- | --- | --- | | `mode` | `"new" \| "add"` | `"new"` | `new` / `add`, `--mode` | | `name` | `string` | **required** | `--name` | | `dir` | `string` | `"."` | `--dir` (the flag wins) | | `starter` | `string` | `"custom"` | `--starter` | | `stack.frontend` | `{ framework: "react", variant?: string }` | none | `--frontend`, `--variant` | | `stack.backend` | `{ framework: "express" }` | none | `--backend` | | `targets` | `{ client?: string, server?: string }` | from the stack | none | | `bsvDir` | `string` | `"src/bsv"` | `--bsv-dir` | | `capabilities` | `string[]` | `[]`, plus `wallet-connect` in new | `--capabilities` | | `glue` | `boolean` | `true` | `--glue` / `--no-glue` | | `install` | `boolean` | `true` | `--install` / `--skip-install` | | `packageManager` | `"npm" \| "pnpm" \| "yarn" \| "bun"` | `"npm"` | `--package-manager` | | `network` | `"main" \| "test" \| "ttn"` | `"test"` | `--network` | ### `mode` `"new"` scaffolds into an empty directory. `"add"` installs capabilities into an existing project. Any value other than `"add"` is treated as `"new"`. On the command line, `--mode` overrides the file. ### `name` Passed to the generators, written to `package.json` and the manifest. Required in every mode. ### `starter` Any id from the [catalogue](https://createbsvapp.vercel.app/docs/starters). With a named starter (`react`, `express`, `full-stack` or a complete example) in `new` mode, `stack` and `targets` come from the starter and your values are ignored. `custom` uses your `stack`. ### `stack` `frontend.framework` must be `"react"` and `backend.framework` must be `"express"`. Anything else is an error. `variant` is the create-vite template (default `"react-ts"`). Omit a side for none. ### `targets` Where each app lives, relative to the project root. The defaults are `client` and `server` when there are both, and the root (`""`) when there's one. They must be safe relative paths: no absolute paths, no `..`. You'll mainly set these in [add mode](https://createbsvapp.vercel.app/docs/add-to-existing#how-the-cli-finds-your-app) for unusual layouts. ### `bsvDir` Where capability files go *inside each target*. Must be a safe relative path. ### `capabilities` Ids from `wallet-connect`, `wallet-login` and `signed-requests`. In `new` mode, required capabilities are added for you and `wallet-connect` is always included. In `add` mode they're **not** expanded, so list `wallet-connect` yourself on a project that doesn't have it. Complete-example starters reject any capabilities. ### `glue` When `true`, `new` mode edits the base app so everything runs immediately. When `false`, files are still written, and `AGENTS.md` prints the snippets to paste. Ignored in `add` mode, which never edits your files. ### `install` Run the package manager's install in each app before exiting. In existing apps, a lockfile decides which package manager is used. ### `packageManager` Used for create-vite, the install and the generated root runner script. ### `network` The default baked into both `config.ts` files. `VITE_BSV_NETWORK` and `BSV_NETWORK` override it at runtime. See [Networks](https://createbsvapp.vercel.app/docs/bsv-primer#networks). ## Validation errors A bad field fails fast with `Invalid config: …`, for example `targets.client must be a safe relative path`, `stack.frontend.framework must be "react"`, `invalid bsvDir: …` or `name is required`. File problems read `config file not found: …`, `cannot read config file: …` or `invalid JSON in …`. --- # bsv-scaffold.json > The manifest every run writes. What's in it, why later runs read it, and how it records exactly where a project came from. > 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. Every run writes `bsv-scaffold.json` at the project root. It's the CLI's memory of your project: later `add` runs read it to reuse your stack, folders and network, and to offer only the capabilities you don't have yet. ## Generated project ```json [bsv-scaffold.json] { "version": 2, "name": "my-app", "network": "test", "stack": { "frontend": { "framework": "react", "variant": "react-ts" }, "backend": { "framework": "express" } }, "targets": { "client": "client", "server": "server" }, "bsvDir": "src/bsv", "capabilities": ["wallet-connect", "wallet-login", "signed-requests"], "starter": { "id": "full-stack", "kind": "generated" } } ``` ## Complete example ```json [bsv-scaffold.json] { "version": 2, "name": "my-meter-app", "starter": { "id": "meter", "kind": "repository", "repository": "https://github.com/p2ppsr/meter.git", "ref": "master", "commit": "<40-character SHA of the cloned commit>" } } ``` (Trimmed. Example manifests also carry the stack and target fields.) ## Fields | Field | Type | What it records | | --- | --- | --- | | `version` | `2` | manifest format. Version 1 files from older CLIs are still read | | `name` | `string` | the project name | | `network` | `"main" \| "test" \| "ttn"` | the default network | | `stack` | `{ frontend?, backend? }` | frameworks, reused by `add` | | `targets` | `{ client?, server? }` | app folders, so `add` puts files in the same place | | `bsvDir` | `string` | where capability files live | | `capabilities` | `string[]` | what's installed. `add` merges new ids in | | `starter.id` | `string` | which starter produced the project | | `starter.kind` | `"generated" \| "repository"` | generated scaffold or cloned example | | `starter.repository` | `string` | complete examples: the source repo | | `starter.ref` | `string` | complete examples: the branch cloned | | `starter.commit` | `string` | complete examples: the exact commit cloned | ## Should I commit it? **Yes.** It's small, it describes the project, and it makes `add` runs (yours, your teammates' or your agent's) behave predictably. Don't edit it by hand. Let the CLI keep it in sync. ## Reproducing a project The manifest is a record, not a recipe: the CLI doesn't rebuild a project from it. A folder containing only `bsv-scaffold.json` is detected as an existing project, so a plain run goes to **add** mode, and `new --yes` there stops with `a new project needs at least a frontend or a backend`. To recreate a generated project, run the same command again, or better, commit a [`config.json`](https://createbsvapp.vercel.app/docs/config) and use `--file`. For complete examples, `repository` + `commit` pin the exact code: `git clone `, then `git checkout `. --- # Client API > Every function and component the scaffold writes to client/src/bsv/, with its exact signature, return value, parameters and errors. > 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. These live in your project, in `client/src/bsv/` (or `src/bsv/` for the `react` starter). They're plain TypeScript you own, so open them, change them, or delete what you don't use. The source of each one is at the end of its section, exactly as create-bsv-app 1.1.2 generates it (with the [build fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails) applied). **Hover any underlined name** in the examples for its real type. ## useWallet() The connected wallet, the user's identity key, and the connect flow. From [`wallet-connect`](https://createbsvapp.vercel.app/docs/capabilities#wallet-connect). ```ts import { useWallet } from './bsv/WalletContext' ``` ### Usage ```tsx [client/src/Profile.tsx] twoslash import { useWallet } from './bsv/WalletContext' export function Profile () { const { status, identityKey, connect } = useWallet() if (status !== 'connected') return return

Signed in as {identityKey}

} ``` ### Returns An object, re-rendered whenever the connection changes: | Field | Type | Meaning | | --- | --- | --- | | `status` | `ConnectStatus` | `'disconnected'`, `'connecting'`, `'choosing'`, `'pairing'` or `'connected'` | | `connected` | `boolean` | `wallet !== null` | | `wallet` | `WalletInterface \| null` | the full [BRC-100](https://createbsvapp.vercel.app/docs/glossary#brc-100) wallet from `@bsv/sdk` | | `identityKey` | `string \| null` | the user's [identity key](https://createbsvapp.vercel.app/docs/glossary#identity-key), 66 hex characters | | `connect` | `() => Promise` | try the desktop wallet; on failure, move to `'choosing'` | | `connectMobile` | `() => Promise` | start QR pairing (`'pairing'`); needs the server's relay | | `cancel` | `() => void` | back to `'disconnected'` | ### Throws - `useWallet must be used within WalletProvider` if the component isn't inside [``](#walletproviders). :::: deep Source: WalletContext.tsx ```tsx [client/src/bsv/WalletContext.tsx] // App-wide wallet state + connect state machine (desktop-first, relay fallback). import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react' import type { WalletInterface } from '@bsv/sdk' import { connectDesktopWallet } from './walletAcquisition.js' import { useWalletConnection } from './WalletConnectionContext.js' export type ConnectStatus = 'disconnected' | 'connecting' | 'choosing' | 'pairing' | 'connected' interface WalletState { wallet: WalletInterface | null identityKey: string | null connected: boolean status: ConnectStatus connect: () => Promise // desktop-first; on failure -> 'choosing' connectMobile: () => Promise // relay QR -> 'pairing' cancel: () => void } const Ctx = createContext(null) export function WalletProvider ({ children }: { children: ReactNode }) { const relay = useWalletConnection() const [wallet, setWallet] = useState(null) const [identityKey, setIdentityKey] = useState(null) const [status, setStatus] = useState('disconnected') const connect = useCallback(async () => { setStatus('connecting') try { const { wallet, identityKey } = await connectDesktopWallet() setWallet(wallet); setIdentityKey(identityKey); setStatus('connected') } catch { setStatus('choosing') // no desktop wallet -> show modal } }, []) const connectMobile = useCallback(async () => { setStatus('pairing') try { await relay.createSession() // shows QR via relay.session.qrDataUrl } catch { setStatus('choosing') // relay unavailable -> back to the choice modal } }, [relay]) const cancel = useCallback(() => { relay.cancelSession?.(); setStatus('disconnected') }, [relay]) // bridge: when the relay session connects, adopt its wallet useEffect(() => { if (relay.session?.status === 'connected' && relay.wallet != null && wallet == null) { const w = relay.wallet as unknown as WalletInterface w.getPublicKey({ identityKey: true }).then(({ publicKey }) => { setWallet(w); setIdentityKey(publicKey); setStatus('connected') }).catch(() => {}) } }, [relay.session?.status, relay.wallet, wallet]) return {children} } export function useWallet (): WalletState { const v = useContext(Ctx) if (v === null) throw new Error('useWallet must be used within WalletProvider') return v } ``` :::: ## WalletProviders Wraps your app so every component can call `useWallet()`. New projects already have it in `main.tsx`; add mode prints the snippet in `AGENTS.md`. ```tsx [client/src/main.tsx] twoslash // @filename: App.tsx export default function App () { return null } // @filename: main.tsx // ---cut--- import { createRoot } from 'react-dom/client' import { WalletProviders } from './bsv/WalletProviders' import App from './App' createRoot(document.getElementById('root')!).render( , ) ``` It nests the mobile relay provider (`WalletConnectionProvider`, from `@bsv/wallet-relay`) above the wallet provider, which is the order `useWallet()` needs. :::: deep Source: WalletProviders.tsx and WalletConnectionContext.tsx ::: code-group ```tsx [WalletProviders.tsx] // Compose the wallet providers in the required order (relay above wallet). import './bsv.css' import type { ReactNode } from 'react' import { WalletConnectionProvider } from './WalletConnectionContext.js' import { WalletProvider } from './WalletContext.js' export function WalletProviders ({ children }: { children: ReactNode }) { return ( {children} ) } ``` ```tsx [WalletConnectionContext.tsx] // Relay-session context: wraps @bsv/wallet-relay's hook so a single relay client // (mobile QR / remote wallet) lives above the router. Port/extend from your app as needed. import { createContext, useContext, type ReactNode } from 'react' import { useWalletRelayClient } from '@bsv/wallet-relay/react' import { API_BASE_URL } from './config.js' type RelayValue = ReturnType const Ctx = createContext(null) export function WalletConnectionProvider ({ children, apiUrl = API_BASE_URL }: { children: ReactNode, apiUrl?: string }) { // apiUrl points at the server running the WalletRelayService (REST /api/session + WS /ws). const relay = useWalletRelayClient({ apiUrl, autoCreate: false }) return {children} } export function useWalletConnection (): RelayValue { const v = useContext(Ctx) if (v === null) throw new Error('useWalletConnection must be used within WalletConnectionProvider') return v } ``` ::: :::: ## ConnectWallet The ready-made button: **Connect wallet**, then `Connected: 02ab…`, with the *No desktop wallet found* dialog (mobile QR or install link) in between. ```tsx twoslash import { ConnectWallet } from './bsv/ConnectWallet' export const Header = () =>
``` It takes no props. Restyle it in `bsv.css`, or build your own UI on `useWallet()`. ## useWalletLogin() Passwordless login: sign a `login` proof and post it to the server. From [`wallet-login`](https://createbsvapp.vercel.app/docs/capabilities#wallet-login). ```ts import { useWalletLogin } from './bsv/useWalletLogin' ``` ### Usage ```tsx [client/src/LoginButton.tsx] twoslash import { useWalletLogin } from './bsv/useWalletLogin' export function LoginButton () { const { login } = useWalletLogin() const onClick = async () => { const { identityKey } = await login() console.log(identityKey) // → 02a1f3c9e8b7d6a5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3a2918f7e6d5c4b3 } return } ``` ### Returns | Field | Type | Meaning | | --- | --- | --- | | `login` | `() => Promise<{ identityKey: string }>` | runs the whole exchange and resolves with the verified key | | `identityKey` | `string \| null` | the connected wallet's key (before login) | | `connected` | `boolean` | whether a wallet is connected | ### Parameters One optional object: #### serverIdentityKey (optional) {#usewalletlogin-serveridentitykey} - **Type:** `string` Pin the server's identity instead of fetching it from `GET /api/identity`. See [why you might](https://createbsvapp.vercel.app/docs/security#server-identity-is-trusted-via-your-api-origin). #### loginEndpoint (optional) {#usewalletlogin-loginendpoint} - **Type:** `string` - **Default:** `'/api/login'` The path to post the proof to. It must be a plain path: [no query strings](https://createbsvapp.vercel.app/docs/errors#api-endpoint-must-be-a-safe-absolute-path). ### Throws - `connect a wallet first (initializeWallet / relay)` when no wallet is connected. - `login failed: ` when the server rejects the proof (usually `401`). - `server returned another wallet identity` when the verified key isn't the connected wallet's. - Anything [`apiFetch`](#apifetch) or [`getServerIdentity`](#getserveridentity) throws. ::: warning On create-bsv-app 1.1.2, apply the import fix Without the [three-line fix](https://createbsvapp.vercel.app/docs/troubleshooting#client-build-fails), `login()` throws `ReferenceError: requireIdentityKey is not defined`. ::: :::: deep Source: useWalletLogin.tsx ```tsx [client/src/bsv/useWalletLogin.tsx] // Wallet login: prove identity with the connected wallet, then POST the proof. import { useCallback } from 'react' import { useWallet } from './WalletContext.js' import { createAuthProof } from './auth.js' import { getServerIdentity, readIdentityKeyResponse, requireIdentityKey } from './serverIdentity.js' import { apiFetch } from './apiClient.js' // serverIdentityKey is optional: when omitted it's fetched from GET /api/identity. export interface UseWalletLoginOptions { serverIdentityKey?: string, loginEndpoint?: string } export function useWalletLogin (opts: UseWalletLoginOptions = {}) { const { wallet, identityKey } = useWallet() const login = useCallback(async (): Promise<{ identityKey: string }> => { if (wallet === null) throw new Error('connect a wallet first (initializeWallet / relay)') const counterparty = requireIdentityKey(opts.serverIdentityKey ?? await getServerIdentity()) const proof = await createAuthProof(wallet, { counterparty, action: 'login' }) const res = await apiFetch(opts.loginEndpoint ?? '/api/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(proof) }) if (!res.ok) throw new Error('login failed: ' + String(res.status)) const loggedInIdentity = await readIdentityKeyResponse(res) if (identityKey !== null && loggedInIdentity !== identityKey) throw new Error('server returned another wallet identity') return { identityKey: loggedInIdentity } }, [wallet, opts.serverIdentityKey, opts.loginEndpoint]) return { login, identityKey, connected: wallet !== null } } ``` :::: ## useSignedRequest() Authenticate a single API call. From [`signed-requests`](https://createbsvapp.vercel.app/docs/capabilities#signed-requests). ```ts import { useSignedRequest } from './bsv/useSignedRequest' ``` ### Usage ```tsx [client/src/NewNote.tsx] twoslash import { useSignedRequest } from './bsv/useSignedRequest' export function NewNote () { const { signedFetch, connected } = useSignedRequest() const save = async () => { const res = await signedFetch('/api/notes', { action: 'create-note', body: { text: 'gm' } }) console.log(res.status) // → 200 } return } ``` ### Returns | Field | Type | Meaning | | --- | --- | --- | | `signedFetch` | `(path: string, opts: { action: string, body?: RequestBody }) => Promise` | signs `{ action, body }` and `POST`s `{ proof, body }` as JSON | | `connected` | `boolean` | whether a wallet is connected | ### Parameters #### serverIdentityKey (optional) {#usesignedrequest-serveridentitykey} - **Type:** `string` The first argument (not an object). Pins the server's identity instead of fetching it. #### signedFetch: path {#signedfetch-path} - **Type:** `string` A plain API path such as `/api/notes`. Query strings aren't allowed. #### signedFetch: opts.action {#signedfetch-action} - **Type:** `string` Names the operation. The server must verify the **same** action, or the request fails with `401`. #### signedFetch: opts.body (optional) {#signedfetch-body} - **Type:** `RequestBody` (a string, binary, or JSON-compatible object or array) Bound into the signature byte for byte. See [why exact bytes matter](https://createbsvapp.vercel.app/docs/capabilities#how-the-proof-works). ### Throws - `connect a wallet first` when no wallet is connected. - Anything [`apiFetch`](#apifetch) or [`getServerIdentity`](#getserveridentity) throws. A rejected proof is **not** thrown: check `res.ok` / `res.status`. :::: deep Source: useSignedRequest.ts and signedRequest.ts ::: code-group ```ts [useSignedRequest.ts] // Hook: signedFetch attaches a proof bound to the route + JSON body. import { useCallback } from 'react' import { useWallet } from './WalletContext.js' import { createSignedRequest } from './signedRequest.js' import { getServerIdentity, requireIdentityKey } from './serverIdentity.js' import { apiFetch } from './apiClient.js' import type { RequestBody } from './auth.js' // serverIdentityKey is optional: when omitted it's fetched from GET /api/identity. export function useSignedRequest (serverIdentityKey?: string) { const { wallet } = useWallet() const signedFetch = useCallback(async (url: string, opts: { action: string, body?: RequestBody }): Promise => { if (wallet === null) throw new Error('connect a wallet first') const counterparty = requireIdentityKey(serverIdentityKey ?? await getServerIdentity()) const proof = await createSignedRequest(wallet, { serverIdentityKey: counterparty, action: opts.action, body: opts.body }) return await apiFetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ proof, body: opts.body }) }) }, [wallet, serverIdentityKey]) return { signedFetch, connected: wallet !== null } } ``` ```ts [signedRequest.ts] // Create a signed request: an @bsv/auth proof bound to a route (action) + body. import type { WalletInterface } from '@bsv/sdk' import { createAuthProof, type AuthProof, type RequestBody } from './auth.js' export async function createSignedRequest ( wallet: WalletInterface, opts: { serverIdentityKey: string, action: string, body?: RequestBody } ): Promise { return await createAuthProof(wallet, { counterparty: opts.serverIdentityKey, action: opts.action, body: opts.body }) } ``` ::: :::: ## apiFetch() The one HTTP client every generated call uses. Bounded and redirect-free by design: see the [security model](https://createbsvapp.vercel.app/docs/security#the-api-client-is-deliberately-strict). ```ts import { apiFetch, readApiJson } from './bsv/apiClient' ``` ### Usage ```ts twoslash import { apiFetch, readApiJson } from './bsv/apiClient' const res = await apiFetch('/api/identity') const data = await readApiJson(res) console.log(data) // → { identityKey: '03d234c27a69dfff…' } ``` ### Returns `Promise`: a fresh `Response` whose body has already been read within the limits, so you can call `.json()`, `.text()` or [`readApiJson`](#readapijson) on it. ### Parameters #### path {#apifetch-path} - **Type:** `string` Must match `/^\/[A-Za-z0-9/_-]*$/` and contain no `..`. It's appended to [`API_BASE_URL`](#config). #### init (optional) {#apifetch-init} - **Type:** `RequestInit` Standard fetch options. `body` must be a string (use `JSON.stringify`). `credentials`, `redirect`, `referrerPolicy` and `signal` are always overridden. ### Throws `API endpoint must be a safe absolute path`, `API request body must be a string`, `API request exceeds the byte limit`, `API response exceeds the byte limit`, `API response has an invalid Content-Length`, `API response length does not match Content-Length`, `API response changed network authority`, plus `AbortError` after 10 seconds. All are listed in [Errors](https://createbsvapp.vercel.app/docs/errors#apiclientts). ## readApiJson() Parses a response body as strict UTF-8 JSON. - **Signature:** `readApiJson(response: Response): Promise` - **Throws:** `API response is not valid UTF-8`, `API response is not valid JSON`. :::: deep Source: apiClient.ts ```ts [client/src/bsv/apiClient.ts] // One bounded client for every generated API request. It refuses redirects so // proofs, identities, and future credentials never move to another network authority. import { API_BASE_URL, API_ORIGIN } from './config.js' const API_TIMEOUT_MS = 10_000 const MAX_API_REQUEST_BYTES = 1024 * 1024 const MAX_API_RESPONSE_BYTES = 1024 * 1024 function endpointUrl (path: string): string { if (!/^\/[A-Za-z0-9/_-]*$/.test(path) || path.includes('..')) { throw new TypeError('API endpoint must be a safe absolute path') } return API_BASE_URL + path } function contentLength (response: Response): number | undefined { // Fetch exposes decoded response bytes while some implementations retain the // encoded Content-Length. The streaming ceiling below remains authoritative. if (response.headers.get('content-encoding') !== null) return undefined const value = response.headers.get('content-length') if (value === null) return undefined if (!/^(?:0|[1-9]\d*)$/.test(value)) throw new Error('API response has an invalid Content-Length') const length = Number(value) if (!Number.isSafeInteger(length)) throw new Error('API response has an invalid Content-Length') return length } async function readBoundedBody (response: Response): Promise> { const declared = contentLength(response) if (declared !== undefined && declared > MAX_API_RESPONSE_BYTES) { throw new Error('API response exceeds the byte limit') } if (response.body === null) return new Uint8Array() const reader = response.body.getReader() const chunks: Uint8Array[] = [] let total = 0 try { while (true) { const { done, value } = await reader.read() if (done) break total += value.byteLength if (total > MAX_API_RESPONSE_BYTES) { await reader.cancel() throw new Error('API response exceeds the byte limit') } chunks.push(value) } } finally { reader.releaseLock() } if (declared !== undefined && declared !== total) { throw new Error('API response length does not match Content-Length') } const body = new Uint8Array(total) let offset = 0 for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.byteLength } return body } export async function apiFetch (path: string, init: RequestInit = {}): Promise { if (init.body != null && typeof init.body !== 'string') { throw new TypeError('API request body must be a string') } if (typeof init.body === 'string' && new TextEncoder().encode(init.body).byteLength > MAX_API_REQUEST_BYTES) { throw new Error('API request exceeds the byte limit') } const controller = new AbortController() const timer = setTimeout(() => { controller.abort() }, API_TIMEOUT_MS) try { const response = await fetch(endpointUrl(path), { ...init, credentials: 'omit', redirect: 'error', referrerPolicy: 'no-referrer', signal: controller.signal }) if (response.redirected || (response.url !== '' && new URL(response.url).origin !== API_ORIGIN)) { throw new Error('API response changed network authority') } const body = await readBoundedBody(response) const headers = new Headers(response.headers) headers.delete('content-encoding') headers.delete('content-length') return new Response(body.length === 0 ? null : body, { status: response.status, statusText: response.statusText, headers }) } finally { clearTimeout(timer) } } export async function readApiJson (response: Response): Promise { const bytes = new Uint8Array(await response.arrayBuffer()) let text: string try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes) } catch { throw new Error('API response is not valid UTF-8') } try { return JSON.parse(text) } catch { throw new Error('API response is not valid JSON') } } ``` :::: ## getServerIdentity() Fetches and caches the server's identity key, the counterparty of every proof. ```ts twoslash import { getServerIdentity } from './bsv/serverIdentity' const serverKey = await getServerIdentity() // → 03d234c27a69dfff14eb8190cfdc1c980c4b31450f9ba412926c0755bfc0d5472f ``` - **Signature:** `getServerIdentity(endpoint?: string): Promise` (default endpoint `'/api/identity'`) - **Caching:** the first successful result is kept for the life of the page; concurrent calls share one request. - **Throws:** `failed to fetch server identity: `, `server returned an invalid identity response`, `server returned an invalid identity key`. `requireIdentityKey(value)` and `readIdentityKeyResponse(res)` are exported from the same file. They validate a key (compressed `02`/`03` hex, on the curve) and a strict `{ identityKey }` response. :::: deep Source: serverIdentity.ts ```ts [client/src/bsv/serverIdentity.ts] // Fetch the configured API's identity public key once and cache it. // This is endpoint/TLS trust, not independent key authentication. Pass a pinned key to // the login/signed-request hooks when the application requires identity continuity. import { PublicKey } from '@bsv/sdk' import { apiFetch, readApiJson } from './apiClient.js' let cached: string | null = null let pending: Promise | null = null export function requireIdentityKey (value: unknown): string { if (typeof value !== 'string' || !/^(?:02|03)[0-9a-f]{64}$/.test(value)) { throw new Error('server returned an invalid identity key') } try { if (PublicKey.fromString(value).toString() !== value) throw new Error() } catch { throw new Error('server returned an invalid identity key') } return value } export async function readIdentityKeyResponse (response: Response): Promise { const parsed = await readApiJson(response) if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) || (Object.getPrototypeOf(parsed) !== Object.prototype && Object.getPrototypeOf(parsed) !== null) || Object.getOwnPropertySymbols(parsed).length !== 0) { throw new Error('server returned an invalid identity response') } const descriptors = Object.getOwnPropertyDescriptors(parsed) if (Object.keys(descriptors).length !== 1 || !Object.prototype.hasOwnProperty.call(descriptors, 'identityKey') || Object.values(descriptors).some(property => property.get != null || property.set != null)) { throw new Error('server returned an invalid identity response') } return requireIdentityKey(descriptors.identityKey?.value) } export async function getServerIdentity (endpoint = '/api/identity'): Promise { if (cached !== null) return cached pending ??= (async () => { const res = await apiFetch(endpoint) if (!res.ok) throw new Error('failed to fetch server identity: ' + String(res.status)) const identityKey = await readIdentityKeyResponse(res) cached = identityKey return identityKey })() try { return await pending } finally { pending = null } } ``` :::: ## Config {#config} `client/src/bsv/config.ts` reads the environment once, at import: | Export | From | Default (dev) | | --- | --- | --- | | `API_BASE_URL` | `VITE_API_URL` | `http://localhost:3000` | | `API_ORIGIN` | derived from `API_BASE_URL` | `http://localhost:3000` | | `BSV_NETWORK` | `VITE_BSV_NETWORK` | `'test'` | In production builds it **throws at import** without an HTTPS `VITE_API_URL`. See [Environment](https://createbsvapp.vercel.app/docs/environment). :::: deep Source: config.ts ```ts [client/src/bsv/config.ts] // Centralized client configuration. Vite loads VITE_-prefixed vars from client/.env. // Base URL of the server API. Defaults to the dev server; set VITE_API_URL in production // (or whenever the client is served from a different origin than the API). const configuredApiUrl = import.meta.env.VITE_API_URL if (import.meta.env.PROD && configuredApiUrl == null) { throw new Error('VITE_API_URL is required in production') } const parsedApiUrl = new URL(configuredApiUrl ?? 'http://localhost:3000') const localDevelopment = parsedApiUrl.protocol === 'http:' && (parsedApiUrl.hostname === 'localhost' || parsedApiUrl.hostname === '127.0.0.1' || parsedApiUrl.hostname === '[::1]') if ((parsedApiUrl.protocol !== 'https:' && !localDevelopment) || parsedApiUrl.username !== '' || parsedApiUrl.password !== '' || parsedApiUrl.search !== '' || parsedApiUrl.hash !== '') { throw new Error('VITE_API_URL must be credential-free HTTPS (or exact HTTP localhost development)') } export const API_BASE_URL = parsedApiUrl.href.replace(/\/$/, '') export const API_ORIGIN = parsedApiUrl.origin // The scaffolded network default is concrete and can be overridden per deployment. const configuredNetwork = import.meta.env.VITE_BSV_NETWORK ?? 'test' if (configuredNetwork !== 'main' && configuredNetwork !== 'test' && configuredNetwork !== 'ttn') { throw new Error('VITE_BSV_NETWORK must be main, test, or ttn') } export const BSV_NETWORK = configuredNetwork ``` :::: ## createAuthProof() The primitive under login and signed requests, shared by client and server (`auth.ts`). You rarely call it directly; [`signedFetch`](#usesignedrequest) and [`login`](#usewalletlogin) do. See [Server API → verifyAuthProof](https://createbsvapp.vercel.app/docs/api-server#verifyauthproof) for the other half. ```ts twoslash import type { WalletInterface } from '@bsv/sdk' declare const wallet: WalletInterface declare const serverKey: string // ---cut--- import { createAuthProof } from './bsv/auth' const proof = await createAuthProof(wallet, { counterparty: serverKey, action: 'login' }) ``` - **Signature:** `createAuthProof(wallet, { counterparty: string, action: string, body?: RequestBody }): Promise` - **Returns:** `{ data, signature }`: the signed `{ action, identityKey, expiresAt, nonce }` and the signature bytes. Valid for 2 minutes. --- # Server API > The functions the scaffold writes to server/src/bsv/, the routes it mounts, and exactly what each one verifies, returns and refuses. > 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. These live in `server/src/bsv/` (or `src/bsv/` for the `express` starter). Every verifier is a plain function, so it works in Express, Fastify, Hono or a Next.js route handler. The source of each one is at the end of its section, exactly as create-bsv-app 1.1.2 generates it. ## Routes What a generated full-stack server answers, before you add your own: | Route | From | Request | Response | | --- | --- | --- | --- | | `GET /health` | base | | `{ "status": "ok" }` | | `GET /api/identity` | wallet-connect | | `{ "identityKey": "03…" }` | | `GET /api/session`, `/ws` | wallet-connect | relay protocol | mobile QR pairing (`@bsv/wallet-relay`) | | `POST /api/login` | wallet-login | an `AuthProof` | `{ identityKey }`, or `401 { "error": "invalid proof" }` | | `POST /api/echo` | signed-requests | `{ proof, body }` | `{ valid: true, identityKey }`, or `401` | Requests are parsed with `express.json({ limit: '64kb', strict: true })`, and CORS allows only [`CLIENT_ORIGIN`](#config). ## verifySignedRequest() Verify one signed request. From [`signed-requests`](https://createbsvapp.vercel.app/docs/capabilities#signed-requests). ```ts import { verifySignedRequest } from './bsv/verifySignedRequest.js' import { consumeNonce } from './bsv/nonceStore.js' ``` ### Usage ```ts [server/src/notes.ts] twoslash import type { Request, Response } from 'express' import { verifySignedRequest } from './bsv/verifySignedRequest.js' import { consumeNonce } from './bsv/nonceStore.js' type ServerWallet = Parameters[0] export const createNote = (serverWallet: ServerWallet) => async (req: Request, res: Response) => { const { proof, body } = req.body const result = await verifySignedRequest(serverWallet, proof, { action: 'create-note', body }, consumeNonce) console.log(result) // → { valid: true, identityKey: '02a1f3c9e8b7…' } if (!result.valid) { res.status(401).json({ error: 'invalid proof' }); return } res.json({ by: result.identityKey }) } ``` ### Returns `Promise<{ valid: boolean, identityKey?: string, error?: string }>`. When `valid` is `true`, `identityKey` is the signer, proven. It's the only user ID you need. Then **authorize** it: a valid proof says *who*, not *whether they're allowed*. ### Parameters #### serverWallet {#verifysignedrequest-serverwallet} - **Type:** `{ verifySignature(args): Promise<{ valid: boolean }> }`, in practice a `ProtoWallet` The server's own wallet: `new ProtoWallet(PrivateKey.fromString(SERVER_PRIVATE_KEY))`. Its identity is the proof's counterparty, so a proof made for another server fails here. #### proof {#verifysignedrequest-proof} - **Type:** `AuthProof` Exactly what the client sent. Don't modify it. #### opts.action {#verifysignedrequest-action} - **Type:** `string` Must equal the `action` the client signed. #### opts.body (optional) {#verifysignedrequest-body} - **Type:** `RequestBody` The body the client sent. It's re-serialized to JSON for verification, so verify **before** any middleware rewrites it. #### consumeNonce {#verifysignedrequest-consumenonce} - **Type:** `(nonce: string, expiresAt: Date) => boolean | Promise` Records a proof's nonce and returns `true` only the first time. Use the generated [`consumeNonce`](#consumenonce) for one process, and a [shared store](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances) for more. ### Fails when The signature doesn't match, `action` or `body` differs, the proof is older than 2 minutes (30 s skew allowed), the nonce was used before, or the proof was made for a different server. All of these return `{ valid: false }`; none of them throw. :::: deep Source: verifySignedRequest.ts ```ts [server/src/bsv/verifySignedRequest.ts] // Framework-agnostic verification of a signed request. Works in Express, Next API // routes, Fastify — it's a plain function. Pass your own single-use nonce store. import { verifyAuthProof, type AuthProof, type RequestBody } from './auth.js' export async function verifySignedRequest ( serverWallet: { verifySignature: (args: any) => Promise<{ valid: boolean }> }, proof: AuthProof, opts: { action: string, body?: RequestBody }, consumeNonce: (nonce: string, expiresAt: Date) => boolean | Promise ): Promise<{ valid: boolean, identityKey?: string, error?: string }> { return await verifyAuthProof(serverWallet, proof, { action: opts.action, body: opts.body }, consumeNonce) } ``` :::: ## loginRoute() An Express handler for `POST /api/login`. From [`wallet-login`](https://createbsvapp.vercel.app/docs/capabilities#wallet-login). ```ts twoslash import express from 'express' import { PrivateKey, ProtoWallet } from '@bsv/sdk' const app = express() const serverWallet = new ProtoWallet(PrivateKey.fromRandom()) // ---cut--- import { loginRoute } from './bsv/loginRoute.js' app.post('/api/login', loginRoute(serverWallet)) ``` - **Signature:** `loginRoute(serverWallet): (req, res) => Promise` - **Request body:** the `AuthProof` itself (not wrapped), signed with `action: 'login'`. - **Responds:** `200 { identityKey }`, or `401 { "error": "invalid proof" }`. - **Sessions:** none. Issue your own after a valid login: [here's a tested JWT pattern](https://createbsvapp.vercel.app/docs/capabilities#turning-login-into-a-session). :::: deep Source: loginRoute.ts ```ts [server/src/bsv/loginRoute.ts] // Express login route. Mount: app.post('/api/login', loginRoute(serverWallet)) import type { Request, Response } from 'express' import { verifyAuthProof } from './auth.js' import { consumeNonce } from './nonceStore.js' export function loginRoute (serverWallet: { verifySignature: (args: any) => Promise<{ valid: boolean }> }) { return async (req: Request, res: Response): Promise => { const result = await verifyAuthProof(serverWallet, req.body, { action: 'login' }, consumeNonce) if (!result.valid) { res.status(401).json({ error: 'invalid proof' }); return } res.json({ identityKey: result.identityKey }) } } ``` :::: ## verifyAuthProof() The lower-level check that `verifySignedRequest` and `loginRoute` both call, from the shared `auth.ts`. - **Signature:** `verifyAuthProof(serverWallet, proof, { action: string, body?: RequestBody }, consumeNonce): Promise<{ valid, identityKey?, error? }>` - **Use it when** you want a login-style proof (no body) on a custom route. :::: deep Source: auth.ts (identical on client and server) ```ts [src/bsv/auth.ts] // Shared, framework-agnostic auth-proof helpers built on @bsv/auth (BRC-103). // One primitive: sign a proof bound to { action, body? }, verify it on the server. import { AuthProofClient, AuthProofServer, type AuthProof, type ProofSignerWallet, type RequestBody } from '@bsv/auth' export type { AuthProof, RequestBody } export async function createAuthProof ( wallet: ProofSignerWallet, opts: { counterparty: string, action: string, body?: RequestBody } ): Promise { const client = new AuthProofClient() return await client.createAuthProof({ wallet, counterparty: opts.counterparty, action: opts.action, body: opts.body }) } export async function verifyAuthProof ( serverWallet: { verifySignature: (args: any) => Promise<{ valid: boolean }> }, proof: AuthProof, opts: { action: string, body?: RequestBody }, consumeNonce: (nonce: string, expiresAt: Date) => boolean | Promise ): Promise<{ valid: boolean, identityKey?: string, error?: string }> { const server = new AuthProofServer() return await server.verifyAuthProof({ wallet: serverWallet, proof, action: opts.action, body: opts.body, consumeNonce }) } ``` :::: ## consumeNonce() Single-use replay protection, in memory. ```ts twoslash import { consumeNonce } from './bsv/nonceStore.js' const first = consumeNonce('n0nce', new Date(Date.now() + 60_000)) const again = consumeNonce('n0nce', new Date(Date.now() + 60_000)) console.log(first, again) // → true false ``` - **Signature:** `consumeNonce(nonce: string, expiresAt: Date): boolean` - **Returns `false`** for a repeat, an already-expired proof, or when the store holds 10,000 live nonces. - **Limits:** one process only, emptied on restart. [Replace it](https://createbsvapp.vercel.app/docs/security#replay-protection-across-instances) before you run more than one instance. :::: deep Source: nonceStore.ts ```ts [server/src/bsv/nonceStore.ts] // Bounded in-memory replay protection for the generated development server. // Replace this with an atomic Redis/DB implementation before horizontally scaling. const usedNonces = new Map() const MAX_NONCES = 10_000 function pruneExpired (now: number): void { for (const [nonce, expiresAt] of usedNonces) { if (expiresAt <= now) usedNonces.delete(nonce) } } export function consumeNonce (nonce: string, expiresAt: Date): boolean { const now = Date.now() pruneExpired(now) if (expiresAt.getTime() <= now || usedNonces.has(nonce) || usedNonces.size >= MAX_NONCES) return false usedNonces.set(nonce, expiresAt.getTime()) return true } ``` :::: ## Config {#config} `server/src/bsv/config.ts` reads and validates the environment once, at startup: | Export | From | Default (dev) | Production | | --- | --- | --- | --- | | `SERVER_PRIVATE_KEY` | `SERVER_PRIVATE_KEY` | random per start | required, canonical hex | | `PORT` | `PORT` | `3000` | optional | | `CLIENT_ORIGIN` | `CLIENT_ORIGIN` | `http://localhost:5173` | required, `https://` origin | | `BSV_NETWORK` | `BSV_NETWORK` | `'test'` | optional | "Production" means `NODE_ENV=production`. Every failure throws at startup with the message listed in [Errors](https://createbsvapp.vercel.app/docs/errors#server-config). :::: deep Source: config.ts ```ts [server/src/bsv/config.ts] // Centralized server configuration, read from the environment. import { PrivateKey } from '@bsv/sdk' // Server wallet key. Set SERVER_PRIVATE_KEY for a stable identity; a random key is // used as a dev fallback (the server's identity then changes on every restart). const configuredServerKey = process.env.SERVER_PRIVATE_KEY if (process.env.NODE_ENV === 'production' && configuredServerKey == null) { throw new Error('SERVER_PRIVATE_KEY is required in production') } const parsedServerKey = configuredServerKey == null ? PrivateKey.fromRandom() : PrivateKey.fromString(configuredServerKey) if (configuredServerKey != null && parsedServerKey.toString() !== configuredServerKey) { throw new Error('SERVER_PRIVATE_KEY must use its canonical encoding') } export const SERVER_PRIVATE_KEY = parsedServerKey.toString() const portText = process.env.PORT ?? '3000' if (!/^(?:[1-9]\d{0,4})$/.test(portText)) throw new Error('PORT must be an integer from 1 to 65535') export const PORT = Number(portText) if (PORT > 65535) throw new Error('PORT must be an integer from 1 to 65535') // Browser origin allowed by CORS — your client's dev URL by default. CORS is not auth. const configuredClientOrigin = process.env.CLIENT_ORIGIN if (process.env.NODE_ENV === 'production' && configuredClientOrigin == null) { throw new Error('CLIENT_ORIGIN is required in production') } const parsedClientOrigin = new URL(configuredClientOrigin ?? 'http://localhost:5173') const localClient = parsedClientOrigin.protocol === 'http:' && (parsedClientOrigin.hostname === 'localhost' || parsedClientOrigin.hostname === '127.0.0.1' || parsedClientOrigin.hostname === '[::1]') if ((parsedClientOrigin.protocol !== 'https:' && !localClient) || parsedClientOrigin.username !== '' || parsedClientOrigin.password !== '' || parsedClientOrigin.pathname !== '/' || parsedClientOrigin.search !== '' || parsedClientOrigin.hash !== '') { throw new Error('CLIENT_ORIGIN must be credential-free HTTPS (or exact HTTP localhost development) origin') } export const CLIENT_ORIGIN = parsedClientOrigin.origin const configuredNetwork = process.env.BSV_NETWORK ?? 'test' if (configuredNetwork !== 'main' && configuredNetwork !== 'test' && configuredNetwork !== 'ttn') { throw new Error('BSV_NETWORK must be main, test, or ttn') } export const BSV_NETWORK = configuredNetwork ``` :::: --- # Errors > Every error the CLI and the generated code can throw, grouped by where it comes from, with what it means and what to do. > 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. The generated helpers throw plain `Error`s with a fixed message. There are no custom error classes, so match on the message. For step-by-step fixes, see [Troubleshooting](https://createbsvapp.vercel.app/docs/troubleshooting). ```ts twoslash import { useWalletLogin } from './bsv/useWalletLogin' declare const login: ReturnType['login'] // ---cut--- try { await login() } catch (err) { const message = err instanceof Error ? err.message : String(err) if (message.startsWith('login failed:')) { // the server answered 401: sign again, or check the server's identity } else if (message.startsWith('API ')) { // network or response limits: see the apiClient.ts table below } else throw err } ``` ## CLI {#cli} Config problems are prefixed `Invalid config:` and exit with code `1`. | Message | Meaning | | --- | --- | | `a new project needs at least a frontend or a backend` | `custom` starter with no stack. Pick `full-stack`, or pass `--frontend` and/or `--backend` | | `target directory is not empty: …` | `new` mode only writes into an empty folder. Use another folder or [add mode](https://createbsvapp.vercel.app/docs/add-to-existing) | | `unknown starter: …` / `unknown capability: …` | typo in an id. See [Starters](https://createbsvapp.vercel.app/docs/starters) and [Capabilities](https://createbsvapp.vercel.app/docs/capabilities) | | `starter … is a complete example and does not accept generated capabilities` | drop `--capabilities` for complete examples | | `cannot infer separate client/server targets from a single package containing both react and express; use --file with explicit targets` | [use `--file` with `targets`](https://createbsvapp.vercel.app/docs/add-to-existing#how-the-cli-finds-your-app) | | `name is required` | a `--file` config needs `"name"` | | `--network must be main, test, or ttn` (and the same for `--mode`, `--frontend`, `--backend`, `--package-manager`) | invalid flag value | | `… requires a value` / `unknown option: …` / `unexpected argument: …` | flag syntax. `--help` lists every flag | | `config file not found: …` / `cannot read config file: …` / `invalid JSON in …` | the `--file` path or contents | | `targets.client must be a safe relative path` / `invalid bsvDir: …` | no absolute paths, no `..` | | `command failed (…): …` | a step such as `git clone` or `npm install` failed. Its own output is just above | ## apiClient.ts {#apiclientts} Thrown by `apiFetch` and `readApiJson` in the browser. | Message | Meaning | | --- | --- | | `API endpoint must be a safe absolute path` | the path has a query string, `..`, or characters outside `A-Z a-z 0-9 / _ -` | | `API request body must be a string` | pass `JSON.stringify(...)` | | `API request exceeds the byte limit` | request body over 1 MiB | | `API response exceeds the byte limit` | response over 1 MiB. Paginate | | `API response has an invalid Content-Length` / `API response length does not match Content-Length` | a broken proxy or server | | `API response changed network authority` | the response came from another origin (redirects are refused) | | `API response is not valid UTF-8` / `API response is not valid JSON` | `readApiJson` couldn't parse it | | `AbortError` | no complete response within 10 seconds | ## Identity and login {#identity} | Message | From | Meaning | | --- | --- | --- | | `failed to fetch server identity: ` | `getServerIdentity` | `GET /api/identity` didn't return 2xx. Is the server up? | | `server returned an invalid identity response` | `serverIdentity.ts` | the response wasn't exactly `{ identityKey }` | | `server returned an invalid identity key` | `serverIdentity.ts` | not a valid compressed public key | | `connect a wallet first (initializeWallet / relay)` | `useWalletLogin` | `login()` called with no wallet | | `connect a wallet first` | `useSignedRequest` | `signedFetch()` called with no wallet | | `login failed: ` | `useWalletLogin` | the server rejected the proof | | `server returned another wallet identity` | login | the verified key isn't the connected wallet's | | `No authenticated desktop wallet found` | `walletAcquisition.ts` | caught internally; the UI moves to *No desktop wallet found* | | `useWallet must be used within WalletProvider` | `WalletContext.tsx` | wrap your app in `` | | `useWalletConnection must be used within WalletConnectionProvider` | `WalletConnectionContext.tsx` | same | ## Server responses {#server-responses} | Status | Body | Meaning | | --- | --- | --- | | `401` | `{ "error": "invalid proof" }` | the proof failed: [why](https://createbsvapp.vercel.app/docs/api-server#fails-when) | | `400` | from `express.json` | malformed JSON, or a body over 64 kB | ## Client config {#client-config} Thrown when the client bundle loads, which shows as a blank page. - `VITE_API_URL is required in production` - `VITE_API_URL must be credential-free HTTPS (or exact HTTP localhost development)` - `VITE_BSV_NETWORK must be main, test, or ttn` ## Server config {#server-config} Thrown at startup. - `SERVER_PRIVATE_KEY is required in production` - `SERVER_PRIVATE_KEY must use its canonical encoding` - `CLIENT_ORIGIN is required in production` - `CLIENT_ORIGIN must be credential-free HTTPS (or exact HTTP localhost development) origin` - `PORT must be an integer from 1 to 65535` - `BSV_NETWORK must be main, test, or ttn` - `JWT_SECRET must contain at least 32 bytes` (only if you add the [session pattern](https://createbsvapp.vercel.app/docs/capabilities#turning-login-into-a-session)) --- # Compatibility > What create-bsv-app and the apps it generates run on, and what these docs have actually been tested against. > 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. "Required" comes from the CLI's own checks. "Tested" means the commands and code in these docs were run there. Everything else is supported by design but not tested for these docs. Reports welcome on [GitHub](https://github.com/bsv-blockchain/ts-stack/issues). ## The CLI | Item | Status | | --- | --- | | Node.js | **22 or newer required** (`engines`). Tested on 23.4 | | macOS | tested | | Linux, Windows | supported (the root runner switches to a shell on Windows); not tested here | | npm | tested | | pnpm, yarn, bun | supported with `--package-manager`; not tested here | | git | required for [complete examples](https://createbsvapp.vercel.app/docs/starters#complete-examples) only | ## Generated apps | Package | Version | Notes | | --- | --- | --- | | React | 19 | via create-vite, template `react-ts` | | Vite | 8 | client dev server and build | | Express | 5 | server, run with `tsx` in dev, `node` in production | | TypeScript | 6 | both packages | | `@bsv/sdk` | 2.x | wallet interface, keys, transactions | | `@bsv/auth` | 0.1.x | proofs | | `@bsv/wallet-relay` | 0.2.x | mobile pairing | ## Wallets and browsers | Item | Status | | --- | --- | | Any BRC-100 wallet | supported | | [BSV Browser](https://browser.bsvb.tech/) | recommended; desktop (macOS, Windows) connects directly, phone (iOS, Android) pairs by QR | | Browsers | current Chrome, Edge, Firefox and Safari (Vite's default build target) | ## Hosting | Part | Needs | | --- | --- | | Client | any static host, with a single-page fallback to `index.html` | | Server | a long-running Node 22+ process with WebSocket support (for `/ws`) | | Networks | `main`, `test` (default) and `ttn` | See [Deploy](https://createbsvapp.vercel.app/docs/deploy) for the details.