# Getting started

---

# What is Wispist

_Wispist / Getting started_

> Wispist gives simple browser sites persistent JSON documents and live updates with no connection step, browser-visible secret, or dedicated application server.

**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:

```js
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:

```text
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](/wispist/problems/revision-conflict.md) 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:

```json
{
  "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](/wispist/getting-started/build-a-shared-checklist.md).

For the data model in full — namespaces, envelopes, revisions, conditional mutation — read [Documents and collections](/wispist/using-wispist/documents-and-collections.md).

For every field the declaration file accepts, read the [wispist.json reference](/wispist/reference/wispist-json.md).

---

# Build a shared checklist

_Wispist / Getting started_

> Add a persistent, live-updating checklist to a plain static site with one wispist.json declaration and the built-in browser client.

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:

```json
{
  "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`:

```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`:

```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:

```js
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](/wispist/problems/revision-conflict.md).

## Subscribe

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

```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:

```text
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](/wispist/using-wispist/documents-and-collections.md) — the envelope, revision, and conditional-mutation model behind `update`.
- [Live updates](/wispist/using-wispist/live-updates.md) — what `subscribe` does underneath: snapshot, cursor, Server-Sent Events, reconnect, reset.
- [wispist.json reference](/wispist/reference/wispist-json.md) — every declaration field, profile, and limit.
