@vibes.diy/prompts 2.4.13 → 2.4.15

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
@@ -15,25 +15,7 @@ Fireproof enforces cryptographic causal consistency and ledger integrity using h
15
15
 
16
16
  ## Installation
17
17
 
18
- The `use-fireproof` package provides both the core API and React hooks.
19
-
20
- ```js
21
- import { useFireproof } from "use-fireproof";
22
- ```
23
-
24
- React hooks are the recommended way to use Fireproof in LLM code generation contexts.
25
-
26
- #### Create or Load a Database
27
-
28
- Fireproof databases store data across sessions and can sync in real-time. 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.
29
-
30
- ```js
31
- import { useFireproof } from "use-fireproof";
32
-
33
- const { database, useLiveQuery, useDocument } = useFireproof("myLedger");
34
- ```
35
-
36
- #### Put and Get Documents
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 store data across sessions and can sync in real-time. 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.
37
19
 
38
20
  Each document has an `_id`, which can be auto-generated or set explicitly. Auto-generation is recommended to ensure uniqueness and avoid conflicts. If multiple replicas update the same database, Fireproof merges them via CRDTs, deterministically choosing the winner for each `_id`.
39
21
 
@@ -43,15 +25,20 @@ Fireproof is a local database, no loading states required, just empty data state
43
25
 
44
26
  ### Basic Example
45
27
 
46
- This example shows Fireproof's `_id` allows easy sorting with `useLiveQuery`.
28
+ This complete app shows Fireproof's core: `useFireproof` gives you hooks, `useDocument` manages form state, and `useLiveQuery` sorts by `_id` for temporal ordering.
47
29
 
48
- ```js
49
- const App = () => {
30
+ App.jsx
31
+
32
+ ```jsx
33
+ import React from "react";
34
+ import { useFireproof } from "use-fireproof";
35
+
36
+ export default function App() {
50
37
  const { useDocument, useLiveQuery } = useFireproof("myLedger");
51
38
 
52
39
  const { doc, merge, submit } = useDocument({ text: "" });
53
40
 
54
- // _id is roughly temporal, this is most recent
41
+ // _id is roughly temporal, this is most recent first
55
42
  const { docs } = useLiveQuery("_id", { descending: true, limit: 100 });
56
43
 
57
44
  return (
@@ -69,7 +56,7 @@ const App = () => {
69
56
  </ul>
70
57
  </div>
71
58
  );
72
- };
59
+ }
73
60
  ```
74
61
 
75
62
  The access function lives in a separate file. Even simple apps include one — it's the server-side authority for who can write:
@@ -85,166 +72,190 @@ export default function (doc, oldDoc, user) {
85
72
 
86
73
  ### Editing Documents
87
74
 
88
- 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.
75
+ 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:
89
76
 
90
- ```js
91
- const { useDocument } = useFireproof("myLedger");
92
-
93
- const { doc, merge, submit, save, reset } = useDocument({ _id: "user-profile:abc@example.com" });
94
- ```
95
-
96
- The `useDocument` hook provides several methods:
97
-
98
- - `merge(updates)`: Update the document with new fields, without saving. Use this instead of keeping a `useState` for document data.
99
- - `submit(e)`: Handles form submission by preventing default, saving, and resetting
100
- - `save()`: Save the current document state
101
- - `reset()`: Reset to initial state
77
+ App.jsx
102
78
 
103
- For form-based creation flows, use `submit`:
79
+ ```jsx
80
+ <<<<<<< SEARCH
81
+ const { doc, merge, submit } = useDocument({ text: "" });
82
+ =======
83
+ const { doc, merge, submit } = useDocument({ text: "" });
104
84
 
105
- ```js
106
- <form onSubmit={submit}>
85
+ const { doc: profile, merge: mergeProfile, save: saveProfile } = useDocument({ _id: "user-profile:abc@example.com" });
86
+ >>>>>>> REPLACE
107
87
  ```
108
88
 
109
- 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.
89
+ 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.
110
90
 
111
91
  ### Updating Documents in Event Handlers
112
92
 
113
- 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.
93
+ 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:
114
94
 
115
- ```js
116
- // ✅ Correct — use database.put() in handlers
117
- onClick={() => database.put({ ...doc, favorite: !doc.favorite })}
95
+ App.jsx
118
96
 
119
- // ❌ Wrong — never call hooks inside handlers
120
- function toggleFavorite(id) {
121
- const { doc, save } = useDocument({ _id: id }) // BREAKS Rules of Hooks
122
- }
97
+ ```jsx
98
+ <<<<<<< SEARCH
99
+ const { useDocument, useLiveQuery } = useFireproof("myLedger");
100
+ =======
101
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
102
+ >>>>>>> REPLACE
123
103
  ```
124
104
 
125
- ### Query Data
105
+ App.jsx
106
+
107
+ ```jsx
108
+ <<<<<<< SEARCH
109
+ <li key={doc._id}>{doc.text}</li>
110
+ =======
111
+ <li key={doc._id}>
112
+ {doc.text}
113
+ <button onClick={() => database.put({ ...doc, favorite: !doc.favorite })}>
114
+ {doc.favorite ? "★" : "☆"}
115
+ </button>
116
+ </li>
117
+ >>>>>>> REPLACE
118
+ ```
126
119
 
127
- 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.
120
+ Never call hooks inside handlers `const { doc, save } = useDocument({ _id: id })` inside an onClick BREAKS the Rules of Hooks.
128
121
 
129
- 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.
122
+ ### Query Data
130
123
 
131
- Here are other common patterns:
124
+ 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.
132
125
 
133
126
  #### Query by Key Range
134
127
 
135
- Passing a string to `useLiveQuery` will index by that field. You can use the key argument to filter by a specific value:
128
+ 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:
136
129
 
137
- ```js
138
- const { docs } = useLiveQuery("agentName", {
139
- key: "agent-1", // all docs where doc.agentName === "agent-1", sorted by _id
140
- });
130
+ App.jsx
131
+
132
+ ```jsx
133
+ <<<<<<< SEARCH
134
+ const { docs } = useLiveQuery("_id", { descending: true, limit: 100 });
135
+ =======
136
+ // all docs where doc.agentName === "agent-1", sorted by _id
137
+ const { docs } = useLiveQuery("agentName", { key: "agent-1" });
138
+ >>>>>>> REPLACE
141
139
  ```
142
140
 
143
- You can also query a range within a key:
141
+ Or query a numeric range:
144
142
 
145
- ```js
146
- const { docs } = useLiveQuery("agentRating", {
147
- range: [3, 5],
148
- });
143
+ App.jsx
144
+
145
+ ```jsx
146
+ <<<<<<< SEARCH
147
+ const { docs } = useLiveQuery("agentName", { key: "agent-1" });
148
+ =======
149
+ // docs with agentRating between 3 and 5
150
+ const { docs } = useLiveQuery("agentRating", { range: [3, 5] });
151
+ >>>>>>> REPLACE
149
152
  ```
150
153
 
151
154
  #### Counter Pattern
152
155
 
153
- 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. Example:
156
+ 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:
154
157
 
155
- ```js
156
- const App = () => {
157
- const { useLiveQuery, database } = useFireproof("myLedger");
158
+ App.jsx
158
159
 
160
+ ```jsx
161
+ <<<<<<< SEARCH
162
+ const { docs } = useLiveQuery("agentRating", { range: [3, 5] });
163
+ =======
159
164
  const { docs } = useLiveQuery("counter", { key: "my-event-name" });
160
165
  const counterValue = docs.length;
161
166
 
162
167
  function countEvent() {
163
- database.put({
164
- counter: "my-event-name",
165
- });
168
+ database.put({ counter: "my-event-name" });
166
169
  }
167
-
168
- // Call countEvent() to count each event, and render counterValue in the UI.
169
- };
170
+ >>>>>>> REPLACE
170
171
  ```
171
172
 
172
- This pattern ensures the count is accurate even during sync.
173
+ This pattern ensures the count is accurate even during sync — each event is its own document, so concurrent writes never conflict.
173
174
 
174
175
  ### Custom Indexes
175
176
 
176
- 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.
177
+ 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:
177
178
 
178
- ```js
179
- const { docs } = useLiveQuery(
180
- (doc) => {
181
- if (doc.type == "listing_v1") {
182
- return doc.sellerId;
183
- } else if (doc.type == "listing") {
184
- return doc.userId;
185
- }
186
- },
187
- { key: routeParams.sellerId }
188
- );
179
+ App.jsx
180
+
181
+ ```jsx
182
+ <<<<<<< SEARCH
183
+ const { docs } = useLiveQuery("counter", { key: "my-event-name" });
184
+ const counterValue = docs.length;
185
+
186
+ function countEvent() {
187
+ database.put({ counter: "my-event-name" });
188
+ }
189
+ =======
190
+ const { docs } = useLiveQuery(
191
+ (doc) => {
192
+ if (doc.type == "listing_v1") return doc.sellerId;
193
+ else if (doc.type == "listing") return doc.userId;
194
+ },
195
+ { key: routeParams.sellerId }
196
+ );
197
+ >>>>>>> REPLACE
189
198
  ```
190
199
 
191
200
  #### Array Indexes and Prefix Queries
192
201
 
193
- When you want to group rows easily, you can use an array index key. This is great for grouping records my year / month / day or other paths. In this example the prefix query is a shorthand for a key range, loading everything from November 2024:
202
+ 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:
194
203
 
195
- ```js
196
- const queryResult = useLiveQuery(
197
- (doc) => {
198
- const date = new Date(doc.date);
199
- if (Number.isNaN(date.getTime())) return; // return nothing to skip docs without a valid date
200
- return [date.getFullYear(), date.getMonth(), date.getDate()];
201
- },
202
- { prefix: [2024, 11] }
203
- );
204
+ App.jsx
205
+
206
+ ```jsx
207
+ <<<<<<< SEARCH
208
+ const { docs } = useLiveQuery(
209
+ (doc) => {
210
+ if (doc.type == "listing_v1") return doc.sellerId;
211
+ else if (doc.type == "listing") return doc.userId;
212
+ },
213
+ { key: routeParams.sellerId }
214
+ );
215
+ =======
216
+ const { docs } = useLiveQuery(
217
+ (doc) => {
218
+ const date = new Date(doc.date);
219
+ if (Number.isNaN(date.getTime())) return; // return nothing to skip docs without a valid date
220
+ return [date.getFullYear(), date.getMonth(), date.getDate()];
221
+ },
222
+ { prefix: [2024, 11] } // everything from November 2024
223
+ );
224
+ >>>>>>> REPLACE
204
225
  ```
205
226
 
206
227
  #### Sortable Lists
207
228
 
208
- Sortable lists are a common pattern. Here's how to implement them using Fireproof:
229
+ Sortable lists are a common pattern. Use evenly spaced positions and insert between items using midpoint calculation:
209
230
 
210
- ```js
211
- function App() {
212
- const { database, useLiveQuery } = useFireproof("myLedger");
231
+ App.jsx
232
+
233
+ ```jsx
234
+ <<<<<<< SEARCH
235
+ const { docs } = useLiveQuery(
236
+ (doc) => {
237
+ const date = new Date(doc.date);
238
+ if (Number.isNaN(date.getTime())) return;
239
+ return [date.getFullYear(), date.getMonth(), date.getDate()];
240
+ },
241
+ { prefix: [2024, 11] }
242
+ );
243
+ =======
244
+ // Query items on list xyz, sorted by position
245
+ // Note: useLiveQuery('list', { key:'xyz' }) would be the same docs, sorted chronologically by _id
246
+ const { docs } = useLiveQuery((doc) => [doc.list, doc.position], { prefix: ["xyz"] });
213
247
 
214
- // Initialize list with evenly spaced positions
215
248
  async function initializeList() {
216
249
  await database.put({ list: "xyz", position: 1000 });
217
250
  await database.put({ list: "xyz", position: 2000 });
218
251
  await database.put({ list: "xyz", position: 3000 });
219
252
  }
220
253
 
221
- // Query items on list xyz, sorted by position. Note that useLiveQuery('list', { key:'xyz' }) would be the same docs, sorted chronologically by _id
222
- const queryResult = useLiveQuery((doc) => [doc.list, doc.position], { prefix: ["xyz"] });
223
-
224
- // Insert between existing items using midpoint calculation
225
254
  async function insertBetween(beforeDoc, afterDoc) {
226
255
  const newPosition = (beforeDoc.position + afterDoc.position) / 2;
227
- await database.put({
228
- list: "xyz",
229
- position: newPosition,
230
- });
256
+ await database.put({ list: "xyz", position: newPosition });
231
257
  }
232
-
233
- return (
234
- <div>
235
- <h3>List xyz (Sorted)</h3>
236
- <ul>
237
- {queryResult.docs.map((doc) => (
238
- <li key={doc._id}>
239
- {doc._id}: position {doc.position}
240
- </li>
241
- ))}
242
- </ul>
243
- <button onClick={initializeList}>Initialize List</button>
244
- <button onClick={() => insertBetween(queryResult.docs[1], queryResult.docs[2])}>Insert new doc at 3rd position</button>
245
- </div>
246
- );
247
- }
258
+ >>>>>>> REPLACE
248
259
  ```
249
260
 
250
261
  ## Per-Database Access Control (`acl` option)
@@ -252,21 +263,17 @@ function App() {
252
263
  On vibes.diy, `useFireproof` accepts an optional `acl` argument that declares who can read, write, or delete documents in that database. The ACL is stored server-side and enforced on every operation — no separate API call needed.
253
264
  Only use the `acl` option when the user explicitly asks for fine-grained access control (or equivalent permission constraints).
254
265
 
255
- ```jsx
256
- import { useFireproof } from "use-vibes";
257
-
258
- // Anyone with a grant can post; only editors can delete
259
- const { useLiveQuery, database } = useFireproof("announcements", {
260
- acl: { write: ["members"], delete: ["editors"] },
261
- });
262
-
263
- // Editors-only space — viewers cannot read at all
264
- const { useLiveQuery, database } = useFireproof("drafts", {
265
- acl: { read: ["editors"], write: ["editors"], delete: ["editors"] },
266
- });
266
+ App.jsx
267
267
 
268
- // No acl — falls back to app-level role gates (existing behavior, always safe)
269
- const { useLiveQuery, database } = useFireproof("publicNotes");
268
+ ```jsx
269
+ <<<<<<< SEARCH
270
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
271
+ =======
272
+ // Anyone with a grant can post; only editors can delete
273
+ const { useLiveQuery, database } = useFireproof("announcements", {
274
+ acl: { write: ["members"], delete: ["editors"] },
275
+ });
276
+ >>>>>>> REPLACE
270
277
  ```
271
278
 
272
279
  **Subject groups** — who each name covers:
@@ -282,60 +289,53 @@ Owner is always implicitly included — never list `owner` explicitly in an ACL.
282
289
 
283
290
  Each capability (`read`, `write`, `delete`) is independent. Omitting one falls back to the app-level role gate for that operation. The `acl` is sent once on first database open and persists across sessions (last-write-wins). Only the **app owner** can set ACLs; non-owner apps opening a database with an `acl` option have it silently ignored — the database still opens and works normally.
284
291
 
285
- ## Reading Resolved Grants (`access`)
286
-
287
- `useFireproof()` returns an `access` property — the viewer's resolved roles and channels for that database, computed server-side from the access function's `members` and `grant` declarations.
288
-
289
- ```jsx
290
- const { database, useLiveQuery, access } = useFireproof("comments");
292
+ Other `acl` variants: `useFireproof("drafts", { acl: { read: ["editors"], write: ["editors"], delete: ["editors"] } })` for editors-only space, or omit `acl` entirely to fall back to app-level role gates (existing behavior, always safe).
291
293
 
292
- access.roles; // ReadonlySet<string> roles the viewer belongs to
293
- access.channels; // ReadonlySet<string> — channels the viewer can access
294
+ ## Reading Resolved Grants (`access`)
294
295
 
295
- access.hasRole("moderator"); // boolean convenience
296
- access.hasChannel("engineering"); // boolean convenience
297
- ```
296
+ `useFireproof()` returns an `access` property — the viewer's resolved roles and channels for that database, computed server-side from the access function's `members` and `grant` declarations. Use `access.roles` (ReadonlySet), `access.channels` (ReadonlySet), `access.hasRole(name)`, and `access.hasChannel(name)`.
298
297
 
299
298
  For databases without an access function export, `access` has empty roles and channels. No separate pending flag — grants arrive alongside the viewer identity, so `useViewer().isViewerPending` covers both.
300
299
 
300
+ App.jsx
301
+
301
302
  ```jsx
302
- function App() {
303
- const { viewer, isViewerPending, ViewerTag } = useViewer();
303
+ <<<<<<< SEARCH
304
+ const { useLiveQuery, database } = useFireproof("announcements", {
305
+ acl: { write: ["members"], delete: ["editors"] },
306
+ });
307
+ =======
304
308
  const { database, useLiveQuery, access } = useFireproof("comments");
309
+ >>>>>>> REPLACE
310
+ ```
305
311
 
306
- if (isViewerPending) return null;
312
+ App.jsx
307
313
 
308
- return (
309
- <div>
310
- <ViewerTag />
314
+ ```jsx
315
+ <<<<<<< SEARCH
316
+ <h3>Recent Documents</h3>
317
+ <ul>
318
+ {docs.map((doc) => (
319
+ <li key={doc._id}>
320
+ {doc.text}
321
+ <button onClick={() => database.put({ ...doc, favorite: !doc.favorite })}>
322
+ {doc.favorite ? "★" : "☆"}
323
+ </button>
324
+ </li>
325
+ ))}
326
+ </ul>
327
+ =======
311
328
  {access.hasRole("poster") && <CommentForm database={database} />}
312
329
  {access.hasRole("moderator") && <ModTools database={database} />}
313
330
  {access.hasChannel("announcements") && <Announcements />}
314
- </div>
315
- );
316
- }
331
+ >>>>>>> REPLACE
317
332
  ```
318
333
 
319
334
  The AI agent writes the access function (so it knows the role names) and writes the UI (so it knows which roles gate which components). The `access` object is the bridge — it lets the UI reflect server-enforced permissions without duplicating the logic.
320
335
 
321
336
  The access function is the single source of truth for permissions. The UI reads its verdict from `access` — gate with `access.hasChannel(name)` and `access.hasRole(name)`. Grants may reflect logic the UI can't see (other documents, role memberships, owner state), so `access` is always the right check.
322
337
 
323
- `access.hasChannel()` covers every grant path — public channels, restricted channels, role-expanded channels. The access function decides who gets access and how; the UI just asks `access.hasChannel(name)`:
324
-
325
- ```jsx
326
- // Channel list — access.hasChannel covers both public and restricted channels
327
- const visibleChannels = channels.filter(ch => access.hasChannel(ch._id));
328
-
329
- // Compose gate — access.hasChannel is the only check needed
330
- {viewer && access.hasChannel(activeChannel) && <ComposeForm />}
331
-
332
- // Channel with mixed access modes — the UI doesn't need to know which mode
333
- // The access function granted public channels via grant.public and
334
- // restricted channels via grant.users — access.hasChannel handles both
335
- {visibleChannels.map(ch => (
336
- <li key={ch._id} onClick={() => setActive(ch._id)}>#{ch.name}</li>
337
- ))}
338
- ```
338
+ `access.hasChannel()` covers every grant path — public channels, restricted channels, role-expanded channels. The access function decides who gets access and how; the UI just asks `access.hasChannel(name)`.
339
339
 
340
340
  ### Complete example: Team announcements with channels
341
341
 
@@ -478,30 +478,32 @@ export function chat(doc, oldDoc, user, ctx) {
478
478
  }
479
479
  ```
480
480
 
481
- App.jsx — the UI uses `access.hasChannel()` for everything. It has no idea which channels are restricted — that's the access function's job.
481
+ App.jsx — the UI uses `access.hasChannel()` for everything. It has no idea which channels are restricted — that's the access function's job:
482
482
 
483
483
  ```jsx
484
- const { database, useLiveQuery, access } = useFireproof("chat");
485
- const { docs: channels } = useLiveQuery("type", { key: "channel" });
486
-
487
- // client creates channels with a deterministic _id
488
- await database.put({ _id: "ch:" + name, type: "channel", createdAt: Date.now() });
489
-
490
- // filter to channels the viewer can see — use _id as channel identifier
491
- const visible = channels.filter((ch) => isOwner || access.hasChannel(ch._id));
484
+ <<<<<<< SEARCH
485
+ const { database, useLiveQuery, access } = useFireproof("announcements");
486
+ =======
487
+ const { database, useLiveQuery, access } = useFireproof("chat");
488
+ const { docs: channels } = useLiveQuery("type", { key: "channel" });
489
+ >>>>>>> REPLACE
490
+ ```
492
491
 
493
- // can post? just check channel access
494
- const canPost = viewer && (isOwner || access.hasChannel(activeChannel._id));
492
+ ```jsx
493
+ <<<<<<< SEARCH
494
+ {viewer && access.hasChannel(channel) && (
495
+ =======
496
+ {/* filter to channels the viewer can see — use _id as channel identifier */}
497
+ {channels.filter((ch) => isOwner || access.hasChannel(ch._id)).map((ch) => (
498
+ <button key={ch._id} onClick={() => setChannel(ch._id)}>{ch.name}</button>
499
+ ))}
495
500
 
496
- // gate the compose form
497
- {canPost ? (
498
- <Composer channel={activeChannel._id} />
499
- ) : (
500
- <p>{viewer ? "Read-only in this channel." : "Sign in to post."}</p>
501
- )}
501
+ {/* can post? just check channel access */}
502
+ {viewer && (isOwner || access.hasChannel(channel)) && (
503
+ >>>>>>> REPLACE
502
504
  ```
503
505
 
504
- Channel `_id` is the channel identifier everywhere. The `canPost` check uses `access.hasChannel(ch._id)`. The access function uses `doc._id` for routing and grants. A deterministic `_id` like `"ch:" + name` enforces uniqueness — two users can't create duplicate channels.
506
+ Channel `_id` is the channel identifier everywhere. The access function uses `doc._id` for routing and grants. A deterministic `_id` like `"ch:" + name` enforces uniqueness — two users can't create duplicate channels.
505
507
 
506
508
  ---
507
509
 
@@ -511,48 +513,13 @@ Access functions are **the room** — they govern what members can do with data
511
513
 
512
514
  Access functions live in `/access.js`, a separate file in the vibe's filesystem alongside `/App.jsx`. **Always emit the access function as a block preceded by the filename `access.js` on its own line — never inside an `App.jsx` block.** Each **named export** maps to a database name — `export function chat(...)` gates `useFireproof("chat")`. An `export default` function acts as a catch-all: it gates any database that doesn't have its own named export. Named exports always take precedence over the default.
513
515
 
514
- access.js
515
- ```js
516
- export function chat(doc, oldDoc, user, ctx) {
517
- if (!user) throw { forbidden: "authentication required" };
518
- if (doc.type === "message") {
519
- if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
520
- ctx.requireAccess(doc.channelId);
521
- return { channels: [doc.channelId] };
522
- }
523
- return {};
524
- }
525
- ```
526
-
527
- App.jsx
528
- ```jsx
529
- const { useLiveQuery, database } = useFireproof("chat");
530
- ```
531
-
532
516
  ### Function signature
533
517
 
534
- ```ts
535
- (doc, oldDoc, user: UserContext | null, ctx: Helpers) => AccessDescriptor;
536
- ```
537
-
538
- - `doc` — the document being written
539
- - `oldDoc` — the previous version (null for new documents)
540
- - `user` — the authenticated user, or `null` for anonymous requests
541
- - `ctx` — server-provided helpers for checking materialized state
542
-
543
- **UserContext:**
518
+ `(doc, oldDoc, user: UserContext | null, ctx: Helpers) => AccessDescriptor` where `doc` is the document being written, `oldDoc` is the previous version (null for new documents), `user` is the authenticated user or `null` for anonymous requests, and `ctx` provides server helpers for checking materialized state.
544
519
 
545
- ```ts
546
- {
547
- userHandle: string // stable unique id — use for all auth checks
548
- displayName?: string // display only — never use for identity checks
549
- }
550
- ```
520
+ **UserContext:** `{ userHandle: string, displayName?: string }` — `userHandle` is stable unique id (use for all auth checks), `displayName` is display only (never use for identity checks).
551
521
 
552
- **Helpers (`ctx`):** Opaque closures over the materialized grant state. They throw or pass — you cannot enumerate channels, list members, or iterate grants. Both helpers also throw when `user` is null.
553
-
554
- - `ctx.requireAccess(channelId)` — throws if user is not in the channel
555
- - `ctx.requireRole(roleName)` — throws if user is not in the role
522
+ **Helpers (`ctx`):** Opaque closures over the materialized grant state. They throw or pass — you cannot enumerate channels, list members, or iterate grants. Both helpers also throw when `user` is null: `ctx.requireAccess(channelId)` throws if user is not in the channel, `ctx.requireRole(roleName)` throws if user is not in the role.
556
523
 
557
524
  ### AccessDescriptor return type
558
525
 
@@ -589,6 +556,7 @@ type AccessDescriptor = {
589
556
  ### Example: Workspace chat with channels
590
557
 
591
558
  access.js
559
+
592
560
  ```js
593
561
  export function chat(doc, oldDoc, user, ctx) {
594
562
  if (!user) throw { forbidden: "authentication required" };
@@ -623,15 +591,12 @@ export function chat(doc, oldDoc, user, ctx) {
623
591
  }
624
592
  ```
625
593
 
626
- This single access function handles three document types:
627
-
628
- - **channel-meta** — owner creates a channel and grants access to listed members
629
- - **message** — only the author can post, must already have channel access
630
- - **channel-invite** — any channel member can invite others; deleting the invite revokes the grant
594
+ This single access function handles three document types: **channel-meta** — owner creates a channel and grants access to listed members, **message** — only the author can post, must already have channel access, **channel-invite** — any channel member can invite others; deleting the invite revokes the grant.
631
595
 
632
596
  ### Example: Anonymous survey with role-gated results
633
597
 
634
598
  access.js
599
+
635
600
  ```js
636
601
  export function survey(doc, oldDoc, user, ctx) {
637
602
  if (doc.type === "survey-response") {
@@ -660,18 +625,14 @@ export function survey(doc, oldDoc, user, ctx) {
660
625
  }
661
626
  ```
662
627
 
663
- Key patterns:
664
-
665
- - `allowAnonymous: true` on survey-response lets unauthenticated visitors submit
666
- - Requiring `doc._id` to be falsy prevents clients from choosing or overwriting response IDs
667
- - `grant.public` on final-results makes them readable by any member without a specific channel grant
668
- - The **singleton grant doc** pattern (survey-config) wires role-to-channel access in one place
628
+ Key patterns: `allowAnonymous: true` on survey-response lets unauthenticated visitors submit, `grant.public` on final-results makes them readable by any member without a specific channel grant, and the **singleton grant doc** pattern (survey-config) wires role-to-channel access in one place.
669
629
 
670
630
  ### Multiple databases in one file
671
631
 
672
632
  Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
673
633
 
674
634
  access.js
635
+
675
636
  ```js
676
637
  export function chat(doc, oldDoc, user, ctx) {
677
638
  if (!user) throw { forbidden: "authentication required" };
@@ -690,6 +651,7 @@ Databases without a matching named export fall through to `export default` if on
690
651
  **Hyphenated database names** are rare — prefer camelCase (`useFireproof("crewChat")`). If you inherit a hyphenated name, use `export { localName as "db-name" }` to map a local function:
691
652
 
692
653
  access.js
654
+
693
655
  ```js
694
656
  function crewChat(doc, oldDoc, user, ctx) {
695
657
  if (!user) throw { forbidden: "authentication required" };
@@ -704,6 +666,7 @@ export { crewChat as "crew-chat" }
704
666
  Use `export default` to gate every database without writing a named export for each one. Named exports (including `as` exports) still take precedence for databases that need custom logic:
705
667
 
706
668
  access.js
669
+
707
670
  ```js
708
671
  export function chat(doc, oldDoc, user, ctx) {
709
672
  if (!user) throw { forbidden: "authentication required" };
@@ -722,52 +685,69 @@ This is especially useful when an app has many databases.
722
685
 
723
686
  ### Roles via `members` reduce
724
687
 
725
- Roles are not a fixed registry. They are materialized from document contributions:
688
+ Roles are not a fixed registry. They are materialized from document contributions. A team-meta doc contributes members to a role:
689
+
690
+ access.js
726
691
 
727
692
  ```js
728
- // A team-meta doc contributes members to a role
729
- if (doc.type === "team-meta") {
730
- if (!user.isOwner) throw { forbidden: "owner only" };
731
- return {
732
- members: { [doc.teamId]: doc.memberHandles },
733
- grant: { roles: { [doc.teamId]: doc.channels } },
734
- };
693
+ <<<<<<< SEARCH
694
+ export function chat(doc, oldDoc, user, ctx) {
695
+ if (!user) throw { forbidden: "authentication required" };
696
+ ctx.requireAccess(doc.channelId);
697
+ return { channels: [doc.channelId] };
735
698
  }
699
+ =======
700
+ export function chat(doc, oldDoc, user, ctx) {
701
+ if (!user) throw { forbidden: "authentication required" };
702
+
703
+ if (doc.type === "team-meta") {
704
+ if (!user.isOwner) throw { forbidden: "owner only" };
705
+ return {
706
+ members: { [doc.teamId]: doc.memberHandles },
707
+ grant: { roles: { [doc.teamId]: doc.channels } },
708
+ };
709
+ }
710
+
711
+ if (doc.type === "membership") {
712
+ return { members: { [doc.role]: [doc.userHandle] } };
713
+ }
736
714
 
737
- // A per-employee membership doc contributes one handle
738
- if (doc.type === "membership") {
739
- return { members: { [doc.role]: [doc.userHandle] } };
715
+ ctx.requireAccess(doc.channelId);
716
+ return { channels: [doc.channelId] };
740
717
  }
718
+ >>>>>>> REPLACE
741
719
  ```
742
720
 
743
721
  Both patterns produce identical reduced state. Deleting a membership doc removes the user from the role automatically.
744
722
 
745
723
  ### Common `oldDoc` patterns
746
724
 
747
- Use `oldDoc` (the previous version of the document) to enforce invariants across updates:
748
-
749
- ```js
750
- // New document (create)
751
- if (oldDoc === null) {
752
- // create-only logic
753
- }
754
-
755
- // Immutable-after-create fields
756
- if (oldDoc && doc.createdBy !== oldDoc.createdBy) {
757
- throw { forbidden: "createdBy is immutable" };
758
- }
725
+ Use `oldDoc` (the previous version of the document) to enforce invariants across updates. Adding update guards to an access function:
759
726
 
760
- // Prevent unauthorized ownership transfer
761
- if (oldDoc && oldDoc.ownerHandle !== user.userHandle) {
762
- throw { forbidden: "not owner" };
763
- }
727
+ access.js
764
728
 
765
- // Monotonic version — can only increase
766
- if (oldDoc && doc.version <= oldDoc.version) {
767
- throw { forbidden: "version must increase" };
768
- }
729
+ ```js
730
+ <<<<<<< SEARCH
731
+ if (doc.type === "team-meta") {
732
+ if (!user.isOwner) throw { forbidden: "owner only" };
733
+ return {
734
+ =======
735
+ if (doc.type === "team-meta") {
736
+ if (!user.isOwner) throw { forbidden: "owner only" };
737
+ // Immutable-after-create fields
738
+ if (oldDoc && doc.createdBy !== oldDoc.createdBy) {
739
+ throw { forbidden: "createdBy is immutable" };
740
+ }
741
+ // Prevent unauthorized ownership transfer
742
+ if (oldDoc && oldDoc.ownerHandle !== user.userHandle) {
743
+ throw { forbidden: "not owner" };
744
+ }
745
+ return {
746
+ >>>>>>> REPLACE
769
747
  ```
770
748
 
749
+ Other common `oldDoc` patterns: `if (oldDoc === null) { /* create-only logic */ }` for new documents, and `if (oldDoc && doc.version <= oldDoc.version) { throw { forbidden: "version must increase" } }` for monotonic versions.
750
+
771
751
  ---
772
752
 
773
753
  ## Architecture: Where's My Data?
@@ -778,30 +758,34 @@ Data is stored in the browser, and is automatically synced with all invited user
778
758
 
779
759
  You can use the core API in HTML or on the backend. Instead of hooks, import the core API directly:
780
760
 
781
- ```js
761
+ App.jsx
762
+
763
+ ```jsx
764
+ <<<<<<< SEARCH
765
+ import React from "react";
766
+ import { useFireproof } from "use-fireproof";
767
+
768
+ export default function App() {
769
+ const { useDocument, useLiveQuery, database } = useFireproof("myLedger");
770
+ =======
782
771
  import { fireproof } from "use-fireproof";
783
772
 
784
773
  const database = fireproof("myLedger");
785
- ```
786
774
 
787
- The document API is async, but doesn't require loading states or error handling.
788
-
789
- ```js
790
- const ok = await database.put({ text: "Sample Data" });
791
- const doc = await database.get(ok.id);
792
- const latest = await database.query("_id", { limit: 10, descending: true });
793
- console.log("Latest documents:", latest.docs);
775
+ // The document API is async, but doesn't require loading states or error handling.
776
+ async function main() {
777
+ const ok = await database.put({ text: "Sample Data" });
778
+ const doc = await database.get(ok.id);
779
+ const latest = await database.query("_id", { limit: 10, descending: true });
780
+ console.log("Latest documents:", latest.docs);
781
+ }
782
+ >>>>>>> REPLACE
794
783
  ```
795
784
 
796
- To subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic. For instance to send an email when the user completes a todo:
785
+ To subscribe to real-time updates, use the `subscribe` method. This is useful for building backend event handlers or other server-side logic for instance to send an email when the user completes a todo:
797
786
 
798
787
  ```js
799
- import { fireproof } from "use-firproof";
800
-
801
- const database = fireproof("todoList");
802
-
803
788
  database.subscribe((changes) => {
804
- console.log("Recent changes:", changes);
805
789
  changes.forEach((change) => {
806
790
  if (change.completed) {
807
791
  sendEmail(change.email, "Todo completed", "You have completed a todo.");
@@ -812,98 +796,151 @@ database.subscribe((changes) => {
812
796
 
813
797
  ### Working with Files
814
798
 
815
- 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.
799
+ 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.
816
800
 
817
- #### Attaching files on save
801
+ 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).
818
802
 
819
- ```jsx
820
- const { useDocument } = useFireproof("photoAlbum");
821
- const { doc, merge, submit } = useDocument({ _files: {}, caption: "" });
803
+ Building an image uploader with `_files`:
822
804
 
823
- // In a file input change handler:
824
- const onPick = (e) => merge({ _files: { photo: e.target.files[0] } });
825
- ```
805
+ App.jsx
826
806
 
827
- `File` and `Blob` are both accepted. Submit the document the normal way (`submit()` from `useDocument`, or `database.put(doc)`).
807
+ ```jsx
808
+ import React from "react";
809
+ import { useFireproof } from "use-fireproof";
828
810
 
829
- For multi-file uploads (e.g. `<input multiple>`), build the `_files` map keyed by filename:
811
+ export default function App() {
812
+ const { useDocument, useLiveQuery } = useFireproof("imageUploads");
830
813
 
831
- ```jsx
832
- const onPickMany = (e) => {
833
- const next = {};
834
- for (const f of e.target.files) next[f.name] = f;
835
- merge({ _files: next });
836
- };
837
- ```
814
+ const { doc, merge, submit } = useDocument({
815
+ _files: {},
816
+ caption: "",
817
+ type: "upload",
818
+ createdAt: Date.now(),
819
+ });
838
820
 
839
- Iterate the map with `Object.entries(doc._files)` to render or process each entry (see "Displaying files" below).
821
+ const { docs } = useLiveQuery("type", { key: "upload", descending: true, limit: 12 });
840
822
 
841
- #### Displaying files
823
+ const onPickFile = (e) => {
824
+ const f = e.target.files?.[0];
825
+ if (f) merge({ _files: { photo: f } });
826
+ };
842
827
 
843
- After a doc round-trips through the database, each `_files.<key>` entry carries a stable `url` you can drop straight into any browser-native subresource — `<img>`, `<video>`, `<audio>`, CSS `background-image`, `@font-face`, `<a download>`, etc.
828
+ const onSubmit = (e) => {
829
+ e.preventDefault();
830
+ if (!doc._files?.photo) return;
831
+ submit();
832
+ };
844
833
 
845
- ```jsx
846
- {
847
- doc._files.photo && <img src={doc._files.photo.url} alt={doc.caption} />;
848
- }
849
- ```
834
+ const c = {
835
+ bg: "bg-white",
836
+ card: "bg-gray-50",
837
+ border: "border-gray-200",
838
+ accent: "bg-blue-500 hover:bg-blue-600",
839
+ text: "text-gray-700",
840
+ };
850
841
 
851
- For galleries with arbitrary keys, iterate the map:
842
+ return (
843
+ <div className={`p-6 max-w-lg mx-auto ${c.bg} shadow-lg rounded-lg`}>
844
+ <h2 className="text-2xl font-bold mb-4">Image Uploader</h2>
845
+ <form onSubmit={onSubmit} className="space-y-3">
846
+ <input type="file" accept="image/*" onChange={onPickFile} className={`w-full ${c.border} border rounded p-2`} />
847
+ <input
848
+ type="text"
849
+ placeholder="Caption"
850
+ value={doc.caption}
851
+ onChange={(e) => merge({ caption: e.target.value })}
852
+ className={`w-full ${c.border} border rounded p-2`}
853
+ />
854
+ <button type="submit" className={`px-4 py-2 ${c.accent} text-white rounded`}>
855
+ Upload
856
+ </button>
857
+ </form>
852
858
 
853
- ```jsx
854
- {
855
- Object.entries(doc._files ?? {}).map(([key, meta]) => <img key={key} src={meta.url} alt={key} />);
859
+ <h3 className="text-lg font-semibold mt-6">Recent Uploads</h3>
860
+ <div className="grid grid-cols-2 gap-4 mt-2">
861
+ {docs.map((d) => (
862
+ <div key={d._id} className={`${c.border} border p-2 rounded shadow-sm ${c.card}`}>
863
+ {d._files?.photo?.url && <img src={d._files.photo.url} alt={d.caption || "upload"} className="w-full h-auto rounded" />}
864
+ <p className={`text-sm ${c.text} mt-2`}>{d.caption || "No caption"}</p>
865
+ </div>
866
+ ))}
867
+ </div>
868
+ </div>
869
+ );
856
870
  }
857
871
  ```
858
872
 
859
- The platform-minted `url` is stable for the lifetime of that file, so the browser cache works normally.
873
+ 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.
860
874
 
861
- #### Reading raw bytes
875
+ Adding multi-file support:
862
876
 
863
- When you need the bytes themselves (transcoding, hashing, ML features, custom downloads), call `meta.file()` on the entry — it returns a `Promise<File>`:
877
+ App.jsx
864
878
 
865
879
  ```jsx
866
- const f = await doc._files.photo.file();
867
- const buf = await f.arrayBuffer();
868
- const hash = await crypto.subtle.digest("SHA-256", buf);
869
- ```
880
+ <<<<<<< SEARCH
881
+ const onPickFile = (e) => {
882
+ const f = e.target.files?.[0];
883
+ if (f) merge({ _files: { photo: f } });
884
+ };
870
885
 
871
- For plain `<img>` rendering, prefer `meta.url` — it skips one fetch and lets the browser handle cache and decoding.
886
+ const onSubmit = (e) => {
887
+ e.preventDefault();
888
+ if (!doc._files?.photo) return;
889
+ submit();
890
+ };
891
+ =======
892
+ const onPickFile = (e) => {
893
+ const next = {};
894
+ for (const f of e.target.files) next[f.name] = f;
895
+ merge({ _files: next });
896
+ };
872
897
 
873
- #### Each `_files.<key>` entry shape
898
+ const onSubmit = (e) => {
899
+ e.preventDefault();
900
+ if (!Object.keys(doc._files || {}).length) return;
901
+ submit();
902
+ };
903
+ >>>>>>> REPLACE
904
+ ```
874
905
 
875
- After save + round-trip, an entry has:
906
+ access.js
876
907
 
877
- ```ts
878
- {
879
- url: string; // ready-to-use URL for <img>/<video>/etc.
880
- type: string; // MIME type
881
- size: number; // bytes
882
- lastModified?: number; // epoch ms (for File only)
883
- file: () => Promise<File>; // bytes on demand
908
+ ```js
909
+ export function imageUploads(doc, oldDoc, user) {
910
+ if (!user) throw { forbidden: "sign in to upload" };
911
+ return {};
884
912
  }
885
913
  ```
886
914
 
887
915
  ### Form Validation
888
916
 
889
- You can use React's `useState` to manage validation states and error messages. Validate inputs at the UI level before allowing submission.
917
+ 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:
890
918
 
891
- ```javascript
892
- const [errors, setErrors] = useState({});
919
+ App.jsx
893
920
 
894
- function validateForm() {
895
- const newErrors = {};
896
- if (!doc.name.trim()) newErrors.name = "Name is required.";
897
- if (!doc.email) newErrors.email = "Email is required.";
898
- if (!doc.message.trim()) newErrors.message = "Message is required.";
899
- setErrors(newErrors);
900
- return Object.keys(newErrors).length === 0;
901
- }
921
+ ```jsx
922
+ <<<<<<< SEARCH
923
+ const onSubmit = (e) => {
924
+ e.preventDefault();
925
+ if (!Object.keys(doc._files || {}).length) return;
926
+ submit();
927
+ };
928
+ =======
929
+ const [errors, setErrors] = React.useState({});
930
+
931
+ function validateForm() {
932
+ const newErrors = {};
933
+ if (!doc.caption.trim()) newErrors.caption = "Caption is required.";
934
+ if (!Object.keys(doc._files || {}).length) newErrors.file = "Pick a file.";
935
+ setErrors(newErrors);
936
+ return Object.keys(newErrors).length === 0;
937
+ }
902
938
 
903
- function handleSubmit(e) {
904
- e.preventDefault();
905
- if (validateForm()) submit();
906
- }
939
+ const onSubmit = (e) => {
940
+ e.preventDefault();
941
+ if (validateForm()) submit();
942
+ };
943
+ >>>>>>> REPLACE
907
944
  ```
908
945
 
909
946
  ## Example React Application
@@ -1010,86 +1047,3 @@ export function todoList(doc, oldDoc, user) {
1010
1047
  return {};
1011
1048
  }
1012
1049
  ```
1013
-
1014
- ## Example Image Uploader
1015
-
1016
- This pattern uses `_files` end-to-end: save a `File` directly, render thumbnails with `<img src={meta.url}>`.
1017
-
1018
- ```jsx
1019
- import React from "react";
1020
- import { useFireproof } from "use-fireproof";
1021
-
1022
- export default function App() {
1023
- // 1. Hooks and document shapes
1024
- const { useDocument, useLiveQuery } = useFireproof("imageUploads");
1025
-
1026
- const { doc, merge, submit } = useDocument({
1027
- _files: {},
1028
- caption: "",
1029
- type: "upload",
1030
- createdAt: Date.now(),
1031
- });
1032
-
1033
- const { docs } = useLiveQuery("type", { key: "upload", descending: true, limit: 12 });
1034
-
1035
- // 2. Event handlers
1036
- const onPickFile = (e) => {
1037
- const f = e.target.files?.[0];
1038
- if (f) merge({ _files: { photo: f } });
1039
- };
1040
-
1041
- const onSubmit = (e) => {
1042
- e.preventDefault();
1043
- if (!doc._files?.photo) return;
1044
- submit();
1045
- };
1046
-
1047
- // 3. ClassNames
1048
- const c = {
1049
- bg: "bg-white",
1050
- card: "bg-gray-50",
1051
- border: "border-gray-200",
1052
- accent: "bg-blue-500 hover:bg-blue-600",
1053
- text: "text-gray-700",
1054
- };
1055
-
1056
- // 4. JSX return
1057
- return (
1058
- <div className={`p-6 max-w-lg mx-auto ${c.bg} shadow-lg rounded-lg`}>
1059
- <h2 className="text-2xl font-bold mb-4">Image Uploader</h2>
1060
- <form onSubmit={onSubmit} className="space-y-3">
1061
- <input type="file" accept="image/*" onChange={onPickFile} className={`w-full ${c.border} border rounded p-2`} />
1062
- <input
1063
- type="text"
1064
- placeholder="Caption"
1065
- value={doc.caption}
1066
- onChange={(e) => merge({ caption: e.target.value })}
1067
- className={`w-full ${c.border} border rounded p-2`}
1068
- />
1069
- <button type="submit" className={`px-4 py-2 ${c.accent} text-white rounded`}>
1070
- Upload
1071
- </button>
1072
- </form>
1073
-
1074
- <h3 className="text-lg font-semibold mt-6">Recent Uploads</h3>
1075
- <div className="grid grid-cols-2 gap-4 mt-2">
1076
- {docs.map((d) => (
1077
- <div key={d._id} className={`${c.border} border p-2 rounded shadow-sm ${c.card}`}>
1078
- {d._files?.photo?.url && <img src={d._files.photo.url} alt={d.caption || "upload"} className="w-full h-auto rounded" />}
1079
- <p className={`text-sm ${c.text} mt-2`}>{d.caption || "No caption"}</p>
1080
- </div>
1081
- ))}
1082
- </div>
1083
- </div>
1084
- );
1085
- }
1086
- ```
1087
-
1088
- access.js
1089
-
1090
- ```js
1091
- export function imageUploads(doc, oldDoc, user) {
1092
- if (!user) throw { forbidden: "sign in to upload" };
1093
- return {};
1094
- }
1095
- ```