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

Getting started

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.

Peios Learn — documentation for the Peios project.

Built with Trail.