# 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<br>`<ConnectWallet />` button and fallback dialog<br>`apiClient.ts`: bounded, redirect-free fetch<br>`getServerIdentity()` | `GET /api/identity`<br>`WalletRelayService`: `/api/session` and `/ws` for mobile QR pairing |
| `wallet-login` | `wallet-connect` | `/login` page<br>`useWalletLogin()` hook | `POST /api/login` via `loginRoute(serverWallet)` |
| `signed-requests` | `wallet-connect` | `/signed-demo` page<br>`useSignedRequest()` and `signedFetch()` | `POST /api/echo`<br>`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<void>` | try the desktop wallet; on failure go to `choosing` |
| `connectMobile()` | `() => Promise<void>` | 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
```

`<ConnectWallet />` 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<typeof verifyAuthProof>[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<void> => {
    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 <token>`; exposes res.locals.identityKey. */
export async function requireSession (req: Request, res: Response, next: NextFunction): Promise<void> {
  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<void> {
  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<Response> {
  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).
