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

Using Wispist

Single-page view · as markdown

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.

Peios Learn — documentation for the Peios project.

Built with Trail.