# 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<typeof verifySignedRequest>[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<boolean>`

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<boolean>
): 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<void>`
- **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<void> => {
    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<AuthProof> {
  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<boolean>
): 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<string, number>()
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
```
::::
