@vibes.diy/prompts 2.4.11 → 2.4.12

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
@@ -7,7 +7,7 @@ Fireproof is a lightweight embedded document database with encrypted live sync,
7
7
  - **Apps run anywhere:** Bundle UI, data, and logic together.
8
8
  - **Real-Time & Offline-First:** Automatic persistence and live queries, runs in the browser - no loading or error states.
9
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("db-name")`.
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
11
 
12
12
  **File structure:** A vibe's source is one or more files. `/App.jsx` is the entry point (React component). `/access.js` is optional — include it when the app needs per-document write validation or channel-based read isolation. Both files are pushed together and the server discovers `/access.js` automatically.
13
13
 
@@ -30,7 +30,7 @@ Fireproof databases store data across sessions and can sync in real-time. Each d
30
30
  ```js
31
31
  import { useFireproof } from "use-fireproof";
32
32
 
33
- const { database, useLiveQuery, useDocument } = useFireproof("my-ledger");
33
+ const { database, useLiveQuery, useDocument } = useFireproof("myLedger");
34
34
  ```
35
35
 
36
36
  #### Put and Get Documents
@@ -47,7 +47,7 @@ This example shows Fireproof's `_id` allows easy sorting with `useLiveQuery`.
47
47
 
48
48
  ```js
49
49
  const App = () => {
50
- const { useDocument, useLiveQuery } = useFireproof("my-ledger");
50
+ const { useDocument, useLiveQuery } = useFireproof("myLedger");
51
51
 
52
52
  const { doc, merge, submit } = useDocument({ text: "" });
53
53
 
@@ -72,12 +72,23 @@ const App = () => {
72
72
  };
73
73
  ```
74
74
 
75
+ The access function lives in a separate file. Even simple apps include one — it's the server-side authority for who can write:
76
+
77
+ access.js
78
+
79
+ ```js
80
+ export default function (doc, oldDoc, user) {
81
+ if (!user) throw { forbidden: "sign in to save" };
82
+ return {};
83
+ }
84
+ ```
85
+
75
86
  ### Editing Documents
76
87
 
77
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.
78
89
 
79
90
  ```js
80
- const { useDocument } = useFireproof("my-ledger");
91
+ const { useDocument } = useFireproof("myLedger");
81
92
 
82
93
  const { doc, merge, submit, save, reset } = useDocument({ _id: "user-profile:abc@example.com" });
83
94
  ```
@@ -143,7 +154,7 @@ Documents can be updated by multiple clients, and synced later. To create an eve
143
154
 
144
155
  ```js
145
156
  const App = () => {
146
- const { useLiveQuery, database } = useFireproof("my-ledger");
157
+ const { useLiveQuery, database } = useFireproof("myLedger");
147
158
 
148
159
  const { docs } = useLiveQuery("counter", { key: "my-event-name" });
149
160
  const counterValue = docs.length;
@@ -198,7 +209,7 @@ Sortable lists are a common pattern. Here's how to implement them using Fireproo
198
209
 
199
210
  ```js
200
211
  function App() {
201
- const { database, useLiveQuery } = useFireproof("my-ledger");
212
+ const { database, useLiveQuery } = useFireproof("myLedger");
202
213
 
203
214
  // Initialize list with evenly spaced positions
204
215
  async function initializeList() {
@@ -255,7 +266,7 @@ const { useLiveQuery, database } = useFireproof("drafts", {
255
266
  });
256
267
 
257
268
  // No acl — falls back to app-level role gates (existing behavior, always safe)
258
- const { useLiveQuery, database } = useFireproof("public-notes");
269
+ const { useLiveQuery, database } = useFireproof("publicNotes");
259
270
  ```
260
271
 
261
272
  **Subject groups** — who each name covers:
@@ -271,16 +282,162 @@ Owner is always implicitly included — never list `owner` explicitly in an ACL.
271
282
 
272
283
  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.
273
284
 
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");
291
+
292
+ access.roles; // ReadonlySet<string> — roles the viewer belongs to
293
+ access.channels; // ReadonlySet<string> — channels the viewer can access
294
+
295
+ access.hasRole("moderator"); // boolean convenience
296
+ access.hasChannel("engineering"); // boolean convenience
297
+ ```
298
+
299
+ 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
+
301
+ ```jsx
302
+ function App() {
303
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
304
+ const { database, useLiveQuery, access } = useFireproof("comments");
305
+
306
+ if (isViewerPending) return null;
307
+
308
+ return (
309
+ <div>
310
+ <ViewerTag />
311
+ {access.hasRole("poster") && <CommentForm database={database} />}
312
+ {access.hasRole("moderator") && <ModTools database={database} />}
313
+ {access.hasChannel("announcements") && <Announcements />}
314
+ </div>
315
+ );
316
+ }
317
+ ```
318
+
319
+ 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
+
321
+ 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
+
323
+ ### Complete example: Team announcements with channels
324
+
325
+ This example shows the full round-trip — access.js declares channels and grants; App.jsx reads them back via `access`. Key details:
326
+
327
+ - **Owner bootstrap:** `user.isOwner` gates management operations (channel setup, role grants, moderation). No bootstrap problem — the owner can always manage without needing a role granted first.
328
+ - **Channel grant:** A `channelSetup` document uses `grant.public` so all members can read, and `grant.roles` so posters can write to specific channels.
329
+ - **Write surfaces** are gated with `viewer` (signed in?), `access.hasChannel()` (channel access), or `isOwner` (management).
330
+ - **`ViewerTag`** takes `ownerHandle` (not `userHandle`) when rendering another user.
331
+
332
+ access.js
333
+
334
+ ```js
335
+ export function announcements(doc, oldDoc, user, ctx) {
336
+ if (!user) throw { forbidden: "sign in" };
337
+
338
+ if (doc.type === "channelSetup") {
339
+ if (!user.isOwner) throw { forbidden: "owner only" };
340
+ return {
341
+ channels: [doc.channel],
342
+ grant: {
343
+ public: [doc.channel],
344
+ roles: { poster: [doc.channel] },
345
+ },
346
+ };
347
+ }
348
+
349
+ if (doc.type === "roleGrant") {
350
+ if (!user.isOwner) throw { forbidden: "owner only" };
351
+ return { members: { [doc.role]: [doc.userHandle] } };
352
+ }
353
+
354
+ if (doc.type === "post") {
355
+ if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
356
+ ctx.requireAccess(doc.channel);
357
+ return { channels: [doc.channel] };
358
+ }
359
+
360
+ return {};
361
+ }
362
+ ```
363
+
364
+ App.jsx — `isOwner` and `access.hasChannel()` gate the UI based on what the access function declared:
365
+
366
+ ```jsx
367
+ import React from "react";
368
+ import { useFireproof } from "use-fireproof";
369
+ import { useViewer } from "use-vibes";
370
+
371
+ export default function App() {
372
+ const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();
373
+ const { database, useLiveQuery, access } = useFireproof("announcements");
374
+
375
+ const { docs: posts } = useLiveQuery("type", { key: "post" });
376
+ const [draft, setDraft] = React.useState("");
377
+ const [channel, setChannel] = React.useState("general");
378
+
379
+ if (isViewerPending) return null;
380
+
381
+ async function submitPost() {
382
+ if (!draft.trim() || !viewer) return;
383
+ await database.put({
384
+ type: "post",
385
+ channel,
386
+ body: draft.trim(),
387
+ authorHandle: viewer.userHandle,
388
+ createdAt: Date.now(),
389
+ });
390
+ setDraft("");
391
+ }
392
+
393
+ return (
394
+ <div>
395
+ <ViewerTag />
396
+
397
+ {/* membership + channel gate */}
398
+ {viewer && access.hasChannel(channel) && (
399
+ <form
400
+ onSubmit={(e) => {
401
+ e.preventDefault();
402
+ submitPost();
403
+ }}
404
+ >
405
+ <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />
406
+ <button type="submit">Post</button>
407
+ </form>
408
+ )}
409
+
410
+ {/* owner-only management */}
411
+ {isOwner && (
412
+ <button onClick={() => database.put({ type: "roleGrant", role: "poster", userHandle: "newUser" })}>
413
+ Grant poster role
414
+ </button>
415
+ )}
416
+
417
+ {posts.map((p) => (
418
+ <div key={p._id}>
419
+ <ViewerTag ownerHandle={p.authorHandle} />
420
+ <p>{p.body}</p>
421
+ {isOwner && <button onClick={() => database.del(p._id)}>Delete</button>}
422
+ </div>
423
+ ))}
424
+ </div>
425
+ );
426
+ }
427
+ ```
428
+
429
+ The pattern: `viewer` checks sign-in, `access.hasChannel()` checks what the access function granted, `isOwner` gates management. The access function is the server-side authority — the UI just reflects its decisions. Real apps can graduate to `access.hasRole("moderator")` when they need to delegate management to non-owners.
430
+
274
431
  ---
275
432
 
276
433
  ## Access Function (`/access.js`)
277
434
 
278
435
  Access functions are **the room** — they govern what members can do with data once inside the app. The per-vibe membership system is **the door** — it decides who can see the app at all. Once a user is through the door (approved as a member), the access function is the sole authority for data permissions. Access functions are server-run on every write (including deletes) before storing the document. They validate writes, route documents to channels, and declare grants that control who can read what. Only create an `/access.js` file when the user asks for per-document routing, channel-based isolation, or document-level write validation.
279
436
 
280
- Access functions live in `/access.js`, a separate file in the vibe's filesystem alongside `/App.jsx`. 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.
437
+ 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.
281
438
 
439
+ access.js
282
440
  ```js
283
- // /access.js — each export name = the database it gates
284
441
  export function chat(doc, oldDoc, user, ctx) {
285
442
  if (!user) throw { forbidden: "authentication required" };
286
443
  if (doc.type === "message") {
@@ -292,8 +449,8 @@ export function chat(doc, oldDoc, user, ctx) {
292
449
  }
293
450
  ```
294
451
 
295
- ```js
296
- // /App.jsx — no access option needed; the server matches by database name
452
+ App.jsx
453
+ ```jsx
297
454
  const { useLiveQuery, database } = useFireproof("chat");
298
455
  ```
299
456
 
@@ -354,8 +511,8 @@ type AccessDescriptor = {
354
511
 
355
512
  ### Example: Workspace chat with channels
356
513
 
514
+ access.js
357
515
  ```js
358
- // /access.js
359
516
  export function chat(doc, oldDoc, user, ctx) {
360
517
  if (!user) throw { forbidden: "authentication required" };
361
518
 
@@ -397,8 +554,8 @@ This single access function handles three document types:
397
554
 
398
555
  ### Example: Anonymous survey with role-gated results
399
556
 
557
+ access.js
400
558
  ```js
401
- // /access.js
402
559
  export function survey(doc, oldDoc, user, ctx) {
403
560
  if (doc.type === "survey-response") {
404
561
  if (oldDoc) throw { forbidden: "responses are write-once" };
@@ -406,11 +563,10 @@ export function survey(doc, oldDoc, user, ctx) {
406
563
  }
407
564
 
408
565
  if (doc.type === "survey-config") {
409
- ctx.requireRole("survey-admin");
566
+ if (!user.isOwner) throw { forbidden: "owner only" };
410
567
  return {
411
568
  grant: {
412
569
  roles: {
413
- "survey-admin": ["inbound-responses"],
414
570
  "feedback-team": ["inbound-responses"],
415
571
  },
416
572
  },
@@ -438,8 +594,8 @@ Key patterns:
438
594
 
439
595
  Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
440
596
 
597
+ access.js
441
598
  ```js
442
- // /access.js
443
599
  export function chat(doc, oldDoc, user, ctx) {
444
600
  if (!user) throw { forbidden: "authentication required" };
445
601
  ctx.requireAccess(doc.channelId);
@@ -454,12 +610,24 @@ export function notes(doc, oldDoc, user, ctx) {
454
610
 
455
611
  Databases without a matching named export fall through to `export default` if one exists. If there is no default export either, the database uses the default app-level permissions (no access function).
456
612
 
613
+ **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:
614
+
615
+ access.js
616
+ ```js
617
+ function crewChat(doc, oldDoc, user, ctx) {
618
+ if (!user) throw { forbidden: "authentication required" };
619
+ ctx.requireAccess(doc.channelId);
620
+ return { channels: [doc.channelId] };
621
+ }
622
+ export { crewChat as "crew-chat" }
623
+ ```
624
+
457
625
  ### Catch-all with `export default`
458
626
 
459
- Use `export default` to gate every database without writing a named export for each one. Named exports still take precedence for databases that need custom logic:
627
+ 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:
460
628
 
629
+ access.js
461
630
  ```js
462
- // /access.js
463
631
  export function chat(doc, oldDoc, user, ctx) {
464
632
  if (!user) throw { forbidden: "authentication required" };
465
633
  ctx.requireAccess(doc.channelId);
@@ -473,7 +641,7 @@ export default function (doc, oldDoc, user, ctx) {
473
641
  }
474
642
  ```
475
643
 
476
- This is especially useful when an app has many databases or uses hyphenated names (`error-log`, `user-prefs`) that can't be JavaScript identifiers.
644
+ This is especially useful when an app has many databases.
477
645
 
478
646
  ### Roles via `members` reduce
479
647
 
@@ -482,7 +650,7 @@ Roles are not a fixed registry. They are materialized from document contribution
482
650
  ```js
483
651
  // A team-meta doc contributes members to a role
484
652
  if (doc.type === "team-meta") {
485
- ctx.requireRole("admin");
653
+ if (!user.isOwner) throw { forbidden: "owner only" };
486
654
  return {
487
655
  members: { [doc.teamId]: doc.memberHandles },
488
656
  grant: { roles: { [doc.teamId]: doc.channels } },
@@ -536,7 +704,7 @@ You can use the core API in HTML or on the backend. Instead of hooks, import the
536
704
  ```js
537
705
  import { fireproof } from "use-fireproof";
538
706
 
539
- const database = fireproof("my-ledger");
707
+ const database = fireproof("myLedger");
540
708
  ```
541
709
 
542
710
  The document API is async, but doesn't require loading states or error handling.
@@ -553,7 +721,7 @@ To subscribe to real-time updates, use the `subscribe` method. This is useful fo
553
721
  ```js
554
722
  import { fireproof } from "use-firproof";
555
723
 
556
- const database = fireproof("todo-list-db");
724
+ const database = fireproof("todoList");
557
725
 
558
726
  database.subscribe((changes) => {
559
727
  console.log("Recent changes:", changes);
@@ -572,7 +740,7 @@ Fireproof documents carry attachments under `_files`. Save a `File` (or `Blob`)
572
740
  #### Attaching files on save
573
741
 
574
742
  ```jsx
575
- const { useDocument } = useFireproof("photo-album");
743
+ const { useDocument } = useFireproof("photoAlbum");
576
744
  const { doc, merge, submit } = useDocument({ _files: {}, caption: "" });
577
745
 
578
746
  // In a file input change handler:
@@ -671,7 +839,7 @@ import { useFireproof } from "use-fireproof";
671
839
 
672
840
  export default function App() {
673
841
  // 1. Hooks and document shapes
674
- const { useLiveQuery, useDocument, database } = useFireproof("todo-list-db");
842
+ const { useLiveQuery, useDocument, database } = useFireproof("todoList");
675
843
 
676
844
  const {
677
845
  doc: newTodo,
@@ -752,6 +920,20 @@ export default function App() {
752
920
 
753
921
  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.
754
922
 
923
+ The todo app's access function validates authorship:
924
+
925
+ access.js
926
+
927
+ ```js
928
+ export function todoList(doc, oldDoc, user) {
929
+ if (!user) throw { forbidden: "sign in" };
930
+ if (doc.type === "todo" && doc.createdBy !== user.userHandle) {
931
+ throw { forbidden: "only the author can edit" };
932
+ }
933
+ return {};
934
+ }
935
+ ```
936
+
755
937
  ## Example Image Uploader
756
938
 
757
939
  This pattern uses `_files` end-to-end: save a `File` directly, render thumbnails with `<img src={meta.url}>`.
@@ -762,7 +944,7 @@ import { useFireproof } from "use-fireproof";
762
944
 
763
945
  export default function App() {
764
946
  // 1. Hooks and document shapes
765
- const { useDocument, useLiveQuery } = useFireproof("image-uploads");
947
+ const { useDocument, useLiveQuery } = useFireproof("imageUploads");
766
948
 
767
949
  const { doc, merge, submit } = useDocument({
768
950
  _files: {},
@@ -825,3 +1007,12 @@ export default function App() {
825
1007
  );
826
1008
  }
827
1009
  ```
1010
+
1011
+ access.js
1012
+
1013
+ ```js
1014
+ export function imageUploads(doc, oldDoc, user) {
1015
+ if (!user) throw { forbidden: "sign in to upload" };
1016
+ return {};
1017
+ }
1018
+ ```
package/llms/three-js.md CHANGED
@@ -1068,7 +1068,7 @@ import { useFireproof } from "use-fireproof";
1068
1068
  import * as THREE from "three";
1069
1069
 
1070
1070
  export default function SkyGlider() {
1071
- const { database, useLiveQuery } = useFireproof("sky-glider-scores");
1071
+ const { database, useLiveQuery } = useFireproof("skyGliderScores");
1072
1072
  const canvasRef = useRef(null);
1073
1073
  const gameStateRef = useRef({
1074
1074
  scene: null,
@@ -1475,7 +1475,7 @@ import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
1475
1475
  import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js";
1476
1476
 
1477
1477
  export default function HalftoneArtStudio() {
1478
- const { database, useLiveQuery } = useFireproof("halftone-studio");
1478
+ const { database, useLiveQuery } = useFireproof("halftoneStudio");
1479
1479
  const canvasRef = useRef(null);
1480
1480
  const sceneRef = useRef(null);
1481
1481
  const [currentPreset, setCurrentPreset] = useState(null);
@@ -2,7 +2,7 @@
2
2
 
3
3
  `useViewer()` is a **read-only window** into runtime-managed access control. The platform owns the rules — who's the owner, who has been granted read or write — and `useViewer()` lets your app see what the runtime decided. You cannot grant or revoke access from code; you can only reflect the runtime's verdict in your UI.
4
4
 
5
- The contract: **every write surface (form, submit button, edit input, delete button) must consult `can("write")`** and render a read-only fallback when it returns false. This applies even when the app sounds single-user sharing is the runtime's decision, not the prompt's.
5
+ The contract: **every write surface (form, submit button, edit input, delete button) must check `viewer`** (signed in?) and render a read-only fallback when null. For apps with access functions, gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()`. The access function is the server-side authority the UI reflects its decisions.
6
6
 
7
7
  ## Basic Usage
8
8
 
@@ -10,7 +10,7 @@ The contract: **every write surface (form, submit button, edit input, delete but
10
10
  import { useViewer } from "use-vibes";
11
11
 
12
12
  function App() {
13
- const { viewer, isViewerPending, can, ViewerTag } = useViewer();
13
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
14
14
 
15
15
  // isViewerPending is true until the platform has resolved the viewer identity.
16
16
  // Gate on it to avoid flashing the anonymous state on first render.
@@ -27,16 +27,17 @@ function App() {
27
27
 
28
28
  ## What you get
29
29
 
30
- - `viewer` — `{ userSlug, displayName?, avatarUrl }` or `null` for anonymous visitors. `avatarUrl` is a stable opaque URL — just use it in `<img src>`, don't construct it yourself.
30
+ - `viewer` — `{ userHandle, displayName?, avatarUrl }` or `null` for anonymous visitors. `avatarUrl` is a stable opaque URL — just use it in `<img src>`, don't construct it yourself.
31
31
  - `isViewerPending` — `true` while the platform is still resolving the viewer identity (e.g. on first render before the parent shell has pushed the identity update). **Gate any auth-dependent UI on `!isViewerPending`** to avoid flashing the wrong state. Once it becomes `false`, `viewer` is either populated or definitively `null`.
32
- - `can(action, dbName?)` — `true`/`false` for `"read"`, `"write"`, `"delete"`. Pass a `dbName` for multi-db apps; omit for single-db apps. Use it to hide forms when the viewer can't post.
32
+ - `isOwner` — `true` when the viewer owns this vibe. Use it for management UI (settings, role grants, moderation).
33
+ - `can(action, dbName?)` — `true`/`false` for `"read"`, `"write"`, `"delete"`. Checks app-level ACLs. In most apps `viewer` and `access.hasRole()`/`access.hasChannel()` are the right gates instead.
33
34
  - `ViewerTag` — ready-made user pill; see the ViewerTag section below.
34
35
 
35
36
  ## Gating UI
36
37
 
37
38
  ```jsx
38
39
  function CommentForm() {
39
- const { viewer, isViewerPending, can, ViewerTag } = useViewer();
40
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
40
41
  if (isViewerPending) return null;
41
42
 
42
43
  return (
@@ -48,8 +49,8 @@ function CommentForm() {
48
49
  <ViewerTag />
49
50
  </div>
50
51
 
51
- {viewer && !can("write", "comments") && <p>Contact the owner to request write access so you can post.</p>}
52
- {viewer && can("write", "comments") && <form>...</form>}
52
+ {!viewer && <p>Sign in to post.</p>}
53
+ {viewer && <form>...</form>}
53
54
  </div>
54
55
  );
55
56
  }
@@ -64,8 +65,8 @@ import { useFireproof } from "use-fireproof";
64
65
  import { useViewer } from "use-vibes";
65
66
 
66
67
  function CommentThread() {
67
- const { viewer, isViewerPending, can, ViewerTag } = useViewer();
68
- const { useLiveQuery, database } = useFireproof("comments");
68
+ const { viewer, isViewerPending, ViewerTag } = useViewer();
69
+ const { useLiveQuery, database, access } = useFireproof("comments");
69
70
  const { docs: comments } = useLiveQuery("createdAt");
70
71
  const [body, setBody] = useState("");
71
72
 
@@ -74,11 +75,7 @@ function CommentThread() {
74
75
  await database.put({
75
76
  body: body.trim(),
76
77
  createdAt: Date.now(),
77
- // Stamp the viewer's identity at write time. Other users will
78
- // render from these fields — no need to look anything up later.
79
- authorUserSlug: viewer.userSlug,
80
- authorDisplayName: viewer.displayName ?? viewer.userSlug,
81
- authorAvatarUrl: viewer.avatarUrl,
78
+ authorHandle: viewer.userHandle,
82
79
  });
83
80
  setBody("");
84
81
  }
@@ -88,8 +85,7 @@ function CommentThread() {
88
85
  <ul>
89
86
  {comments.map((c) => (
90
87
  <li key={c._id}>
91
- <img src={c.authorAvatarUrl} alt={c.authorUserSlug} className="avatar" />
92
- <strong>{c.authorDisplayName}</strong>
88
+ <ViewerTag ownerHandle={c.authorHandle} />
93
89
  <p>{c.body}</p>
94
90
  </li>
95
91
  ))}
@@ -101,8 +97,8 @@ function CommentThread() {
101
97
  {viewer && <span style={{ fontSize: 13, color: "var(--muted, #888)" }}>commenting as</span>}
102
98
  <ViewerTag />
103
99
  </div>
104
- {viewer && !can("write", "comments") && <p>Contact the owner to request write access so you can post.</p>}
105
- {viewer && can("write", "comments") && (
100
+ {!viewer && <p>Sign in to post.</p>}
101
+ {viewer && (
106
102
  <form
107
103
  onSubmit={(e) => {
108
104
  e.preventDefault();
@@ -122,14 +118,15 @@ function CommentThread() {
122
118
 
123
119
  Key points:
124
120
 
125
- - **Write-time stamping** — the doc carries the author info, not a foreign-key lookup. Old comments keep working even if the author later deletes their account.
126
- - **`avatarUrl` is stable** — if the author changes their avatar tomorrow, every historical comment shows the new image because the URL stays the same, the bytes change.
127
- - **One source of identity** — never store the viewer's user ID, only `userSlug` + `displayName` + `avatarUrl`. The trio is everything a renderer needs.
121
+ - **Stamp `authorHandle` at write time** — persist the author's handle on the doc. Render with `<ViewerTag ownerHandle={authorHandle} />` which resolves display name and avatar automatically.
122
+ - **`avatarUrl` is stable** — if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
123
+ - **One source of identity** — persist `authorHandle` on the doc. ViewerTag does the rest.
128
124
 
129
125
  ## Notes
130
126
 
131
- - Never use Clerk user IDs. Only `userSlug` crosses into vibe code.
127
+ - Never use Clerk user IDs. Only `userHandle` crosses into vibe code.
132
128
  - Avatar URLs are stable indirection URLs — when a user changes their avatar, the URL stays the same and the bytes update. Treat them as opaque strings.
129
+ - For per-database permissions (roles and channels), use `access` from `useFireproof()`: `access.hasRole("moderator")`, `access.hasChannel("engineering")`. The access function (access.js) is the server-side authority; `access` in the UI reflects its decisions.
133
130
 
134
131
  ## ViewerTag
135
132
 
@@ -142,14 +139,14 @@ const { viewer, ViewerTag } = useViewer();
142
139
  <ViewerTag />
143
140
 
144
141
  // Show another user read-only (no edit affordance):
145
- <ViewerTag userSlug={comment.authorUserSlug} />
142
+ <ViewerTag ownerHandle={comment.authorHandle} />
146
143
  ```
147
144
 
148
145
  **Self-detection is automatic.** When `ViewerTag` renders the current viewer it shows a dashed indigo ring and pencil overlay on the avatar. Clicking it opens a file picker; the upload and profile save happen internally.
149
146
 
150
- **Undefined safety.** If `userSlug` is present in props but falsy (e.g. a missing field from a loop lookup), `ViewerTag` renders a dim italic placeholder instead of the edit ring. This prevents a broken data source from accidentally granting photo-edit access to an arbitrary pill.
147
+ **Undefined safety.** If `ownerHandle` is present in props but falsy (e.g. a missing field from a loop lookup), `ViewerTag` renders a dim italic placeholder instead of the edit ring. This prevents a broken data source from accidentally granting photo-edit access to an arbitrary pill.
151
148
 
152
- **Anonymous safety.** `ViewerTag` is always safe to call regardless of login state — it never throws. When the viewer is anonymous and no `userSlug` prop is given, it renders a "Sign in" button that opens the platform login UI when clicked. Wrap it in a `{viewer && <ViewerTag />}` guard if you want to suppress it entirely for anonymous users.
149
+ **Anonymous safety.** `ViewerTag` is always safe to call regardless of login state — it never throws. When the viewer is anonymous and no `ownerHandle` prop is given, it renders a "Sign in" button that opens the platform login UI when clicked. Wrap it in a `{viewer && <ViewerTag />}` guard if you want to suppress it entirely for anonymous users.
153
150
 
154
151
  **Theming.** `ViewerTag` reads `--accent`, `--accent-text`, `--card-bg`, `--border`, `--text`, and `--muted` from the app's CSS variables with sensible fallbacks. If your app defines these on `:root` (which most generated themes do), `ViewerTag` inherits the theme automatically with no extra props.
155
152
 
@@ -159,4 +156,4 @@ const { viewer, ViewerTag } = useViewer();
159
156
  <ViewerTag style={{ borderRadius: 8, fontSize: 12 }} />
160
157
  ```
161
158
 
162
- Use `<ViewerTag />` (no props) for the current user and `<ViewerTag userSlug={...} />` for others. That's the whole API.
159
+ Use `<ViewerTag />` (no props) for the current user and `<ViewerTag ownerHandle={...} />` for others. That's the whole API.
package/llms/webxr.md CHANGED
@@ -455,14 +455,14 @@ function buildGalaxy(scene) {
455
455
 
456
456
  function buildCoreShader(scene) {
457
457
  if (!BABYLON.Effect.ShadersStore["galaxyCoreVertexShader"]) {
458
- BABYLON.Effect.ShadersStore["galaxyCoreVertexShader"] = `
458
+ BABYLON.Effect.ShadersStore["galaxyCoreVertexShader"] = `
459
459
  precision highp float;
460
460
  attribute vec3 position; attribute vec2 uv;
461
461
  uniform mat4 worldViewProjection;
462
462
  varying vec2 vUv;
463
463
  void main() { vUv = uv; gl_Position = worldViewProjection * vec4(position, 1.0); }
464
464
  `;
465
- BABYLON.Effect.ShadersStore["galaxyCoreFragmentShader"] = `
465
+ BABYLON.Effect.ShadersStore["galaxyCoreFragmentShader"] = `
466
466
  precision highp float;
467
467
  varying vec2 vUv; uniform float time;
468
468
  void main() {
@@ -506,7 +506,7 @@ async function enableVR(scene) {
506
506
  // ── React container component ──────────────────────────────────────────────
507
507
 
508
508
  export default function App() {
509
- const { database, useLiveQuery } = useFireproof("galaxy-sessions");
509
+ const { database, useLiveQuery } = useFireproof("galaxySessions");
510
510
  const canvasRef = useRef(null);
511
511
  const { docs: sessions } = useLiveQuery("type", { key: "session" });
512
512
 
@@ -661,7 +661,7 @@ async function enableAR(scene, onPlace) {
661
661
  // ── React container ────────────────────────────────────────────────────────
662
662
 
663
663
  export default function App() {
664
- const { database, useLiveQuery } = useFireproof("ar-orbs");
664
+ const { database, useLiveQuery } = useFireproof("arOrbs");
665
665
  const canvasRef = useRef(null);
666
666
  const sceneRef = useRef(null);
667
667
  const orbMatRef = useRef(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "2.4.11",
3
+ "version": "2.4.12",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -30,8 +30,8 @@
30
30
  "@fireproof/core-types-base": "~0.24.19",
31
31
  "@fireproof/core-types-protocols-cloud": "~0.24.19",
32
32
  "@fireproof/use-fireproof": "~0.24.19",
33
- "@vibes.diy/call-ai-v2": "^2.4.11",
34
- "@vibes.diy/use-vibes-types": "^2.4.11",
33
+ "@vibes.diy/call-ai-v2": "^2.4.12",
34
+ "@vibes.diy/use-vibes-types": "^2.4.12",
35
35
  "arktype": "~2.2.0",
36
36
  "json-schema-faker": "~0.6.1"
37
37
  },
@@ -14,7 +14,7 @@ You are an AI assistant tasked with creating React components. You should create
14
14
  - Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
15
15
  - Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
16
16
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
17
- - Access control (who can read, who can write) is decided by the runtime, not by your code. `useViewer()` is the app's read-only window into that decision — call `const { viewer, can } = useViewer();` from `"use-vibes"`. `viewer` is `{ userSlug, displayName?, avatarUrl } | null` (null for anonymous). `can("write")` returns the runtime's verdict; you cannot grant or override it. Your only job is to reflect that verdict in the UI: **every write surface (form, submit button, edit input, delete button) must be wrapped in `can("write")` — show it only when true, render a read-only state otherwise.** Pattern: `if (!can("write")) return <p>Read-only view contact the owner for write access.</p>;` Render avatars with `<img src={viewer.avatarUrl} />` (opaque URL — use directly). For multi-db apps pass the dbName: `can("write", "comments")`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
17
+ - Access control is decided by the runtime, not by your code. `useViewer()` from `"use-vibes"` gives you `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`. `viewer` is `{ userHandle, displayName?, avatarUrl } | null` (null for anonymous). **Gate write surfaces on `viewer`** show forms only when signed in, render a read-only fallback otherwise. For apps with an access function (`access.js`), gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()` — never re-derive permissions from document fields client-side. Use `isOwner` for management UI (settings, moderation). Render avatars with `<ViewerTag ownerHandle={authorHandle} />`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
18
18
  - Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
19
19
  - Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
20
20
  - List data items on the main page of your app so users don't have to hunt for them
@@ -23,13 +23,13 @@ You are an AI assistant tasked with creating React components. You should create
23
23
 
24
24
  {{CONCATENATED_LLMS}}
25
25
  {{THEME_DESIGN}}
26
- {{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: You are working in one JavaScript file (`App.jsx`). This is the **first turn** — `App.jsx` does not exist yet. You'll paint a colored shell once, then grow each feature into it through small fill-then-wire passes the user watches land in the preview.
26
+ {{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: Your main file is `App.jsx` (the React component). If the app needs an access function for per-document write validation or channel-based read isolation, emit it as a separate file named `access.js` — never put access function code inside `App.jsx`. This is the **first turn** — `App.jsx` does not exist yet. You'll paint a colored shell once, then grow each feature into it through small fill-then-wire passes the user watches land in the preview.
27
27
 
28
28
  Before writing code, provide a title and brief description of the app. Then list the top 3 features that are the best fit for a mobile web database with real-time collaboration and describe a short planned workflow showing how those features connect into a coherent user experience.
29
29
 
30
30
  ## Output format (one colored shell + 4–6 feature passes)
31
31
 
32
- Every code block must be preceded by the file name on its own line. The file is `App.jsx`.
32
+ Every code block must be preceded by the file name on its own line — `App.jsx` for the React component, or `access.js` for the access function (if needed).
33
33
 
34
34
  **Step 1 — Colored shell (one full-file `create` block).** Emit a single fenced ```jsx block containing the full initial file. No SEARCH/REPLACE markers, no `=======`, no `>>>>>>> REPLACE`—`App.jsx` doesn't exist yet.
35
35
 
@@ -85,7 +85,33 @@ If a feature needs hooks at the top of the component (a `useFireproof` whose `da
85
85
 
86
86
  **Each pair is small — typically 20–50 lines on each side of the `=======`.** Fill passes are smaller (structure only). Wire passes are slightly larger (add hooks + handlers). If a wire pair would exceed ~60 lines per side, split the section into a smaller scope or move the hooks insertion to its own tiny pair as described above. **Bias toward many small visible deltas over fewer giant ones** — each pass should be a watchable paint.
87
87
 
88
- After your final edit, add a short 1-2 sentence message describing the core workflow the app supports.
88
+ **If the app needs an `access.js`, emit it right after the colored shell — before any fill/wire passes.** Write it as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design early so every subsequent fill/wire pass can use `access.hasRole()` / `access.hasChannel()` from the start. If later passes introduce new doc types, emit a follow-up `access.js` block with the additions.
89
+
90
+ > Access function — owner manages channels, authenticated users post to channels they have access to.
91
+ >
92
+ > access.js
93
+ > ```js
94
+ > // Each channel doc grants public read access to that channel.
95
+ > // Posts require channel access — the server enforces this via ctx.requireAccess.
96
+ > // Only the owner can create channels.
97
+ > export function chat(doc, oldDoc, user, ctx) {
98
+ > if (!user) throw { forbidden: "sign in" }
99
+ > if (doc.type === "channel") {
100
+ > if (!user.isOwner) throw { forbidden: "owner only" }
101
+ > return { channels: [doc.name], grant: { public: [doc.name] } }
102
+ > }
103
+ > if (doc.type === "message") {
104
+ > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }
105
+ > ctx.requireAccess(doc.channelId)
106
+ > return { channels: [doc.channelId] }
107
+ > }
108
+ > return {}
109
+ > }
110
+ > ```
111
+
112
+ **Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
113
+
114
+ After the final edit (and `access.js` if applicable), add a short 1-2 sentence message describing the core workflow the app supports.
89
115
 
90
116
  ## Pass-1 scaffold rules
91
117
 
@@ -93,16 +119,12 @@ After your final edit, add a short 1-2 sentence message describing the core work
93
119
  - A `classNames` / `c` object with **real, final-ish Tailwind colors** for the layout-level keys (`page`, `header`, `title`, `section`, `btn`, `input`, list rows, etc.). Pick a coherent palette that fits the app's vibe — page background, header chrome, section frames, button accents, text colors all land here so the first paint is colored, not monochrome. Bracket notation is fine (`bg-[#0f172a]`, `text-[#f8fafc]`). Reference via `className={c.page}` / `className={classNames.foo}`. Real layout values (sizing, spacing, flex/grid) live here too.
94
120
  - Semantic HTML tags throughout: `<header>`, `<main>`, `<form>`, `<button>`, `<ul>`, `<li>`, `<section>`. Each planned feature is its own `<section>` with a stable `id` named after the feature.
95
121
  - **Be creative with the layout, but respect mobile idioms.** Don't default to a single centered column every time — pick a layout that fits the app (sticky bottom action bar, hero + horizontal scroll, tabbed switcher, split header/feed, etc.). Mobile rules: thumb-reachable primary actions, generous tap targets (`min-h-[44px]` or `py-3`), comfortable line height, scrollable lists, no hover-only interactions, no fixed widths that break on 360px screens. Mobile-first, then `md:` / `lg:` for larger viewports.
96
- <<<<<<< HEAD
97
- - **Real layout content per feature**, not just `{/* feature lands here */}` stubs. Drop in form fields, list rows, button placements, and headings the feature will need. Use placeholder copy ("Add a task", "No items yet") and a couple of static example rows where a list will go.
98
- - Placeholder event handlers (e.g. `function handleSubmit(e) { e.preventDefault(); }`) wired onto `<form>` / `<button>`.
99
- - NO `useFireproof`, NO `useLiveQuery`, NO `callAI` calls, NO `useState` data wiring (the edit stream lands those). **EXCEPTION:** if `useViewer` is in the imports, destructure it on `App()`'s first line — `const { viewer, can } = useViewer();` — so subsequent edits can gate write surfaces with `can("write")` and render avatars with `viewer.avatarUrl` without having to add the call later.
100
- - # A default-exported `App` function composing the features inside `<main id="app">` with `<header id="app-header">`. When `useViewer` is in the imports, the first line of `App()` must be `const { viewer, can } = useViewer();`.
101
122
  - **Empty section shells per feature, NOT filled content.** Each `<section id="feature-id" className={c.section}>` holds just a single `<h2>{/* feature-name pass */}</h2>` line (or equivalent placeholder). Do NOT drop in form fields, list rows, sample rows, or button placements yet — those land in each section's fill pass. The shell is for shape + color only; the content lands when the feature grows in.
102
123
  - The `<header>` IS filled — real brand title, any always-visible chrome (tagline, top nav buttons) all final in the shell. The header doesn't get a fill pass; it ships finished.
103
- - NO `useFireproof`, NO `useLiveQuery`, NO `callAI` calls, NO `useState` data wiring (the wire passes land those).
104
- - A default-exported `App` function composing the features inside `<main id="app">` with `<header id="app-header">`.
105
- > > > > > > > b24507d2 (feat(prompts): colored shell + fill-then-wire passes for faster TTFR)
124
+ - NO `useFireproof`, NO `useLiveQuery`, NO `callAI` calls, NO `useState` data wiring (the wire passes land those). **EXCEPTION:** if `useViewer` is in the imports, destructure it on `App()`'s first line — `const { viewer, isOwner, ViewerTag } = useViewer();` — so subsequent edits can gate write surfaces with `viewer` and render identity with `ViewerTag` without having to add the call later.
125
+ - A default-exported `App` function composing the features inside `<main id="app">` with `<header id="app-header">`. When `useViewer` is in the imports, the first line of `App()` must be `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`.
126
+
127
+ Since `access.js` was emitted before the feature passes, the first `useFireproof` wire pass should destructure `access` — `const { database, useLiveQuery, access } = useFireproof("dbName")` — so permission gates can use `access.hasRole()` and `access.hasChannel()` immediately.
106
128
 
107
129
  ## Your starter scaffold (Pass 1 imports — use these as-is)
108
130
 
@@ -147,7 +169,7 @@ Invent fresh, app-specific options every time. Don't reuse generic answers.
147
169
 
148
170
  Map user answers to architecture for the next turn:
149
171
 
150
- - "Just me" — all persistent data in a single Fireproof database (`useFireproof("vibe-…")`), no user attribution needed; Fireproof sync handles cross-device access.
172
+ - "Just me" — all persistent data in a single Fireproof database (`useFireproof("myApp")`), no user attribution needed; Fireproof sync handles cross-device access.
151
173
  - "Shared with a group" — same Fireproof database for everyone in the group, with `createdBy: user?.email || 'anonymous'` on user-owned docs.
152
174
  - "Real-time with others" — shared Fireproof database with `createdBy` on every doc; ephemeral interaction (drag position, cursor, hover) stays in `useState` and is never written to Fireproof.
153
175
  - "Personal views" — every doc tagged `createdBy`, filtered on read via `useLiveQuery` keyed on the current user.
package/system-prompt.md CHANGED
@@ -14,7 +14,7 @@ You are an AI assistant tasked with creating React components. You should create
14
14
  - Use `callAI` to fetch AI, use schema like this: `JSON.parse(await callAI(prompt, { schema: { properties: { todos: { type: 'array', items: { type: 'string' } } } } }))` and save final responses as individual Fireproof documents.
15
15
  - Always show loading states during any async operation (callAI, fetch, database queries): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) disable the trigger button with `disabled={isLoading}`, (2) replace the button text with a spinning SVG icon using CSS animation `animate-spin` (a simple circle with a gap), (3) optionally show a short status text like 'Loading...' near the button. Never leave the user clicking a button with no visual feedback. Pattern: `setIsLoading(true); try { await callAI(...); } finally { setIsLoading(false); }`
16
16
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
17
- - Access control (who can read, who can write) is decided by the runtime, not by your code. `useViewer()` is the app's read-only window into that decision — call `const { viewer, can } = useViewer();` from `"use-vibes"`. `viewer` is `{ userSlug, displayName?, avatarUrl } | null` (null for anonymous). `can("write")` returns the runtime's verdict; you cannot grant or override it. Your only job is to reflect that verdict in the UI: **every write surface (form, submit button, edit input, delete button) must be wrapped in `can("write")` — show it only when true, render a read-only state otherwise.** Pattern: `if (!can("write")) return <p>Read-only view contact the owner for write access.</p>;` Render avatars with `<img src={viewer.avatarUrl} />` (opaque URL — use directly). For multi-db apps pass the dbName: `can("write", "comments")`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
17
+ - Access control is decided by the runtime, not by your code. `useViewer()` from `"use-vibes"` gives you `const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer();`. `viewer` is `{ userHandle, displayName?, avatarUrl } | null` (null for anonymous). **Gate write surfaces on `viewer`** show forms only when signed in, render a read-only fallback otherwise. For apps with an access function (`access.js`), gate further with `access.hasRole()` or `access.hasChannel()` from `useFireproof()` — never re-derive permissions from document fields client-side. Use `isOwner` for management UI (settings, moderation). Render avatars with `<ViewerTag ownerHandle={authorHandle} />`. This applies to every app — never skip useViewer because the app "sounds single-user"; the runtime decides sharing, not the prompt. See use-viewer docs.
18
18
  - Don't try to generate png or base64 data, use placeholder image APIs instead, like https://picsum.photos/400 where 400 is the square size
19
19
  - Never use emojis in the UI. Use inline SVG icons instead — simple, single-color, stroke-based SVGs (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round). Build icons directly in JSX, do not import icon libraries.
20
20
  - Consider and potentially reuse/extend code from previous responses if relevant
@@ -29,13 +29,13 @@ You are an AI assistant tasked with creating React components. You should create
29
29
 
30
30
  {{CONCATENATED_LLMS}}
31
31
  {{THEME_DESIGN}}
32
- {{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: You are working in one JavaScript file (`App.jsx`). The first pass is a thin scaffold the user sees immediately — features and styling land afterwards via incremental SEARCH/REPLACE edits.
32
+ {{TITLE_SECTION}}{{ENRICHED_PROMPT}}{{USER_PROMPT}}IMPORTANT: Your main file is `App.jsx` (the React component). If the app needs an access function for per-document write validation or channel-based read isolation, emit it as a separate file named `access.js` — never put access function code inside `App.jsx`. The first pass is a thin scaffold the user sees immediately — features and styling land afterwards via incremental SEARCH/REPLACE edits.
33
33
 
34
34
  Before writing code, provide a title and brief description of the app. Then list the top 3 features that are the best fit for a mobile web database with real-time collaboration and describe a short planned workflow showing how those features connect into a coherent user experience.
35
35
 
36
36
  ## Output format (incremental edits)
37
37
 
38
- Every code block must be preceded by the file name on its own line. The file is `App.jsx`.
38
+ Every code block must be preceded by the file name on its own line — `App.jsx` for the React component, or `access.js` for the access function (if needed).
39
39
 
40
40
  **After the description prose, emit a thin scaffold as a single fenced block. Target ~40 lines.** The scaffold renders immediately and gives later edits unique anchors to target. It must contain:
41
41
 
@@ -45,7 +45,7 @@ Every code block must be preceded by the file name on its own line. The file is
45
45
  - a default-exported `App` function that composes them inside a `<main id="app">` with `<header id="app-header">`
46
46
  - name the section ids and feature components after the features you just described (e.g. for a kanban board: `id="board"`, `id="add-task"`, `id="ai-expand"`), not literal `feature-one`
47
47
  - plain JSX placeholders in each stub (e.g. `<h2>Feature</h2>` and a `{/* ... */}` comment) — the placeholders inherit the scaffold's layout styling so the empty state already looks intentional
48
- - NO hooks (no useState, no useFireproof, no useLiveQuery), NO callAI calls, NO event handlers, NO long color/shadow Tailwind chains (those land via edits) — **EXCEPTION:** if `useViewer` is in the import list, destructure it at the top of `App()` (`const { viewer, can } = useViewer();`) so subsequent edits can gate write surfaces with `can("write")` without having to add the call later
48
+ - NO hooks (no useState, no useFireproof, no useLiveQuery), NO callAI calls, NO event handlers, NO long color/shadow Tailwind chains (those land via edits) — **EXCEPTION:** if `useViewer` is in the import list, destructure it at the top of `App()` (`const { viewer, isOwner, ViewerTag } = useViewer();`) so subsequent edits can gate write surfaces with `viewer` and render identity with `ViewerTag` without having to add the call later
49
49
 
50
50
  **Every edit block must be preceded by exactly one line of prose. No exceptions.** Before each fenced SEARCH/REPLACE block, write a single sentence (≤25 words) telling the user what this specific edit does. Never emit two fenced blocks back-to-back without a prose line between them — the user is watching the preview update and needs that one-line cadence to follow what's happening. No multi-paragraph essays either: just one sentence, then the edit. Styling edits (filling in `classNames` values, color tokens, layout polish) follow the same rule — one line of description, then the edit.
51
51
 
@@ -268,6 +268,27 @@ Below is a tiny worked example showing the format end-to-end. Description → sc
268
268
 
269
269
  Note how each edit is preceded by exactly one prose line, the visible structure (input + button) lands before the data wiring (`useDocument` / state), and each SEARCH block is the smallest unique snippet that targets the change.
270
270
 
271
+ ### access.js output format (when needed)
272
+
273
+ When the app uses channel-based read isolation or per-document write validation, emit the access function as a **separate file block** after all `App.jsx` edits. One prose line, then the filename `access.js`, then the fenced block:
274
+
275
+ > Server-side access function gates the chat database — only channel members can read, only authors can post.
276
+ >
277
+ > access.js
278
+ > ```js
279
+ > export function chat(doc, oldDoc, user, ctx) {
280
+ > if (!user) throw { forbidden: "authentication required" };
281
+ > if (doc.type === "message") {
282
+ > if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
283
+ > ctx.requireAccess(doc.channelId);
284
+ > return { channels: [doc.channelId] };
285
+ > }
286
+ > return {};
287
+ > }
288
+ > ```
289
+
290
+ **Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
291
+
271
292
  ## Your starter scaffold
272
293
 
273
294
  Adapt this to your features (rename `FeatureOne/Two/Three` and the `id` values to match what you described above; tweak the Tailwind defaults to fit your style prompt). Then start emitting prose+edit pairs per the rules above.
@@ -313,7 +334,7 @@ function FeatureThree() {
313
334
  }
314
335
 
315
336
  export default function App() {
316
- const { viewer, can } = useViewer();
337
+ const { viewer, isOwner, ViewerTag } = useViewer();
317
338
  return (
318
339
  <main id="app" className={classNames.page}>
319
340
  <header id="app-header" className={classNames.header}>
@@ -327,7 +348,102 @@ export default function App() {
327
348
  }
328
349
  ````
329
350
 
330
- Keep the `useViewer` destructure on `App`'s first line whenever `useViewer` is in the imports — later edits will reach for `can("write")` and `viewer.avatarUrl` and need them already in scope.
351
+ Keep the `useViewer` destructure on `App`'s first line whenever `useViewer` is in the imports — later edits will reach for `viewer`, `isOwner`, and `ViewerTag` and need them already in scope.
352
+
353
+ **If the app needs an `access.js`, emit it right after the scaffold — before any feature edits.** Write it as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design early so every subsequent App.jsx edit can destructure `access` and gate with `access.hasRole()` / `access.hasChannel()` from the start. If later feature edits introduce new doc types, emit a follow-up `access.js` block with the additions.
354
+
355
+ Example streamed output for a team board app:
356
+
357
+ > **Crew Board** — team channel board with live posts, pinned announcements, and owner-managed channels.
358
+ >
359
+ > App.jsx
360
+ > ```jsx
361
+ > import React from "react"
362
+ > import { useFireproof } from "use-fireproof"
363
+ > import { useViewer } from "use-vibes"
364
+ >
365
+ > function Channels() { return <section id="channels"><h2>{/* channels pass */}</h2></section> }
366
+ > function Feed() { return <section id="feed"><h2>{/* feed pass */}</h2></section> }
367
+ > function Compose() { return <section id="compose"><h2>{/* compose pass */}</h2></section> }
368
+ >
369
+ > export default function App() {
370
+ > const { viewer, isOwner, isViewerPending, ViewerTag } = useViewer()
371
+ > const c = { page: "min-h-screen bg-[#0a0a0a] text-white", header: "..." }
372
+ > if (isViewerPending) return null
373
+ > return (
374
+ > <div className={c.page}>
375
+ > <header id="app-header" className={c.header}><h1>Crew Board</h1><ViewerTag /></header>
376
+ > <main id="app"><Channels /><Feed /><Compose /></main>
377
+ > </div>
378
+ > )
379
+ > }
380
+ > ```
381
+ >
382
+ > Access function — owner manages channels, members post to channels they have access to.
383
+ >
384
+ > access.js
385
+ > ```js
386
+ > // Each channel doc grants public read access to that channel.
387
+ > // Posts require channel access — the server enforces this via ctx.requireAccess.
388
+ > // Only the owner can create channels or grant roles.
389
+ > export function crewBoard(doc, oldDoc, user, ctx) {
390
+ > if (!user) throw { forbidden: "sign in" }
391
+ >
392
+ > if (doc.type === "channel") {
393
+ > if (!user.isOwner) throw { forbidden: "owner only" }
394
+ > return { channels: [doc.name], grant: { public: [doc.name] } }
395
+ > }
396
+ >
397
+ > if (doc.type === "post") {
398
+ > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }
399
+ > ctx.requireAccess(doc.channelId)
400
+ > return { channels: [doc.channelId] }
401
+ > }
402
+ >
403
+ > return {}
404
+ > }
405
+ > ```
406
+ >
407
+ > Fill the channel sidebar with chip buttons and owner-only add form.
408
+ >
409
+ > App.jsx
410
+ > ```jsx
411
+ > <<<<<<< SEARCH
412
+ > function Channels() { return <section id="channels"><h2>{/* channels pass */}</h2></section> }
413
+ > =======
414
+ > function Channels({ channels, active, setActive, isOwner, database, c }) {
415
+ > // ... channel list + owner add form, gated on isOwner
416
+ > }
417
+ > >>>>>>> REPLACE
418
+ > ```
419
+ >
420
+ > Wire the feed with live query, filtered by active channel.
421
+ >
422
+ > App.jsx
423
+ > ```jsx
424
+ > <<<<<<< SEARCH
425
+ > function Feed() { return <section id="feed"><h2>{/* feed pass */}</h2></section> }
426
+ > =======
427
+ > function Feed({ channel, useLiveQuery, isOwner, ViewerTag, database, c }) {
428
+ > // ... useLiveQuery("channelId", { key: channel }), posts with ViewerTag
429
+ > }
430
+ > >>>>>>> REPLACE
431
+ > ```
432
+ >
433
+ > Wire the compose box — gated on viewer and channel access.
434
+ >
435
+ > App.jsx
436
+ > ```jsx
437
+ > <<<<<<< SEARCH
438
+ > function Compose() { return <section id="compose"><h2>{/* compose pass */}</h2></section> }
439
+ > =======
440
+ > function Compose({ channel, viewer, access, database, c }) {
441
+ > if (!viewer) return <p className={c.muted}>Sign in to post.</p>
442
+ > if (!access.hasChannel(channel)) return <p className={c.muted}>No access to this channel.</p>
443
+ > // ... compose form stamping authorHandle
444
+ > }
445
+ > >>>>>>> REPLACE
446
+ > ```
331
447
 
332
448
  ## End every turn with one improvement question
333
449
 
@@ -366,7 +482,7 @@ Invent fresh, app-specific options every time. Don't reuse generic answers.
366
482
 
367
483
  Map user answers to architecture for the next turn:
368
484
 
369
- - "Just me" — all persistent data in a single Fireproof database (`useFireproof("vibe-…")`), no user attribution needed; Fireproof sync handles cross-device access.
485
+ - "Just me" — all persistent data in a single Fireproof database (`useFireproof("myApp")`), no user attribution needed; Fireproof sync handles cross-device access.
370
486
  - "Shared with a group" — same Fireproof database for everyone in the group, with `createdBy: user?.email || 'anonymous'` on user-owned docs.
371
487
  - "Real-time with others" — shared Fireproof database with `createdBy` on every doc; ephemeral interaction (drag position, cursor, hover) stays in `useState` and is never written to Fireproof.
372
488
  - "Personal views" — every doc tagged `createdBy`, filtered on read via `useLiveQuery` keyed on the current user.