# Reference

---

# wispist.json reference

_Wispist / Reference_

> The complete v1 release declaration schema, collection-name grammar, access profiles, operation policies, and collection-specific limits.

`wispist.json` is an optional file at the site archive root. It declares the collections exposed to browser code by that release.

The parser is strict:

- The file must be UTF-8 JSON no larger than 64 KiB.
- Duplicate object members and unknown fields are errors.
- `version` must be `1`.
- `collections` is required and may contain at most 32 entries.
- Collection limits may lower, but never raise, host defaults.
- Missing `wispist.json` means zero declared collections.

An invalid declaration rejects the complete upload. It never falls back to a permissive policy.

## Top-level object

| Member | Type | Required | Meaning |
| --- | --- | --- | --- |
| `version` | integer | yes | Protocol declaration version; exactly `1`. |
| `collections` | object | yes | Collection name to collection declaration. |

## Collection names

Names match:

```text
[a-z][a-z0-9_-]{0,47}
```

They are ASCII, case-sensitive, and compared byte-for-byte.

## Collection declaration

| Member | Type | Required | Meaning |
| --- | --- | --- | --- |
| `access` | string or object | yes | A built-in profile or all six operation policies. |
| `limits` | object | no | Lower document count and byte limits. |

### Shared profile

```json
{
  "access": "shared"
}
```

`shared` expands to `anyone` for `list`, `read`, `create`, `update`, `delete`, and `subscribe`. Every visitor can read and change the collection.

### Expanded access

```json
{
  "access": {
    "list": "anyone",
    "read": "anyone",
    "create": "nobody",
    "update": "nobody",
    "delete": "nobody",
    "subscribe": "anyone"
  }
}
```

Every operation must appear exactly once:

| Operation | Controls |
| --- | --- |
| `list` | Listing collection documents. |
| `read` | Reading one document and receiving its body in a subscription. |
| `create` | Generated-ID POST and selected-ID create. |
| `update` | Replacing an existing document. |
| `delete` | Deleting an existing document. |
| `subscribe` | Opening a change stream for the collection. |

Each value is one of:

| Value | Meaning |
| --- | --- |
| `anyone` | Permit every principal. |
| `authenticated` | Require an authenticated principal asserted by the host. |
| `nobody` | Deny the operation. |

Wispdeck initially supplies anonymous principals to ordinary hosted-site and preview JavaScript. Preview authorisation grants access to the preview, not authenticated Wispist authority.

`read` and `list` are intentionally independent, as are `read` and `subscribe`.

### Collection limits

```json
{
  "limits": {
    "maxDocuments": 250,
    "maxDocumentBytes": 4096
  }
}
```

| Member | Type | Range | Default |
| --- | --- | --- | --- |
| `maxDocuments` | integer | 1 to host maximum | 1,000 |
| `maxDocumentBytes` | integer | 2 to host maximum | 32 KiB |

These limits apply to that collection. The namespace also has a total live or draft byte limit.

## See also

- [Limits and abuse controls](/wispist/reference/limits.md) — the installation defaults these declarations may lower.
- [Documents and collections](/wispist/using-wispist/documents-and-collections.md) — what a declared collection contains.
- [Forbidden](/wispist/problems/forbidden.md) — what a denied operation returns.

---

# Browser API reference

_Wispist / Reference_

> Complete reference for the automatic global Wispist client, collection CRUD methods, subscriptions, environment fields, and WispistError.

Wispdeck defines one non-enumerable, non-writable global before site-authored elements run:

```text
wispist.version
wispist.mode
wispist.readOnly
wispist.collection(name)
```

There is no connect method. The client makes no request during evaluation.

## Environment fields

| Field | Type | Meaning |
| --- | --- | --- |
| `version` | number | Client protocol major; `1`. |
| `mode` | string | `live`, `draft`, or `live-preview`. |
| `readOnly` | boolean | Whether the host has forced mutations off. |

`mode` and `readOnly` are synchronous presentation metadata. The server enforces the real binding independently.

## Collection handle

```js
const items = wispist.collection("before-you-go");
```

`collection(name)` validates the collection-name grammar locally and returns a frozen handle. The server still requires the active release to declare it.

### `list(options?)`

Returns a promise for every document in creation order. `options.limit` selects the page size from 1 to 250; the client follows pagination automatically.

```js
const documents = await items.list({ limit: 100 });
```

### `get(id)`

Returns one complete document envelope or rejects with `not_found`.

```js
const document = await items.get("passport");
```

### `add(data)`

Creates a generated-ID document and returns it. `data` must be a non-null object and not an array. The client generates the required idempotency key.

```js
const document = await items.add({ text: "Passport", done: false });
```

### `create(id, data)`

Creates exactly the selected ID and returns it. It uses create-only semantics and rejects with `revision_conflict` if that ID exists.

### `replace(document, data)`

Conditionally replaces the observed document with `data` and returns the new envelope. A concurrent change rejects with `revision_conflict`.

### `update(document, changes)`

Shallow-merges `changes` into `document.data` locally, then performs `replace`.

```js
const updated = await items.update(document, { done: true });
```

An `undefined` change is omitted by JSON serialisation but does not delete the old property from the local merge. To remove properties or make nested changes, construct the complete new object and call `replace`.

### `delete(document)`

Conditionally deletes the observed document. The promise resolves with no value.

## Subscribe

```js
const unsubscribe = items.subscribe((documents, event) => {
  render(documents);
}, {
  limit: 250,
  onError(error) {
    showError(error);
  },
});
```

The callback runs once with `event.type === "initial"` and after each applied change. Documents remain in creation order, with ID as the deterministic tie-breaker.

The client reconnects after recoverable EventSource failures with bounded exponential backoff and jitter. A server `reset` causes a fresh list. `onError` receives initial or terminal errors; ordinary reconnects do not create unhandled promise rejections.

`subscribe` returns the unsubscribe function immediately.

## `WispistError`

Rejected operations use an Error subclass with:

| Field | Meaning |
| --- | --- |
| `name` | Always `WispistError`. |
| `type` | RFC 9457 problem type URI, when an HTTP problem was returned. |
| `code` | Ergonomic code mapped from a known problem type. |
| `title` | Stable problem title. |
| `detail` | Safe occurrence-specific explanation. |
| `status` | HTTP status, when available. |
| `instance` | Problem occurrence URI, when available. |
| `requestId` | `X-Request-ID` value, when available. |
| `problem` | Complete decoded Problem Details object. |

The wire protocol does not contain a separate code. `type` is the stable machine identifier; `code` is client convenience. Network failures use `network_error` without a type or HTTP status. An invalid non-problem response uses `invalid_response`.

Known codes are listed in [Problem Details](/wispist/problems/overview.md).

## Browser dependencies and side effects

The client uses `fetch`, `EventSource`, and Web Crypto. It has no package dependency, local-storage use, service worker, analytics, or telemetry. Requests are same-origin, use same-origin credentials, and disable browser caching.

## See also

- [HTTP API reference](/wispist/reference/http-api.md) — the routes these methods call.
- [Problem Details](/wispist/problems/overview.md) — every `code` a `WispistError` can carry.
- [Live updates](/wispist/using-wispist/live-updates.md) — how `subscribe` behaves across disconnects and resets.

---

# HTTP API reference

_Wispist / Reference_

> Wispist v1 routes, JSON envelopes, conditional requests, idempotency, pagination, change streams, headers, and same-origin requirements.

Wispist v1 is served from the hosted site's own origin under `/_wispist/`:

```text
GET    /_wispist/client/v1.js
GET    /_wispist/v1
GET    /_wispist/v1/collections/{collection}/documents
POST   /_wispist/v1/collections/{collection}/documents
GET    /_wispist/v1/collections/{collection}/documents/{document}
PUT    /_wispist/v1/collections/{collection}/documents/{document}
DELETE /_wispist/v1/collections/{collection}/documents/{document}
GET    /_wispist/v1/changes
```

Use the browser client unless direct HTTP control is necessary.

## General rules

- API responses use `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
- JSON responses use `application/json; charset=utf-8`.
- JSON request bodies require `Content-Type: application/json`.
- Every response includes `X-Request-ID`. A caller-provided UUID may be retained.
- Errors use RFC 9457 `application/problem+json`.
- Unsupported methods on known resources return `405` with `Allow`.
- Paths and identifiers must be decoded, canonical, and free of encoded separators or dot segments.
- No CORS permission is emitted.

Unsafe requests require one exact same-origin `Origin` header. If `Sec-Fetch-Site` is present, it must be `same-origin`.

## Service description

```http
GET /_wispist/v1
```

```json
{
  "name": "Wispist",
  "version": 1,
  "mode": "live",
  "readOnly": false,
  "collections": ["before-you-go"]
}
```

## List documents

```http
GET /_wispist/v1/collections/before-you-go/documents?limit=100&after=opaque
```

`limit` defaults to 100 and may be 1 to 250. `after` is an opaque pagination cursor.

```json
{
  "documents": [
    {
      "id": "passport",
      "revision": "opaque",
      "createdAt": "2026-07-13T12:34:56.123Z",
      "updatedAt": "2026-07-13T12:34:56.123Z",
      "data": { "text": "Pack passport", "done": false }
    }
  ],
  "after": null,
  "changes": "opaque-change-cursor"
}
```

The `changes` cursor is captured by the first page and repeated by later pages. List all pages, then consume changes after it to reconcile concurrent mutations.

## Create a generated ID

```http
POST /_wispist/v1/collections/before-you-go/documents
Origin: https://site.example
Idempotency-Key: a-strong-random-value
Content-Type: application/json

{"data":{"text":"Buy insurance","done":false}}
```

The key is required and contains 16 to 128 visible ASCII characters. The same namespace, key, and request fingerprint returns the original `201 Created` response for at least 24 hours. Reusing the key for a different request returns [idempotency conflict](/wispist/problems/idempotency-conflict.md).

The response includes `Location`, the document ETag, and the complete envelope.

## Read

```http
GET /_wispist/v1/collections/before-you-go/documents/passport
```

The ETag is the quoted opaque revision. `If-None-Match` supports wildcard, lists, and weak comparison for GET/HEAD and may return `304 Not Modified`.

## Create or replace a selected ID

Create only:

```http
PUT /_wispist/v1/collections/before-you-go/documents/passport
Origin: https://site.example
If-None-Match: *
Content-Type: application/json

{"data":{"text":"Pack passport","done":false}}
```

Replace:

```http
PUT /_wispist/v1/collections/before-you-go/documents/passport
Origin: https://site.example
If-Match: "observed-revision"
Content-Type: application/json

{"data":{"text":"Pack passport","done":true}}
```

Exactly one precondition is required. Create returns `201`; replacement returns `200`. Both return a new document and ETag.

## Delete

```http
DELETE /_wispist/v1/collections/before-you-go/documents/passport
Origin: https://site.example
If-Match: "observed-revision"
```

Success is `204 No Content`. The deletion appends a change containing ID and deleted revision but no document body.

## Change stream

```http
GET /_wispist/v1/changes?collections=before-you-go&after=opaque
Accept: text/event-stream
```

`collections` is repeatable with 1 to 8 distinct declared values. Every collection must permit `subscribe` and `read`.

```text
id: opaque-change-cursor
event: change
data: {"collection":"before-you-go","operation":"update","document":{...}}

```

A deletion supplies `id` and `revision` instead of `document`. A `reset` event means the client must list again. If `after` is absent, the stream begins after the current high-water mark and does not replay the collection.

The server sends heartbeat comments while otherwise idle. Delivery is at least once; de-duplicate by cursor.

## See also

- [Browser API reference](/wispist/reference/browser-api.md) — the supported client over these routes.
- [Problem Details](/wispist/problems/overview.md) — the error shape every route uses.
- [Limits and abuse controls](/wispist/reference/limits.md) — body, pagination, stream, and rate bounds.

---

# Limits and abuse controls

_Wispist / Reference_

> Wispist v1 storage, document, request, pagination, subscription, change-retention, idempotency, and rate-limit defaults.

Wispist applies hard resource limits and bounded rate buckets before expensive work. A release may lower collection limits but cannot raise installation policy.

## Resource defaults

| Resource | Default |
| --- | ---: |
| Collections per release | 32 |
| Documents per collection | 1,000 |
| Document JSON | 32 KiB |
| Live document data per site | 10 MiB |
| Draft document data per site | 5 MiB |
| List page | 100; maximum 250 |
| Collections per SSE stream | 8 |
| SSE streams per site and client | 6 |
| SSE streams per site | 100 |
| Pending events per stream | 128 |
| Request envelope allowance | document limit plus 4 KiB |
| Live idempotency records per namespace | 10,000 |
| Retained changes per namespace | 10,000 |
| Change age | 7 days |
| Idempotency retention | at least 24 hours |

The document-data quota counts compact document JSON, not release files. Change and idempotency tables are separately bounded.

## Initial Wispdeck rate buckets

| Bucket | Sustained | Burst |
| --- | ---: | ---: |
| Reads per site and client | 600/minute | 100 |
| Mutations per site and client | 60/minute | 20 |
| Mutations across one site | 300/minute | 60 |
| Generated-ID creates across one site | 10,000/day | 100 |
| All Wispist requests | 6,000/minute | 1,000 |

Client keys come from Wispdeck's trusted-proxy-aware address resolution. Untrusted forwarding headers are ignored.

Buckets use bounded memory and expire after inactivity. Concurrent streams use counters rather than token rates. A rejected request returns [rate limited](/wispist/problems/rate-limited.md) with `Retry-After`.

Rate limits are availability controls, not authorisation. A public `shared` collection remains publicly writable below the rate allowance.

## See also

- [wispist.json reference](/wispist/reference/wispist-json.md) — how a release lowers collection limits.
- [Quota exceeded](/wispist/problems/quota-exceeded.md), [Request too large](/wispist/problems/request-too-large.md), [Rate limited](/wispist/problems/rate-limited.md) — the problems these bounds produce.
