@vibes.diy/prompts 14.3.119 → 14.3.120

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
@@ -1,6 +1,6 @@
1
1
  # Access Control (`access.js`) — permission design
2
2
 
3
- This doc is in view because the app is getting its `access.js`: a new app's first build writes its rules with its screens, and a later edit adds or changes them. Either way the records carry their keys (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record), and this build writes the rules that key on them. The rules and those keys are plumbing with nothing on screen of their own; the invite screen joins the build when the ask names other people, sharing, a group or a person by name, and is otherwise offered as an option after the build. Every app gets record rules, and the group shape taught below is the resting default. A person the ask names is let in by a record — see _A person the ask names is let in by a record_ below — so the build states the membership record's shape and the invite handler that writes it.
3
+ This doc is in view because the app is getting its `access.js`: a new app's first build writes its rules with its screens, and a later edit adds or changes them. Either way the records carry their keys (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record), and this build writes the rules that key on them. The rules and those keys are plumbing with nothing on screen of their own; the invite screen joins the build when the ask names other people, sharing, a group or a person by name, and when the app is a person's own collection it is the first screen's add-a-friend control, since adding a friend is how a collection is shared; otherwise it is offered as an option after the build. Every app gets record rules, and the group shape taught below is the resting default. A person the ask names is let in by a record — see _A person the ask names is let in by a record_ below — so the build states the membership record's shape and the invite handler that writes it.
4
4
 
5
5
  **When an edit adds the rules to an app that already exists, the edit owes only what the rules need.** Read `App.jsx` for the object type, the field that names its creator and the parent id its children carry; name the channels from those records; write the complete `access.js`; then change `App.jsx` only where the rules require it — stable creator membership, typed content queries, any write surface not yet gated on `can`, and the invite screen when the ask names other people, sharing or a group — with SEARCH/REPLACE edits. A file the rules do not touch is not emitted, and stays as it is.
6
6
 
@@ -478,7 +478,7 @@ export function board(doc, oldDoc, user, ctx) {
478
478
 
479
479
  **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 entries, 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.
480
480
 
481
- **Every app ships `access.js`. When the person says "just for me", "no sharing" or "keep it single-user", each signed-in person gets their own record tree, and a publish step shares a chosen item with everyone who can open the app.** The rules key on the writer's own handle, so they read the same for the person who made the app and for anyone they invite later. The app's nature is sensitive, and who can open the app is its sharing setting, not the rules: a published item is world-readable in `access.js` and reaches whoever can open the app — its members while it stays private, everyone once the owner opens it up. Publish copy says that plainly ("Published — everyone who can open this app sees it"):
481
+ **Every app ships `access.js`. When the person says "just for me", "no sharing" or "keep it single-user", each signed-in person gets their own collection, private to them, and the way to share it is adding a friend, who then sees the collection.** That is the private per-user shape below (the notes example, whose `member` branch is the add-a-friend control's write), or the default-object variant further down when the app is built around a thing people come back to: either way the add-a-friend control writes the `member` doc that grants the friend the collection's channel. The rules key on the writer's own handle, so they read the same for the person who made the app and for anyone they invite later. The app's nature is sensitive, and who can open the app is its sharing setting, not the rules. A piece made for readers keeps its own publish step instead — a sketch drafted for a gallery, a post, an article — on the per-item channel this example shows, whose read-grant flips from a `visibility` field: a published piece is world-readable in `access.js` and reaches whoever can open the app — its members while it stays private, everyone once the owner opens it up. Publish copy says that plainly ("Published — everyone who can open this app sees it"):
482
482
 
483
483
  ### `access.js`
484
484
  ```js partial
@@ -508,6 +508,7 @@ export function sketches(doc, oldDoc, user, ctx) {
508
508
  ```js partial
509
509
  export function notes(doc, oldDoc, user, ctx) {
510
510
  if (!user) throw { forbidden: "sign in" };
511
+ if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" };
511
512
  const ch = `user:${user.userHandle}`; // this visitor's own private space
512
513
 
513
514
  if (doc.type === "note") {
@@ -520,17 +521,32 @@ export function notes(doc, oldDoc, user, ctx) {
520
521
  return { channels: [ch], grant: { users: { [user.userHandle]: [ch] } } };
521
522
  }
522
523
 
524
+ if (doc.type === "member") {
525
+ // the add-a-friend control's write: the named friend reads this person's whole collection.
526
+ // A membership stays its writer's, and who it names is fixed, so a friend who can read these
527
+ // records rewrites or removes none of them.
528
+ if (doc.addedBy !== user.userHandle) throw { forbidden: "addedBy must be you" };
529
+ if (oldDoc && (oldDoc.addedBy !== user.userHandle || doc.userHandle !== oldDoc.userHandle)) throw { forbidden: "a membership is its writer's" };
530
+ return { channels: [ch], grant: { users: { [doc.userHandle]: [ch] } } };
531
+ }
532
+
523
533
  throw { forbidden: "unknown document type" };
524
534
  }
525
535
 
526
- // Summary: each person's notes are theirs alone, private from the first one they write. A note carries its writer's handle in authorHandle.
536
+ // Summary: each person's notes are theirs alone, private from the first one they write, and a friend they add reads their notes. A note carries its writer's handle in authorHandle; a member record names the friend in userHandle and who added them in addedBy.
527
537
  ```
528
538
 
529
- Every item a visitor creates lands on their own `user:<handle>` channel, private to them from the first load. Pair it with a small "Only you can see this" cue beside the publish control (beside their content when the app has none), while the title, subtitle and hero say what the app is for. This is the shape whenever the app is one person's own collection kept for themselves — the whole space is one private channel, no membership step in the way. The word "personal" routes on what the data is FOR: a personal tracker/journal/notes app is this private per-user shape, while a personal blog or portfolio exists to be READ — that one is the publication shape below (public read + author-owned posts, a roster of one), not a private channel.
539
+ Every item a visitor creates lands on their own `user:<handle>` channel, private to them from the first load. Pair it with a small who-can-see cue beside the add-a-friend control ("Yours alone — add a friend to share your sill"), while the title, subtitle and hero say what the app is for. This is the shape whenever the app is one person's own collection kept for themselves — the whole space is one private channel, no membership step in the way — and the `member` doc the add-a-friend control writes grants the friend that one channel (`grant: { users: { [doc.userHandle]: [`user:${user.userHandle}`] } }`), or the default object's channel in the variant below. The word "personal" routes on what the data is FOR: a personal tracker/journal/notes app is this private per-user shape, while a personal blog or portfolio exists to be READ — that one is the publication shape below (public read + author-owned posts, a roster of one), not a private channel.
530
540
 
531
541
  **When the prompt asks to share, invite, or collaborate — bring in a partner, a buddy, a friend, a team, or a group that co-edits — each shared thing becomes its own object others can be invited into.** A seed-swap shelf you invite your sibling to, a board a group co-edits, a document you open to a collaborator: route that shared thing to its **own object channel** (`list:<id>`/`board:<id>`) and self-grant it at creation (`grant: { users: { [user.userHandle]: [ch] } }`), with the `authorHandle` create + `oldDoc` author checks so each item stays on its object. Then let the creator invite a chosen friend in: a `share` doc the creator authors grants that friend the same channel (`grant: { users: { [doc.invitee]: [ch] } }`), so they collaborate on that one list — sharing a single list or a whole space is the same grant at a different node of the object graph. A wall, guestbook, or map where each visitor adds their _own_ items is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
532
542
 
533
- **When a user's work is private by default, show it — and offer a way to publish.** If everything a user does routes to a channel only they can read (a per-user `user:<handle>`, a private journal/notes/tracker with no `grant.public`), the UI must say so: a small, persistent "Only you can see this" / "Private to you" cue beside the publish control that changes it (beside their content when the app has none), so no one wonders who's watching their unfinished work. Then give them a publish control — sharing fits every app that is not privacy-sensitive, so this one ships on the first build (or, where the records are made to be seen, put them on an always-on public channel instead, as the character-cast example below does) — but publish at the granularity of a **channel**, not a doc: channels are the unit of read isolation, so adding `grant.public` to the shared `user:<handle>` channel would expose _every_ private item on it, not just the one they meant to share. To publish a single item, route it to its **own** channel and flip only that channel's read-grant from a `visibility` field the access fn reads — exactly the per-item-channel shape the worked example below uses (`const ch = \`entry:${doc._id}\`; const grant = { users: { [user.userHandle]: [ch] } }; if (doc.visibility === "public") grant.public = [ch];`) — for anonymous visitors, or grant a shared app channel for all granted members. Gate the control on `useVibe(dbName).can`, and reflect the result back in the affordance ("Published — anyone can see this", with an unpublish to flip it back). Making the user's _whole_ space public is fine when that's the intent; silently leaking their other private items by publishing one is the trap to avoid. **When the person asked, in their own words, to keep it to themselves — "only I", "just me", "nobody else" — the publish control is the one sharing surface:** one tap on an item they choose, shared with everyone who can open the app. A privacy-sensitive domain on its own (a journal, a health tracker) is not that ask: it gets the two controls from _Two controls_ above, off until the person says so. Keep publish opt-in and one tap for user-selected items. **Every app that is neither privacy-sensitive nor an only-me ask ships with a sharing path already in it: either an always-on shared channel its records land on, or a publish control.** **An open-nature app — an image or meme wall, a bingo card, a party game — puts its records on an always-on `grant.public` channel from the first rules it ships, builds no publish step, and admits anonymous readers**, so everyone with the link sees what people make the moment they make it. The always-on channel is the least-surprise shape for anything playful and low-stakes (an image or meme wall, a group list, a guestbook): a `grant.public` channel, a members' channel, or an `audience` arm, so a thing someone makes is seen the moment they make it. The publish control is the shape for work people draft first: the per-item channel whose read-grant flips from a `visibility` field, behind one tap. A publish control that names its destination — "Publish to the wall", "Share to the group" — is the best shape of the second, not a requirement. An ordinary app with neither is a bug: every record lands on a channel only its writer can read, and nobody asked for that.
543
+ **The sharing shape follows what one record is.** A record a person keeps for their own life — a check-in, an entry, a line in a list, a plant on a windowsill, a stretch in a routine — belongs to the collection it sits in: the whole collection is private to its person, and the way to share it is adding a friend, who then sees the collection. A piece made for readers — a post, an article, a gallery piece, a portfolio entry — carries its own draft-then-publish step, the ritual readers already know from blogs. A thing made to be seen the moment it exists — a meme, a caption, a guestbook line, a character in a cast — lands on an always-on shared channel. The person's own words move the dial: "let me pick which ones each friend sees" adds choosing items per friend to the add-a-friend control, and "drafts" or "publish" brings the per-item step to any record. Each shape ships with its one who-can-see line beside the control it describes, while the title, subtitle and hero say what the app is for:
544
+
545
+ - **A collection, shared by adding a friend.** Everything the person keeps routes to a channel only they read (a per-user `user:<handle>`, or their default object's channel), and the first screen carries the add-a-friend control — the platform's `HandleInput` writing a `member` doc that grants that handle the collection's channel (`grant: { users: { [doc.userHandle]: [ch] } }`), the shape _The same shape with a default object per person_ below works through. The friend sees the collection, and the cue beside the control reads in the app's words ("Yours alone — add a friend to share your sill"). This is the resting shape for a tracker, a log, a list, a routine, a shelf, and for an ask kept to themselves ("only I", "just me", "nobody else"). When the person asks to choose what each friend sees, the `member` doc carries the chosen ids and grants that friend each item's own channel — the worked example _A friend sees the items picked for them_ below.
546
+ - **A piece, drafted and then published.** Publish at the granularity of a **channel**: channels are the unit of read isolation, so the piece gets its **own** channel and only that channel's read-grant flips, from a `visibility` field the access fn reads — the sketches shape above (`const ch = \`entry:${doc._id}\`; const grant = { users: { [user.userHandle]: [ch] } }; if (doc.visibility === "public") grant.public = [ch];`) — for anonymous visitors, or grant a shared app channel for all granted members; the person's other pieces stay on their own channels, each with its own switch. Gate the control on `useVibe(dbName).can`, and reflect the result back in the affordance ("Published — anyone can see this", with an unpublish to flip it back). A publish control that names its destination — "Publish to the wall", "Share to the group" — is the best shape. Making the person's _whole_ space public is fine when that is the intent.
547
+ - **A thing, seen the moment it is made.** **An open-nature app — an image or meme wall, a bingo card, a party game — puts its records on an always-on `grant.public` channel from the first rules it ships, builds no publish step, and admits anonymous readers**, so everyone with the link sees what people make the moment they make it. The always-on channel is the least-surprise shape for anything playful and low-stakes (an image or meme wall, a group list, a guestbook): a `grant.public` channel, a members' channel, or an `audience` arm.
548
+
549
+ A privacy-sensitive domain on its own (a journal, a health tracker) gets the two controls from _Two controls_ above, off until the person says so — its buddy picker is this same add-a-friend control. **Every app that is neither privacy-sensitive nor an only-me ask ships with a sharing path already in it: either an always-on shared channel its records land on, a publish control, or an add-a-friend control on its first screen**, so what a person makes has a way to reach someone from the first build.
534
550
 
535
551
  **Worked example — a cast everyone can meet: shared characters, private chats (a character chat, a creature gallery, a songbook).** Sharing is the baseline, so an app where people make things ships a way to see them on its first build, even when the ask names nobody else and the invite screen waits for an option. Here every character lands on one always-on public channel, so every visitor meets the whole cast with no step at all, and each maker edits only their own. A one-tap publish per character (the sketches shape above) is an equally good answer; this example shows the always-on one. Each chat, the thread and the story so far, is private to the person having it. Letting a friend join a chat is an option the app can offer later, as a membership on that chat. The app's code holds no notion of an owner. The canned characters the ask starts with are a fixed set of documents in `seed.json`, seeded where everyone reads them, and the app writes no copy of them for each visitor. Adding the seed is the owner's choice, so the app works without it: until then the cast opens empty, inviting the first person to make a character. The screen says who can see the cast ("Anyone with the link can meet the cast") beside a copy-link.
536
552
 
@@ -1161,6 +1177,86 @@ function BoardWorkspace({ ensureDefaultBoard }) {
1161
1177
  }
1162
1178
  ```
1163
1179
 
1180
+ #### A friend sees the items picked for them
1181
+
1182
+ Reach for this only when the person asks to choose what each friend sees — "when I add a friend, let me pick which plants they can see". Each item lands on the collection's channel and on its own channel, and the `member` doc carries the chosen ids and grants that friend each chosen item's channel: the friend sees those items and nothing else on the sill, and saving the member doc again with a different list changes what they see. The add-a-friend control is the same `HandleInput`, with the person's items as checkboxes beside it, and the cue beside it says what the friend will see ("Pick the plants this friend sees").
1183
+
1184
+ ### `access.js`
1185
+ ```js partial
1186
+ export function sill(doc, oldDoc, user, ctx) {
1187
+ if (!user?.userHandle) throw { forbidden: "sign in" };
1188
+ if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" };
1189
+ const safeId = (id) => {
1190
+ if (typeof id !== "string" || !/^[A-Za-z0-9_-]+$/.test(id)) throw { forbidden: "bad id" };
1191
+ return id;
1192
+ };
1193
+ const myDefault = `default-${user.userHandle}`;
1194
+ const sillId = safeId(doc.type === "sill" ? doc._id : doc.sillId);
1195
+ const chan = `sill:${sillId}`;
1196
+ const mine = sillId === myDefault; // the person's own sill, saved or not yet saved
1197
+ if (doc.type === "sill") {
1198
+ // The creator holds the sill and its admin channel, and a saved sill keeps its creator:
1199
+ // a rewrite is an admin act, and only the person whose handle the default id names starts it.
1200
+ if (oldDoc) {
1201
+ if (doc.creatorHandle !== oldDoc.creatorHandle) throw { forbidden: "creator is fixed" };
1202
+ ctx.requireAccess(`${chan}/admin`);
1203
+ } else if (doc.creatorHandle !== user.userHandle || (doc._id.startsWith("default-") && doc._id !== myDefault)) {
1204
+ throw { forbidden: "start your own sill" };
1205
+ }
1206
+ return { channels: [chan], grant: { users: { [doc.creatorHandle]: [chan, `${chan}/admin`] } } };
1207
+ }
1208
+ if (doc.type === "plant") {
1209
+ if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
1210
+ if (oldDoc && (oldDoc.authorHandle !== user.userHandle || doc.sillId !== oldDoc.sillId)) throw { forbidden: "a plant stays its writer's, on its sill" };
1211
+ if (!mine) ctx.requireAccess(`${chan}/admin`);
1212
+ return { channels: [chan, `plant:${safeId(doc._id)}`] }; // on the sill, and on its own channel a member doc can grant
1213
+ }
1214
+ if (doc.type === "member") {
1215
+ // The sill's keeper writes a membership; who it names, where it lives and who wrote it stay
1216
+ // fixed, and the picked plants are the one thing a later save changes.
1217
+ if (doc.addedBy !== user.userHandle) throw { forbidden: "addedBy must be you" };
1218
+ if (oldDoc && (doc.userHandle !== oldDoc.userHandle || doc.sillId !== oldDoc.sillId || doc.addedBy !== oldDoc.addedBy))
1219
+ throw { forbidden: "who and where are fixed" };
1220
+ if (!mine) ctx.requireAccess(`${chan}/admin`);
1221
+ const picked = (doc.plantIds || []).map((id) => `plant:${safeId(id)}`);
1222
+ if (picked.length === 0) throw { forbidden: "pick a plant to share" };
1223
+ return { channels: [chan], grant: { users: { [doc.userHandle]: picked } } };
1224
+ }
1225
+ throw { forbidden: "unknown document type" };
1226
+ }
1227
+
1228
+ // Summary: each person's plants are theirs alone, and a friend they add sees exactly the plants picked for them. A plant carries its writer's handle in authorHandle and its sill in sillId; a member record names the friend in userHandle and the plants they see in plantIds.
1229
+ ```
1230
+
1231
+ ### `App.jsx`
1232
+ ```jsx partial
1233
+ const { HandleInput } = useViewer();
1234
+ const { database, useLiveQuery } = useFireproof("sill");
1235
+ const { me, can } = useVibe("sill");
1236
+ const [picked, setPicked] = React.useState([]);
1237
+ const addFriend = async (handle) => {
1238
+ await ensureDefaultSill(sill._id); // the read/create helper from the boards variant
1239
+ const member = { _id: `member:${sill._id}:${handle}`, type: "member", sillId: sill._id, userHandle: handle, plantIds: picked, addedBy: me.userHandle };
1240
+ if (can.create(member).ok) await database.put(member);
1241
+ };
1242
+ // Changing what a friend sees is the same record, saved again with the new list.
1243
+ const repick = async (member, plantIds) => {
1244
+ const next = { ...member, plantIds };
1245
+ if (can.edit(next).ok) await database.put(next);
1246
+ };
1247
+ // …
1248
+ {plants.map((p) => (
1249
+ <Checkbox
1250
+ key={p._id}
1251
+ label={p.name}
1252
+ checked={picked.includes(p._id)}
1253
+ onChange={(checked) => setPicked(checked === true ? [...picked, p._id] : picked.filter((id) => id !== p._id))}
1254
+ />
1255
+ ))}
1256
+ <HandleInput onChange={(h) => h && addFriend(h)} placeholder="Add a friend who sees these…" />
1257
+ ```
1258
+
1259
+
1164
1260
  #### The list holds what the person made and what they were let into
1165
1261
 
1166
1262
  **The app's list of objects reads them by their `type`, so it holds the ones the signed-in person made and the ones a membership lets them into, read through the same rules.** The `member` record grants its board's channel, so the friend's own `useLiveQuery("type", { key: "board" })` returns that board beside their own the moment the membership lands; `creatorHandle` then says who made each one. A shared board carries `ViewerTag` for the person who made it, in the same list or in a section that renders once there is at least one, so a person keeping the app to themselves sees one plain list. Opening a shared board is the same `BoardContents`: its cards are read by its id and written with that id, the rules route them to its channel, and `can` gates each write.
package/llms/callai.md CHANGED
@@ -96,7 +96,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
96
96
  }
97
97
 
98
98
  export default function App() {
99
- const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
99
+ const { database, useLiveQuery } = useFireproof("tasks");
100
100
  const { me, can, ready } = useVibe("tasks");
101
101
  const { docs } = useLiveQuery("type", { key: "task" });
102
102
  const [sentence, setSentence] = useState("");
@@ -182,7 +182,6 @@ export default function App() {
182
182
  </form>
183
183
  <Text>{answer}</Text>
184
184
  <p role="status">{message}</p>
185
- {writeRejection && <Row gap={2} align="center" wrap><p role="status">{writeRejection.message}</p><Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button></Row>}
186
185
  <Row gap={2} wrap>
187
186
  {lastWrite !== undefined && <Button type="button" size="sm" variant="quiet" disabled={busy} onClick={undoLast}>Undo</Button>}
188
187
  {pending !== undefined && !busy && <Button type="button" size="sm" variant="quiet" tone="down" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</Button>}
package/llms/fireproof.md CHANGED
@@ -5,7 +5,7 @@ Fireproof is a document database with live sync, designed to make browser apps e
5
5
  ## Key Features
6
6
 
7
7
  - **Apps run anywhere:** Bundle UI, data, and logic together.
8
- - **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform surfaces the reason whenever there is one (an access-rule `forbidden("…")`, or no write access for a signed-in visitor); a reason-less rejection converges silently. Keep the UI reactive to the store rather than depending on a toast; don't assume every write lands, but don't hand-roll denial errors either.
8
+ - **Real-Time, local-first:** Writes land in the local replica instantly and sync in the background; the server validates each on ingest and streams the accepted ones live to every viewer. `useLiveQuery` keeps the UI in sync as data arrives, so you render empty states rather than loading spinners — but a synced write can still be rejected on ingest (access denied, conflicts): the local optimistic revision converges **server-wins** — and the platform tells the visitor about every refusal, in the app's own `forbidden("…")` words when it gave some. Keep the UI reactive to the store rather than depending on a toast; don't assume every write lands, but don't hand-roll denial errors either.
9
9
  - **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
10
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")`. A child component that was handed `database` may call `useFireproof(database)` instead of repeating the name; it is the same database.
11
11
 
@@ -21,38 +21,50 @@ Each document has an `_id`, which can be auto-generated or set explicitly. Auto-
21
21
 
22
22
  Use granular documents, e.g. one document per user action, so saving a form or clicking a button should typically create or update a single document, or just a few documents. Avoid patterns that require a single document to grow without bound.
23
23
 
24
- `useLiveQuery` populates and refreshes the UI reactively as data arrives, so you usually render empty states rather than loading spinners. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry automatically. If the server's access rules reject a synced write, the local optimistic revision converges **server-wins** (it is overwritten by the server's version) — and the **platform** surfaces the reason in a write-fail toast whenever there is one: the app's own `forbidden("…")` text, or the platform's message when a signed-in visitor has no write access to the app (both also reach the app as `writeRejection`, below); a reason-less rejection converges silently. So do **not** wrap plain writes in `try/catch` to show denial errors to the user (a save carrying `_files` is the one exception, covered with the image uploader below); keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state is the feedback, and gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so a disallowed action never renders in the first place.
24
+ `useLiveQuery` populates and refreshes the UI reactively as data arrives, so render an inviting empty state while a list has nothing in it. Writes are optimistic and local: `put()`/`save()`/`del()` land in the local replica instantly, and transport or offline failures queue and retry on their own. The server's access rules have the final word: a refused write converges **server-wins** to the server's version, and the **platform** tells the visitor — in the app's own `forbidden("…")` words when it gave some, its own line otherwise, and "saved on this device" when a signed-out visitor's save waits for sign-in. So write plainly and let the store be the feedback: render from `useLiveQuery`/`useDocument`, gate write surfaces up front with `useVibe(dbName).can.create/edit/delete` so only allowed actions render, and keep `try/catch` for a save carrying `_files` (covered with the image uploader below). A toggle shows the whole shape — flip it at once, follow its receipt beside the control, and the list re-reads itself:
25
25
 
26
- When the server later refuses a write the visitor can act on — a signed-in visitor saving to a private app, say — the platform shows a notice and `useFireproof` hands the app the same reason as `writeRejection` (`{ docId, message, code }`, or `null` until a save is refused). Render `writeRejection.message` beside the form it came from and call `clearWriteRejection()` when the visitor dismisses it:
26
+ ```jsx
27
+ const { database, useLiveQuery } = useFireproof("tasks");
28
+ const { docs } = useLiveQuery("type", { key: "task" });
29
+ async function toggle(task) {
30
+ const { synced } = await database.put({ ...task, done: !task.done }); // the row flips now
31
+ synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));
32
+ }
33
+ // each row can show its own state: task._synced.status === "pending" → a quiet "saving" mark
34
+ ```
35
+
36
+ `put()` and `del()` save locally at once and resolve right away, so a resolved put means the record is saved **on this device** — never treat it as proof the server has it. Most apps need nothing more, because the platform shows refusals. Every result also carries `synced`, a receipt for this one write: `synced.status` is `"pending"`, then `"accepted"` (with `synced.seq`), `"refused"` (with `synced.code` and, when the app's rules gave one, `synced.message`), `"held"` (kept on this device until the visitor signs in, then it goes out), or `"superseded"` (a later save to the same record went out instead). It never rejects. Use `synced.on(...)` for feedback beside the control that caused the write: it calls your function at once and on each change, lets go after the last one, and returns an unsubscribe. Every doc from `useLiveQuery` carries the same shape as `doc._synced` for per-row state, such as a quiet "saving" mark while `doc._synced.status === "pending"`; it is not part of the document, so spreading or saving the doc leaves it behind:
27
37
 
28
38
  ```jsx
29
39
  function NoteForm({ text }) {
30
- const { database, writeRejection, clearWriteRejection } = useFireproof("notes");
40
+ const { database } = useFireproof("notes");
41
+ const [note, setNote] = useState("");
42
+ async function save() {
43
+ const { synced } = await database.put({ type: "note", text });
44
+ synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));
45
+ }
31
46
  return (
32
47
  <Stack gap={2}>
33
48
  <Row>
34
- <Button type="button" onClick={() => database.put({ type: "note", text })}>Save</Button>
49
+ <Button type="button" onClick={save}>Save</Button>
35
50
  </Row>
36
- {writeRejection && (
37
- <Row gap={2} align="center" wrap>
38
- <p role="status">{writeRejection.message}</p>
39
- <Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button>
40
- </Row>
41
- )}
51
+ {note && <p role="status">{note}</p>}
42
52
  </Stack>
43
53
  );
44
54
  }
45
55
  ```
46
56
 
47
- While the app's first build is still finishing, the preview runs on its example records, and saving opens up the moment the build completes. A save in that window comes back as a `writeRejection` with `code: "draft-read-only"` and a friendly ready-made `message`. Treat it as a calm status line: the same quiet `role="status"` note as above (or nothing at all), with the visitor's typed text and selections kept in component state so the next save carries them. The platform clears this `writeRejection` itself the moment saving opens up (and on the next save that lands), so the note goes away on its own:
57
+ While the app's first build is still finishing, the preview runs on its example records, and saving opens up the moment the build completes. A save in that window resolves with `ok: false` and a receipt that is already `refused`, with `code: "draft-read-only"` and a friendly ready-made `message`. Treat it as a calm status line: the same quiet `role="status"` note as above (or nothing at all), with the visitor's typed text and selections kept in component state so the next save carries them. The next save that lands clears the note:
48
58
 
49
59
  ```jsx
50
60
  function AddItem() {
51
- const { database, writeRejection } = useFireproof("items");
61
+ const { database } = useFireproof("items");
52
62
  const [text, setText] = useState("");
63
+ const [note, setNote] = useState("");
53
64
  async function add() {
54
- const res = await database.put({ type: "item", text });
55
- if (res.ok) setText(""); // clear the field once the record is saved
65
+ const { ok, synced } = await database.put({ type: "item", text });
66
+ setNote(synced.code === "draft-read-only" ? synced.message : "");
67
+ if (ok) setText(""); // clear the field once the record is saved
56
68
  }
57
69
  return (
58
70
  <Stack gap={2}>
@@ -60,7 +72,7 @@ function AddItem() {
60
72
  <Row>
61
73
  <Button type="button" onClick={add}>Add</Button>
62
74
  </Row>
63
- {writeRejection?.code === "draft-read-only" && <p role="status">{writeRejection.message}</p>}
75
+ {note && <p role="status">{note}</p>}
64
76
  </Stack>
65
77
  );
66
78
  }
@@ -146,7 +158,7 @@ function TaskCard({ task, database, can, onCorrect, onSaved, disabled }) {
146
158
  }
147
159
 
148
160
  export default function App() {
149
- const { database, useLiveQuery, writeRejection, clearWriteRejection } = useFireproof("tasks");
161
+ const { database, useLiveQuery } = useFireproof("tasks");
150
162
  const { me, can, ready } = useVibe("tasks");
151
163
  const { docs } = useLiveQuery("type", { key: "task" });
152
164
  const [sentence, setSentence] = useState("");
@@ -232,7 +244,6 @@ export default function App() {
232
244
  </form>
233
245
  <Text>{answer}</Text>
234
246
  <p role="status">{message}</p>
235
- {writeRejection && <Row gap={2} align="center" wrap><p role="status">{writeRejection.message}</p><Button type="button" size="sm" variant="quiet" onClick={clearWriteRejection}>Dismiss</Button></Row>}
236
247
  <Row gap={2} wrap>
237
248
  {lastWrite !== undefined && <Button type="button" size="sm" variant="quiet" disabled={busy} onClick={undoLast}>Undo</Button>}
238
249
  {pending !== undefined && !busy && <Button type="button" size="sm" variant="quiet" tone="down" onClick={() => { setPending(undefined); setSentence(""); setCurrent(undefined); }}>Discard unsaved tasks</Button>}
@@ -465,7 +476,7 @@ export default function App() {
465
476
 
466
477
  function Canvas({ me }) {
467
478
  // Many people write the same pixel docs: fail fast rather than replay a stale queued write.
468
- const { database, useLiveQuery, useDocument, writeRejection } = useFireproof("pixels", { offlineQueue: false });
479
+ const { database, useLiveQuery, useDocument } = useFireproof("pixels", { offlineQueue: false });
469
480
  const { ready, can } = useVibe("pixels");
470
481
  const canPaint = ready && can.create({ _id: "pixel-0-0", type: "pixel", x: 0, y: 0, color: COLOR, paintedBy: me }).ok;
471
482
  const { docs: pixels } = useLiveQuery("type", { key: "pixel" });
@@ -514,7 +525,6 @@ function Canvas({ me }) {
514
525
 
515
526
  return (
516
527
  <div>
517
- {writeRejection && <p role="status">{writeRejection.message}</p>}
518
528
  <canvas
519
529
  ref={canvasRef}
520
530
  width={SIZE * CELL}
@@ -710,7 +720,7 @@ An index function can call `emit(key, value)` with a second argument to store a
710
720
 
711
721
  ## Offline writes are on by default (`offlineQueue`)
712
722
 
713
- For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version); the platform surfaces the reason whenever there is one, and a reason-less rejection converges silently — so keep the UI reactive to the store rather than depending on a toast. Only transport failures queue and retry.
723
+ For signed-in users, writes are **local-first by default**: a `put`/`del` that fails on the network is durably queued on the device (it resolves, stays visible, and syncs to the cloud when you're back online) instead of rolling back. You don't opt in — every signed-in vibe gets this. An access-denied/validation rejection is **not** queued: it converges **server-wins** (the local optimistic revision is overwritten by the server's version) and the platform tells the visitor, so keep the UI reactive to the store rather than depending on a toast. Only transport failures queue and retry.
714
724
 
715
725
  Pass `{ offlineQueue: false }` for **server-first, fail-fast** writes: a `put` resolves only when the server accepts it, and a network failure rejects immediately with nothing queued. Choose this for **collaborative multi-writer apps** where two people may edit the same doc — sync is blind last-arrival-wins (Firefly has no `_rev`), so a stale write replayed on reconnect can silently overwrite a newer one. When a lost write is safer than a surprise overwrite, opt out.
716
726
 
@@ -724,7 +734,7 @@ const { useLiveQuery } = useFireproof("shared-board", { offlineQueue: false });
724
734
 
725
735
  ## Anonymous writes (local-first by default)
726
736
 
727
- Letting a logged-out visitor try the app and build a little state before signing in needs **no opt-in** — it is the universal default on every vibe. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against the local replica with the identical API — no auth branching in your code — so favorites, a draft, or a scratch list just work. On first sign-in that local state drains automatically into the user's cloud database, each document exactly as stored (an author field left empty while signed out stays empty), and then syncs live; nothing is lost if a drain is interrupted. The sign-in and drain-confirmation toast ("saved to your account") is platform-provided chrome — do not build your own "saved"/"synced" notice for it.
737
+ Letting a logged-out visitor try the app and build a little state before signing in needs **no opt-in** — it is the universal default on every vibe. While logged out, `put`/`del`/`useLiveQuery`/`useDocument` run against the local replica with the identical API — no auth branching in your code — so favorites, a draft, or a scratch list just work. When the app's rules keep writes for signed-in people, such a save's `synced.status` reads `"held"` until then. On first sign-in that local state drains automatically into the user's cloud database, each document exactly as stored (an author field left empty while signed out stays empty), and then syncs live; nothing is lost if a drain is interrupted. The sign-in and drain-confirmation toast ("saved to your account") is platform-provided chrome — do not build your own "saved"/"synced" notice for it.
728
738
 
729
739
  The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecated and ignored** — its behavior is now the default, so the runtime warns once and drains any legacy data on its own. Do not emit the flag in new code.
730
740
 
@@ -736,7 +746,7 @@ The old `{ anonymousLocal: true }` option (and its `migrate` hook) is **deprecat
736
746
 
737
747
  ## Architecture: Where's My Data?
738
748
 
739
- Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform surfaces the reason whenever there is one — a reason-less rejection converges silently, so keep the UI reactive to the store rather than depending on a toast. Don't assume the write always lands, but don't hand-roll your own denial error UI — let it converge.
749
+ Data lives in a local replica (IndexedDB) that the app reads and writes instantly; that replica syncs to the Firefly server in the background. The server is the authority on acceptance — each synced write is validated by the app's access rules on ingest, persisted, and then synced to all users who have read access. A write that fails validation or hits a conflict is rejected on ingest: the local optimistic revision converges **server-wins**, and the platform tells the visitor about it, so keep the UI reactive to the store rather than depending on a toast. Don't assume the write always lands, but don't hand-roll your own denial error UI — let it converge.
740
750
 
741
751
  ## Using Fireproof in JavaScript
742
752
 
package/llms/use-vibe.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # useVibe Hook — write gating
2
2
 
3
- `useVibe(dbName)` is how you gate write surfaces. It runs the app's own `access.js` — the **same function the server enforces** — against a candidate document, so the UI's enabled/disabled state matches what the server will actually allow. You never re-implement permissions; you ask the access function.
3
+ `useVibe(dbName)` is how you gate write surfaces. It runs the app's own `access.js` — the **same function the server enforces** — against a candidate document, so `can.*` is a hint about whether this person may write a record like this, good for deciding which controls to show. The server checks each change against the saved version of the record, so it has the final word. You never re-implement permissions; you ask the access function.
4
4
 
5
5
  ```jsx
6
6
  const { me, can, ready } = useVibe("comments");
@@ -82,4 +82,4 @@ Don't gate management UI on a display flag directly. Encode the rule in `access.
82
82
 
83
83
  ## The server is still the authority
84
84
 
85
- `can.*` is a fast, faithful preview, not the final word. A write can still be rejected server-side (the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Keep the optimistic-write + rollback pattern: apply the change immediately, revert and surface an error if the `put` rejects.
85
+ `can.*` is a fast preview, not the final word. A write can still be refused server-side (the server compares the change with the saved record, and the source may be stale, async, or unevaluable — in which case `can.*` optimistically returns `ok` and defers to the server). Write optimistically: `put()` saves locally at once and never rejects for access. A refused write converges to the server's version on its own, and the platform tells the visitor, so most apps need nothing more. For feedback beside the control, read the put's receipt (`const { synced } = await database.put(doc)`, then `synced.on(...)`), or `doc._synced.status` on a row — see the fireproof skill.
@@ -387,4 +387,4 @@ async function addMember(handle) {
387
387
  {canInvite && <HandleInput onChange={addMember} placeholder="Add a friend…" />}
388
388
  ```
389
389
 
390
- Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing" — builds no picker: each person keeps their own record tree and shares a chosen item with its publish step, and the records stay shaped so a picker can join later. Everywhere else the picker belongs.
390
+ Members render with `<ViewerTag userHandle={m.userHandle} />`, the same way any other stored handle does. An app the person asked to keep to themselves — "just for me", "no sharing" — builds the picker as its add-a-friend control: each person keeps their own collection, private to them, and adding a friend shares that collection with the friend it names (the access skill's rule for a collection of a person's own records). Everywhere else the picker belongs too.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.119",
3
+ "version": "14.3.120",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,10 +39,10 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/api-types": "14.3.119",
43
- "@vibes.diy/call-ai-v2": "14.3.119",
44
- "@vibes.diy/identity": "14.3.119",
45
- "@vibes.diy/use-vibes-types": "14.3.119",
42
+ "@vibes.diy/api-types": "14.3.120",
43
+ "@vibes.diy/call-ai-v2": "14.3.120",
44
+ "@vibes.diy/identity": "14.3.120",
45
+ "@vibes.diy/use-vibes-types": "14.3.120",
46
46
  "arktype": "~2.2.6",
47
47
  "json-schema-faker": "~0.6.3"
48
48
  },
@@ -106,7 +106,7 @@ Every feature you described above should work when this one block lands. Don't l
106
106
  ```
107
107
  - **Load Google Fonts with `&display=swap` (or `&display=optional`), never `&display=block`.** Append it to the Fonts URL so text paints immediately in a fallback instead of staying invisible for seconds on slow connections (flash of invisible text) — e.g. `https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap`.
108
108
  - **The bottom-right corner belongs to the platform — never pin your own control there.** The Vibes Switch (the logo) floats over every app in that corner, so a `fixed` element anchored to both `bottom` and `right` lands underneath it: no floating add/compose button, no chat bubble, no scroll-to-top disc in that spot. Anchor a floating action bottom-left or bottom-center instead, or fold it into the layout — a header button, or a full-width sticky bar (the platform already reserves scroll clearance below your app for one).
109
- **This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own record tree keyed on the writer's handle, with a publish step that shares a chosen item with everyone who can open the app, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give, in one short line beside the share, invite or publish control: "only you can see this" for records the rules keep to their writer, and an open board says everyone with the link can see it. The title, subtitle and hero say what the app is for. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook"). The invite may wait for an option; the way to share does not: every first build that is not privacy-sensitive ships one on its first screen — what people make on an always-on shared channel (the access skill's character-cast example), or a one-tap publish that puts each finished thing on its own channel shared with everyone who can open the app (its sketches example).
109
+ **This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own collection keyed on the writer's handle, shared by adding a friend, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give, in one short line beside the share, invite or publish control: "yours alone — add a friend to share it" beside the add-a-friend control of a collection the rules keep to its person, and an open board says everyone with the link can see it. The title, subtitle and hero say what the app is for. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; a person's own collection carries it on the first screen as its add-a-friend control; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook"). The invite may wait for an option; the way to share does not: every first build that is not privacy-sensitive ships one on its first screen, and the access skill's rule picks it from what one record is — a record a person keeps for their own life (a check-in, an entry, a line in a list) belongs to a collection that is private to its person and shared by adding a friend, so the add-a-friend control is that app's first-screen share; a piece made for readers (a post, an article, a gallery piece) carries its own draft-then-publish step (the sketches example); a thing made to be seen the moment it exists lands on an always-on shared channel (the character-cast example).
110
110
 
111
111
  After your last code block, add a short 1-2 sentence message describing the core workflow the app supports.
112
112
 
@@ -283,7 +283,7 @@ The control and its handler check the **same complete candidate** that `database
283
283
  Write this turn's `access.js` against precisely these shapes: a loom grants its creator the loom channel and its admin channel; content and choices route to their stored `loomId`; membership grants that same loom channel. Keep each record's type, author and parent fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes both parent fields from the displayed weaving, and the rule holds them fixed on subsequent writes. Follow `access.md`'s stable-default-object and membership pattern: read the fixed default id before creating it, preserve existing name/settings, and await that parent before the first content write or invitation. This is ordinary initialization through live access rules, independent of importing example records. The creator's membership survives deletion of their last content row.
284
284
 
285
285
  - **Keep the opening view useful.** The default id is `default-<handle>` for each person. The person arrives inside that default object: the first screen is the app at work with a sensible name they can change, and a second-object form joins the object list when useful. On arrival the object's name is the app's name, so the header carries that name once; a separate object-name line becomes useful after a rename or a second object. Show the whole readable type-query result in the picker, including objects someone else made; `creatorHandle` identifies their makers.
286
- - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When sharing was not asked for, offer inviting as a closing option.
286
+ - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When the app is a person's own collection, this same control is its first-screen share, with the line "Yours alone — add a friend to share it" beside it; otherwise, when sharing was not asked for, offer inviting as a closing option.
287
287
  - **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
288
288
  - **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the object's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
289
289
  - **Keep examples and the file protocol consistent.** The first build writes the files and privacy rules the surrounding prompt requires. `seed.json` uses these same types, field names and parent ids, with `"@owner"` in writer fields and literal owner-derived default ids from `<app-owner>` where needed. The access rules admit every example record, and the final `// Summary:` accurately describes those rules; private records begin private. Follow the existing attachment/file protocol for uploaded files and their visibility.
@@ -334,7 +334,7 @@ A second visitor should arrive at a party, not a parking lot: within five second
334
334
  - **Motion on state changes.** Small fast transitions (150–300ms) when things appear, complete, or score — enough that actions feel acknowledged, not a light show.
335
335
  - **Empty states invite, they don't announce.** "Draw the first monster" with the button right there, not "No data yet".
336
336
  - **Carry the occasion into every word.** Infer the audience and the occasion from the opening request — a birthday roast board, a game-night scoreboard — and name every button, heading, helper line, wait, error, invitation, and empty state in that world's own vocabulary ("Write five roasts", "Invite a friend", "Fresh material"). Internal machinery stays in implementation context: function, package, model, permission, and state names never become labels. One plain sentence about AI is right where it helps the person understand the experience; the function name is not.
337
- - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, in either of two shapes: what they make lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link; or each thing carries a one-tap publish that opens that one thing to everyone who can open the app. Either fits a cast of characters; the publish step suits work people draft first, such as a sketch or an essay. Put it where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Only you can see your seashell finds" beside Publish, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
337
+ - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, and the shape follows what one record is: a record a person keeps for their own life (a check-in, an entry, a line in a list, a plant on a windowsill) belongs to a collection that is private to its person, and the add-a-friend control on the first screen shares the whole collection with the friend it names; a piece made for readers (a post, an article, a gallery piece) carries a one-tap publish that opens that one piece to everyone who can open the app, the drafting ritual readers already know; a thing made to be seen the moment it exists (a character in a cast, a caption, a guestbook line) lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link. The person's own words move the dial: "let me pick which ones each friend sees" adds choosing items per friend to the add-a-friend control, and "drafts" or "publish" brings the per-item step to any record. Put the control where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Yours alone — add a friend to share your sill" beside the add-a-friend control, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
338
338
 
339
339
  **How specific the request is sets what reaches the screen.** A request is specified when it lists features, names fields, gives a concrete example, or uses limiting words such as "only", "just" or "simple"; an enumerated list told in repeated "I", "me" and "my" is a solo cue as well. A specified request gets on screen exactly the features it names plus what those features need to work, and every idea of your own becomes one of the closing `▸` options; its sample content is the `seed.json` demo, and adding it is one of those options. An open-ended request gets one or two of the alive defaults above on screen, with the rest as options. The way to share is part of the baseline rather than one of your own ideas, so it reaches the screen of a specified request too.
340
340
 
@@ -110,7 +110,7 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
110
110
 
111
111
  **Step 2 — Feature edits.** Wire each feature with SEARCH/REPLACE edits. Each edit gets exactly one prose line (≤25 words) before it. Wire hooks, data, handlers, and `useFireproof` in these edits. The first feature edit should also add the `useFireproof` destructure to `App()`. Keep edits focused — one feature per edit, fully working after it lands.
112
112
 
113
- **This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own record tree keyed on the writer's handle, with a publish step that shares a chosen item with everyone who can open the app, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give, in one short line beside the share, invite or publish control: "only you can see this" for records the rules keep to their writer, and an open board says everyone with the link can see it. The title, subtitle and hero say what the app is for. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook"). The invite may wait for an option; the way to share does not: every first build that is not privacy-sensitive ships one on its first screen — what people make on an always-on shared channel (the access skill's character-cast example), or a one-tap publish that puts each finished thing on its own channel shared with everyone who can open the app (its sketches example).
113
+ **This turn writes the app's `access.js` with the app, every app** — the rules that decide who sees and changes each record: an app the person keeps to themselves gives each signed-in person their own collection keyed on the writer's handle, shared by adding a friend, and an ask for no access control gets rules that admit everyone; the access skill doc carries its emit format, placement and worked examples, and the file ends with its one plain-language `// Summary:` line saying what people can do with each record. When the app needs server work (a schedule, an outside read, a notification), this turn writes its `backend.js` too, in the format the backend skill doc teaches. UI copy names the sharing those rules give, in one short line beside the share, invite or publish control: "yours alone — add a friend to share it" beside the add-a-friend control of a collection the rules keep to its person, and an open board says everyone with the link can see it. The title, subtitle and hero say what the app is for. Gate every write surface on `useVibe(dbName).can` — the liveness gate every app carries. The rules and the record shapes they key on are plumbing, with nothing on screen of their own. The object list is part of that plumbing: `App.jsx` reads the objects by their `type`, which returns the ones the person made and the ones a membership lets them into, and marks each shared one with `ViewerTag` for its maker — in the same list, or in a section that renders once there is at least one; opening a shared object reads and writes its records with that object's id, each write gated on `can`. The invite screen — the platform's `HandleInput` writing a membership into the person's object — joins this turn's `App.jsx` when the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name; a person's own collection carries it on the first screen as its add-a-friend control; every other first build offers it as one of the closing `▸` options, in the app's own words ("Let a friend add pages to your sketchbook"). The invite may wait for an option; the way to share does not: every first build that is not privacy-sensitive ships one on its first screen, and the access skill's rule picks it from what one record is — a record a person keeps for their own life (a check-in, an entry, a line in a list) belongs to a collection that is private to its person and shared by adding a friend, so the add-a-friend control is that app's first-screen share; a piece made for readers (a post, an article, a gallery piece) carries its own draft-then-publish step (the sketches example); a thing made to be seen the moment it exists lands on an always-on shared channel (the character-cast example).
114
114
 
115
115
  After your last code block, add a short 1-2 sentence message describing the core workflow the app supports.
116
116
 
@@ -287,7 +287,7 @@ The control and its handler check the **same complete candidate** that `database
287
287
  Write this turn's `access.js` against precisely these shapes: a loom grants its creator the loom channel and its admin channel; content and choices route to their stored `loomId`; membership grants that same loom channel. Keep each record's type, author and parent fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes both parent fields from the displayed weaving, and the rule holds them fixed on subsequent writes. Follow `access.md`'s stable-default-object and membership pattern: read the fixed default id before creating it, preserve existing name/settings, and await that parent before the first content write or invitation. This is ordinary initialization through live access rules, independent of importing example records. The creator's membership survives deletion of their last content row.
288
288
 
289
289
  - **Keep the opening view useful.** The default id is `default-<handle>` for each person. The person arrives inside that default object: the first screen is the app at work with a sensible name they can change, and a second-object form joins the object list when useful. On arrival the object's name is the app's name, so the header carries that name once; a separate object-name line becomes useful after a rename or a second object. Show the whole readable type-query result in the picker, including objects someone else made; `creatorHandle` identifies their makers.
290
- - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When sharing was not asked for, offer inviting as a closing option.
290
+ - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + loom._id + ":" + handle, type: "member", loomId: loom._id, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }`. Construct that value after the picker resolves the handle, gate it with `can.create`, await `ensureDefault(loom._id)`, then persist the same value. The `member` branch admits exactly that shape, with creator-admin authority and fixed subject/parent/writer. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When the app is a person's own collection, this same control is its first-screen share, with the line "Yours alone — add a friend to share it" beside it; otherwise, when sharing was not asked for, offer inviting as a closing option.
291
291
  - **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
292
292
  - **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the object's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
293
293
  - **Keep examples and the file protocol consistent.** The first build writes the files and privacy rules the surrounding prompt requires. `seed.json` uses these same types, field names and parent ids, with `"@owner"` in writer fields and literal owner-derived default ids from `<app-owner>` where needed. The access rules admit every example record, and the final `// Summary:` accurately describes those rules; private records begin private. Follow the existing attachment/file protocol for uploaded files and their visibility.
@@ -338,7 +338,7 @@ A second visitor should arrive at a party, not a parking lot: within five second
338
338
  - **Motion on state changes.** Small fast transitions (150–300ms) when things appear, complete, or score — enough that actions feel acknowledged, not a light show.
339
339
  - **Empty states invite, they don't announce.** "Draw the first monster" with the button right there, not "No data yet".
340
340
  - **Carry the occasion into every word.** Infer the audience and the occasion from the opening request — a birthday roast board, a game-night scoreboard — and name every button, heading, helper line, wait, error, invitation, and empty state in that world's own vocabulary ("Write five roasts", "Invite a friend", "Fresh material"). Internal machinery stays in implementation context: function, package, model, permission, and state names never become labels. One plain sentence about AI is right where it helps the person understand the experience; the function name is not.
341
- - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, in either of two shapes: what they make lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link; or each thing carries a one-tap publish that opens that one thing to everyone who can open the app. Either fits a cast of characters; the publish step suits work people draft first, such as a sketch or an essay. Put it where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Only you can see your seashell finds" beside Publish, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
341
+ - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, and the shape follows what one record is: a record a person keeps for their own life (a check-in, an entry, a line in a list, a plant on a windowsill) belongs to a collection that is private to its person, and the add-a-friend control on the first screen shares the whole collection with the friend it names; a piece made for readers (a post, an article, a gallery piece) carries a one-tap publish that opens that one piece to everyone who can open the app, the drafting ritual readers already know; a thing made to be seen the moment it exists (a character in a cast, a caption, a guestbook line) lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link. The person's own words move the dial: "let me pick which ones each friend sees" adds choosing items per friend to the add-a-friend control, and "drafts" or "publish" brings the per-item step to any record. Put the control where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Yours alone — add a friend to share your sill" beside the add-a-friend control, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
342
342
 
343
343
  **How specific the request is sets what reaches the screen.** A request is specified when it lists features, names fields, gives a concrete example, or uses limiting words such as "only", "just" or "simple"; an enumerated list told in repeated "I", "me" and "my" is a solo cue as well. A specified request gets on screen exactly the features it names plus what those features need to work, and every idea of your own becomes one of the closing `▸` options; its sample content is the `seed.json` demo, and adding it is one of those options. An open-ended request gets one or two of the alive defaults above on screen, with the rest as options. The way to share is part of the baseline rather than one of your own ideas, so it reaches the screen of a specified request too.
344
344
 
package/system-prompt.md CHANGED
@@ -35,9 +35,9 @@ You are an AI assistant tasked with creating React components. You should create
35
35
  - Write `callAI` prompts that suit a small, fast model: state the task in one or two sentences, put every fact the answer needs inside the prompt (the model knows nothing about your app's data), ask for short structured fields, and give any persona in a single line (`Answer as a cheerful innkeeper.`). A request can also be declined: `callAI` then rejects with an error whose `reason` is `'refusal'` and whose `explanation` says why — catch it and show a short in-character message that invites the person to rephrase, keeping the rest of the screen working. Write one version of the code; the platform picks the engine.
36
36
  - Always show loading states during genuinely-network async operations (callAI, fetch): use a useState boolean (e.g. `isLoading`), set it true before the call and false in .finally(). While loading: (1) pass `loading={isLoading}` to the trigger `Button`, which shows the package's own spinner and blocks a second click, (2) 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); }`
37
37
  - Database reads are not a network operation — never show a loading state or spinner for them. `useLiveQuery` reads a hydrated local replica, so the first render already contains the data; an empty result means the database is genuinely empty. Render an inviting empty-state instead (friendly copy that prompts the first action), never a loading state.
38
- - Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves against the local replica instantly — there is no visible in-flight window to spinner over — but the UI must still react the moment the user acts. Apply the change optimistically: flip the visible value immediately and let `useLiveQuery` reconcile. If a synced write is refused, the local optimistic revision converges to the server's version automatically — server-wins, and your code never catches it, so don't `try/catch` writes or hand-roll retry UI. Keep your UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state shows the truth. When `access.js` provides a reason (`forbidden("…")`) the platform surfaces it to the person; a reason-less refusal converges silently, so don't strip your own UI expecting a popup — gate write surfaces up front with `useVibe(dbName).can`. Transport and offline failures are handled by the built-in queue, so don't hand-roll retry either. Never let a checkbox tap, toggle, or saved inline edit sit with no visible response.
38
+ - Give instant feedback for Fireproof writes too, not just for callAI/fetch. A `database.put` (toggling a checkbox, marking done, inline edits, reorder, like/vote counters) resolves against the local replica instantly, so make the UI react the moment the person acts: flip the visible value immediately and let `useLiveQuery` reconcile. The server has the final word — a refused write converges to the server's version on its own, and the platform tells the person, in `access.js`'s own `forbidden("…")` words when it gives some — so write plainly, keep the UI reactive to the store (`useLiveQuery`/`useDocument`) so the converged state shows the truth, and gate write surfaces up front with `useVibe(dbName).can`. A resolved `put` means saved on this device; for feedback beside a control, follow its receipt, and every `useLiveQuery` row carries `doc._synced.status`: `const { synced } = await database.put({ ...task, done: !task.done }); synced.on((s) => setNote(s.status === "refused" && s.message ? s.message : ""));`. The built-in queue retries transport and offline failures for you. Every checkbox tap, toggle, and saved inline edit shows a visible response.
39
39
  - For file uploads use drag and drop and store using the `doc._files` API; for AI image generation use `<ImgGen prompt="..." />`
40
- - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); the verdict has three states — while it is `pending` (or `ready` is false) hold the surface in a quiet checking state, and on a resolved denial write the sign-in or join copy in your app's own words, letting `reason` inform the sentence you write. `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill is system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill for the current user. But ASKING an anonymous visitor to sign in is the app's job: call `useViewer().requestLogin()` from your own button, gated on `!isViewerPending`, at the moment the visitor has made something worth keeping — never a sentence telling them to find the logo, and never a wall in front of an app that already works for them. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. Writes can still be rejected server-side even when `can.*` allows, so keep the optimistic-write + rollback handling. See use-vibe docs.
40
+ - Access control is decided by the runtime, not your code. Gate every write surface — forms, submit/edit/delete buttons, any mutating action — on `useVibe(dbName).can`. `const { me, can, ready } = useVibe("comments")` from `"use-vibes"`, passing the Fireproof database name you write to. Show the editor when `can.create(draft).ok` (or `can.edit(doc)` / `can.delete(doc)`); the verdict has three states — while it is `pending` (or `ready` is false) hold the surface in a quiet checking state, and on a resolved denial write the sign-in or join copy in your app's own words, letting `reason` inform the sentence you write. `can.*` runs the app's own `access.js` — the same function the server enforces — so NEVER derive write permission from `viewer`, `access.hasRole()`/`access.hasChannel()`, or document fields. `useViewer()` is identity/display only: `const { ViewerTag } = useViewer()` renders **other** people (`<ViewerTag userHandle={...} />` for comment authors, rosters, "added by" labels). The current viewer's own pill is system chrome in the Vibes Switch (the panel the logo opens) — don't add a header pill for the current user. But ASKING an anonymous visitor to sign in is the app's job: call `useViewer().requestLogin()` from your own button, gated on `!isViewerPending`, at the moment the visitor has made something worth keeping — never a sentence telling them to find the logo, and never a wall in front of an app that already works for them. Owner-only management UI is gated on `can.*` too (the access.js encodes the owner rule). This applies to every app — the runtime decides sharing, not the prompt. `can.*` is a hint: the server checks each change against the saved record, so a write `can.*` allowed can still be refused — it converges on its own and the platform says so. See use-vibe docs.
41
41
  - 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
42
42
  - Never use emojis in the UI. Icons come from the component package's set: `<Icon name="star" />`, and an icon-only action is `<IconButton label="Favorite" icon={<Icon name="star" />} />`. For a glyph the set lacks, draw a simple single-color stroke SVG in JSX (24x24 viewBox, strokeWidth 2, strokeLinecap round, strokeLinejoin round); icon libraries stay out of the imports.
43
43
  - Consider and potentially reuse/extend code from previous responses if relevant
@@ -561,7 +561,7 @@ docs in your database, and never build follow UI state machines.
561
561
 
562
562
  **`access.js` must be one self-contained exported function.** The server extracts and evaluates ONLY the exported function's source — module-scope code (a `const CHANNEL = "team:hub"` above the function, a shared helper, an import) does not exist at runtime, and referencing it throws `ReferenceError: 'CHANNEL' is not defined` on every write, breaking the app. Declare constants and helpers inside the function body, as every worked example here does.
563
563
 
564
- **Every app gets record rules, and the group shape is the default.** Whatever the prompt's pronouns, build the app so a second person can be let in later: a sourdough feeding log one person keeps is the same shape with one object in it, and a baking friend they invite next month is a grant on an object that already exists. The rules and record shapes are plumbing with nothing on screen of their own; the invite screen joins the app when the request names other people, sharing, a group or a person by name, and is otherwise one of the closing `▸` options. An app the person keeps to themselves — "just for me", "keep it single-user", "no sharing" — ships `access.js` too: each signed-in person gets their own record tree on a channel keyed on the writer's own handle, and a publish step shares a chosen item with everyone who can open the app — its members while the app stays private, everyone once the owner opens it up; an ask for "no access control" or "anyone can edit" ships rules that admit everyone. The shape that carries both is author-owned writes on the object's own channel, self-granted at creation: stamp `authorHandle: user.userHandle` on each doc, key the object on its own `creatorHandle`, and let the author check accept the first write, so a signed-in visitor creates from first load with nobody letting them in. Save `ctx.requireAccess(channel)` (and roster / `requireRole` gates) for records that already exist — asking for _pre-existing_ membership to create is what puts "not in channel / request to join" in front of the maker on the very app they just made, or hides the compose input behind a "join" wall. For a tool that wants no sign-in at all, `allowAnonymous: true` in `access.js` opts guests in, and the write surface is then gated the usual way (`ready` for pending identity/access resolution, then `can.create(candidate).ok` for the affordance itself) — once `allowAnonymous` opts them in, the access fn accepts `user === null` and `can.create()` returns the real verdict rather than `authentication required`. The footgun is gating the affordance on `can.create()` _without_ setting `allowAnonymous` — then it stays `authentication required` and the control vanishes for the very guests it's for.
564
+ **Every app gets record rules, and the group shape is the default.** Whatever the prompt's pronouns, build the app so a second person can be let in later: a sourdough feeding log one person keeps is the same shape with one object in it, and a baking friend they invite next month is a grant on an object that already exists. The rules and record shapes are plumbing with nothing on screen of their own; the invite screen joins the app when the request names other people, sharing, a group or a person by name, and when the app is a person's own collection it is the first screen's add-a-friend control, since adding a friend is how a collection is shared; otherwise it is one of the closing `▸` options. An app the person keeps to themselves — "just for me", "keep it single-user", "no sharing" — ships `access.js` too: each signed-in person gets their own collection on a channel keyed on the writer's own handle, and the add-a-friend control shares that collection with the friend it names — the access skill's rule picks the sharing shape from what one record is, so a piece made for readers keeps its draft-then-publish step while a collection of a person's own records is shared whole by adding a friend; an ask for "no access control" or "anyone can edit" ships rules that admit everyone. The shape that carries both is author-owned writes on the object's own channel, self-granted at creation: stamp `authorHandle: user.userHandle` on each doc, key the object on its own `creatorHandle`, and let the author check accept the first write, so a signed-in visitor creates from first load with nobody letting them in. Save `ctx.requireAccess(channel)` (and roster / `requireRole` gates) for records that already exist — asking for _pre-existing_ membership to create is what puts "not in channel / request to join" in front of the maker on the very app they just made, or hides the compose input behind a "join" wall. For a tool that wants no sign-in at all, `allowAnonymous: true` in `access.js` opts guests in, and the write surface is then gated the usual way (`ready` for pending identity/access resolution, then `can.create(candidate).ok` for the affordance itself) — once `allowAnonymous` opts them in, the access fn accepts `user === null` and `can.create()` returns the real verdict rather than `authentication required`. The footgun is gating the affordance on `can.create()` _without_ setting `allowAnonymous` — then it stays `authentication required` and the control vanishes for the very guests it's for.
565
565
 
566
566
  **A group-shaped ask — more than one person acting on the same records: several people adding, voting, sharing or joining, or a named group such as a quilting circle, a beekeeping club or a team keeping records together — gives each signed-in person their own object plus a way to invite others into it with the platform's `HandleInput` (from `useViewer()`), and keys its rules on that object's own creator (`doc.creatorHandle === user.userHandle`), which places the app owner exactly where everyone else stands.**
567
567
 
@@ -765,7 +765,7 @@ A second visitor should arrive at a party, not a parking lot: within five second
765
765
  - **Motion on state changes.** Small fast transitions (150–300ms) when things appear, complete, or score — enough that actions feel acknowledged, not a light show.
766
766
  - **Empty states invite, they don't announce.** "Draw the first monster" with the button right there, not "No data yet".
767
767
  - **Carry the occasion into every word.** Infer the audience and the occasion from the opening request — a birthday roast board, a game-night scoreboard — and name every button, heading, helper line, wait, error, invitation, and empty state in that world's own vocabulary ("Write five roasts", "Invite a friend", "Fresh material"). Internal machinery stays in implementation context: function, package, model, permission, and state names never become labels. One plain sentence about AI is right where it helps the person understand the experience; the function name is not.
768
- - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, in either of two shapes: what they make lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link; or each thing carries a one-tap publish that opens that one thing to everyone who can open the app. Either fits a cast of characters; the publish step suits work people draft first, such as a sketch or an essay. Put it where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Only you can see your seashell finds" beside Publish, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
768
+ - **A way to share on the FIRST screen — every app that is not privacy-sensitive.** Sharing is the baseline: what people make here is made to be seen. When the request names other people, sharing, a group, a team, a family, "we" or "our", or a person by name, the first screen carries its invite action. Every other app shares what people make, and the shape follows what one record is: a record a person keeps for their own life (a check-in, an entry, a line in a list, a plant on a windowsill) belongs to a collection that is private to its person, and the add-a-friend control on the first screen shares the whole collection with the friend it names; a piece made for readers (a post, an article, a gallery piece) carries a one-tap publish that opens that one piece to everyone who can open the app, the drafting ritual readers already know; a thing made to be seen the moment it exists (a character in a cast, a caption, a guestbook line) lands on an always-on shared channel, so every visitor meets it with no step at all and the screen says so ("Anyone with the link can meet your cast") beside a copy-link. The person's own words move the dial: "let me pick which ones each friend sees" adds choosing items per friend to the add-a-friend control, and "drafts" or "publish" brings the per-item step to any record. Put the control where a first-time user sees it without digging, gate it like any write surface (`can.create({...draft}).ok` or `can.edit(doc).ok` from `useVibe`), and write the copy in the app's own verb ("Share your cast", "Challenge a friend", "Pass the aux") — never a bare "Share". The one plain line that says who can see what people make sits beside the share, invite or publish control it describes ("Yours alone — add a friend to share your sill" beside the add-a-friend control, "Anyone with the link can meet your cast" beside the copy-link), and in an app with no such control, beside the records it describes; the title, subtitle and hero say what the app is for. A privacy-sensitive app — a diary, journal, health or money log — keeps its sharing off until the person turns it on, and for a request that names nobody the invite screen is one of the closing `▸` options.
769
769
 
770
770
  **How specific the request is sets what reaches the screen.** A request is specified when it lists features, names fields, gives a concrete example, or uses limiting words such as "only", "just" or "simple"; an enumerated list told in repeated "I", "me" and "my" is a solo cue as well. A specified request gets on screen exactly the features it names plus what those features need to work, and every idea of your own becomes one of the closing `▸` options; its sample content is the `seed.json` demo, and adding it is one of those options. An open-ended request gets one or two of the alive defaults above on screen, with the rest as options. The way to share is part of the baseline rather than one of your own ideas, so it reaches the screen of a specified request too.
771
771