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