@vibes.diy/prompts 14.3.20 → 14.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms/access.md CHANGED
@@ -66,7 +66,8 @@ Keep the two visually distinct (a checkbox with its own explanatory line; a sepa
66
66
 
67
67
  App.jsx — the checkbox is the consent call; the picker is a doc write:
68
68
 
69
- ```jsx file=App.jsx
69
+ ### `App.jsx`
70
+ ```jsx
70
71
  const { can } = useVibe("logs"); // the write gate is always the server's own verdict
71
72
 
72
73
  // checking is the consent preflight; unchecking is the off switch. Never one verb for both edges.
@@ -93,7 +94,8 @@ const { can } = useVibe("logs"); // the write gate is always the server's own ve
93
94
 
94
95
  access.js — the buddy grant and the followers audience arm are independent:
95
96
 
96
- ```js file=access.js
97
+ ### `access.js`
98
+ ```js
97
99
  const ch = `log:${user.userHandle}`;
98
100
 
99
101
  if (doc.type === "share") {
@@ -265,7 +267,8 @@ Use `oldDoc` (the previous version of the document) to enforce invariants across
265
267
 
266
268
  Some documents belong to the machinery rather than to a person: a scheduled run's state doc, a counter a `scheduled` sweep advances, a notification the backend generates. Gate those on the **origin** with `ctx.requireBackend(...)` — one line, no conditional:
267
269
 
268
- ```js file=access.js partial
270
+ ### `access.js`
271
+ ```js partial
269
272
  if (doc.type === "runState") {
270
273
  ctx.requireBackend("scheduled");
271
274
  return { channels: ["show"] };
@@ -284,7 +287,7 @@ One trap: applying a release's `seed.json` is the **platform** writing, not the
284
287
 
285
288
  The **placement** of `access.js` depends on which turn shape you are in — one-shot whole-app generation vs. incremental scaffold/follow-up edits. Follow the subsection that matches your turn; the other one's ordering is wrong for you and will strand writes.
286
289
 
287
- **Variant-neutral rules (always):** write `access.js` 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. **Never put access function code inside an `App.jsx` block** — it will overwrite the React component; the filename line (e.g. `access.js` vs `App.jsx`) is how the system knows which file to write. Whatever the turn shape, `App.jsx` gates its write surfaces on `useVibe(dbName).can` — the same rules this access function enforces.
290
+ **Variant-neutral rules (always):** write `access.js` 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. **Never put access function code inside an `App.jsx` block** — it will overwrite the React component; the path heading (e.g. `access.js` vs `App.jsx`) is how the system knows which file to write. Whatever the turn shape, `App.jsx` gates its write surfaces on `useVibe(dbName).can` — the same rules this access function enforces.
288
291
 
289
292
  **One-shot generation (the whole app in one full-file block):** emit `access.js` as the **last file of the turn** — after the complete `App.jsx` and any companion feature files. You are NOT emitting incremental `SEARCH`/`REPLACE` edits here — write the finished files in full. The worked examples below show the access-function shapes; ignore any edit-by-edit cadence in them and emit the access function as one complete block.
290
293
 
@@ -292,9 +295,8 @@ The **placement** of `access.js` depends on which turn shape you are in — one-
292
295
 
293
296
  Worked example — members-only chat writes
294
297
 
295
- access.js
296
-
297
- ```js file=access.js partial
298
+ ### `access.js`
299
+ ```js partial
298
300
  export function chat(doc, oldDoc, user, ctx) {
299
301
  if (!user) throw { forbidden: "authentication required" };
300
302
 
@@ -349,9 +351,8 @@ _Each example below closes its type dispatch with the default terminal `throw {
349
351
 
350
352
  ### Worked example — open channel wall (author-owned writes)
351
353
 
352
- access.js
353
-
354
- ```js file=access.js partial
354
+ ### `access.js`
355
+ ```js partial
355
356
  export function wall(doc, oldDoc, user, ctx) {
356
357
  if (!user) throw { forbidden: "sign in" };
357
358
 
@@ -380,9 +381,8 @@ export function wall(doc, oldDoc, user, ctx) {
380
381
 
381
382
  ### Worked example — per-object collaboration with join request
382
383
 
383
- access.js
384
-
385
- ```js file=access.js partial
384
+ ### `access.js`
385
+ ```js partial
386
386
  export function board(doc, oldDoc, user, ctx) {
387
387
  if (!user) throw { forbidden: "sign in" };
388
388
  const channel = `board:${doc.boardId}`;
@@ -413,9 +413,8 @@ export function board(doc, oldDoc, user, ctx) {
413
413
 
414
414
  **When the person asks for private per-user permissions — "each person sees only their own", "private to every user" — every visitor gets their own private space on a single per-user channel.** A todo list, a daily habit tracker, a reading list, a diary, a notes app, a workout log, or a budget where the data is one person's own routes every doc that visitor creates to the one channel keyed on their handle — `user:${user.userHandle}` — self-granted so only they read it, with `authorHandle: user.userHandle` fixed at create and held immutable on update (`oldDoc.authorHandle === user.userHandle`). Their whole collection lives on that single private channel, reachable from first load, each visitor's space entirely their own:
415
415
 
416
- access.js
417
-
418
- ```js file=access.js partial
416
+ ### `access.js`
417
+ ```js partial
419
418
  export function notes(doc, oldDoc, user, ctx) {
420
419
  if (!user) throw { forbidden: "sign in" };
421
420
  const ch = `user:${user.userHandle}`; // this visitor's own private space
@@ -442,9 +441,8 @@ Every item a visitor creates lands on their own `user:<handle>` channel, private
442
441
 
443
442
  **Worked example — a shared catalog people track against, with per-thing visibility (a social habit app, a reading challenge, a fitness ladder).** The catalog items are public objects anyone proposes; each person's progress is their own; and each person chooses — _once per item, never per entry_ — whether their streak is public (on the leaderboard) or buddy-only. The visibility choice lives on a per-`(person, item)` **tracking** record that sets the read-grant the entries routed to it inherit.
444
443
 
445
- access.js
446
-
447
- ```js file=access.js partial
444
+ ### `access.js`
445
+ ```js partial
448
446
  export function habits(doc, oldDoc, user, ctx) {
449
447
  if (!user) throw { forbidden: "sign in" };
450
448
 
@@ -486,7 +484,8 @@ The leaderboard is just the access model: read the public `track:` channels and
486
484
 
487
485
  The personal blog's post branch is small — public read is the blog's resting state, declared inline on every post result (readers are the point of publishing):
488
486
 
489
- ```js file=access.js partial
487
+ ### `access.js`
488
+ ```js partial
490
489
  if (doc.type === "post") {
491
490
  if (!oldDoc) {
492
491
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
@@ -505,8 +504,7 @@ When the prompt also asks for drafts, keep the same public resting state for pos
505
504
 
506
505
  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.
507
506
 
508
- App.jsx
509
-
507
+ ### `App.jsx`
510
508
  ```jsx
511
509
  <<<<<<< SEARCH
512
510
  import { useFireproof } from "use-fireproof";
@@ -516,8 +514,7 @@ import { useVibe } from "use-vibes";
516
514
  >>>>>>> REPLACE
517
515
  ```
518
516
 
519
- App.jsx
520
-
517
+ ### `App.jsx`
521
518
  ```jsx
522
519
  <<<<<<< SEARCH
523
520
  const { useLiveQuery, database } = useFireproof("comments");
@@ -527,8 +524,7 @@ App.jsx
527
524
  >>>>>>> REPLACE
528
525
  ```
529
526
 
530
- App.jsx
531
-
527
+ ### `App.jsx`
532
528
  ```jsx
533
529
  <<<<<<< SEARCH
534
530
  <h3>Recent Documents</h3>
@@ -566,9 +562,8 @@ This example shows the full round-trip — access.js declares channels and grant
566
562
  - **Write surfaces** are gated with `useVibe(dbName).can.create/edit/delete` — it runs this same access function, so the UI verdict matches the server. Render `.reason` when denied. (See use-vibe docs.)
567
563
  - **`ViewerTag`** takes `userHandle` to render another user (authors, rosters). The current viewer's own pill is system chrome in the Vibes Switch (the logo) — don't add one to the app's UI, except a guarded no-prop `{viewer && <ViewerTag />}` when you want inline avatar self-edit for any signed-in member (see use-viewer docs). The sign-in ASK is the app's, though: `useViewer().requestLogin()` from your own button, at the moment the visitor's work becomes worth keeping.
568
564
 
569
- access.js
570
-
571
- ```js file=access.js
565
+ ### `access.js`
566
+ ```js
572
567
  export function announcements(doc, oldDoc, user, ctx) {
573
568
  if (!user) throw { forbidden: "sign in" };
574
569
 
@@ -618,7 +613,8 @@ export function announcements(doc, oldDoc, user, ctx) {
618
613
 
619
614
  App.jsx — `useVibe().can` gates every write surface (posts AND owner-only management); `access.hasChannel()` reflects display-only membership:
620
615
 
621
- ```jsx file=App.jsx
616
+ ### `App.jsx`
617
+ ```jsx
622
618
  import React from "react";
623
619
  import { useFireproof } from "use-fireproof";
624
620
  import { useViewer, useVibe } from "use-vibes";
@@ -723,9 +719,8 @@ const canRevoke = can.delete(grantDoc);
723
719
 
724
720
  Channels everyone can read, and any signed-in user can post to. The channel doc is `grant.public` (read for everyone) and the post rule checks only the author — **no `ctx.requireAccess`**, because public is read-only and would block every non-owner. (For a members-only board where the owner appoints who can post, grant a poster role and `requireAccess` it, as in the announcements example above.) The UI uses `access.hasChannel()` to filter which channels to display, and `useVibe().can` to gate writes.
725
721
 
726
- access.js
727
-
728
- ```js file=access.js partial
722
+ ### `access.js`
723
+ ```js partial
729
724
  export function chat(doc, oldDoc, user, ctx) {
730
725
  if (!user) throw { forbidden: "sign in" };
731
726
 
@@ -793,9 +788,8 @@ Channel `_id` is the channel identifier everywhere. The access function uses `do
793
788
 
794
789
  Reach for this by default, and certainly whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared shopping list you invite a partner to, a whiteboard people can join, a trip a group plans together, a tracker one person starts and later shares with a coach. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships. Nothing a person writes reaches anyone else until they chose it: a per-object channel starts with its creator as the only reader, and every other reader arrives through a grant that creator wrote.
795
790
 
796
- access.js
797
-
798
- ```js file=access.js partial
791
+ ### `access.js`
792
+ ```js partial
799
793
  export default function (doc, oldDoc, user, ctx) {
800
794
  if (!user) throw { forbidden: "sign in" };
801
795
  const ch = (id) => `list:${id}`;
@@ -881,9 +875,8 @@ Reach for this variant whenever the app is built around a *thing* people come ba
881
875
  - **The object's creator is its admin**, held on a second channel (`` `${chan}/admin` ``) that the creator self-grants at creation. Adding a member is gated on that admin channel, so members collaborate and the creator decides who joins. The app owner holds no place in any of this: every rule reads `user.userHandle` or the object's own `creatorHandle`.
882
876
  - **Recipients can select every readable object.** The ordinary object query already applies the access rules, including grants from independent membership records. Its complete result supplies the picker. Prefer the person's own object as the initial selection while keeping invited objects selectable alongside it. Resolve the selected id against the current readable results on each render, so a revoked object leaves both the picker and the content view. Use the selected object's id for child queries, new content, and invitations; `can` continues to gate each write.
883
877
 
884
- access.js
885
-
886
- ```js file=access.js partial
878
+ ### `access.js`
879
+ ```js partial
887
880
  export function boards(doc, oldDoc, user, ctx) {
888
881
  if (!user?.userHandle) throw { forbidden: "sign in" };
889
882
  const safeId = (id) => {
@@ -970,7 +963,8 @@ const addCard = async (text, boardId = `default-${me.userHandle}`) => {
970
963
 
971
964
  App.jsx — the same board list serves the creator and the invited recipient. `BoardContents` uses `board._id` for its content queries, calls `addCard(text, board._id)` and `addMember(board._id, handle)`, and gates its write controls with `can`. Pass the `ensureDefaultBoard` helper above into `BoardWorkspace`; its creation action stays reachable with an empty readable list and selects the person's new board after saving it.
972
965
 
973
- ```jsx file=App.jsx partial
966
+ ### `App.jsx`
967
+ ```jsx partial
974
968
  function BoardWorkspace({ ensureDefaultBoard }) {
975
969
  const { useLiveQuery } = useFireproof("boards");
976
970
  const { me, can } = useVibe("boards");
@@ -1033,9 +1027,8 @@ That bounds what the build's own closing line says: the rules and the screen it
1033
1027
 
1034
1028
  ### Example: Workspace chat with channels
1035
1029
 
1036
- access.js
1037
-
1038
- ```js file=access.js partial
1030
+ ### `access.js`
1031
+ ```js partial
1039
1032
  export function chat(doc, oldDoc, user, ctx) {
1040
1033
  if (!user) throw { forbidden: "authentication required" };
1041
1034
 
@@ -1119,9 +1112,8 @@ App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the
1119
1112
 
1120
1113
  ### Example: Anonymous survey with role-gated results
1121
1114
 
1122
- access.js
1123
-
1124
- ```js file=access.js partial
1115
+ ### `access.js`
1116
+ ```js partial
1125
1117
  export function survey(doc, oldDoc, user, ctx) {
1126
1118
  if (doc.type === "survey-response") {
1127
1119
  if (oldDoc) throw { forbidden: "responses are write-once" };
@@ -1195,9 +1187,8 @@ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated
1195
1187
 
1196
1188
  When the prompt says **anyone can sign / submit without logging in** (a guestbook, a contact form, an RSVP), do **not** throw on `!user` — return `allowAnonymous: true` so the write is accepted for anonymous visitors. `useVibe().can.create(...)` then returns `ok` for an anonymous viewer, and the form shows instead of a sign-in wall. Stamp `authorHandle` only when there is a user.
1197
1189
 
1198
- access.js
1199
-
1200
- ```js file=access.js partial
1190
+ ### `access.js`
1191
+ ```js partial
1201
1192
  export function guestbook(doc, oldDoc, user, ctx) {
1202
1193
  if (doc.type === "entry") {
1203
1194
  if (oldDoc) throw { forbidden: "entries are write-once" };
package/llms/backend.md CHANGED
@@ -50,7 +50,7 @@ the declared fallback, with its freshness and failure visible in saved status.
50
50
  ## Output format
51
51
 
52
52
  `backend.js` is a separate file, exactly like `access.js`: one prose line, the
53
- filename `backend.js` on its own line, then one **complete** fenced block (the
53
+ heading naming `backend.js`, then one **complete** fenced block below it (the
54
54
  whole file — don't use SEARCH/REPLACE for it; re-emit the full file to change
55
55
  it). **Never put backend code inside an `App.jsx` block**, and never import
56
56
  `backend.js` from `App.jsx` — the browser can't run it.
@@ -468,9 +468,8 @@ This complete example makes one curated GitHub API request every 15 minutes
468
468
  (well inside the documented caps), stores at most 20 normalized `release` docs
469
469
  in `nodeReleases`, and records refresh state in that same database.
470
470
 
471
- backend.js
472
-
473
- ```js file=backend.js
471
+ ### `backend.js`
472
+ ```js
474
473
  export const config = { scheduled: { interval: "15m" } };
475
474
 
476
475
  const SNAPSHOT = [
@@ -565,9 +564,8 @@ export async function scheduled(event, ctx) {
565
564
  }
566
565
  ```
567
566
 
568
- App.jsx
569
-
570
- ```jsx file=App.jsx
567
+ ### `App.jsx`
568
+ ```jsx
571
569
  import React from "react";
572
570
  import { useFireproof } from "use-fireproof";
573
571
  import { Badge, Card, CardContent } from "@vibes.diy/look";
@@ -644,7 +642,8 @@ Runs for requests to the app's `/_api` route. The request path is rooted after
644
642
  `/_api`: a call to `https://slug--owner.host/_api/webhooks/pay` arrives with
645
643
  pathname `/webhooks/pay`. Return a standard `Response`.
646
644
 
647
- ```js file=backend.js
645
+ ### `backend.js`
646
+ ```js
648
647
  export async function fetch(request, ctx) {
649
648
  const url = new URL(request.url);
650
649
  if (url.pathname === "/rsvp" && request.method === "POST") {
@@ -660,7 +659,8 @@ export async function fetch(request, ctx) {
660
659
 
661
660
  From `App.jsx`, call it with a relative fetch — no host needed:
662
661
 
663
- ```js file=App.jsx partial
662
+ ### `App.jsx`
663
+ ```js partial
664
664
  const res = await fetch("/_api/rsvp", { method: "POST", body: JSON.stringify({ name }) });
665
665
  ```
666
666
 
@@ -707,7 +707,8 @@ needs to know the visitor — their own records, their connection, their
707
707
  permissions — the page calls it with `apiFetch`.** The path is the route under
708
708
  `/_api`, and the answer reads like a `Response`:
709
709
 
710
- ```jsx file=App.jsx partial
710
+ ### `App.jsx`
711
+ ```jsx partial
711
712
  import { apiFetch } from "use-vibes";
712
713
 
713
714
  const res = await apiFetch("/my-rsvp", {
@@ -735,7 +736,8 @@ owner, declare `config.fetch.unfilteredReads` for the collections it reads, page
735
736
  `ctx.db.query` returns, and keep progress in a state document read by id so a second run continues
736
737
  where the first stopped.
737
738
 
738
- ```js file=backend.js
739
+ ### `backend.js`
740
+ ```js
739
741
  export const config = { fetch: { unfilteredReads: ["rsvps"] } };
740
742
 
741
743
  export async function fetch(request, ctx) {
@@ -774,9 +776,8 @@ browser, a consent wall, a host answering with an empty body — is a read that
774
776
  it answers `ok: false` with a reason. That is what makes `page.ok` a sound thing for the app to
775
777
  branch on: a score or a summary is only ever computed from text the handler actually returned.
776
778
 
777
- backend.js
778
-
779
- ```js file=backend.js
779
+ ### `backend.js`
780
+ ```js
780
781
  export async function fetch(request, ctx) {
781
782
  const url = new URL(request.url);
782
783
  if (url.pathname === "/page") {
@@ -815,7 +816,8 @@ export async function fetch(request, ctx) {
815
816
 
816
817
  From `App.jsx`, the page asks the handler for the address it was given:
817
818
 
818
- ```js file=App.jsx partial
819
+ ### `App.jsx`
820
+ ```js partial
819
821
  const res = await fetch(`/_api/page?url=${encodeURIComponent(url)}`);
820
822
  const page = await res.json();
821
823
  if (page.ok === false) {
@@ -842,7 +844,8 @@ server owes in response.
842
844
  Runs after any document write to the app's databases commits (user writes and
843
845
  backend writes alike). The event:
844
846
 
845
- ```js file=backend.js
847
+ ### `backend.js`
848
+ ```js
846
849
  export async function onChange(event, ctx) {
847
850
  // event: { dbName, docId, doc, oldDoc, seq, deleted } — doc carries _id and _rev
848
851
  if (event.dbName !== "votes" || event.deleted) return;
@@ -894,7 +897,8 @@ sweep's `query` row), then `put` the next status with `ifRev: doc._rev`. A
894
897
  - Keep each run short; a run past 15 minutes is delivered again while it is
895
898
  still going, so chunk long work into `scheduled` ticks.
896
899
 
897
- ```js file=backend.js
900
+ ### `backend.js`
901
+ ```js
898
902
  export const config = { scheduled: { interval: "30s" } };
899
903
  const DB = "jobs", LEASE_MS = 30_000, MAX_ATTEMPTS = 3;
900
904
  const rid = () => Math.random().toString(36).slice(2, 10);
@@ -941,7 +945,8 @@ production are at https://good.vibes.diy/docs/patterns/doc-state-machines.
941
945
  Requires a `config` export with a **static string-literal** interval between
942
946
  `"5s"` and `"1h"` (e.g. `"30s"`, `"5m"`, `"1h"` — computed values are rejected):
943
947
 
944
- ```js file=backend.js
948
+ ### `backend.js`
949
+ ```js
945
950
  export const config = { scheduled: { interval: "15m" } };
946
951
 
947
952
  export async function scheduled(event, ctx) {
@@ -969,7 +974,8 @@ saving on its own, which a stored hour-offset does not.
969
974
 
970
975
  App.jsx — when the owner saves the schedule:
971
976
 
972
- ```jsx file=App.jsx partial
977
+ ### `App.jsx`
978
+ ```jsx partial
973
979
  await merge({
974
980
  hour: 12, // the hour they picked, in their own day
975
981
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, // e.g. "America/Detroit"
@@ -978,7 +984,8 @@ await merge({
978
984
 
979
985
  backend.js — the tick compares in that zone:
980
986
 
981
- ```js file=backend.js partial
987
+ ### `backend.js`
988
+ ```js partial
982
989
  const hourLocal = Number(
983
990
  new Intl.DateTimeFormat("en-US", {
984
991
  timeZone: settings.timeZone ?? "UTC",
@@ -1044,7 +1051,8 @@ repeats" above: the interval is only how often the handler wakes up.
1044
1051
 
1045
1052
  App.jsx — write the zone once, when they arrive:
1046
1053
 
1047
- ```jsx file=App.jsx
1054
+ ### `App.jsx`
1055
+ ```jsx
1048
1056
  useEffect(() => {
1049
1057
  if (!me?.userHandle) return;
1050
1058
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -1055,7 +1063,8 @@ useEffect(() => {
1055
1063
 
1056
1064
  backend.js — evaluate each person in their own zone:
1057
1065
 
1058
- ```js file=backend.js
1066
+ ### `backend.js`
1067
+ ```js
1059
1068
  export const config = { scheduled: { interval: "15m" } };
1060
1069
  const WEEKDAYS = new Set(["Mon", "Tue", "Wed", "Thu", "Fri"]);
1061
1070
 
@@ -1111,7 +1120,8 @@ is the only place an owner can tell. For a job that acts twice a month that mean
1111
1120
  weeks of silence that reads exactly like a dead alarm. So log once per tick,
1112
1121
  before any early return:
1113
1122
 
1114
- ```js file=backend.js partial
1123
+ ### `backend.js`
1124
+ ```js partial
1115
1125
  export async function scheduled(event, ctx) {
1116
1126
  const due = eventsDueNow(event.scheduledTime);
1117
1127
  ctx.log("debug", "tick", { due: due.length }); // proof of life; no reads, no spend
@@ -1195,7 +1205,8 @@ genuinely produced something is a real state change and belongs in that doc —
1195
1205
  unlike a per-tick `checkedAt`, which is the churn the zero-churn rule above is
1196
1206
  about.
1197
1207
 
1198
- ```js file=backend.js
1208
+ ### `backend.js`
1209
+ ```js
1199
1210
  export const config = { scheduled: { interval: "15m" } };
1200
1211
 
1201
1212
  const SHOW_DB = "show"; // the one database App.jsx live-queries
@@ -1291,7 +1302,8 @@ including the owner's own — while still no-opping under admin mode, so a hand
1291
1302
  repair runs the function and keeps the routing. `access.js` is where that is
1292
1303
  said:
1293
1304
 
1294
- ```js file=access.js
1305
+ ### `access.js`
1306
+ ```js
1295
1307
  // access.js
1296
1308
  export default function (doc, oldDoc, user, ctx) {
1297
1309
  if (doc.type === "runState" || doc.type === "runStatus") {
@@ -1308,7 +1320,8 @@ picked and, beside it, the zone their own browser reported — and reads the sam
1308
1320
  database back, rendering the status beside the drafts so a failed run and a good
1309
1321
  one never look alike:
1310
1322
 
1311
- ```jsx file=App.jsx
1323
+ ### `App.jsx`
1324
+ ```jsx
1312
1325
  const { useLiveQuery, database } = useFireproof("show");
1313
1326
 
1314
1327
  async function saveSchedule(hour) {
@@ -1340,7 +1353,8 @@ mirrors each note into a per-author `activity` entry (acting as that author —
1340
1353
  same privilege, just automated), and a `scheduled` sweep maintains one `digest`
1341
1354
  doc that `access.js` restricts to the **owner**, so no user can forge it.
1342
1355
 
1343
- ```js file=backend.js
1356
+ ### `backend.js`
1357
+ ```js
1344
1358
  export const config = { scheduled: { interval: "15m" } };
1345
1359
 
1346
1360
  export async function onChange(event, ctx) {
package/llms/bluesky.md CHANGED
@@ -139,7 +139,8 @@ person can act on — "no data" is not.
139
139
 
140
140
  ## Complete backend.js
141
141
 
142
- ```js file=backend.js app=bluesky-browser
142
+ ### `backend.js`
143
+ ```js app=bluesky-browser
143
144
  const XRPC = "https://public.api.bsky.app/xrpc";
144
145
  const DB = "bluesky";
145
146
 
@@ -260,7 +261,8 @@ export async function fetch(request, ctx) {
260
261
  A profile card, its follow graph, and the account's posts — one handle in, no
261
262
  account of the visitor's involved.
262
263
 
263
- ```jsx file=App.jsx app=bluesky-browser
264
+ ### `App.jsx`
265
+ ```jsx app=bluesky-browser
264
266
  import React, { useState } from "react";
265
267
 
266
268
  export default function App() {
package/llms/calendar.md CHANGED
@@ -110,7 +110,8 @@ button is simply there once the user has subscribable content.
110
110
  there means the Subscribe link never renders, even though the mint effect and
111
111
  the feed itself work.
112
112
 
113
- ```jsx file=App.jsx app=calendar-feed
113
+ ### `App.jsx`
114
+ ```jsx app=calendar-feed
114
115
  // access.js needs a `caltoken` branch routing the doc to the owner's PRIVATE
115
116
  // channel (like notes) — the token is the secret.
116
117
  const { docs: calTokens } = useLiveQuery(byTypeUser, { key: ["caltoken", me.userHandle] });
@@ -153,7 +154,8 @@ the button tooltip.
153
154
  "2026-07-31", time: "18:00:00", endtime: "", title, venue, url } }` — adapt
154
155
  the doc shape, db name, and timezone to the app.
155
156
 
156
- ```js file=backend.js app=calendar-feed
157
+ ### `backend.js`
158
+ ```js app=calendar-feed
157
159
  export const config = { scheduled: { interval: "1m" } };
158
160
 
159
161
  const TZ = "America/Los_Angeles"; // the app's local timezone
@@ -20,7 +20,8 @@ After each successful creation, sentence correction or field edit, the parent ow
20
20
 
21
21
  This complete example covers **one sentence → one task**, **pasted list → many tasks**, **a selected task + correction → the same task**, **questions from the same box**, and **Undo after a save**. Every new submission uses the array schema, whether it names one task or several. The person sets a due date in the saved task’s date control. A malformed response leaves the typed sentence and saved records available; the waiting state ends when the request settles.
22
22
 
23
- ```jsx file=App.jsx
23
+ ### `App.jsx`
24
+ ```jsx
24
25
  import React, { useState } from "react";
25
26
  import { callAI } from "call-ai";
26
27
  import { useFireproof } from "use-fireproof";
package/llms/callai.md CHANGED
@@ -20,7 +20,8 @@ After each successful creation, sentence correction or field edit, the parent ow
20
20
 
21
21
  This complete example covers **one sentence → one task**, **pasted list → many tasks**, **a selected task + correction → the same task**, **questions from the same box**, and **Undo after a save**. Every new submission uses the array schema, whether it names one task or several. The person sets a due date in the saved task’s date control. A malformed response leaves the typed sentence and saved records available; the waiting state ends when the request settles.
22
22
 
23
- ```jsx file=App.jsx
23
+ ### `App.jsx`
24
+ ```jsx
24
25
  import React, { useState } from "react";
25
26
  import { callAI } from "call-ai";
26
27
  import { useFireproof } from "use-fireproof";
@@ -201,7 +202,8 @@ Send an image by passing the prompt as an array of content parts: an `image_url`
201
202
 
202
203
  The prompt is the content of one user message, so a plain string means exactly one text part and the array is that same message's parts. Read the picture the user picked with a `FileReader` and hand over the data URL it produces, straight from the camera or photo library — `callAI` shrinks a large photo to a model-friendly size (1568px on its long edge) before sending it, so the app passes the whole picture and leaves the resizing to the platform:
203
204
 
204
- ```javascript file=App.jsx
205
+ ### `App.jsx`
206
+ ```javascript
205
207
  import { callAI } from "call-ai";
206
208
 
207
209
  function readAsDataUrl(file) {
@@ -32,7 +32,8 @@ reaches the third with `apiFetch` (section 4), so it runs as the visitor.
32
32
 
33
33
  ### 1. Declare it — `backend.js`
34
34
 
35
- ```js file=backend.js app=instagram-connection
35
+ ### `backend.js`
36
+ ```js app=instagram-connection
36
37
  export const config = {
37
38
  connections: [
38
39
  {
@@ -71,7 +72,8 @@ for the rest.
71
72
 
72
73
  ### 2. Ask for it — `App.jsx`
73
74
 
74
- ```jsx file=App.jsx app=instagram-connection
75
+ ### `App.jsx`
76
+ ```jsx app=instagram-connection
75
77
  import { useConnection } from "use-vibes";
76
78
 
77
79
  function ConnectInstagram() {
@@ -113,7 +115,8 @@ the platform's Settings page, not in your app.
113
115
 
114
116
  ### 3. Read it — `backend.js`
115
117
 
116
- ```js file=backend.js app=instagram-connection
118
+ ### `backend.js`
119
+ ```js app=instagram-connection
117
120
  import { instagram, connectionState } from "@vibes.diy/social";
118
121
 
119
122
  export async function fetch(request, ctx) {
@@ -188,7 +191,8 @@ connection. The path is the route under `/_api`, so `apiFetch("/top-posts")`
188
191
  reaches the `/top-posts` branch above. Wait for the connection before calling,
189
192
  so the first answer is the real one.
190
193
 
191
- ```jsx file=App.jsx app=instagram-connection
194
+ ### `App.jsx`
195
+ ```jsx app=instagram-connection
192
196
  import React, { useEffect, useState } from "react";
193
197
  import { useConnection, apiFetch } from "use-vibes";
194
198
 
@@ -24,7 +24,8 @@ never automatically from an effect, a timer, or straight out of an async `callAI
24
24
  resolution. Gate it behind an explicit "Create my ___" button the user presses
25
25
  when they're ready.
26
26
 
27
- ```jsx file=App.jsx
27
+ ### `App.jsx`
28
+ ```jsx
28
29
  import React from "react";
29
30
  import { createVibe } from "use-vibes";
30
31
  import { Button } from "@vibes.diy/look";
@@ -63,7 +64,8 @@ Let the user edit each slide inline and present full-screen.`;
63
64
 
64
65
  ## Full pattern — interview, then hand off
65
66
 
66
- ```jsx file=App.jsx
67
+ ### `App.jsx`
68
+ ```jsx
67
69
  import React from "react";
68
70
  import { callAI } from "call-ai";
69
71
  import { createVibe } from "use-vibes";
package/llms/d3.md CHANGED
@@ -138,7 +138,8 @@ Structure your component in this order:
138
138
 
139
139
  ### 1. Simple Bar Chart
140
140
 
141
- ```js file=App.jsx
141
+ ### `App.jsx`
142
+ ```js
142
143
  import * as d3 from "d3";
143
144
 
144
145
  // 1. Design tokens and dimensions
@@ -0,0 +1,47 @@
1
+ # Fireproof on vibes.diy — the short version
2
+
3
+ Fireproof is the app's database: a local-first document store that syncs in the background. Records are plain JSON documents. Everything on screen comes from a live query, so the UI updates itself after every save.
4
+
5
+ ```jsx
6
+ import { useFireproof } from "use-fireproof";
7
+
8
+ export default function App() {
9
+ const { database, useLiveQuery, useDocument } = useFireproof("myApp"); // one plain camelCase name per app
10
+
11
+ // Read: every doc whose `type` is "note", newest first. `docs` re-renders on every change.
12
+ const { docs: notes } = useLiveQuery("type", { key: "note", descending: true });
13
+
14
+ // Create: one document per record. `type` names the record kind; stamp the author and the parent id.
15
+ const addNote = (text) => database.put({ type: "note", text, authorHandle: "me", createdAt: Date.now() });
16
+
17
+ // Update: spread the existing doc and change fields. Delete: by id.
18
+ const toggle = (note) => database.put({ ...note, done: !note.done });
19
+ const remove = (note) => database.del(note._id);
20
+
21
+ // A form bound to one document (edit view): `doc`, `merge` to change fields, `save` to persist.
22
+ const { doc, merge, save } = useDocument({ type: "note", text: "" });
23
+
24
+ return (
25
+ <main>
26
+ <input value={doc.text} onChange={(e) => merge({ text: e.target.value })} />
27
+ <button onClick={() => save()}>Save</button>
28
+ {notes.map((n) => (
29
+ <div key={n._id}>
30
+ <span onClick={() => toggle(n)}>{n.done ? "✓" : "○"} {n.text}</span>
31
+ <button onClick={() => remove(n)}>Delete</button>
32
+ </div>
33
+ ))}
34
+ </main>
35
+ );
36
+ }
37
+ ```
38
+
39
+ Rules of the road:
40
+
41
+ - `useFireproof(name)` is called once at the top of a component; `useLiveQuery` and `useDocument` come from it and are also called at the top, outside handlers and loops.
42
+ - `useLiveQuery(field, { key })` returns `{ docs }` matching `doc[field] === key`; `useLiveQuery(field)` returns every doc that has the field, sorted by it. `{ descending: true }` and `{ limit: n }` are available. Filter and sort further in plain JavaScript.
43
+ - Documents get an auto `_id`; write one explicitly only for a well-known singleton (`"settings"`) or a one-per-person-per-thing record (`` `vote-${handle}-${postId}` ``), so re-saves replace rather than duplicate.
44
+ - Writes land instantly and the store is the feedback: after `database.put`, the live query shows the new record. A saved card is the confirmation.
45
+ - One document per event or record, and a `type` string on every document. Keep documents small; a list is many documents, each with the id of the list it belongs to (`listId`), rather than one document holding an array that grows.
46
+ - Related records reference each other by `_id`. Seed items that other seed items point at carry an explicit `_id`, and the reference uses that exact string.
47
+ - A list that renders one field per row can carry that field on the row: `useLiveQuery((doc, emit) => emit(doc.listId, { label: doc.label }), { key: listId })` puts the summary in `rows[].value` — exactly what was passed, including `0`, `""`, `false`, `null`, or an explicit `undefined` — while `docs` still holds the whole documents. Calling `emit(key)` alone leaves the whole document as the value.