@vibes.diy/prompts 14.3.119 → 14.3.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms/fireproof.md CHANGED
@@ -5,7 +5,7 @@ Fireproof is a document database with live sync, designed to make browser apps e
5
5
  ## Key Features
6
6
 
7
7
  - **Apps run anywhere:** Bundle UI, data, and logic together.
8
- - **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform surfaces the reason whenever there is one (an access-rule `forbidden("…")`, or no write access for a signed-in visitor); a reason-less rejection converges silently. Keep the UI reactive to the store rather than depending on a toast; don't assume every write lands, but don't hand-roll denial errors either.
8
+ - **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform tells the visitor about every refusal, in the app's own `forbidden("…")` words when it gave some. Keep the UI reactive to the store rather than depending on a toast; don't assume every write lands, but don't hand-roll denial errors either.
9
9
  - **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
10
10
  - **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("dbName")`. A child component that was handed `database` may call `useFireproof(database)` instead of repeating the name; it is the same database.
11
11
 
@@ -21,38 +21,50 @@ Each document has an `_id`, which can be auto-generated or set explicitly. Auto-
21
21
 
22
22
  Use granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.
23
23
 
24
- `useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server's access rules reject a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast whenever there is one: the app's own `forbidden("…")` text, or the platform's message when a signed-in visitor has no write access to the app (both also reach the app as `writeRejection`, below); a reason-less rejection converges silently. So do **not** wrap plain writes in `try/catch` to show denial errors to the user (a save carrying `_files` is the one exception, covered with the image uploader below); keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state is the feedback, and gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so a disallowed action never renders in the first place.
24
+ `useLiveQuery` populates and refreshes the UI reactively as data arrives, so render an inviting empty state while a list has nothing in it. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry on their own. The server's access rules have the final word: a refused write converges **server-wins** to the server's version, and the **platform** tells the visitor — in the app's own `forbidden("…")` words when it gave some, its own line otherwise, and "saved on this device" when a signed-out visitor's save waits for sign-in. So write plainly and let the store be the feedback: render from `useLiveQuery`/`useDocument`, gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so only allowed actions render, and keep `try/catch` for a save carrying `_files` (covered with the image uploader below). A toggle shows the whole shape — flip it at once, follow its receipt beside the control, and the list re-reads itself:
25
25
 
26
- When the server later refuses a write the visitor can act on — a signed-in visitor saving to a private app, say — the platform shows a notice and `useFireproof` hands the app the same reason as `writeRejection` (`{ docId, message, code }`, or `null` until a save is refused). Render `writeRejection.message` beside the form it came from and call `clearWriteRejection()` when the visitor dismisses it:
26
+ ```jsx
27
+ const { database, useLiveQuery } = useFireproof("tasks");
28
+ const { docs } = useLiveQuery("type", { key: "task" });
29
+ async function toggle(task) {
30
+ const { synced } = await database.put({ ...task, done: !task.done }); // the row flips now
31
+ synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));
32
+ }
33
+ // each row can show its own state: task._synced.status === "pending" → a quiet "saving" mark
34
+ ```
35
+
36
+ `put()` and `del()` save locally at once and resolve right away, so a resolved put means the record is saved **on this device** — never treat it as proof the server has it. Most apps need nothing more, because the platform shows refusals. Every result also carries `synced`, a receipt for this one write: `synced.status` is `"pending"`, then `"accepted"` (with `synced.seq`), `"refused"` (with `synced.code` and, when the app's rules gave one, `synced.message`), `"held"` (kept on this device until the visitor signs in, then it goes out), or `"superseded"` (a later save to the same record went out instead). It never rejects. Use `synced.on(...)` for feedback beside the control that caused the write: it calls your function at once and on each change, lets go after the last one, and returns an unsubscribe. Every doc from `useLiveQuery` carries the same shape as `doc._synced` for per-row state, such as a quiet "saving" mark while `doc._synced.status === "pending"`; it is not part of the document, so spreading or saving the doc leaves it behind:
27
37
 
28
38
  ```jsx
29
39
  function NoteForm({ text }) {
30
- const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
40
+ const { database } = useFireproof("notes");
41
+ const [note, setNote] = useState("");
42
+ async function save() {
43
+ const { synced } = await database.put({ type: "note", text });
44
+ synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));
45
+ }
31
46
  return (
32
47
  <Stack gap={2}>
33
48
  <Row>
34
- <Button type="button" onClick={() => database.put({ type: "note", text })}>Save</Button>
49
+ <Button type="button" onClick={save}>Save</Button>
35
50
  </Row>
36
- {writeRejection && (
37
- <Row gap={2} align="center" wrap>
38
- <p role="status">{writeRejection.message}</p>
39
- <Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button>
40
- </Row>
41
- )}
51
+ {note && <p role="status">{note}</p>}
42
52
  </Stack>
43
53
  );
44
54
  }
45
55
  ```
46
56
 
47
- While the app's first build is still finishing, the preview runs on its example records, and saving opens up the moment the build completes. A save in that window comes back as a `writeRejection` with `code: "draft-read-only"` and a friendly ready-made `message`. Treat it as a calm status line: the same quiet `role="status"` note as above (or nothing at all), with the visitor's typed text and selections kept in component state so the next save carries them. The platform clears this `writeRejection` itself the moment saving opens up (and on the next save that lands), so the note goes away on its own:
57
+ While the app's first build is still finishing, the preview runs on its example records, and saving opens up the moment the build completes. A save in that window resolves with `ok: false` and a receipt that is already `refused`, with `code: "draft-read-only"` and a friendly ready-made `message`. Treat it as a calm status line: the same quiet `role="status"` note as above (or nothing at all), with the visitor's typed text and selections kept in component state so the next save carries them. The next save that lands clears the note:
48
58
 
49
59
  ```jsx
50
60
  function AddItem() {
51
- const { database, writeRejection } = useFireproof("items");
61
+ const { database } = useFireproof("items");
52
62
  const [text, setText] = useState("");
63
+ const [note, setNote] = useState("");
53
64
  async function add() {
54
- const res = await database.put({ type: "item", text });
55
- if (res.ok) setText(""); // clear the field once the record is saved
65
+ const { ok, synced } = await database.put({ type: "item", text });
66
+ setNote(synced.code === "draft-read-only" ? synced.message : "");
67
+ if (ok) setText(""); // clear the field once the record is saved
56
68
  }
57
69
  return (
58
70
  <Stack gap={2}>
@@ -60,7 +72,7 @@ function AddItem() {
60
72
  <Row>
61
73
  <Button type="button" onClick={add}>Add</Button>
62
74
  </Row>
63
- {writeRejection?.code === "draft-read-only" && <p role="status">{writeRejection.message}</p>}
75
+ {note && <p role="status">{note}</p>}
64
76
  </Stack>
65
77
  );
66
78
  }
@@ -146,7 +158,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
146
158
  }
147
159
 
148
160
  export default function App() {
149
- const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
161
+ const { database, useLiveQuery } = useFireproof("tasks");
150
162
  const { me, can, ready } = useVibe("tasks");
151
163
  const { docs } = useLiveQuery("type", { key: "task" });
152
164
  const [sentence, setSentence] = useState("");
@@ -232,7 +244,6 @@ export default function App() {
232
244
  </form>
233
245
  <Text>{answer}</Text>
234
246
  <p role="status">{message}</p>
235
- {writeRejection && <Row gap={2} align="center" wrap><p role="status">{writeRejection.message}</p><Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button></Row>}
236
247
  <Row gap={2} wrap>
237
248
  {lastWrite !== undefined && <Button type="button" size="sm" variant="quiet" disabled={busy} onClick={undoLast}>Undo</Button>}
238
249
  {pending !== undefined && !busy && <Button type="button" size="sm" variant="quiet" tone="down" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</Button>}
@@ -248,110 +259,91 @@ Address an existing record by its `_id`. `useDocument(task)` above holds the sel
248
259
 
249
260
  ## Required starting records
250
261
 
251
- When a request names records the app must already have — the three projects in a
252
- studio, a template's starter rows, the rooms in a planner — those records are
253
- created by the app's own first render through the ordinary write path. That is
254
- the mechanism, and it is the same one described below for starter content.
255
- `seed.json` is a separate thing: an optional import a person runs by hand from
256
- the Data tab. Its sample records also populate the temporary, view-only preview
257
- while saving is being prepared; they become real saved records only through an
258
- explicit import. In that file, give each item a stable `key`; its derived identity
259
- is `seed:<key>`. References to another item use that derived identity. An `_id`
260
- that repeats the item's own derived identity is equivalent to leaving it out.
261
- Explicit singleton identities such as `config:board` remain available.
262
+ A person's own action — a tap, a submit, a pick — is what starts a write; nothing in the app writes on load or on mount. Starting state comes in three kinds, and none of them is written by the app's own code:
262
263
 
263
- Four rules make it hold:
264
+ - **A person's own starting state needs no record.** Their records live on the channel named by their handle (see `access.md`), so the app opens on their tree with nothing to create first, and the view renders its defaults — a name, a setting, an empty list's invitation — from constants until the person's own action saves one.
265
+ - **App-wide named containers or participants the request requires** — the pottery studio and its named apprentice, the three projects in a studio, the rooms in a planner — are written by the agent building the app, with `put_doc` as the owner, once the build whose rules admit them is saved, then read back. That save is the owner's own action. The app's opening view shows them when they are there and an inviting add flow until they are.
266
+ - **Example content stays the optional `seed.json` import** — believable entries the screenshot renders and the owner adds with one ask. A name present only in `seed.json` describes an offered import, not an existing record or grant. In that file, give each item a stable `key`; its derived identity is `seed:<key>`. References to another item use that derived identity. An `_id` that repeats the item's own derived identity is equivalent to leaving it out. Explicit singleton identities such as `config:board` remain available. The file also populates the temporary, view-only preview while saving is being prepared.
267
+
268
+ Four rules make the required records hold:
264
269
 
265
270
  - **Each required record gets a fixed `_id` you choose** (`"project:atlas"`), so
266
- a second write of it lands on the same document instead of minting another.
267
- - **Create them after `ready` and after `can.create` says yes**, so a write that
268
- the enforced access rules do not admit yet is skipped quietly and retried on a
269
- later run.
270
- - **Restrict their creation to the owner in `access.js`** when the records
271
- describe the app itself rather than a visitor's own work.
272
- - **Write parents and membership grants before the records that depend on
273
- them**, so a dependent document never lands against a container or a grant
274
- that does not exist yet.
275
-
276
- Fixed ids are also what makes two tabs safe. Two browsers opening the app for the
277
- first time at the same moment both see an empty query and both write; with fixed
278
- ids those are two writes of the same document, and last-writer-wins converges on
279
- one copy. Reading the query first and then deciding whether to write is the shape
280
- that races — the read is empty in both tabs before either write lands.
271
+ a second save of it lands on the same document instead of minting another.
272
+ - **Gate their creation on a role in `access.js`** — `ctx.requireRole("owner")`,
273
+ or a role the owner hands out with a role record — when the records describe the
274
+ app itself rather than a visitor's own work.
275
+ - **List parents and membership or role grants before the records that depend on
276
+ them**, so a dependent document never lands against a container or a grant that
277
+ does not exist yet.
278
+ - **Leave the app's render paths write-free.** A write in an effect that fires when
279
+ a query reads empty runs on every fresh load, on every device and for every
280
+ visitor, because the live query renders empty before data arrives.
281
+
282
+ Fixed ids are also what makes a repeated save safe: two devices saving the same
283
+ record write the same document, and last-writer-wins converges on one copy.
281
284
 
282
285
  ### Match sample records to the owner's opening query
283
286
 
284
- The initial build's `<app-owner>` context supplies the app owner's handle.
285
- Use it to evaluate the opening query's group ID when writing `seed.json`.
286
- For an owner named `mira`, the runtime expression below selects `default-mira`,
287
- so every sample card in this opening board carries that exact `boardId`:
287
+ The initial build's `<app-owner>` context supplies the app owner's handle. A
288
+ sample record in the owner's own tree names that tree with `"@owner"` as the whole
289
+ field value, which the platform fills in for whoever applies the seed. The opening
290
+ view reads the signed-in person's own tree by their handle, so every sample card
291
+ carries `"ownerHandle": "@owner"`:
288
292
 
289
293
  ```jsx
290
- const boardId = pickedBoardId ?? `default-${me?.userHandle}`;
291
- const { docs: cards } = useLiveQuery("boardId", { key: boardId });
294
+ const { docs: cards } = useLiveQuery("ownerHandle", { key: me?.userHandle });
292
295
  ```
293
296
 
294
297
  ```json
295
298
  {
296
299
  "boards": [
297
- { "key": "tent", "type": "card", "boardId": "default-mira", "text": "Blue tent, sleeps four" },
298
- { "key": "stove", "type": "card", "boardId": "default-mira", "text": "Two-burner camp stove" },
299
- { "key": "lantern", "type": "card", "boardId": "default-mira", "text": "Two rechargeable lanterns" },
300
- { "key": "cooler", "type": "card", "boardId": "default-mira", "text": "Wheeled cooler" }
300
+ { "key": "tent", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Blue tent, sleeps four" },
301
+ { "key": "stove", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Two-burner camp stove" },
302
+ { "key": "lantern", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Two rechargeable lanterns" },
303
+ { "key": "cooler", "type": "card", "ownerHandle": "@owner", "authorHandle": "@owner", "text": "Wheeled cooler" }
301
304
  ]
302
305
  }
303
306
  ```
304
307
 
305
- Use the supplied owner handle in these parent IDs; keep the app's query
306
- and real writes driven by the current viewer and selected object. The database
307
- name, record type, parent references and query filters agree across the sample
308
- file and opening view. Later access rules preserve those parent references.
309
- The platform labels the temporary preview as sample data; those records reach
310
- persistent storage only through the person's explicit import.
308
+ An id that only contains the handle (`settings-mira`) is not a whole value, so
309
+ write the supplied owner handle into it literally; keep the app's query and real
310
+ writes driven by the current viewer and selected object. The database name,
311
+ record type, parent references and query filters agree across the sample file and
312
+ opening view. Later access rules preserve those parent references. The platform
313
+ labels the temporary preview as sample data; those records reach persistent
314
+ storage only through the person's explicit import.
311
315
 
312
316
  ### Seeding starter data — give each seed a deterministic `_id`
313
317
 
314
- When an app ships with default content — starter slides, template rows, example cards — and writes it into the database on first load, give **each seed document a deterministic `_id`** (`"seed:intro"`, `"seed:" + key`). This makes the seeding write **idempotent**: if it ever runs again, it overwrites the same documents instead of creating fresh copies.
318
+ When an app ships with default content — starter slides, template rows, example cards — the content is a set of `seed.json` items, each with a **deterministic identity** (a stable `key`, or `_id: "seed:intro"`). This makes adding it **idempotent**: a second add overwrites the same documents instead of creating fresh copies.
315
319
 
316
- The trap is the live-query empty state. `useLiveQuery` renders empty before data arrives, so `docs.length === 0` is briefly true on **every** fresh load — and a `useRef`/`useState` "already seeded" flag only guards the current session, never the next reload, a second device, or another collaborator. With **auto-generated** `_id`s, each of those re-seeds the full set and duplicate copies pile up without bound. Deterministic `_id`s collapse every re-seed back onto the same N documents.
320
+ The app itself writes no copy. `useLiveQuery` renders empty before data arrives, so `docs.length === 0` is briefly true on **every** fresh load, and a "seed when empty" effect re-runs on every reload, every device and for every visitor. The empty view instead invites the person's own first action, and that action writes the record it checked:
317
321
 
318
322
  ### `App.jsx`
319
- ```jsx
320
- import React from "react";
321
- import { useFireproof } from "use-fireproof";
322
- import { useVibe } from "use-vibes";
323
-
324
- // Stable _id per seed, and no Date.now()/random fields — keep the payload
325
- // constant so a redundant seed is a true no-op, not a churning rewrite.
326
- const SEED = [
327
- { _id: "seed:intro", type: "slide", title: "Welcome", position: 1000 },
328
- { _id: "seed:problem", type: "slide", title: "The problem", position: 2000 },
329
- { _id: "seed:ask", type: "slide", title: "The ask", position: 3000 },
330
- ];
331
-
332
- export default function App() {
323
+ ```jsx partial
324
+ function Deck() {
333
325
  const { useLiveQuery, database } = useFireproof("deck");
334
326
  const { ready, can } = useVibe("deck");
335
327
  const { docs: slides } = useLiveQuery("position");
336
-
337
- React.useEffect(() => {
338
- if (slides.length > 0) return; // already has content — don't seed
339
- // Ask the access rules before writing. A doc type the currently-enforced
340
- // access rules don't allow yet (e.g. one this very edit just introduced —
341
- // the update binds only after the whole turn lands) would be rejected:
342
- // skip quietly now and seed on a later run instead of spamming errors.
343
- if (!ready || !can.create(SEED[0]).ok) return;
344
- // Deterministic _id → idempotent. Even if this runs again (another load or
345
- // device before the query resolves), it overwrites the same docs instead of
346
- // minting new ones, so seed slides never duplicate.
347
- SEED.forEach((s) => database.put(s).catch((e) => console.error(e)));
348
- }, [slides.length, ready, can, database]);
349
-
350
- return <Deck slides={slides} />;
328
+ // The person's own tap writes the first slide: the control checks this exact record.
329
+ const first = { type: "slide", title: "Welcome", position: 1000 };
330
+ const allowed = ready && can.create(first).ok;
331
+
332
+ if (slides.length === 0) {
333
+ return (
334
+ <Stack gap={2}>
335
+ <Text>Your deck is ready for its first slide.</Text>
336
+ <Button type="button" disabled={!allowed} onClick={() => allowed && database.put(first)}>
337
+ Add the first slide
338
+ </Button>
339
+ </Stack>
340
+ );
341
+ }
342
+ return <Slides slides={slides} />;
351
343
  }
352
344
  ```
353
345
 
354
- Don't stamp `Date.now()` or other changing values into seed documents — that makes each re-seed rewrite the doc with new content, churning revisions even though the `_id` is stable. Resource-like docs — channels, profiles, **seeds** — get deterministic `_id`s; event/content docs let `_id` auto-generate.
346
+ Don't stamp `Date.now()` or other changing values into seed documents — that makes each re-add rewrite the doc with new content, churning revisions even though the `_id` is stable. Resource-like docs — channels, profiles, **seeds** — get deterministic `_id`s; event/content docs let `_id` auto-generate.
355
347
 
356
348
  ### Updating Documents in Event Handlers
357
349
 
@@ -443,7 +435,7 @@ A drag that touches many documents (painting pixels, drawing on a shared board)
443
435
 
444
436
  Keep each cell its own document with a deterministic `_id` (`pixel-<x>-<y>`). Never fold the canvas into one document every visitor saves: the server keeps whichever save arrives last, so one visitor's save erases everyone else's strokes. An unsaved draft drops out for peers about 12 seconds after it stops updating.
445
437
 
446
- A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. Create a fixed-id room record on that channel (see Required starting records) so even the first stroke on an empty canvas is seen live.
438
+ A draft reaches only the peers already subscribed to its channel, and a channel's grant comes from a saved document. The painter's own pointer-down saves a fixed-id room record on that channel before the first stroke is drawn, so the stroke is seen live; nothing writes on load or on mount.
447
439
 
448
440
  ### `App.jsx`
449
441
  ```jsx
@@ -465,7 +457,7 @@ export default function App() {
465
457
 
466
458
  function Canvas({ me }) {
467
459
  // Many people write the same pixel docs: fail fast rather than replay a stale queued write.
468
- const { database, useLiveQuery, useDocument, writeRejection } = useFireproof("pixels", { offlineQueue: false });
460
+ const { database, useLiveQuery, useDocument } = useFireproof("pixels", { offlineQueue: false });
469
461
  const { ready, can } = useVibe("pixels");
470
462
  const canPaint = ready && can.create({ _id: "pixel-0-0", type: "pixel", x: 0, y: 0, color: COLOR, paintedBy: me }).ok;
471
463
  const { docs: pixels } = useLiveQuery("type", { key: "pixel" });
@@ -474,12 +466,6 @@ function Canvas({ me }) {
474
466
  const cellsRef = useRef([]);
475
467
  const canvasRef = useRef(null);
476
468
 
477
- useEffect(() => {
478
- // The room record grants the channel, so peers subscribe before the first stroke.
479
- const room = { _id: "canvas:main", type: "canvas" };
480
- if (ready && can.create(room).ok) database.put(room);
481
- }, [ready, can, database]);
482
-
483
469
  useEffect(() => {
484
470
  const ctx = canvasRef.current.getContext("2d");
485
471
  ctx.fillStyle = "#ffffff";
@@ -493,6 +479,13 @@ function Canvas({ me }) {
493
479
  for (const [x, y] of stroke.cells) fill(x, y, stroke.color);
494
480
  }, [pixels, strokes, stroke, me]);
495
481
 
482
+ function startStroke(e) {
483
+ // The painter's own pointer-down saves the room record that grants the channel; the
484
+ // fixed id makes every later save the same document.
485
+ const room = { _id: "canvas:main", type: "canvas" };
486
+ if (ready && can.create(room).ok) database.put(room);
487
+ extendStroke(e);
488
+ }
496
489
  function extendStroke(e) {
497
490
  if (!canPaint) return;
498
491
  const r = canvasRef.current.getBoundingClientRect();
@@ -514,7 +507,6 @@ function Canvas({ me }) {
514
507
 
515
508
  return (
516
509
  <div>
517
- {writeRejection && <p role="status">{writeRejection.message}</p>}
518
510
  <canvas
519
511
  ref={canvasRef}
520
512
  width={SIZE * CELL}
@@ -522,7 +514,7 @@ function Canvas({ me }) {
522
514
  className="w-full max-w-lg aspect-square touch-none"
523
515
  onPointerDown={(e) => {
524
516
  e.currentTarget.setPointerCapture(e.pointerId);
525
- extendStroke(e);
517
+ startStroke(e);
526
518
  }}
527
519
  onPointerMove={(e) => e.buttons && extendStroke(e)}
528
520
  onPointerUp={endStroke}
@@ -710,7 +702,7 @@ An index function can call `emit(key, value)` with a second argument to store a
710
702
 
711
703
  ## Offline writes are on by default (`offlineQueue`)
712
704
 
713
- For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version); the platform surfaces the reason whenever there is one, and a reason-less rejection converges silently — so keep the UI reactive to the store rather than depending on a toast. Only transport failures queue and retry.
705
+ For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version) and the platform tells the visitor, so keep the UI reactive to the store rather than depending on a toast. Only transport failures queue and retry.
714
706
 
715
707
  Pass `{ offlineQueue: false }` for **server-first, fail-fast** writes: a `put` resolves only when the server accepts it, and a network failure rejects immediately with nothing queued. Choose this for **collaborative multi-writer apps** where two people may edit the same doc — sync is blind last-arrival-wins (Firefly has no `_rev`), so a stale write replayed on reconnect can silently overwrite a newer one. When a lost write is safer than a surprise overwrite, opt out.
716
708
 
@@ -724,7 +716,7 @@ const { useLiveQuery } = useFireproof("shared-board", { offlineQueue: false });
724
716
 
725
717
  ## Anonymous writes (local-first by default)
726
718
 
727
- Letting a logged-out visitor try the app and build a little state before signing in needs **no opt-in** — it is the universal default on every vibe. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against the local replica with the identical API — no auth branching in your code — so favorites, a draft, or a scratch list just work. On first sign-in that local state drains automatically into the user's cloud database, each document exactly as stored (an author field left empty while signed out stays empty), and then syncs live; nothing is lost if a drain is interrupted. The sign-in and drain-confirmation toast ("saved to your account") is platform-provided chrome — do not build your own "saved"/"synced" notice for it.
719
+ Letting a logged-out visitor try the app and build a little state before signing in needs **no opt-in** — it is the universal default on every vibe. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against the local replica with the identical API — no auth branching in your code — so favorites, a draft, or a scratch list just work. When the app's rules keep writes for signed-in people, such a save's `synced.status` reads `"held"` until then. On first sign-in that local state drains automatically into the user's cloud database, each document exactly as stored (an author field left empty while signed out stays empty), and then syncs live; nothing is lost if a drain is interrupted. The sign-in and drain-confirmation toast ("saved to your account") is platform-provided chrome — do not build your own "saved"/"synced" notice for it.
728
720
 
729
721
  The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecated and ignored** — its behavior is now the default, so the runtime warns once and drains any legacy data on its own. Do not emit the flag in new code.
730
722
 
@@ -736,7 +728,7 @@ The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecat
736
728
 
737
729
  ## Architecture: Where's My Data?
738
730
 
739
- Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform surfaces the reason whenever there is one — a reason-less rejection converges silently, so keep the UI reactive to the store rather than depending on a toast. Don't assume the write always lands, but don't hand-roll your own denial error UI — let it converge.
731
+ Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform tells the visitor about it, so keep the UI reactive to the store rather than depending on a toast. Don't assume the write always lands, but don't hand-roll your own denial error UI — let it converge.
740
732
 
741
733
  ## Using Fireproof in JavaScript
742
734
 
package/llms/use-vibe.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # useVibe Hook — write gating
2
2
 
3
- `useVibe(dbName)` is how you gate write surfaces. It runs the app's own `access.js` — the **same function the server enforces** — against a candidate document, so the UI's enabled/disabled state matches what the server will actually allow. You never re-implement permissions; you ask the access function.
3
+ `useVibe(dbName)` is how you gate write surfaces. It runs the app's own `access.js` — the **same function the server enforces** — against a candidate document, so `can.*` is a hint about whether this person may write a record like this, good for deciding which controls to show. The server checks each change against the saved version of the record, so it has the final word. You never re-implement permissions; you ask the access function.
4
4
 
5
5
  ```jsx
6
6
  const { me, can, ready } = useVibe("comments");
@@ -82,4 +82,4 @@ Don't gate management UI on a display flag directly. Encode the rule in `access.
82
82
 
83
83
  ## The server is still the authority
84
84
 
85
- `can.*` is a fast, faithful preview, not the final word. A write can still be rejected server-side (the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Keep the optimistic-write + rollback pattern: apply the change immediately, revert and surface an error if the `put` rejects.
85
+ `can.*` is a fast preview, not the final word. A write can still be refused server-side (the server compares the change with the saved record, and the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Write optimistically: `put()` saves locally at once and never rejects for access. A refused write converges to the server's version on its own, and the platform tells the visitor, so most apps need nothing more. For feedback beside the control, read the put's receipt (`const { synced } = await database.put(doc)`, then `synced.on(...)`), or `doc._synced.status` on a row — see the fireproof skill.
@@ -387,4 +387,4 @@ async function addMember(handle) {
387
387
  {canInvite && <HandleInput onChange={addMember} placeholder="Add a friend…" />}
388
388
  ```
389
389
 
390
- Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing" — builds no picker: each person keeps their own record tree and shares a chosen item with its publish step, and the records stay shaped so a picker can join later. Everywhere else the picker belongs.
390
+ Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing" — builds the picker as its add-a-friend control: each person keeps their own collection, private to them, and adding a friend shares that collection with the friend it names (the access skill's rule for a collection of a person's own records). Everywhere else the picker belongs too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.119",
3
+ "version": "14.3.121",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,10 +39,10 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/api-types": "14.3.119",
43
- "@vibes.diy/call-ai-v2": "14.3.119",
44
- "@vibes.diy/identity": "14.3.119",
45
- "@vibes.diy/use-vibes-types": "14.3.119",
42
+ "@vibes.diy/api-types": "14.3.121",
43
+ "@vibes.diy/call-ai-v2": "14.3.121",
44
+ "@vibes.diy/identity": "14.3.121",
45
+ "@vibes.diy/use-vibes-types": "14.3.121",
46
46
  "arktype": "~2.2.6",
47
47
  "json-schema-faker": "~0.6.3"
48
48
  },