@vibes.diy/prompts 14.1.21 → 14.1.22

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.
@@ -0,0 +1,697 @@
1
+ # Fireproof Database API Guide
2
+
3
+ Fireproof is a document database with live sync, designed to make browser apps easy. On vibes.diy it runs against Firefly: each app holds a local replica (IndexedDB) that IS the database, so writes succeed locally and instantly and then sync in the background. The Firefly server validates every synced write on ingest and can still reject it (a conflict, or a write the platform does not admit); accepted writes stream live to every viewer. Use it in any JavaScript environment with a unified API that works both in React (with hooks) and as a standalone core API.
4
+
5
+ ## Key Features
6
+
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 (a refusal, a conflict): the local optimistic revision converges **server-wins**, and the platform surfaces a reason only when it has one; 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.
9
+ - **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
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")`.
11
+
12
+ **File structure:** A vibe's source is one or more files. `/App.jsx` is the entry point (React component).
13
+
14
+ Fireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. On vibes.diy, a write commits to the local replica immediately and syncs to the Firefly server in the background; the server is the authority on acceptance: it stores each document in a per-document append-only sequence, validates and routes it on ingest, and then syncs it to viewers. Because every synced write is validated server-side on ingest, it can be rejected.
15
+
16
+ ## Installation
17
+
18
+ The `use-fireproof` package provides both the core API and React hooks. React hooks are the recommended way to use Fireproof in LLM code generation contexts. Fireproof databases persist data through the Firefly server and sync it live to every viewer. Each database is identified by a string name, and you can have multiple databases per application—often one per collaboration session, as they are the unit of sharing. Database names are the app's own to choose — pick a plain camelCase name for every database the app creates; prefix-namespaced names like `media:*` are platform-reserved for the databases platform components bring with them.
19
+
20
+ Each document has an `_id`, which can be auto-generated or set explicitly. Auto-generation is recommended to ensure uniqueness and avoid conflicts. The server keeps a per-document sequence, so two clients writing the same `_id` at the same time can collide and one write will be rejected — see the note on continuous updates below. Prefer one document per event over many rapid writes to a single hot document.
21
+
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
+
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 rejects 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 only when it has one; a reason-less rejection converges silently. So do **not** wrap writes in `try/catch` to show denial errors to the user; 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.
25
+
26
+ ### Basic Example
27
+
28
+ This complete app shows Fireproof's core: `useFireproof` gives you hooks, `useDocument` manages form state, and `useLiveQuery` sorts by `_id` for temporal ordering.
29
+
30
+ App.jsx
31
+
32
+ ```jsx
33
+ import React from "react";
34
+ import { useFireproof } from "use-fireproof";
35
+
36
+ export default function App() {
37
+ const { useDocument, useLiveQuery } = useFireproof("myLedger");
38
+
39
+ const { doc, merge, submit } = useDocument({ text: "" });
40
+
41
+ // _id is roughly temporal, this is most recent first
42
+ const { docs } = useLiveQuery("_id", { descending: true, limit: 100 });
43
+
44
+ return (
45
+ <div>
46
+ <form onSubmit={submit}>
47
+ <input value={doc.text} onChange={(e) => merge({ text: e.target.value })} placeholder="New document" />
48
+ <button type="submit">Submit</button>
49
+ </form>
50
+
51
+ <h3>Recent Documents</h3>
52
+ <ul>
53
+ {docs.map((doc) => (
54
+ <li key={doc._id}>{doc.text}</li>
55
+ ))}
56
+ </ul>
57
+ </div>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### Editing Documents
63
+
64
+ Address documents by a known `_id` if you want to force conflict resolution or work with a real world resource, like a schedule slot or a user profile. In a complex app this might come from a route parameter or correspond to an outside identifier. To add a profile editor to the app above:
65
+
66
+ App.jsx
67
+
68
+ ```jsx
69
+ <<<<<<< SEARCH
70
+ const { doc, merge, submit } = useDocument({ text: "" });
71
+ =======
72
+ const { doc, merge, submit } = useDocument({ text: "" });
73
+
74
+ const { doc: profile, merge: mergeProfile, save: saveProfile } = useDocument({ _id: "user-profile:abc@example.com" });
75
+ >>>>>>> REPLACE
76
+ ```
77
+
78
+ The `useDocument` hook provides several methods: `merge(updates)` updates the document with new fields without saving (use this instead of keeping a `useState` for document data), `submit(e)` handles form submission by preventing default, saving, and resetting, `save()` saves the current document state, and `reset()` resets to initial state. When you call submit, the document is reset, so if you didn't provide an `_id` then you can use the form to create a stream of new documents as in the basic example above.
79
+
80
+ ## Required starting records
81
+
82
+ When a request names records the app must already have — the three projects in a
83
+ studio, a template's starter rows, the rooms in a planner — those records are
84
+ created by the app's own first render through the ordinary write path. That is
85
+ the mechanism, and it is the same one described below for starter content.
86
+ `seed.json` is a separate thing: an optional import a person runs by hand from
87
+ the Data tab, applied by no deploy path, so a record that exists only there does
88
+ not exist for anyone who opens the app.
89
+
90
+ Four rules make it hold:
91
+
92
+ - **Each required record gets a fixed `_id` you choose** (`"project:atlas"`), so
93
+ a second write of it lands on the same document instead of minting another.
94
+ - **Create them after `ready` and after `can.create` says yes**, so a write the
95
+ platform does not admit yet is skipped quietly and retried on a later run.
96
+ - **Write the object before the records that belong to it**, so a dependent
97
+ document never lands against a container that does not exist yet.
98
+
99
+ Fixed ids are also what makes two tabs safe. Two browsers opening the app for the
100
+ first time at the same moment both see an empty query and both write; with fixed
101
+ ids those are two writes of the same document, and last-writer-wins converges on
102
+ one copy. Reading the query first and then deciding whether to write is the shape
103
+ that races — the read is empty in both tabs before either write lands.
104
+
105
+ ### How records are shaped
106
+
107
+ Every record a person writes carries two things beyond its own content: **the id of the object it belongs to**, and **`authorHandle`, the handle of whoever wrote it**. The object itself carries **`creatorHandle`**. Write them at create and leave them alone afterwards — a later editor's handle then always finds the ids already on the doc rather than supplying one.
108
+
109
+ Each signed-in person also gets **an object of their own with no doc to create**: its id is derived from their handle, `` `default-${me.userHandle}` ``. So someone who has just arrived types once and their first record lands, with nothing for them to set up first — and an app one person keeps for themselves is just this, with one object in it.
110
+
111
+ ```jsx
112
+ import { useFireproof } from "use-fireproof";
113
+ import { useVibe } from "use-vibes";
114
+
115
+ function Board({ pickedBoardId }) {
116
+ const { useLiveQuery, database } = useFireproof("boards");
117
+ const { me, can } = useVibe("boards");
118
+ // No board doc needed to start: every person has one derived from their handle.
119
+ const boardId = pickedBoardId ?? `default-${me?.userHandle}`;
120
+ const { docs: cards } = useLiveQuery("boardId", { key: boardId });
121
+
122
+ // A card names its board and its author, both at create.
123
+ const addCard = (text) => database.put({ type: "card", boardId, authorHandle: me?.userHandle, text });
124
+ // A board someone explicitly makes names the person who made it.
125
+ const addBoard = (title) => database.put({ type: "board", creatorHandle: me.userHandle, title });
126
+
127
+ return (
128
+ <div>
129
+ {cards.map((card) => (
130
+ <p key={card._id}>{card.text}</p>
131
+ ))}
132
+ {can.create({ type: "card", boardId, authorHandle: me?.userHandle }).ok && <AddCardForm onAdd={addCard} />}
133
+ </div>
134
+ );
135
+ }
136
+ ```
137
+
138
+ Queries read off those same ids — `useLiveQuery("boardId", { key: boardId })` above — so the fields earn their place immediately rather than being bookkeeping for later.
139
+
140
+ ### Seeding starter data — give each seed a deterministic `_id`
141
+
142
+ 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.
143
+
144
+ 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.
145
+
146
+ ```jsx
147
+ import React from "react";
148
+ import { useFireproof } from "use-fireproof";
149
+ import { useVibe } from "use-vibes";
150
+
151
+ // Stable _id per seed, and no Date.now()/random fields — keep the payload
152
+ // constant so a redundant seed is a true no-op, not a churning rewrite.
153
+ const SEED = [
154
+ { _id: "seed:intro", type: "slide", title: "Welcome", position: 1000 },
155
+ { _id: "seed:problem", type: "slide", title: "The problem", position: 2000 },
156
+ { _id: "seed:ask", type: "slide", title: "The ask", position: 3000 },
157
+ ];
158
+
159
+ export default function App() {
160
+ const { useLiveQuery, database } = useFireproof("deck");
161
+ const { ready, can } = useVibe("deck");
162
+ const { docs: slides } = useLiveQuery("position");
163
+
164
+ React.useEffect(() => {
165
+ if (slides.length > 0) return; // already has content — don't seed
166
+ // Ask `can.create` before writing. A doc type the platform doesn't admit
167
+ // yet (e.g. one this very edit just introduced — the update binds only
168
+ // after the whole turn lands) would be rejected: skip quietly now and seed
169
+ // on a later run instead of spamming errors.
170
+ if (!ready || !can.create(SEED[0]).ok) return;
171
+ // Deterministic _id → idempotent. Even if this runs again (another load or
172
+ // device before the query resolves), it overwrites the same docs instead of
173
+ // minting new ones, so seed slides never duplicate.
174
+ SEED.forEach((s) => database.put(s).catch((e) => console.error(e)));
175
+ }, [slides.length, ready, can, database]);
176
+
177
+ return <Deck slides={slides} />;
178
+ }
179
+ ```
180
+
181
+ 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 — categories, profiles, **seeds** — get deterministic `_id`s; event/content docs let `_id` auto-generate.
182
+
183
+ ### Updating Documents in Event Handlers
184
+
185
+ To update an existing document from a click handler or callback, use `database.put()` directly. Never call `useDocument` inside an event handler — that violates React's Rules of Hooks. Adding a toggle to list items:
186
+
187
+ App.jsx
188
+
189
+ ```jsx
190
+ <<<<<<< SEARCH
191
+ const { useDocument, useLiveQuery } = useFireproof("myLedger");
192
+ =======
193
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
194
+ >>>>>>> REPLACE
195
+ ```
196
+
197
+ App.jsx
198
+
199
+ ```jsx
200
+ <<<<<<< SEARCH
201
+ <li key={doc._id}>{doc.text}</li>
202
+ =======
203
+ <li key={doc._id}>
204
+ {doc.text}
205
+ <button onClick={() => database.put({ ...doc, favorite: !doc.favorite })}>
206
+ {doc.favorite ? "★" : "☆"}
207
+ </button>
208
+ </li>
209
+ >>>>>>> REPLACE
210
+ ```
211
+
212
+ Never call hooks inside handlers — `const { doc, save } = useDocument({ _id: id })` inside an onClick BREAKS the Rules of Hooks.
213
+
214
+ ### Continuous Controls — `merge()` on every event, `save()` once on commit
215
+
216
+ A continuous control (slider, drag, color picker, live-typed text bound to one doc) fires many events in quick succession. **Never call `database.put()` or `save()` on every `onChange`** — that floods the server with rapid concurrent writes to the same `_id`, which collide on the per-document sequence and get rejected (`Failed to put document …`). Instead, `merge()` locally on each event to keep the UI live, and `save()` once when the interaction commits (`onPointerUp`, `onBlur`, `onChange` for a range input's final value, or a debounced trailing call). Editing a single doc with a slider:
217
+
218
+ App.jsx
219
+
220
+ ```jsx
221
+ <<<<<<< SEARCH
222
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
223
+ =======
224
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
225
+
226
+ // One shared mix doc; slider updates the UI on every event, persists on commit
227
+ const { doc: mix, merge: mergeMix, save: saveMix } = useDocument({ _id: "mix:current", level: 50 });
228
+ >>>>>>> REPLACE
229
+ ```
230
+
231
+ App.jsx
232
+
233
+ ```jsx
234
+ <<<<<<< SEARCH
235
+ <h3>Recent Documents</h3>
236
+ =======
237
+ <input
238
+ type="range"
239
+ min="0"
240
+ max="100"
241
+ value={mix.level}
242
+ onChange={(e) => mergeMix({ level: Number(e.target.value) })} // live, no write
243
+ onPointerUp={() => saveMix().catch((err) => console.error("save failed", err))} // one write on commit
244
+ />
245
+
246
+ <h3>Recent Documents</h3>
247
+ >>>>>>> REPLACE
248
+ ```
249
+
250
+ If you genuinely need to persist mid-drag, debounce the `save()` so at most one write is in flight, and still `.catch()` the rejection. Better yet, prefer one document per event (see the Counter Pattern) over hammering a single hot document.
251
+
252
+ ### Query Data
253
+
254
+ Data is queried by sorted indexes defined by the application. Sort by strings, numbers, or booleans, as well as arrays for grouping. Use numbers when possible for sorting continuous data. You can use the `_id` field for temporal sorting so you don't have to write code to get simple recent document lists, as in the basic example above.
255
+
256
+ #### Query by Key Range
257
+
258
+ Passing a string to `useLiveQuery` will index by that field. Use the key argument to filter by a specific value, or range for bounded queries. Switching from temporal to filtered query:
259
+
260
+ App.jsx
261
+
262
+ ```jsx
263
+ <<<<<<< SEARCH
264
+ const { docs } = useLiveQuery("_id", { descending: true, limit: 100 });
265
+ =======
266
+ // all docs where doc.agentName === "agent-1", sorted by _id
267
+ const { docs } = useLiveQuery("agentName", { key: "agent-1" });
268
+ >>>>>>> REPLACE
269
+ ```
270
+
271
+ Or query a numeric range:
272
+
273
+ App.jsx
274
+
275
+ ```jsx
276
+ <<<<<<< SEARCH
277
+ const { docs } = useLiveQuery("agentName", { key: "agent-1" });
278
+ =======
279
+ // docs with agentRating between 3 and 5
280
+ const { docs } = useLiveQuery("agentRating", { range: [3, 5] });
281
+ >>>>>>> REPLACE
282
+ ```
283
+
284
+ #### Counter Pattern
285
+
286
+ Documents can be updated by multiple clients, and synced later. To create an event counter, don't increment a number on a single doc, instead write a small document per counted event, and query them with an index:
287
+
288
+ App.jsx
289
+
290
+ ```jsx
291
+ <<<<<<< SEARCH
292
+ const { docs } = useLiveQuery("agentRating", { range: [3, 5] });
293
+ =======
294
+ // The counter's scope: the board being viewed and who is counting (useVibe from "use-vibes", as above).
295
+ const { me } = useVibe("boards");
296
+ const boardId = `default-${me?.userHandle}`;
297
+ // Keyed on the board AND the event name, so one board's count never includes another board's events.
298
+ const { docs } = useLiveQuery((doc) => [doc.boardId, doc.counter], { key: [boardId, "my-event-name"] });
299
+ const counterValue = docs.length;
300
+
301
+ // Each counted event is its own record, and it names the board it belongs to and who wrote it.
302
+ function countEvent() {
303
+ database.put({ type: "count", counter: "my-event-name", boardId, authorHandle: me?.userHandle });
304
+ }
305
+ >>>>>>> REPLACE
306
+ ```
307
+
308
+ This pattern ensures the count is accurate even during sync — each event is its own document, so concurrent writes never conflict.
309
+
310
+ ### Custom Indexes
311
+
312
+ Use a custom index function to normalize and transform document data, for instance if you have both new and old document versions in your app:
313
+
314
+ App.jsx
315
+
316
+ ```jsx
317
+ <<<<<<< SEARCH
318
+ // The counter's scope: the board being viewed and who is counting (useVibe from "use-vibes", as above).
319
+ const { me } = useVibe("boards");
320
+ const boardId = `default-${me?.userHandle}`;
321
+ // Keyed on the board AND the event name, so one board's count never includes another board's events.
322
+ const { docs } = useLiveQuery((doc) => [doc.boardId, doc.counter], { key: [boardId, "my-event-name"] });
323
+ const counterValue = docs.length;
324
+
325
+ // Each counted event is its own record, and it names the board it belongs to and who wrote it.
326
+ function countEvent() {
327
+ database.put({ type: "count", counter: "my-event-name", boardId, authorHandle: me?.userHandle });
328
+ }
329
+ =======
330
+ const { docs } = useLiveQuery(
331
+ (doc) => {
332
+ if (doc.type == "listing_v1") return doc.sellerId;
333
+ else if (doc.type == "listing") return doc.userId;
334
+ },
335
+ { key: routeParams.sellerId }
336
+ );
337
+ >>>>>>> REPLACE
338
+ ```
339
+
340
+ #### Array Indexes and Prefix Queries
341
+
342
+ When you want to group rows easily, you can use an array index key. This is great for grouping records by year/month/day or other paths. The prefix query is a shorthand for a key range:
343
+
344
+ App.jsx
345
+
346
+ ```jsx
347
+ <<<<<<< SEARCH
348
+ const { docs } = useLiveQuery(
349
+ (doc) => {
350
+ if (doc.type == "listing_v1") return doc.sellerId;
351
+ else if (doc.type == "listing") return doc.userId;
352
+ },
353
+ { key: routeParams.sellerId }
354
+ );
355
+ =======
356
+ const { docs } = useLiveQuery(
357
+ (doc) => {
358
+ const date = new Date(doc.date);
359
+ if (Number.isNaN(date.getTime())) return; // return nothing to skip docs without a valid date
360
+ return [date.getFullYear(), date.getMonth(), date.getDate()];
361
+ },
362
+ { prefix: [2024, 11] } // everything from November 2024
363
+ );
364
+ >>>>>>> REPLACE
365
+ ```
366
+
367
+ #### Sortable Lists
368
+
369
+ Sortable lists are a common pattern. Use evenly spaced positions and insert between items using midpoint calculation:
370
+
371
+ App.jsx
372
+
373
+ ```jsx
374
+ <<<<<<< SEARCH
375
+ const { docs } = useLiveQuery(
376
+ (doc) => {
377
+ const date = new Date(doc.date);
378
+ if (Number.isNaN(date.getTime())) return;
379
+ return [date.getFullYear(), date.getMonth(), date.getDate()];
380
+ },
381
+ { prefix: [2024, 11] }
382
+ );
383
+ =======
384
+ // Query items on list xyz, sorted by position
385
+ // Note: useLiveQuery('listId', { key:'xyz' }) would be the same docs, sorted chronologically by _id
386
+ const { docs } = useLiveQuery((doc) => [doc.listId, doc.position], { prefix: ["xyz"] });
387
+
388
+ // Each item names the list it belongs to and who wrote it, both at create.
389
+ async function initializeList() {
390
+ await database.put({ type: "item", listId: "xyz", authorHandle: me?.userHandle, position: 1000 });
391
+ await database.put({ type: "item", listId: "xyz", authorHandle: me?.userHandle, position: 2000 });
392
+ await database.put({ type: "item", listId: "xyz", authorHandle: me?.userHandle, position: 3000 });
393
+ }
394
+
395
+ async function insertBetween(beforeDoc, afterDoc) {
396
+ const newPosition = (beforeDoc.position + afterDoc.position) / 2;
397
+ await database.put({ type: "item", listId: "xyz", authorHandle: me?.userHandle, position: newPosition });
398
+ }
399
+ >>>>>>> REPLACE
400
+ ```
401
+
402
+ ## Offline writes are on by default (`offlineQueue`)
403
+
404
+ 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. A refusal or 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 only when it has 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.
405
+
406
+ 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.
407
+
408
+ ```js
409
+ // Default (single-user apps): durable offline writes, no config needed.
410
+ const { useLiveQuery } = useFireproof("todos");
411
+
412
+ // Collaborative app: fail fast instead of queueing a stale write.
413
+ const { useLiveQuery } = useFireproof("shared-board", { offlineQueue: false });
414
+ ```
415
+
416
+ ## Anonymous writes (local-first by default)
417
+
418
+ 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 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.
419
+
420
+ 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.
421
+
422
+ ---
423
+
424
+ ## Architecture: Where's My Data?
425
+
426
+ 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 on ingest, persisted, and then synced to every viewer who may read it. 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 only when it has 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.
427
+
428
+ ## Using Fireproof in JavaScript
429
+
430
+ You can use the core API outside a React component — in a plain script or a helper module. Instead of hooks, import the core API directly:
431
+
432
+ App.jsx
433
+
434
+ ```jsx
435
+ <<<<<<< SEARCH
436
+ import React from "react";
437
+ import { useFireproof } from "use-fireproof";
438
+
439
+ export default function App() {
440
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
441
+ =======
442
+ import { fireproof } from "use-fireproof";
443
+
444
+ const database = fireproof("myLedger");
445
+
446
+ // The document API is async — reads and writes both await the server.
447
+ // Writes can fail (refused, conflict, network), so wrap them.
448
+ async function main() {
449
+ try {
450
+ const ok = await database.put({ text: "Sample Data" });
451
+ const doc = await database.get(ok.id);
452
+ const latest = await database.query("_id", { limit: 10, descending: true });
453
+ console.log("Latest documents:", latest.docs);
454
+ } catch (err) {
455
+ console.error("write failed (refused, conflict, or network):", err);
456
+ }
457
+ }
458
+ >>>>>>> REPLACE
459
+ ```
460
+
461
+ ### Working with Files
462
+
463
+ Fireproof documents carry attachments under `_files`. Save a `File` (or `Blob`) by assigning it to a key on `_files`, and Fireproof handles upload, durable storage, and URL minting for you. After a doc round-trips through the database, each `_files.<key>` entry carries a stable `url` you can drop straight into `<img>`, `<video>`, `<audio>`, CSS `background-image`, etc.
464
+
465
+ Each `_files.<key>` entry shape after save + round-trip: `{ url: string, type: string, size: number, lastModified?: number, file: () => Promise<File> }`. The platform-minted `url` is stable for the lifetime of that file, so the browser cache works normally. For plain `<img>` rendering, prefer `meta.url` — it skips one fetch and lets the browser handle cache and decoding. Use `meta.file()` only when you need the bytes themselves (transcoding, hashing, ML features).
466
+
467
+ Building an image uploader with `_files`:
468
+
469
+ App.jsx
470
+
471
+ ```jsx
472
+ import React from "react";
473
+ import { useFireproof } from "use-fireproof";
474
+
475
+ export default function App() {
476
+ const { useDocument, useLiveQuery } = useFireproof("imageUploads");
477
+
478
+ const { doc, merge, submit } = useDocument({
479
+ _files: {},
480
+ caption: "",
481
+ type: "upload",
482
+ createdAt: Date.now(),
483
+ });
484
+
485
+ const { docs } = useLiveQuery("type", { key: "upload", descending: true, limit: 12 });
486
+
487
+ const onPickFile = (e) => {
488
+ const f = e.target.files?.[0];
489
+ if (f) merge({ _files: { photo: f } });
490
+ };
491
+
492
+ const onSubmit = (e) => {
493
+ e.preventDefault();
494
+ if (!doc._files?.photo) return;
495
+ submit();
496
+ };
497
+
498
+ const c = {
499
+ bg: "bg-white",
500
+ card: "bg-gray-50",
501
+ border: "border-gray-200",
502
+ accent: "bg-blue-500 hover:bg-blue-600",
503
+ text: "text-gray-700",
504
+ };
505
+
506
+ return (
507
+ <div className={`p-6 max-w-lg mx-auto ${c.bg} shadow-lg rounded-lg`}>
508
+ <h2 className="text-2xl font-bold mb-4">Image Uploader</h2>
509
+ <form onSubmit={onSubmit} className="space-y-3">
510
+ <input type="file" accept="image/*" onChange={onPickFile} className={`w-full ${c.border} border rounded p-2`} />
511
+ <input
512
+ type="text"
513
+ placeholder="Caption"
514
+ value={doc.caption}
515
+ onChange={(e) => merge({ caption: e.target.value })}
516
+ className={`w-full ${c.border} border rounded p-2`}
517
+ />
518
+ <button type="submit" className={`px-4 py-2 ${c.accent} text-white rounded`}>
519
+ Upload
520
+ </button>
521
+ </form>
522
+
523
+ <h3 className="text-lg font-semibold mt-6">Recent Uploads</h3>
524
+ <div className="grid grid-cols-2 gap-4 mt-2">
525
+ {docs.map((d) => (
526
+ <div key={d._id} className={`${c.border} border p-2 rounded shadow-sm ${c.card}`}>
527
+ {d._files?.photo?.url && <img src={d._files.photo.url} alt={d.caption || "upload"} className="w-full h-auto rounded" />}
528
+ <p className={`text-sm ${c.text} mt-2`}>{d.caption || "No caption"}</p>
529
+ </div>
530
+ ))}
531
+ </div>
532
+ </div>
533
+ );
534
+ }
535
+ ```
536
+
537
+ For multi-file uploads (e.g. `<input multiple>`), build the `_files` map keyed by filename and iterate with `Object.entries(doc._files)` to render each entry.
538
+
539
+ Adding multi-file support:
540
+
541
+ App.jsx
542
+
543
+ ```jsx
544
+ <<<<<<< SEARCH
545
+ const onPickFile = (e) => {
546
+ const f = e.target.files?.[0];
547
+ if (f) merge({ _files: { photo: f } });
548
+ };
549
+
550
+ const onSubmit = (e) => {
551
+ e.preventDefault();
552
+ if (!doc._files?.photo) return;
553
+ submit();
554
+ };
555
+ =======
556
+ const onPickFile = (e) => {
557
+ const next = {};
558
+ for (const f of e.target.files) next[f.name] = f;
559
+ merge({ _files: next });
560
+ };
561
+
562
+ const onSubmit = (e) => {
563
+ e.preventDefault();
564
+ if (!Object.keys(doc._files || {}).length) return;
565
+ submit();
566
+ };
567
+ >>>>>>> REPLACE
568
+ ```
569
+
570
+ ### Form Validation
571
+
572
+ You can use React's `useState` to manage validation states and error messages. Validate inputs at the UI level before allowing submission. Adding validation to the uploader:
573
+
574
+ App.jsx
575
+
576
+ ```jsx
577
+ <<<<<<< SEARCH
578
+ const onSubmit = (e) => {
579
+ e.preventDefault();
580
+ if (!Object.keys(doc._files || {}).length) return;
581
+ submit();
582
+ };
583
+ =======
584
+ const [errors, setErrors] = React.useState({});
585
+
586
+ function validateForm() {
587
+ const newErrors = {};
588
+ if (!doc.caption.trim()) newErrors.caption = "Caption is required.";
589
+ if (!Object.keys(doc._files || {}).length) newErrors.file = "Pick a file.";
590
+ setErrors(newErrors);
591
+ return Object.keys(newErrors).length === 0;
592
+ }
593
+
594
+ const onSubmit = (e) => {
595
+ e.preventDefault();
596
+ if (validateForm()) submit();
597
+ };
598
+ >>>>>>> REPLACE
599
+ ```
600
+
601
+ ## Example React Application
602
+
603
+ Code listing for todo tracker App.jsx. Note the code ordering: hooks, then handlers, then classNames right before JSX.
604
+
605
+ ```js
606
+ import React from "react";
607
+ import { useFireproof } from "use-fireproof";
608
+ import { useVibe } from "use-vibes";
609
+
610
+ export default function App() {
611
+ // 1. Hooks and document shapes
612
+ const { useLiveQuery, useDocument, database } = useFireproof("todoList");
613
+ const { me } = useVibe("todoList");
614
+
615
+ // Every person has a list of their own without creating one first.
616
+ const listId = `default-${me?.userHandle}`;
617
+
618
+ const {
619
+ doc: newTodo,
620
+ merge: mergeNewTodo,
621
+ submit: submitNewTodo,
622
+ } = useDocument({
623
+ todo: "",
624
+ type: "todo",
625
+ listId, // the list this todo belongs to, on the todo itself
626
+ authorHandle: me?.userHandle,
627
+ completed: false,
628
+ createdAt: Date.now(),
629
+ });
630
+
631
+ const { docs: todos } = useLiveQuery("listId", {
632
+ key: listId,
633
+ descending: true,
634
+ });
635
+
636
+ // 2. Event handlers
637
+ const handleInputChange = (e) => {
638
+ mergeNewTodo({ todo: e.target.value });
639
+ };
640
+
641
+ const handleSubmit = (e) => {
642
+ e.preventDefault();
643
+ submitNewTodo();
644
+ };
645
+
646
+ // 3. ClassNames — right before JSX so colors stay consistent
647
+ const c = {
648
+ bg: "bg-white",
649
+ card: "bg-gray-50",
650
+ border: "border-gray-200",
651
+ accent: "bg-[#e63946]",
652
+ text: "text-gray-500",
653
+ };
654
+
655
+ // 4. JSX return
656
+ return (
657
+ <div className={`max-w-md mx-auto p-4 ${c.bg} shadow rounded`}>
658
+ <h2 className="text-2xl font-bold mb-4">Todo List</h2>
659
+ <form onSubmit={handleSubmit} className="mb-4">
660
+ <label htmlFor="todo" className="block mb-2 font-semibold">
661
+ Todo
662
+ </label>
663
+ <input
664
+ className={`w-full ${c.border} border rounded px-2 py-1`}
665
+ id="todo"
666
+ type="text"
667
+ onChange={handleInputChange}
668
+ value={newTodo.todo}
669
+ />
670
+ </form>
671
+ <ul className="space-y-3">
672
+ {todos.map((doc) => (
673
+ <li className={`flex flex-col items-start p-2 ${c.border} border rounded ${c.card}`} key={doc._id}>
674
+ <div className="flex items-center justify-between w-full">
675
+ <div className="flex items-center">
676
+ <input
677
+ className="mr-2"
678
+ type="checkbox"
679
+ checked={doc.completed}
680
+ onChange={() => database.put({ ...doc, completed: !doc.completed })}
681
+ />
682
+ <span className="font-medium">{doc.todo}</span>
683
+ </div>
684
+ <button className={`text-sm ${c.accent} text-white px-2 py-1 rounded`} onClick={() => database.del(doc._id)}>
685
+ Delete
686
+ </button>
687
+ </div>
688
+ <div className={`text-xs ${c.text} mt-1`}>{new Date(doc.createdAt).toISOString()}</div>
689
+ </li>
690
+ ))}
691
+ </ul>
692
+ </div>
693
+ );
694
+ }
695
+ ```
696
+
697
+ IMPORTANT: Don't use `useState()` on form data, instead use `merge()` and `submit()` from `useDocument`. Only use `useState` for ephemeral UI state (active tabs, open/closed panels, cursor positions). Keep your data model in Fireproof.