@vibes.diy/prompts 2.5.0 → 2.5.1-dev.1

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
@@ -59,14 +59,16 @@ export default function App() {
59
59
  }
60
60
  ```
61
61
 
62
- The access function lives in a separate file. Even simple apps include one — it's the server-side authority for who can write:
62
+ The access function lives in a separate file. Even simple apps include one — it's the server-side authority for who can write, and it routes each document to a channel so the author can read it back:
63
63
 
64
64
  access.js
65
65
 
66
66
  ```js
67
67
  export default function (doc, oldDoc, user) {
68
68
  if (!user) throw { forbidden: "sign in to save" };
69
- return {};
69
+ // Private to the author: one channel per user holds all of their documents.
70
+ const mine = `user:${user.userHandle}`;
71
+ return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };
70
72
  }
71
73
  ```
72
74
 
@@ -376,7 +378,7 @@ export function announcements(doc, oldDoc, user, ctx) {
376
378
  return { channels: [doc.channel] };
377
379
  }
378
380
 
379
- return {};
381
+ throw { forbidden: "unknown document type" };
380
382
  }
381
383
  ```
382
384
 
@@ -474,7 +476,7 @@ export function chat(doc, oldDoc, user, ctx) {
474
476
  return { channels: [doc.channel] };
475
477
  }
476
478
 
477
- return {};
479
+ throw { forbidden: "unknown document type" };
478
480
  }
479
481
  ```
480
482
 
@@ -523,7 +525,7 @@ Access functions live in `/access.js`, a separate file in the vibe's filesystem
523
525
 
524
526
  ### AccessDescriptor return type
525
527
 
526
- All fields are optional. `{}` is a valid return. `throw { forbidden: "reason" }` rejects the write.
528
+ All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time. To reject a write outright, `throw { forbidden: "reason" }`.
527
529
 
528
530
  ```ts
529
531
  type AccessDescriptor = {
@@ -553,6 +555,15 @@ type AccessDescriptor = {
553
555
 
554
556
  **Access functions are server-enforced policy code.** Checks should be deterministic over `(doc, oldDoc, user, ctx)` and deny with `throw { forbidden: "reason" }` when violated.
555
557
 
558
+ ### Choosing channels — keep the count low
559
+
560
+ A channel is a _reusable_ unit of read access: grant a user into a channel once and they can read every document routed there. Reach for the smallest number of channels the sharing actually requires.
561
+
562
+ - **A reusable group reads many docs** (a team, a board, a project): route to one channel the _collaboration_ owns — `return { channels: [doc.channelId] }` — and grant membership once via a meta or invite doc. Many documents share the one channel.
563
+ - **Only the author reads it** (private notes, a user's own uploads): route to one channel the _user_ owns. `const mine = \`user:${user.userHandle}\`; return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };` — all of that user's private documents live in this single channel.
564
+ - **A document goes to a one-off set with no reusable group:** route to several channels at once — `return { channels: [\`user:${aHandle}\`, \`user:${bHandle}\`] }`. Mint a per-document channel (`channels: [doc._id]`) only when each document genuinely has its own disjoint audience.
565
+ - **Refusing a write:** `throw { forbidden: "reason" }`. Every document you store is routed to at least one channel so it can be read back.
566
+
556
567
  ### Example: Workspace chat with channels
557
568
 
558
569
  access.js
@@ -587,7 +598,7 @@ export function chat(doc, oldDoc, user, ctx) {
587
598
  };
588
599
  }
589
600
 
590
- return {};
601
+ throw { forbidden: "unknown document type" };
591
602
  }
592
603
  ```
593
604
 
@@ -620,8 +631,7 @@ export function survey(doc, oldDoc, user, ctx) {
620
631
  return { channels: [doc._id], grant: { public: [doc._id] } };
621
632
  }
622
633
 
623
- if (!user) throw { forbidden: "authentication required" };
624
- return {};
634
+ throw { forbidden: "unknown document type" };
625
635
  }
626
636
  ```
627
637
 
@@ -642,7 +652,9 @@ export function chat(doc, oldDoc, user, ctx) {
642
652
 
643
653
  export function notes(doc, oldDoc, user, ctx) {
644
654
  if (!user) throw { forbidden: "authentication required" };
645
- return {};
655
+ // Private to the author — one channel per user.
656
+ const mine = `user:${user.userHandle}`;
657
+ return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };
646
658
  }
647
659
  ```
648
660
 
@@ -674,10 +686,11 @@ export function chat(doc, oldDoc, user, ctx) {
674
686
  return { channels: [doc.channelId] };
675
687
  }
676
688
 
677
- // Everything else: require authentication, no channel routing
689
+ // Everything else: authenticated users get a private per-user channel
678
690
  export default function (doc, oldDoc, user, ctx) {
679
691
  if (!user) throw { forbidden: "authentication required" };
680
- return {};
692
+ const mine = `user:${user.userHandle}`;
693
+ return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };
681
694
  }
682
695
  ```
683
696
 
@@ -908,7 +921,9 @@ access.js
908
921
  ```js
909
922
  export function imageUploads(doc, oldDoc, user) {
910
923
  if (!user) throw { forbidden: "sign in to upload" };
911
- return {};
924
+ // Each uploader reads their own images — one private channel per user.
925
+ const mine = `user:${user.userHandle}`;
926
+ return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };
912
927
  }
913
928
  ```
914
929
 
@@ -1044,6 +1059,8 @@ export function todoList(doc, oldDoc, user) {
1044
1059
  if (doc.type === "todo" && doc.createdBy !== user.userHandle) {
1045
1060
  throw { forbidden: "only the author can edit" };
1046
1061
  }
1047
- return {};
1062
+ // Private to the author — one channel per user.
1063
+ const mine = `user:${user.userHandle}`;
1064
+ return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };
1048
1065
  }
1049
1066
  ```
@@ -34,7 +34,7 @@ export default function App() {
34
34
 
35
35
  ## What you get
36
36
 
37
- - `viewer` — `{ userHandle, displayName?, avatarUrl }` or `null` for anonymous visitors. `avatarUrl` is a stable opaque URLjust use it in `<img src>`, don't construct it yourself.
37
+ - `viewer` — `{ userHandle, displayName? }` or `null` for anonymous visitors. Avatars are not on the payloadrender them with `<ViewerTag userHandle={...} />`, which resolves the avatar from the handle. Don't build avatar URLs yourself.
38
38
  - `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`.
39
39
  - `isOwner` — `true` when the viewer owns this vibe. Use it for management UI (settings, role grants, moderation).
40
40
  - `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.
@@ -147,7 +147,7 @@ export default function App() {
147
147
  Key points:
148
148
 
149
149
  - **Stamp `authorHandle` at write time** — persist the author's handle on the doc. Render with `<ViewerTag userHandle={authorHandle} />` which resolves display name and avatar automatically.
150
- - **`avatarUrl` is stable** — if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
150
+ - **Avatars are stable** — ViewerTag resolves the avatar from the handle; if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
151
151
  - **One source of identity** — persist `authorHandle` on the doc. ViewerTag does the rest.
152
152
 
153
153
  ## Notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "2.5.0",
3
+ "version": "2.5.1-dev.1",
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.5.0",
34
- "@vibes.diy/use-vibes-types": "^2.5.0",
33
+ "@vibes.diy/call-ai-v2": "^2.5.1-dev.1",
34
+ "@vibes.diy/use-vibes-types": "^2.5.1-dev.1",
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 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 userHandle={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.
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? } | 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 userHandle={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
@@ -66,7 +66,7 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
66
66
  > ctx.requireAccess(doc.channelId)
67
67
  > return { channels: [doc.channelId] }
68
68
  > }
69
- > return {}
69
+ > throw { forbidden: "unknown document type" }
70
70
  > }
71
71
  > ```
72
72
 
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 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 userHandle={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.
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? } | 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 userHandle={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
@@ -274,7 +274,7 @@ When the app uses channel-based read isolation or per-document write validation,
274
274
  > ctx.requireAccess(doc.channelId);
275
275
  > return { channels: [doc.channelId] };
276
276
  > }
277
- > return {};
277
+ > throw { forbidden: "unknown document type" };
278
278
  > }
279
279
  > ```
280
280