Serving data

Widget datastore

Per-widget SQLite on the Mac hub. A widget remembers things across reloads via window.PREEN.db — durable, queryable, isolated.

push_data gives a widget a single last-value feed — the latest snapshot, pushed in. The datastore is the other half: a place for a widget to keep things itself, across reloads — a high score, a log of every run, anything it computes.

Each widget gets its own SQLite database on the Mac (Application Support/Preen/ store/<widgetId>.sqlite). One widget can’t read or corrupt another’s; deleting a widget deletes its database. Records are grouped into author-named collections and hold raw JSON — the hub never interprets the shape, exactly like a data feed.

Why not localStorage? Widgets load from an opaque origin, where localStorage is unreliable (it can silently fail to persist). The datastore is durable, queryable, survives reloads, and works in release builds.

From a widget: window.PREEN.db

The widget calls its store through the window.PREEN bridge — no network, no token, no widget id. Every call is scoped to this widget and returns a Promise.

// Append a record to a collection. → { id, ts }
const { id } = await PREEN.db.put("sessions", { dur: 5400, end: Date.now() });

// List records, newest first. → [{ id, ts, body }]
const recent = await PREEN.db.get("sessions", { limit: 30 });
const best = Math.max(0, ...recent.map((r) => r.body.dur));

// Replace a record by id (upsert). → { id, ts }
await PREEN.db.update("state", id, { paused: true });

// Delete one record, or clear the whole collection. → { deleted }
await PREEN.db.delete("sessions", id);
await PREEN.db.clear("sessions");
CallReturnsNotes
put(collection, body){ id, ts }Append a record. body is any JSON object.
get(collection, query?)[{ id, ts, body }]List records. See query options below.
update(collection, id, body){ id, ts }Replace an existing record’s body. Rejects if id is gone.
delete(collection, id){ deleted }Delete one record.
clear(collection){ deleted }Delete every record in the collection.

A record is { id, ts, body }: id is a hub-assigned autoincrement key, ts is the write time (Unix seconds), and body is the JSON you stored.

Query options (all optional):

  • limit — max rows (default 100, max 1000).
  • since — only rows with ts >= since (Unix seconds).
  • order"desc" (newest first, default) or "asc".

Collection names are author-chosen: [A-Za-z0-9_-], up to 64 characters. Each collection is capped at 5,000 rows — the oldest are trimmed past that, so an append loop can’t grow unbounded.

Worked example: a focus timer’s history

The built-in Do-Not-Disturb widget records every focus session, then derives its “personal best” from real history instead of a fragile single value:

const hasDB = !!(window.PREEN && PREEN.db);

// On end-of-session, append it.
async function recordSession(durMs) {
  if (hasDB) await PREEN.db.put("sessions", { dur: durMs, end: Date.now() });
}

// On load, the best is the longest session ever recorded.
async function loadBest() {
  if (!hasDB) return 0;
  const rows = await PREEN.db.get("sessions", { limit: 1000 });
  return rows.reduce((m, r) => Math.max(m, r.body.dur || 0), 0);
}

Because the raw sessions are kept, you can later compute streaks, weekly averages, or a sparkline — all from the same collection, no schema change.

From an agent, the CLI, or a script

The same store is reachable off-device so an agent can seed, inspect, or aggregate it.

Via the MCPstore_put, store_query, store_delete:

// store_query
{ "id": "wgt_abc123", "collection": "sessions", "limit": 5 }
// → { "records": [ { "id": 42, "ts": 1718900000, "body": { "dur": 5400 } }, … ] }

Via the CLI:

preen db put    dnd sessions '{"dur":5400,"end":1718900000000}'
preen db get    dnd sessions --limit 5 --order desc
preen db delete dnd sessions --record 42      # omit --record to clear the collection

When to use which

  • push_data / data endpoint — live, externally-sourced values you want on screen now (weather, a build status, now-playing). Last value wins; no history.
  • Datastore — state the widget owns and accumulates over time (scores, logs, settings, history). Durable and queryable.

Both are raw JSON and never run code on the phone (D7). The datastore is side-effect-free (no shell), so it works in release builds — unlike interactive actions.