@vibes.diy/prompts 14.1.20 → 14.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms/access.md CHANGED
@@ -299,7 +299,9 @@ export function board(doc, oldDoc, user, ctx) {
299
299
 
300
300
  **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own todos, post a note, drop a pin, join a shared canvas. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
301
301
 
302
- **A personal list, tracker, journal, or notes app — one the prompt frames as the user's own, with no sharing asked for — gives every visitor 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:
302
+ **When the person says "no sharing", "no access control" or "keep it single-user", that is the one case where `access.js` is left out altogether:** the app ships with no rules file, and the object id and `authorHandle` its records already carry are what let rules be added later without touching a doc.
303
+
304
+ **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:
303
305
 
304
306
  access.js
305
307
 
@@ -366,9 +368,9 @@ export function habits(doc, oldDoc, user, ctx) {
366
368
  }
367
369
  ```
368
370
 
369
- The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own streaks and any buddy who granted them in. A **plain daily habit tracker** — one framed as the user's own, with no catalog, leaderboard, or buddy asked for — is instead the per-visitor shape shown above: every visitor's habits and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
371
+ The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own streaks and any buddy who granted them in. A habit tracker whose owner **asked to keep it to themselves** is instead the per-visitor shape shown above: every visitor's habits and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
370
372
 
371
- **"Invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or a board/canvas/room/whiteboard a group co-edits → per-object collaboration** (the second worked example above) — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object (`board:<id>`/`list:<id>`); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request).
373
+ **Per-object collaboration is the resting shape of almost every app** (the second worked example above), and certainly of anything that says "invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or names a board/canvas/room/whiteboard a group co-edits — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object (`board:<id>`/`list:<id>`); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request). The app owner is not special in the data model: any signed-in person creates their own object and invites a friend into it, and the rule keys on `user.userHandle` or the object's creator, never on `user.isOwner`.
372
374
 
373
375
  **Ownership is just the object graph** — whoever authored or created a doc owns it (`doc.authorHandle === user.userHandle`, checked against `oldDoc` on updates). There's no broadcaster shape to reach for by default, and **owner-only publishing is a dead end** — never gate the content itself on `requireRole("owner")`. **A blog, magazine, or publication is public read + author-owned posts, with the owner controlling the _author roster_:** the owner approves authors with a grant doc (`if (doc.type === "author") { ctx.requireRole("owner"); return { channels: ["blog:authors"], grant: { users: { [doc.authorHandle]: ["blog:authors"] }, roles: { owner: ["blog:authors"] } } }; }` — the **one** place `requireRole("owner")` belongs, gating who may author, never the posts); a post then gates on `ctx.requireAccess("blog:authors")` (membership) and is author-owned (`doc.authorHandle === user.userHandle` + the `oldDoc` check), so once approved each author's post is _their own_ object — only they edit it, and they moderate the comments on it (a comment is allowed if it's your comment **or** you own the post: `doc.authorHandle === user.userHandle || doc.postAuthorHandle === user.userHandle`). A personal blog is just this with a roster of one. Always gate write UI on `useVibe(dbName).can`.
374
376
 
@@ -674,7 +676,7 @@ Channel `_id` is the channel identifier everywhere. The access function uses `do
674
676
 
675
677
  ### Example: Per-object sharing (collaborate on your own objects, no admin)
676
678
 
677
- Reach for this 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 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.
679
+ 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.
678
680
 
679
681
  access.js
680
682
 
@@ -755,6 +757,83 @@ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `u
755
757
  >>>>>>> REPLACE
756
758
  ```
757
759
 
760
+ #### The same shape with a default object per person, and a member doc that names the friend
761
+
762
+ Reach for this variant whenever the app is built around a *thing* people come back to — a board, a trip, a game night's scoreboard, a hiking group's trail list, a habit tracker one person keeps and a coach is invited into next month. Everything above still holds; three additions make it feel finished from the first load:
763
+
764
+ - **Every signed-in person already has one object, with no doc to create.** Their default channel is derived from their own handle — `` `board:default-${user.userHandle}` `` — so a brand-new visitor opens the app, types, and their first item lands. Each write to it re-grants that person their own channel, because there is no object doc carrying the grant. **The child doc stores that board id rather than having each writer derive one**: the create writes `` boardId: pickedBoardId || `default-${me.userHandle}` `` and the rule holds it fixed from then on, so a card stays on the board its creator put it on however many people edit it later.
765
+ - **Membership is a `member` doc, and picking the friend is `HandleInput`** (`useViewer()`, see use-viewer.md — the platform's people-picker, so the handle on the doc is one the platform resolved). The doc grants the named person the object's channel: `` grant: { users: { [doc.userHandle]: [chan] } } ``.
766
+ - **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`.
767
+
768
+ access.js
769
+
770
+ ```js
771
+ export function boards(doc, oldDoc, user, ctx) {
772
+ if (!user?.userHandle) throw { forbidden: "sign in" };
773
+ const safeId = (id) => {
774
+ if (typeof id !== "string" || !/^[A-Za-z0-9_-]+$/.test(id)) throw { forbidden: "bad id" };
775
+ return id;
776
+ };
777
+ const myDefault = "default-" + user.userHandle;
778
+ const ch = (id) => "board:" + safeId(id);
779
+
780
+ if (doc.type === "board") {
781
+ // Anyone signed in makes their own board; the creator holds it and its admin channel.
782
+ if (oldDoc) {
783
+ if (doc.creatorHandle !== oldDoc.creatorHandle) throw { forbidden: "creator is fixed" };
784
+ ctx.requireAccess(ch(doc._id) + "/admin"); // renaming is an admin act — updates only
785
+ } else if (doc.creatorHandle !== user.userHandle) {
786
+ throw { forbidden: "you must be the creator" };
787
+ }
788
+ return { channels: [ch(doc._id)], grant: { users: { [doc.creatorHandle]: [ch(doc._id), ch(doc._id) + "/admin"] } } };
789
+ }
790
+
791
+ if (doc.type === "card") {
792
+ // The card STORES its board id from the moment it is created (the client writes the
793
+ // creator's own default when the person picked no board) and holds it fixed after —
794
+ // including the absent-to-present move, so a later editor's handle always finds the
795
+ // id already on the doc rather than supplying one.
796
+ if (!oldDoc && (typeof doc.boardId !== "string" || doc.boardId.length === 0)) {
797
+ throw { forbidden: "a card names its board" };
798
+ }
799
+ if (oldDoc && doc.boardId !== oldDoc.boardId) throw { forbidden: "card stays on its board" };
800
+ const chan = ch(doc.boardId);
801
+ if (doc.boardId !== myDefault) ctx.requireAccess(chan); // any member edits any card on it
802
+ // The implicit default board has no board doc, so each write re-grants its own person.
803
+ if (doc.boardId === myDefault) {
804
+ return { channels: [chan], grant: { users: { [user.userHandle]: [chan, chan + "/admin"] } } };
805
+ }
806
+ return { channels: [chan] };
807
+ }
808
+
809
+ if (doc.type === "member") {
810
+ // The board's admin invites a person the picker resolved; the doc IS the grant.
811
+ if (oldDoc) throw { forbidden: "membership grants are fixed" };
812
+ if (doc.addedBy !== user.userHandle) throw { forbidden: "addedBy must be you" };
813
+ if (doc.boardId !== myDefault) ctx.requireAccess(ch(doc.boardId) + "/admin");
814
+ return { channels: [ch(doc.boardId)], grant: { users: { [doc.userHandle]: [ch(doc.boardId)] } } };
815
+ }
816
+
817
+ throw { forbidden: "unknown document type" };
818
+ }
819
+ ```
820
+
821
+ App.jsx — the invite surface is `HandleInput` plus one `member` write, shown to whoever the access fn would accept:
822
+
823
+ ```jsx
824
+ const { HandleInput } = useViewer();
825
+ const { me, can } = useVibe("boards");
826
+ const canInvite = (boardId) => can.create({ type: "member", boardId, userHandle: "x", addedBy: me?.userHandle }).ok;
827
+ const addMember = (boardId, handle) =>
828
+ handle && database.put({ type: "member", boardId, userHandle: handle, addedBy: me.userHandle });
829
+ // Every card names its board at creation — the person's own default board when they picked none.
830
+ const addCard = (text) => database.put({ type: "card", boardId: pickedBoardId || `default-${me.userHandle}`, text });
831
+ // …
832
+ {canInvite(board._id) && <HandleInput onChange={(h) => addMember(board._id, h)} placeholder="Add a friend…" />}
833
+ ```
834
+
835
+ Where the app's own maintainer needs to move records through the gate — a CLI migration re-homing cards — `!user.isOwner` reads as a bypass written beside a check (`if (!user.isOwner) ctx.requireAccess(chan)`), never as the arm that decides who may act.
836
+
758
837
  ## More worked round-trip examples
759
838
 
760
839
  ### Example: Workspace chat with channels
package/llms/backend.md CHANGED
@@ -601,6 +601,33 @@ From `App.jsx`, call it with a relative fetch — no host needed:
601
601
  const res = await fetch("/_api/rsvp", { method: "POST", body: JSON.stringify({ name }) });
602
602
  ```
603
603
 
604
+ ### Bulk transformations run as a route
605
+
606
+ A change that touches many stored records is written once, as a named `/_api` route, and run
607
+ once — not looped from outside the app. Gate the route inside the handler on the caller being the
608
+ owner, declare `config.fetch.unfilteredReads` for the collections it reads, page with the cursor
609
+ `ctx.db.query` returns, and keep progress in a state document read by id so a second run continues
610
+ where the first stopped.
611
+
612
+ ```js
613
+ export const config = { fetch: { unfilteredReads: ["rsvps"] } };
614
+
615
+ export async function fetch(request, ctx) {
616
+ const url = new URL(request.url);
617
+ if (url.pathname !== "/admin/normalize") return new Response("not found", { status: 404 });
618
+ if (ctx.user?.userHandle !== ctx.ownerHandle) return new Response("forbidden", { status: 403 });
619
+ const page = await ctx.db.query("kind", { key: "rsvp", limit: 100, db: "rsvps" });
620
+ for (const doc of page) {
621
+ if (typeof doc.name === "string" && doc.name !== doc.name.trim()) {
622
+ await ctx.db.put({ ...doc, name: doc.name.trim() }, { db: "rsvps" });
623
+ }
624
+ }
625
+ return new Response(JSON.stringify({ ok: true, more: page.next ?? null }), {
626
+ headers: { "content-type": "application/json" },
627
+ });
628
+ }
629
+ ```
630
+
604
631
  ### Reading a page the person names — the `/_api` proxy
605
632
 
606
633
  A page the person names is read in `backend.js` and handed to the app through `/_api`. A browser
@@ -0,0 +1,121 @@
1
+ # CallAI Helper Function
2
+
3
+ The `callAI` function asks an AI model and returns a string. With a `schema` the string is structured JSON you `JSON.parse()`; without one it is ordinary text — a chat reply, a caption, a paragraph — used as is. Reach for a schema when the app needs fields, and leave it out when it needs prose.
4
+
5
+ ## Basic Usage
6
+
7
+ ```javascript
8
+ import { callAI } from "call-ai";
9
+
10
+ const response = await callAI("Give me a todo list for learning React", {
11
+ schema: {
12
+ properties: {
13
+ todos: {
14
+ type: "array",
15
+ description: "List of actionable todo items",
16
+ items: { type: "string" },
17
+ },
18
+ },
19
+ },
20
+ });
21
+ const todoData = JSON.parse(response);
22
+ console.log(todoData.todos);
23
+ ```
24
+
25
+ ## Items with properties
26
+
27
+ ```javascript
28
+ const response = await callAI(
29
+ "Generate 4 items with label, status, priority (low, medium, high, critical), and notes. Return as structured JSON with these fields.",
30
+ {
31
+ schema: {
32
+ properties: {
33
+ items: {
34
+ type: "array",
35
+ items: {
36
+ type: "object",
37
+ properties: {
38
+ label: { type: "string", description: "Short name for the item" },
39
+ status: { type: "string", description: "Current status (active, done, blocked)" },
40
+ priority: { type: "string", description: "Priority level (low, medium, high, critical)" },
41
+ notes: { type: "string", description: "Additional context or details" },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ }
48
+ );
49
+ const demoData = JSON.parse(response);
50
+ console.log(demoData.items); // Array of item objects
51
+ ```
52
+
53
+ ### Schema Tips
54
+
55
+ - Flat schemas perform better across all models. Avoid nesting deeper than 2 levels.
56
+ - Use simple property names like `name`, `type`, `items`, `price`.
57
+ - Keep array items simple (strings or flat objects).
58
+
59
+ ## Reading an Image
60
+
61
+ Send an image by passing the prompt as an array of content parts: an `image_url` part carrying the picture beside a `text` part asking the question.
62
+
63
+ 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:
64
+
65
+ ```javascript
66
+ import { callAI } from "call-ai";
67
+
68
+ function readAsDataUrl(file) {
69
+ return new Promise((resolve, reject) => {
70
+ const reader = new FileReader();
71
+ reader.onload = () => resolve(reader.result);
72
+ reader.onerror = () => reject(reader.error);
73
+ reader.readAsDataURL(file);
74
+ });
75
+ }
76
+
77
+ async function readFlyer(file) {
78
+ const dataUrl = await readAsDataUrl(file);
79
+ const response = await callAI(
80
+ [
81
+ { type: "text", text: "Read this event flyer and pull out the event details." },
82
+ { type: "image_url", image_url: { url: dataUrl } },
83
+ ],
84
+ {
85
+ schema: {
86
+ properties: {
87
+ title: { type: "string", description: "Name of the event" },
88
+ date: { type: "string", description: "Date as printed on the flyer" },
89
+ time: { type: "string" },
90
+ venue: { type: "string" },
91
+ description: { type: "string" },
92
+ },
93
+ },
94
+ }
95
+ );
96
+ return JSON.parse(response);
97
+ }
98
+ ```
99
+
100
+ A string prompt stays the common path for text-only work and behaves exactly as it always has. A model that reads text alone answers an image request with an error naming that model, so an app that asks about a picture hears when the picture went unread — surface that message the way you surface any other `callAI` error.
101
+
102
+ ## Error Handling
103
+
104
+ ```javascript
105
+ import { callAI } from "call-ai";
106
+
107
+ try {
108
+ const response = await callAI("Generate some content", {
109
+ schema: {
110
+ properties: {
111
+ result: { type: "string" },
112
+ },
113
+ },
114
+ });
115
+ console.log(JSON.parse(response));
116
+ } catch (error) {
117
+ console.error("API error:", error.message);
118
+ }
119
+ ```
120
+
121
+ Let the request finish. Reading an image takes several seconds, so a slow answer is still an answer: show a waiting state ("reading your photo…") and keep awaiting the call. The `catch` above is what tells the app a request actually failed.
package/llms/callai.js CHANGED
@@ -2,6 +2,7 @@ export const callaiConfig = {
2
2
  name: "callai",
3
3
  label: "callAI",
4
4
  description: "easy API for LLM requests with streaming support",
5
+ initialVariant: true,
5
6
  importModule: "call-ai",
6
7
  importName: "callAI",
7
8
  };
@@ -1 +1 @@
1
- {"version":3,"file":"callai.js","sourceRoot":"","sources":["../../jsr/llms/callai.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,kDAAkD;IAC/D,YAAY,EAAE,SAAS;IACvB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
1
+ {"version":3,"file":"callai.js","sourceRoot":"","sources":["../../jsr/llms/callai.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAc;IACrC,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,WAAW,EAAE,kDAAkD;IAC/D,cAAc,EAAE,IAAI;IACpB,YAAY,EAAE,SAAS;IACvB,UAAU,EAAE,QAAQ;CACrB,CAAC"}
@@ -0,0 +1,114 @@
1
+ # debugging — working out what a deployed app is actually doing
2
+
3
+ This guide is for you, the agent, when something in a live app does not match
4
+ what the code says it should do. It is the method, not a feature the app author
5
+ types.
6
+
7
+ ## The loop
8
+
9
+ 1. **Reproduce in words.** Say what the person did, what they expected and what
10
+ they saw. A reproduction you can state is a reproduction you can check.
11
+ 2. **Read the release state.** Which version is serving, whether newer work is
12
+ waiting behind it, whether the version that is live carries the record rules
13
+ and the server file you think it does. A change that was never released is
14
+ the most common explanation for "I fixed that already".
15
+ 3. **Read the logs.** What the deployed app has been recording about itself,
16
+ newest first. A line the app wrote beats an expectation about the code.
17
+ 4. **Read the records.** The data settles which of two stories is true. Read it
18
+ as the person whose app it is, so you see what their app shows them.
19
+ 5. **One hypothesis.** Name the single thing you now believe is wrong, in one
20
+ sentence.
21
+ 6. **The smallest change that tests it.** One record, one line, one handler.
22
+ 7. **Verify from a read.** Read the thing back after the change. A claim rests
23
+ on what a read returned, never on the fact that a call succeeded.
24
+
25
+ ## Instrumentation the platform already has
26
+
27
+ `ctx.log` writes a line from server code, and `vibe.log` writes one from the
28
+ page. Both land in the app's own log stream, stamped with the version that
29
+ produced them, so a deploy boundary is visible in the stream.
30
+
31
+ ```js
32
+ export async function scheduled(ctx) {
33
+ ctx.log("info", "tick start", { at: new Date().toISOString() });
34
+ const page = await ctx.db.query("kind", { key: "rsvp", limit: 100, db: "rsvps" });
35
+ ctx.log("info", "tick read", { count: page.length });
36
+ }
37
+ ```
38
+
39
+ A probe that only paints its result on screen is unreadable from outside the
40
+ app, so a self-checking probe echoes each case on one line:
41
+
42
+ ```js
43
+ console.log(`[RSVP] ${ok ? "PASS" : "FAIL"} duplicate-guard ${JSON.stringify({ id, count })}`);
44
+ ```
45
+
46
+ Start the run with a marker — `console.log("===== START =====")` — so a later
47
+ read knows which lines belong to this attempt.
48
+
49
+ ## Bulk work is a route, not a loop
50
+
51
+ Changing a handful of records one at a time is fine. Past that, write the change
52
+ once, as a named route in the app's own server file, and have the person run it.
53
+ A route runs beside the data, keeps its own progress, and can be read and
54
+ re-read before it is trusted.
55
+
56
+ Gate the route inside the handler on the caller being the owner, declare
57
+ `config.fetch.unfilteredReads` for the collections it reads, page with a cursor,
58
+ and remember progress in a state document read by id so a second run continues
59
+ rather than starting over. An empty page can still carry `next`, so loop on the
60
+ cursor.
61
+
62
+ ```js
63
+ export const config = { fetch: { unfilteredReads: ["rsvps"] } };
64
+
65
+ export async function fetch(request, ctx) {
66
+ const url = new URL(request.url);
67
+ if (url.pathname !== "/admin/normalize") return new Response("not found", { status: 404 });
68
+ if (ctx.user?.userHandle !== ctx.ownerHandle) return new Response("forbidden", { status: 403 });
69
+
70
+ const state = (await ctx.db.get("0-normalize-state", { db: "rsvps" })) ?? { _id: "0-normalize-state", after: null };
71
+ let after = state.after;
72
+ let changed = 0;
73
+ for (let round = 0; round < 20; round++) {
74
+ const page = await ctx.db.query("kind", { key: "rsvp", limit: 100, ...(after ? { after } : {}), db: "rsvps" });
75
+ for (const doc of page) {
76
+ if (typeof doc.name === "string" && doc.name !== doc.name.trim()) {
77
+ await ctx.db.put({ ...doc, name: doc.name.trim() }, { db: "rsvps" });
78
+ changed++;
79
+ }
80
+ }
81
+ if (!page.next) { after = null; break; }
82
+ after = page.next;
83
+ }
84
+ await ctx.db.put({ ...state, after, at: Date.now() }, { db: "rsvps" });
85
+ ctx.log("info", "normalize round", { changed, after });
86
+ return new Response(JSON.stringify({ ok: true, changed, more: after !== null }), {
87
+ headers: { "content-type": "application/json" },
88
+ });
89
+ }
90
+ ```
91
+
92
+ Once the route is published, run it with `call_own_api`: it calls the app's own
93
+ routes as the person whose app this is, so the handler sees the owner and the
94
+ answer comes back in the conversation. Read the release state first when unsure
95
+ which routes the live version carries, and read the records back afterwards to
96
+ confirm what the run did.
97
+
98
+ The person can run the same route from their own machine with their own
99
+ credential:
100
+
101
+ ```sh
102
+ curl -sS -X POST \
103
+ -H "Authorization: Bearer $VIBES_TOKEN" \
104
+ https://vibes.diy/vibe/<owner>/<slug>/_api/admin/normalize
105
+ ```
106
+
107
+ ## A route is live code
108
+
109
+ `backend.js` and `access.js` always run the app's live release, so a route has
110
+ to be published before anyone can call it, and publishing it puts it in front of
111
+ everyone the app serves. Try a route whose effect you are unsure of under a
112
+ second address first, confirm what it did by reading the records back, then ship
113
+ it where the real data is. `call_own_api` reaches the live release too, so what
114
+ it runs is what everyone else is being served.