Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Wispist

Wispist

Persistent JSON documents and live updates for simple browser sites — no connection step, no application server.

Single-page view · as markdown

What is Wispist

Wispist / Getting started

Wispist is a lightweight data engine for simple websites. It gives ordinary HTML and JavaScript a persistent document store, safe concurrent mutations, and live updates while keeping the site itself static.

On a Wispdeck-hosted site, Wispist is already there:

const checklist = wispist.collection("before-you-go");

const item = await checklist.add({
  text: "Buy travel insurance",
  done: false,
});

await checklist.update(item, { done: true });

There is no connection call, project identifier, endpoint, API key, or secret in that code. Wispdeck identifies the site and its active release from the request, then supplies an opaque namespace and policy to the embedded Wispist Engine. The same-origin browser client cannot choose another site's store.

Static code, persistent data #

A Wispist site is still a static bundle:

site.zip
├── index.html
├── app.js
├── styles.css
└── wispist.json

wispist.json declares which collections the release may use and who may perform each operation. Wispdeck validates that declaration when the bundle is uploaded, and inserts a small parser-blocking client before site-authored elements in every served HTML head, so globalThis.wispist exists before later site scripts run.

The site's documents live in SQLite, outside the immutable release. Republishing code therefore keeps production data. An idle site has no dedicated process, timer, poller, goroutine, or database connection; work happens only while a viewer or mutation is active.

The data model #

Wispist v1 has three main concepts:

  • A namespace is the host-selected isolation boundary. Wispdeck uses separate live and draft namespaces for each site.
  • A collection is a declared, ordered set of documents, such as before-you-go.
  • A document contains an opaque ID, an opaque revision, server timestamps, and a caller-owned JSON object in data.

Every replacement and deletion is conditional on an observed revision. If two browsers edit the same document concurrently, one succeeds and the other receives a revision conflict rather than silently overwriting work it did not see.

Subscriptions use Server-Sent Events. The client first lists a consistent collection view, then consumes retained changes after that list cursor. It reconnects after ordinary network interruptions and relists if the retained history is no longer sufficient.

Policy is public, enforcement is not #

The release declaration contains policy, not credentials. The concise shared profile deliberately allows every visitor to list, read, create, update, delete, and subscribe:

{
  "version": 1,
  "collections": {
    "before-you-go": {
      "access": "shared"
    }
  }
}

That is suitable for a genuinely public collaborative checklist. It is not user authorisation. Exact-origin checks, quotas, and request limits reduce cross-site and automated abuse, but any visitor can still change a shared collection.

Expanded declarations can set every operation to anyone, authenticated, or nobody. Wispdeck's first integration supplies anonymous principals to hosted site code, so authenticated is reserved for future identity integration rather than a way to reuse the dashboard cookie.

Draft and live state #

Wispdeck previews use a fresh private origin. Draft code reads and writes the site's draft namespace. The Current selector on that preview origin reads the live namespace with an unconditional server-side read-only override.

Publishing changes which release supplies public code and policy. It does not promote draft documents, clear live documents, or create a new production store. This prevents test data from silently replacing real data.

What v1 deliberately leaves out #

Wispist v1 is document CRUD plus live collection updates. It does not expose SQL, run user functions, execute scheduled jobs, perform joins, provide offline conflict resolution, accept file uploads, or offer multi-document transactions. A data directory supports one active process.

The small surface is intentional: Wispist is for the state that turns a static page into a useful little shared application, not a general application server.

Where to go next #

To build the checklist from the snippet above end to end, follow Build a shared checklist.

For the data model in full — namespaces, envelopes, revisions, conditional mutation — read Documents and collections.

For every field the declaration file accepts, read the wispist.json reference.

Build a shared checklist

Wispist / Getting started

This page builds the smallest useful Wispist site: a checklist that persists and updates in every open browser. The site has no build step and no connection configuration.

Declare the collection #

Create wispist.json at the root of the site bundle:

{
  "version": 1,
  "collections": {
    "before-you-go": {
      "access": "shared",
      "limits": {
        "maxDocuments": 250,
        "maxDocumentBytes": 4096
      }
    }
  }
}

The shared profile lets every viewer read and change the collection. Use it only when that is the intended product behaviour. The two limits lower Wispist's installation defaults for this collection.

Write the page #

Create index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Before you go</title>
  <script src="/app.js" defer></script>
</head>
<body>
  <main>
    <h1>Before you go</h1>

    <form id="new-item">
      <label>
        New item
        <input name="text" maxlength="180" required>
      </label>
      <button>Add</button>
    </form>

    <p id="status" aria-live="polite">Loading…</p>
    <ul id="items"></ul>
  </main>
</body>
</html>

Do not add the Wispist client script yourself on Wispdeck. Wispdeck inserts it before the page's own script element, so wispist exists when app.js runs.

Add and render documents #

Create app.js:

const checklist = wispist.collection("before-you-go");
const form = document.querySelector("#new-item");
const list = document.querySelector("#items");
const status = document.querySelector("#status");

let documents = new Map();

function render(items) {
  documents = new Map(items.map((item) => [item.id, item]));
  list.replaceChildren();

  for (const item of items) {
    const row = document.createElement("li");
    const checkbox = document.createElement("input");
    const text = document.createElement("span");

    checkbox.type = "checkbox";
    checkbox.checked = Boolean(item.data.done);
    checkbox.dataset.id = item.id;
    text.textContent = String(item.data.text);
    row.append(checkbox, text);
    list.append(row);
  }

  status.textContent = `${items.length} shared item(s)`;
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const input = new FormData(form).get("text").trim();
  if (!input) return;

  try {
    await checklist.add({ text: input, done: false });
    form.reset();
  } catch (error) {
    status.textContent = error.detail || "The item could not be added.";
  }
});

add creates a document with a generated ID and returns the complete document envelope. The client supplies the required idempotency key automatically.

Apply safe updates #

Add a delegated change listener:

list.addEventListener("change", async (event) => {
  const checkbox = event.target.closest('input[type="checkbox"][data-id]');
  const item = checkbox && documents.get(checkbox.dataset.id);
  if (!item) return;

  checkbox.disabled = true;
  try {
    await checklist.update(item, { done: checkbox.checked });
  } catch (error) {
    checkbox.checked = Boolean(item.data.done);
    status.textContent = error.code === "revision_conflict"
      ? "Somebody else changed that item. Try again from the latest version."
      : error.detail || "The item could not be changed.";
  } finally {
    checkbox.disabled = false;
  }
});

update accepts the observed document, not only its ID. It shallow-merges the new fields locally and sends a replacement conditional on item.revision. A stale revision becomes a typed revision conflict.

Subscribe #

Start the live collection at the end of app.js:

const unsubscribe = checklist.subscribe(render, {
  onError(error) {
    status.textContent = error.detail || "Live updates are unavailable.";
  },
});

subscribe returns immediately. It lists every page, opens a change stream from that list cursor, maintains a document map, and calls render with the initial state and every later state.

Call unsubscribe() when a long-lived page no longer needs the collection. Closing a page closes its EventSource automatically.

Upload and test #

Put the three files at the ZIP root:

checklist.zip
├── index.html
├── app.js
└── wispist.json

Upload the archive as a Wispdeck site draft. In preview, the checklist uses draft data. Publish the release, open the public site in two independent browser windows, and add or check an item in one. The other should update without a refresh.

When a later draft exists, changes made in Draft remain separate. Select Current to see live data in read-only mode. Publishing that draft changes the code and declaration while leaving the public checklist intact.

Where to go next #

  • Documents and collections — the envelope, revision, and conditional-mutation model behind update.
  • Live updates — what subscribe does underneath: snapshot, cursor, Server-Sent Events, reconnect, reset.
  • wispist.json reference — every declaration field, profile, and limit.

Documents and collections

Wispist / Using Wispist

Wispist stores JSON objects as revisioned documents inside declared collections. The host supplies the namespace; site code selects only a collection and, where needed, a document ID.

Namespaces are the isolation boundary #

A namespace is opaque to Wispist. It cannot be selected through the browser API and is never inferred from a collection name. Wispdeck binds every request to a stable site store and either its live or draft namespace.

The same collection name in two sites, or in one site's live and draft views, addresses different data. Republishing does not change the site's stable store key.

Collections must be declared #

A collection exists for browser access only when the active release declares it in wispist.json. Removing the declaration hides stored documents but does not delete them. Re-adding it makes them accessible again.

Names are 1 to 48 ASCII bytes, begin with a lowercase letter, and continue with lowercase letters, digits, hyphens, or underscores:

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

Collection order is document creation order, with ID as the deterministic tie-breaker.

A document has server and caller fields #

{
  "id": "passport",
  "revision": "opaque-revision-token",
  "createdAt": "2026-07-13T12:34:56.123Z",
  "updatedAt": "2026-07-13T12:35:04.456Z",
  "data": {
    "text": "Pack passport",
    "done": false
  }
}

Wispist owns id, revision, createdAt, and updatedAt. The caller owns data, whose root must be a JSON object. Nested arrays, objects, strings, numbers, booleans, and null are allowed.

JSON is strict. Invalid UTF-8, duplicate member names, more than 32 nesting levels, keys longer than 256 UTF-8 bytes, and oversized representations are rejected. Wispist stores compact JSON but does not give object-member order any meaning.

IDs may be caller-selected or generated. They contain 1 to 64 ASCII letters, digits, underscores, or hyphens and are case-sensitive. . and .. are not valid IDs.

Revisions prevent silent overwrite #

A revision identifies one incarnation of one document. Every successful create or replacement produces a new opaque value, and deleting then recreating an ID does not reuse an old revision.

The easiest safe operations are the browser client's document-envelope methods:

const observed = await checklist.get("passport");
const changed = await checklist.update(observed, { done: true });
await checklist.delete(changed);

update and delete carry the observed revision. If another caller changed the document first, Wispist returns revision_conflict. It never falls back to last-write-wins.

Creating a deterministic ID uses create-only semantics:

await checklist.create("passport", {
  text: "Pack passport",
  done: false,
});

That operation fails with a revision conflict if passport already exists.

One document per transaction #

Each create, replacement, or deletion commits the document state and one ordered change record in the same SQLite transaction. A subscriber can therefore never observe a change whose document mutation failed to commit.

V1 does not provide multi-document transactions. Model one atomic unit as one document when several fields must change together.

Where to go next #

For the client methods that carry these semantics — get, create, replace, update, delete — read the Browser API reference.

For what happens when a conditional mutation loses a race, read Revision conflict.

For how draft and live namespaces relate across releases, read Drafts and publishing.

Live updates

Wispist / Using Wispist

collection.subscribe(callback) keeps one collection view current without polling. It combines an initial paginated list with a same-origin Server-Sent Events stream.

Snapshot, then changes #

Opening a subscription does four things:

  1. Lists every page of the collection.
  2. Records the change cursor captured at the first page.
  3. Opens /_wispist/v1/changes after that cursor.
  4. Applies create, update, and delete events to an in-memory map.

Pagination is not a frozen SQLite snapshot across requests. The cursor is what closes the gap: mutations that race the list appear in the retained change stream after the first page's watermark.

The callback receives the complete ordered document array and an event:

const stop = checklist.subscribe((documents, event) => {
  render(documents);
  if (event.type === "initial") {
    console.log("Initial state is ready");
  }
}, {
  onError(error) {
    showStatus(error.detail);
  },
});

Call stop() to close the stream and cancel reconnect work.

Delivery and reconnection #

Wispist commits a mutation before publishing it to in-memory listeners. On connection, the server registers the listener before reading retained changes, sends the backlog, removes duplicate queued events by cursor, and then continues live.

Delivery is at least once. Cursors are opaque and the client de-duplicates them. Ordinary disconnects reconnect with bounded exponential backoff and jitter.

Change history is finite. If a cursor is older than retained history, or a subscriber falls behind its bounded event queue, the server sends a reset event and closes. The browser client relists automatically rather than risking silent divergence.

Active compute only #

An open EventSource is active work. Wispist caps streams per site and client address and sends periodic comments to keep healthy intermediaries from treating an idle connection as abandoned.

There is no listener, poller, or goroutine for a site with no active subscription. Reading an untouched collection or opening its first stream does not create a SQLite file; the first mutation does.

Where to go next #

For the wire-level change stream — cursors, event shapes, heartbeats — read the HTTP API reference.

For the stream caps and retention bounds behind reset, read Limits and abuse controls.

Drafts and publishing

Wispist / Using Wispist

Wispist data belongs to a site, not to one release. Releases select code and access policy; they do not each get a new production database.

Wispdeck binds each request as follows:

ViewDeclarationNamespaceMutations
Public published sitePublished releaseLivePer collection policy
Public site with no releaseNoneNoneDenied
Private preview, DraftDraft releaseDraftPer collection policy
Private preview, CurrentPublished releaseLiveAlways denied

The Draft namespace remains stable across successive draft uploads for the same site, so ordinary iteration does not discard test data.

Publishing changes code, not data #

Publishing or rolling back atomically changes the release pointer used by the public origin. Existing live documents remain where they are.

In particular:

  • Publishing the first release does not copy draft documents into live.
  • Republishing preserves live documents.
  • Rolling back code preserves live documents.
  • Removing a collection declaration hides its documents without deleting them.
  • Re-adding the declaration exposes those documents again.

Promotion is deliberately not implicit. Draft data can be incomplete, destructive, private, or nonsensical. A future copy or promotion feature must be an explicit management action.

Current is a safe comparison view #

When both a public release and a new draft exist, preview offers Current and Draft. Current loads public code and live data on the private preview origin, but Wispdeck sets an unconditional read-only binding. A collection declaration cannot relax it.

wispist.mode reports live-preview and wispist.readOnly is true, allowing the page to hide editing controls. Those values are only presentation hints; the server rejects mutations even if site code ignores them.

Where to go next #

For what a rejected Current-preview mutation looks like to code, read Forbidden.

For the declaration each release carries, read the wispist.json reference.

wispist.json reference

Wispist / Reference

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 #

MemberTypeRequiredMeaning
versionintegeryesProtocol declaration version; exactly 1.
collectionsobjectyesCollection name to collection declaration.

Collection names #

Names match:

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

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

Collection declaration #

MemberTypeRequiredMeaning
accessstring or objectyesA built-in profile or all six operation policies.
limitsobjectnoLower document count and byte limits.

Shared profile #

{
  "access": "shared"
}

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

Expanded access #

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

Every operation must appear exactly once:

OperationControls
listListing collection documents.
readReading one document and receiving its body in a subscription.
createGenerated-ID POST and selected-ID create.
updateReplacing an existing document.
deleteDeleting an existing document.
subscribeOpening a change stream for the collection.

Each value is one of:

ValueMeaning
anyonePermit every principal.
authenticatedRequire an authenticated principal asserted by the host.
nobodyDeny 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 #

{
  "limits": {
    "maxDocuments": 250,
    "maxDocumentBytes": 4096
  }
}
MemberTypeRangeDefault
maxDocumentsinteger1 to host maximum1,000
maxDocumentBytesinteger2 to host maximum32 KiB

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

See also #

  • Limits and abuse controls — the installation defaults these declarations may lower.
  • Documents and collections — what a declared collection contains.
  • Forbidden — what a denied operation returns.

Browser API reference

Wispist / Reference

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

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

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

Environment fields #

FieldTypeMeaning
versionnumberClient protocol major; 1.
modestringlive, draft, or live-preview.
readOnlybooleanWhether the host has forced mutations off.

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

Collection handle #

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.

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

get(id) #

Returns one complete document envelope or rejects with not_found.

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.

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.

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 #

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:

FieldMeaning
nameAlways WispistError.
typeRFC 9457 problem type URI, when an HTTP problem was returned.
codeErgonomic code mapped from a known problem type.
titleStable problem title.
detailSafe occurrence-specific explanation.
statusHTTP status, when available.
instanceProblem occurrence URI, when available.
requestIdX-Request-ID value, when available.
problemComplete 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.

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 — the routes these methods call.
  • Problem Details — every code a WispistError can carry.
  • Live updates — how subscribe behaves across disconnects and resets.

HTTP API reference

Wispist / Reference

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

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 #

GET /_wispist/v1
{
  "name": "Wispist",
  "version": 1,
  "mode": "live",
  "readOnly": false,
  "collections": ["before-you-go"]
}

List documents #

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.

{
  "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 #

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.

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

Read #

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:

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:

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 #

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 #

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.

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 — the supported client over these routes.
  • Problem Details — the error shape every route uses.
  • Limits and abuse controls — body, pagination, stream, and rate bounds.

Limits and abuse controls

Wispist / Reference

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 #

ResourceDefault
Collections per release32
Documents per collection1,000
Document JSON32 KiB
Live document data per site10 MiB
Draft document data per site5 MiB
List page100; maximum 250
Collections per SSE stream8
SSE streams per site and client6
SSE streams per site100
Pending events per stream128
Request envelope allowancedocument limit plus 4 KiB
Live idempotency records per namespace10,000
Retained changes per namespace10,000
Change age7 days
Idempotency retentionat 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 #

BucketSustainedBurst
Reads per site and client600/minute100
Mutations per site and client60/minute20
Mutations across one site300/minute60
Generated-ID creates across one site10,000/day100
All Wispist requests6,000/minute1,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 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 — how a release lowers collection limits.
  • Quota exceeded, Request too large, Rate limited — the problems these bounds produce.

Problem Details

Wispist / Problems

Wispist API errors use RFC 9457 Problem Details and application/problem+json:

{
  "type": "https://learn.peios.org/wispist/problems/revision-conflict/",
  "title": "Revision conflict",
  "status": 412,
  "detail": "The document changed after it was read.",
  "instance": "urn:uuid:019c0000-0000-7000-8000-000000000000"
}

The type URI is the primary machine identifier. It is permanent even if the implementation or documentation later moves. title is stable for that type; detail describes this occurrence. status matches the HTTP status.

instance is a urn:uuid: URI built from the same UUID returned in X-Request-ID. Give that request ID to an operator when investigating a server-side failure.

Validation problems may add an errors array of JSON Pointer and detail objects. Internal SQL, paths, stack traces, document bodies, tokens, and policy predicates are never returned.

The browser client maps known type URIs to convenient error.code values:

HTTPTypeBrowser code
400Invalid requestinvalid_request
400Invalid JSONinvalid_json
401Authentication requiredauthentication_required
403Forbiddenforbidden
404Not foundnot_found
405Method not allowedmethod_not_allowed
409Idempotency conflictidempotency_conflict
409Quota exceededquota_exceeded
412Revision conflictrevision_conflict
413Request too largerequest_too_large
415Unsupported media typeunsupported_media_type
428Precondition requiredprecondition_required
429Rate limitedrate_limited
503Temporarily unavailabletemporarily_unavailable

HTTP headers retain their normal authority. Retry-After, ETag, Allow, and WWW-Authenticate are not replaced by Problem Details members.

Invalid request

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/invalid-request/

Status: 400 Bad Request. Browser code: invalid_request.

Wispist returns this problem when the request cannot be interpreted under the v1 resource contract. Examples include an unknown or repeated query parameter, invalid collection or document ID, non-canonical path, malformed cursor, unknown request-envelope member, or contradictory conditional headers.

This differs from Invalid JSON: the JSON may be syntactically valid while its protocol envelope or request metadata is not.

Correct the request rather than retrying it unchanged. Use the browser client where possible; it constructs canonical paths, envelopes, pagination, and mutation preconditions.

Invalid JSON

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/invalid-json/

Status: 400 Bad Request. Browser code: invalid_json.

The submitted document representation is not permitted JSON. Wispist rejects malformed or invalid UTF-8 input, a data root that is not an object, duplicate object member names, more than 32 nesting levels, keys longer than 256 UTF-8 bytes, and trailing JSON values.

Generate the body with a normal JSON serialiser and pass a non-null object to the browser client. Duplicate members must be fixed at the producer; Wispist does not choose which ambiguous value wins.

An oversized representation uses Request too large instead.

Authentication required

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/authentication-required/

Status: 401 Unauthorized. Browser code: authentication_required.

The collection operation is declared authenticated, but the host did not bind an authenticated principal. The response includes WWW-Authenticate.

Wispdeck's initial hosted-site integration supplies anonymous principals to public and preview JavaScript. A dashboard session or private preview cookie does not automatically become Wispist identity. Until a host identity integration is configured, change the operation to anyone only if public access is genuinely intended, or leave it inaccessible.

An authenticated principal that still lacks permission receives Forbidden.

Forbidden

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/forbidden/

Status: 403 Forbidden. Browser code: forbidden.

The bound principal may not perform the operation. Common causes are:

  • The collection operation is nobody.
  • A mutation did not carry the exact bound site Origin, or Fetch Metadata identified it as cross-site.
  • The request is using Wispdeck's Current preview, whose live-data binding is unconditionally read-only.
  • A configured authoriser denied the current or proposed document.

Do not retry unchanged. Check wispist.readOnly to adjust Current-preview UI, and compare the active release's wispist.json with the operation being attempted. Client metadata is only a UI hint; the server remains authoritative.

Not found

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/not-found/

Status: 404 Not Found. Browser code: not_found.

The requested resource is unavailable. A document may not exist, the active release may not declare the collection, or the path may not identify a v1 resource.

Wispist does not reveal hidden stored documents merely because an older release once declared them. Removing a declaration makes the collection unavailable without deleting its data.

For get, treat this as an absent document where that is valid application state. For an entire collection, inspect the declaration attached to the release serving this request.

Method not allowed

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/method-not-allowed/

Status: 405 Method Not Allowed. Browser code: method_not_allowed.

The path names a known Wispist resource, but its method is not supported. The Allow response header lists valid methods and is authoritative.

For example, a collection document resource supports GET, HEAD, PUT, and DELETE, while the change stream supports only GET. Use the route table in the HTTP API reference or the browser client.

Idempotency conflict

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/idempotency-conflict/

Status: 409 Conflict. Browser code: idempotency_conflict.

The Idempotency-Key was already used in this namespace for a different generated-ID POST. Wispist retains each successful key and response for at least 24 hours so a lost response can be retried safely.

If this is a retry, send exactly the same method, path, and document data. If it is a new logical create, generate a new strong random key. The built-in collection.add method does this automatically.

This problem concerns POST request identity. Concurrent document revisions use Revision conflict.

Quota exceeded

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/quota-exceeded/

Status: 409 Conflict. Browser code: quota_exceeded.

The mutation is valid in isolation but would exceed a storage bound. The transactional check may cover collection document count, total live or draft document bytes, or the maximum number of unexpired idempotency records.

Delete data that is no longer needed, reduce the proposed document, or ask the operator to review installation policy. A release declaration may lower limits but cannot raise them.

A single request or document that exceeds its byte limit uses Request too large.

Revision conflict

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/revision-conflict/

Status: 412 Precondition Failed. Browser code: revision_conflict.

The mutation's HTTP precondition did not match current state:

  • If-None-Match: * was used to create an ID that already exists.
  • If-Match carried an older revision for replacement or deletion.

Fetch or wait for the latest document, reconcile the user's intent, then submit a new conditional mutation using that revision. Never loop blindly: another caller may have made a meaningful conflicting change.

The browser client's replace, update, and delete methods accept an observed document envelope so safe concurrency is the normal path.

Request too large

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/request-too-large/

Status: 413 Content Too Large. Browser code: request_too_large.

The request body or data representation exceeds the active collection limit. Wispist limits the body before decoding and allows only a small fixed amount beyond the document limit for the JSON envelope.

Reduce the document or split independent records into separate documents. A release may request a higher collection limit only up to the operator's host maximum.

Total namespace storage exhaustion uses Quota exceeded.

Unsupported media type

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/unsupported-media-type/

Status: 415 Unsupported Media Type. Browser code: unsupported_media_type.

Document create and replacement bodies require:

Content-Type: application/json

Media-type parameters such as charset=utf-8 are accepted, but another base type or a missing header is not. Set the header and send the documented {"data": {...}} envelope.

JSON that has the correct media type but invalid content uses Invalid JSON or Invalid request.

Precondition required

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/precondition-required/

Status: 428 Precondition Required. Browser code: precondition_required.

Wispist does not offer unconditional last-write-wins mutation. A selected-ID create requires If-None-Match: *. Replacing or deleting an existing document requires If-Match with the quoted observed revision.

Read the current document and use replace, update, or delete with that complete envelope. If the supplied revision is no longer current, the next response is Revision conflict.

Rate limited

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/rate-limited/

Status: 429 Too Many Requests. Browser code: rate_limited.

A read, mutation, generated-ID create, installation-wide request, or concurrent change stream exceeded its bounded allowance. Retry-After gives the minimum server-selected delay in seconds.

Wait for that delay and retry with backoff. Avoid tight loops, duplicate subscriptions, and unnecessary full-list refreshes. The built-in subscription client already reconnects with bounded exponential backoff.

Rate limiting is an availability control. It does not change collection policy or make public writes private.

Temporarily unavailable

Wispist / Problems

Canonical type:

https://learn.peios.org/wispist/problems/temporarily-unavailable/

Status: 503 Service Unavailable. Browser code: temporarily_unavailable.

Wispist could not safely complete the operation. Expected transient causes include a busy SQLite store or temporary saturation of the bounded open-store cache. The response may include Retry-After.

Retry idempotent reads after a short backoff. Retry generated-ID POST only with the same idempotency key and identical request. Conditional replacements and deletions may be retried with the same observed revision; a committed competing change then becomes Revision conflict.

If the problem persists, give the operator the X-Request-ID. Wispist does not expose internal error text in the response.

Peios Learn — documentation for the Peios project.

Built with Trail.