@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/access.md +273 -79
- package/llms/board-games.md +1 -1
- package/llms/callai.md +1 -2
- package/llms/connections.md +1 -1
- package/llms/fireproof.md +98 -106
- package/llms/use-vibe.md +2 -2
- package/llms/use-viewer.md +1 -1
- package/package.json +5 -5
- package/system-prompt-initial-oneshot.md +182 -66
- package/system-prompt-initial.md +182 -66
- package/system-prompt.md +12 -11
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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={
|
|
49
|
+
<Button type="button" onClick={save}>Save</Button>
|
|
35
50
|
</Row>
|
|
36
|
-
{
|
|
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
|
|
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
|
|
61
|
+
const { database } = useFireproof("items");
|
|
52
62
|
const [text, setText] = useState("");
|
|
63
|
+
const [note, setNote] = useState("");
|
|
53
64
|
async function add() {
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
267
|
-
- **
|
|
268
|
-
the
|
|
269
|
-
|
|
270
|
-
- **
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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", "
|
|
298
|
-
{ "key": "stove", "type": "card", "
|
|
299
|
-
{ "key": "lantern", "type": "card", "
|
|
300
|
-
{ "key": "cooler", "type": "card", "
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
return <
|
|
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-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/llms/use-viewer.md
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
43
|
-
"@vibes.diy/call-ai-v2": "14.3.
|
|
44
|
-
"@vibes.diy/identity": "14.3.
|
|
45
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
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
|
},
|