makersclaw 0.5.2 → 0.7.0

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.
Files changed (148) hide show
  1. package/README.md +73 -7
  2. package/package.json +3 -2
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +3 -3
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +3 -3
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +121 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +2 -2
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -5
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +65 -9
  117. package/src/install.mjs +360 -21
  118. package/src/lockfile.mjs +468 -0
  119. package/src/pm.mjs +473 -0
  120. package/src/registry.mjs +125 -1
  121. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  122. package/templates/next/browser/app/layout.jsx +94 -0
  123. package/templates/next/browser/app/page.jsx +60 -0
  124. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  125. package/templates/next/browser/lib/actions.js +16 -0
  126. package/templates/next/browser/lib/data.js +43 -0
  127. package/templates/next/files/README.md +818 -82
  128. package/templates/next/files/app/claw-alive/route.js +2 -2
  129. package/templates/next/files/app/frame.js +134 -9
  130. package/templates/next/files/app/layout.jsx +2 -2
  131. package/templates/next/files/check/run.mjs +1491 -53
  132. package/templates/next/files/components/ui/item.jsx +2 -2
  133. package/templates/next/files/components/ui/notice.jsx +1 -1
  134. package/templates/next/files/components/ui/separator.jsx +1 -1
  135. package/templates/next/files/lib/actions.js +171 -52
  136. package/templates/next/files/lib/brain-server.js +96 -0
  137. package/templates/next/files/lib/brain.js +413 -0
  138. package/templates/next/files/lib/claw.js +11 -0
  139. package/templates/next/files/lib/data.js +120 -30
  140. package/templates/next/files/lib/db.js +97 -17
  141. package/templates/next/files/lib/door.js +680 -0
  142. package/templates/next/files/next.config.mjs +85 -9
  143. package/templates/next/files/package.json +3 -3
  144. package/templates/next/files/{middleware.js → proxy.js} +13 -6
  145. package/templates/next/files/surfaces.js +3 -2
  146. package/templates/next/pnpm-lock.yaml +5870 -0
  147. package/templates/next/template.json +13 -6
  148. package/templates/next/files/schema.sql +0 -22
@@ -1,11 +1,12 @@
1
1
  # artifact template
2
2
 
3
- The app every artifact starts from: Next.js 15 (app router, JSX, server
4
- actions), Postgres via `DATABASE_URL`, Tailwind v4, and a kit of parts
3
+ The app every artifact starts from: Next.js 16 (app router, JSX, server
4
+ actions, Turbopack), Postgres via `DATABASE_URL` or, in a browser app,
5
+ the platform's data door (§2), Tailwind v4, and a kit of parts
5
6
  (`components/ui`, `components/parts`). It carries the behaviour every
6
7
  app shares, the parts every app is assembled from, and the workspace's
7
- colours. You design the surfaces: what is on them, how they are laid
8
- out, how dense, how they read — never what colour anything is.
8
+ colours. You design the surfaces: what is on them, how they
9
+ are laid out, how dense, how they read — never what colour anything is.
9
10
 
10
11
  ## 1. What you are inside
11
12
 
@@ -48,6 +49,14 @@ Paths here are relative to this folder — the artifact's `app/`.
48
49
  `app/`: this file's `app/shell.jsx` is written as `app/app/shell.jsx`,
49
50
  `surfaces/<key>.jsx` as `app/surfaces/<key>.jsx`.
50
51
 
52
+ An app is one of two modes, and `makersclaw.json`'s `mode` says which.
53
+ A **server app** runs on a Next server of its own (`"server"`, or no
54
+ key). A **browser app** runs in the member's browser: its pages are
55
+ files built once, and every read and write goes through the platform's
56
+ data door (`"browser"`). People add, edit and delete in it as in any
57
+ app. Both lay the files below; a browser app swaps six of them and
58
+ leaves out four (Per mode, below). Which to build: §4.
59
+
51
60
  Yours — write and rewrite freely:
52
61
 
53
62
  ```
@@ -56,7 +65,7 @@ app/DESIGN.md the design, written before the code (§3)
56
65
  surfaces/<key>.jsx one file per domain surface
57
66
  surfaces.js the manifest entries (import + component when built)
58
67
  components/*.jsx anything shared between surfaces
59
- components/*-actions.js your own server actions, for a write lib/actions.js cannot make (§5)
68
+ components/*-actions.js a server app's own server actions, for a write lib/actions.js cannot make (§5)
60
69
  components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
61
70
  ```
62
71
 
@@ -72,14 +81,56 @@ app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its
72
81
  components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
73
82
  components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
74
83
  check/pins.json the fixed files by hash; `npm run check` refuses one that differs
75
- lib/* db, data, actions, claw, platform, hooks, sample (§5)
76
84
  check/ `npm run check` (§6)
77
- middleware.js product-token check, x-claw-surface and x-claw-theme headers
78
- next.config.mjs basePath + Server Actions allowedOrigins, both from env (§4b)
85
+ proxy.js product-token check, x-claw-surface and x-claw-theme headers (Next 16's name for middleware.js)
86
+ next.config.mjs basePath + Server Actions allowedOrigins (and the dev server's allowedDevOrigins), from env (§4b);
87
+ a browser app's export (output "export"), from makersclaw.json's mode
79
88
  postcss.config.mjs Tailwind v4
80
- schema.sql public.artifact_activity, for reference
81
89
  ```
82
90
 
91
+ And `lib/`, fixed too (§5):
92
+
93
+ ```
94
+ lib/data.js the reads of your own tables (Reads)
95
+ lib/actions.js the writes of your own tables: the one-row writes and runAction (Writes)
96
+ lib/db.js the database pool: query(), logActivity() — a server app's, for its server files
97
+ lib/door.js your own tables from the browser, through the data door; the hooks (Door)
98
+ lib/brain.js the company brain from the browser, in both modes; its hooks (Brain)
99
+ lib/brain-server.js the brain's reads for a server component — a server app's
100
+ lib/claw.js the _claw API the gateway answers, from the browser
101
+ lib/platform.js typed wrappers over lib/claw.js (Platform)
102
+ lib/hooks.js the React hooks over them: useJob, useArtifact… (Hooks)
103
+ lib/sample.js markSample, for sample rows (§3a)
104
+ ```
105
+
106
+ ### Per mode
107
+
108
+ A browser app lays its own copy of six fixed files (the template's
109
+ `templates/next/browser/`). Each is fixed and pinned like the rest, and
110
+ keeps the server copy's names, so a surface imports what it always did:
111
+
112
+ ```
113
+ app/layout.jsx built once: the theme set by a script in <head> before the page paints; examples decided at build (§3a)
114
+ app/page.jsx / → a page that forwards to the home surface, keeping the query
115
+ app/s/[key]/page.jsx one file per surface, whose surface renders in the browser inside <Suspense>
116
+ app/auto-refresh.jsx asks the data door what changed, and every hook on the page reads again (§7)
117
+ lib/data.js lib/door.js's reads and hooks, under the usual name
118
+ lib/actions.js lib/door.js's writes, under the usual name
119
+ ```
120
+
121
+ And it has none of these, since nothing of it runs on a server:
122
+
123
+ ```
124
+ proxy.js the platform serves the files; there is no request of the app's to check
125
+ app/claw-alive/route.js no route handler runs; the auto-refresh asks the door instead
126
+ lib/db.js no database login in the app: the door holds it, and knows who is asking
127
+ lib/brain-server.js the brain's reads run in the browser, through lib/brain.js
128
+ ```
129
+
130
+ `npx makersclaw mode <browser|server>` is the only switch: it lays that
131
+ mode's fixed files, removes the other's, and says what your own code
132
+ must change (§4).
133
+
83
134
  ## 3. Design first
84
135
 
85
136
  Before any code, write `app/DESIGN.md` — one screen of text:
@@ -168,7 +219,9 @@ shadcn project. There a surface may still open on a worked example —
168
219
  `useFixture(rows, SAMPLE)`, §3b — so its author sees it filled.
169
220
  `app/layout.jsx` decides per request: examples are off whenever
170
221
  `CLAW_ARTIFACT_ID` is set (`product_run` sets it for every workspace app)
171
- and on otherwise; `CLAW_EXAMPLES=1` forces them on. Inside a workspace
222
+ and on otherwise; `CLAW_EXAMPLES=1` forces them on. A browser app's
223
+ layout decides the same once, at build: the platform builds every browser
224
+ app with its `CLAW_ARTIFACT_ID`. Inside a workspace
172
225
  `useFixture` hands back the real rows — none — so the same surface falls
173
226
  through to `LiveEmpty`. Up to 0.3.x a workspace app opened on its example
174
227
  until its table had a row; since 0.4.0 it opens empty.
@@ -494,7 +547,7 @@ default import, framed wherever it is rendered, and is never dropped
494
547
  into the shell, the kit or `lib/`: rendered outside its frame it inherits
495
548
  the workspace, and the check says so.
496
549
 
497
- **One door, one bundle.** The folder is reached one way: a static
550
+ **One door, one bundle.** The folder is reached one way: a plain
498
551
  `import XPost from "…/depictions/x-post"` at the top of the file that
499
552
  frames it. No `import()`, no `next/dynamic`, no re-export module, no
500
553
  alias (`const P = XPost`), no picture passed along as a value. Through
@@ -507,15 +560,83 @@ never reaches the page.
507
560
 
508
561
  ## 4. Server and client
509
562
 
563
+ ### Which mode
564
+
565
+ **An app is a browser app unless it needs server code.** Server code
566
+ means one of three things:
567
+
568
+ - a route handler: a proxy to a service with a secret, a render to PDF;
569
+ - an upload larger than the data door takes;
570
+ - SQL a member types (`sql-console`).
571
+
572
+ Nothing else needs a server. A browser app reads and writes its own
573
+ tables and the company brain through the door, runs a write of more
574
+ than one row as an action (§5), and has no process of its own to start,
575
+ stop or crash. `makersclaw.json`'s `mode` says which this app is
576
+ (`"browser"` is a browser app; §2). An app moves with `npx makersclaw
577
+ mode server` (or `browser`), and its own code moves in the same build:
578
+ the command prints what must change.
579
+
580
+ ### A browser app
581
+
582
+ Its pages are built once, with no request, into files the platform
583
+ serves. Everything below runs in the member's browser:
584
+
585
+ - **Every surface is a client component.** Its file starts with `"use
586
+ client"`. It reads through the hooks: `useRows`, `useRow`, `useCount`,
587
+ `useCountBy` and `useActivity` from `lib/data.js`, and the brain's
588
+ `useRecords`, `useRecord`, `useRecordCounts` and `useBrainSearch` from
589
+ `lib/brain.js`. It hands rows to blocks once they have loaded, and
590
+ wires the blocks' callbacks to `lib/actions.js` (§5, Door, has a
591
+ whole surface).
592
+ - **It receives `surface` alone.** No `searchParams`: the page has no
593
+ request. `?row=`, `?list=` and any other view state come from
594
+ `useSearchParams()` (`next/navigation`).
595
+ - **No `process.env` in its code but `CLAW_BASE_PATH`, `CLAW_PRODUCT`
596
+ and `NEXT_PUBLIC_*`.** A prerendered page prints what it reads into a
597
+ file anyone who opens the app can fetch. A secret stays with the
598
+ platform; the build never sees `.env`.
599
+ - No `"use server"`, no `route.js`, no `proxy.js`, no import of
600
+ `next/headers`, `next/cache`, `next/server`, `pg` or `lib/db`, no
601
+ route segment config (`dynamic`, `revalidate`…). There is no server to
602
+ run them, and `npm run check` names each one (§6, `browser`).
603
+
604
+ ```jsx
605
+ // surfaces/investors.jsx (a browser app)
606
+ "use client";
607
+ import { useSearchParams } from "next/navigation";
608
+ import { useRows } from "../lib/data";
609
+ import InvestorBoard from "../components/InvestorBoard";
610
+
611
+ export default function Investors() {
612
+ const highlight = useSearchParams().get("row");
613
+ const { rows, error, loading } = useRows({ source: "investors", orderBy: "updated_at desc" });
614
+ if (loading) return null; // nothing until the first answer: useFixture must not take a read in flight for an empty table
615
+ return <InvestorBoard rows={rows} error={error} highlight={highlight} />;
616
+ }
617
+ ```
618
+
619
+ `components/InvestorBoard.jsx` is the same file in both modes (below):
620
+ it calls `moveRow` from `lib/actions.js`, which in a browser app goes
621
+ through the door.
622
+
623
+ ### A server app
624
+
510
625
  One split, stated once:
511
626
 
512
627
  ```
513
628
  lib/data.js, lib/db.js server components and server actions only
514
629
  lib/platform.js, lib/hooks.js "use client" files only
515
630
  lib/actions.js either (they are server actions)
631
+ lib/brain.js "use client" files only, in a server app too (§5, Brain)
632
+ lib/door.js "use client" files only: the own tables from the browser (§5, Door)
633
+ lib/brain-server.js server components only: the brain's reads
516
634
  surfaces.js server files only (entries import components)
517
635
  ```
518
636
 
637
+ In a browser app every `lib/` file but `sample.js` is for `"use client"`
638
+ files, and `surfaces.js` is the pages'.
639
+
519
640
  Pass plain data across the boundary — strings, numbers, arrays, objects.
520
641
  `plain(row)` already turns a timestamp into its ISO string. A `date`
521
642
  column is its day, `"2026-09-28"`: text, the same day for every reader,
@@ -537,7 +658,7 @@ import { loadRows } from "../lib/data";
537
658
  import InvestorBoard from "../components/InvestorBoard";
538
659
 
539
660
  export default async function Investors({ surface, searchParams }) {
540
- const { rows, error } = await loadRows({ source: "public.investors", orderBy: "updated_at desc" });
661
+ const { rows, error } = await loadRows({ source: "investors", orderBy: "updated_at desc" });
541
662
  return <InvestorBoard rows={rows} error={error} highlight={searchParams.row ?? null} />;
542
663
  }
543
664
  ```
@@ -559,7 +680,11 @@ export default function InvestorBoard({ rows: real, error, highlight }) {
559
680
 
560
681
  Every surface component receives `surface` (`{ key, kind, label, source
561
682
  }`) and `searchParams` (the URL query; `?row=<id>` is the convention for
562
- "highlight this row" — an activity line opens its surface with it).
683
+ "highlight this row" — an activity line opens its surface with it). A
684
+ browser app's surface receives `surface` alone and reads the query with
685
+ `useSearchParams()` (above).
686
+ `source` is the table's bare name, `"investors"`, with no schema in
687
+ front: the app's tables are in its own schema (§5, Your tables).
563
688
 
564
689
  ### Views by link
565
690
 
@@ -572,10 +697,12 @@ part of the contract, the same way `?row=` is:
572
697
  - `?list=<value>` shows one list. An app that keeps separate lists in
573
698
  shared tables declares which column names them (`lists: { table,
574
699
  column }`), and every surface over that table reads `list` and filters
575
- by that column — `loadRows({ source, filter: searchParams.list ? {
576
- [column]: searchParams.list } : undefined })`. With no `list` it shows
577
- every list: a filter of `{ [column]: undefined }` matches no row at
578
- all. It is the one key every app with lists honours.
700
+ by that column — `loadRows({ source, filter: { [column]:
701
+ searchParams.list } })`; in a browser app, `useRows({ source, filter:
702
+ { [column]: useSearchParams().get("list") ?? undefined } })`. With no
703
+ `list` it shows every list: a column
704
+ whose condition is `undefined` is no condition (up to 0.6.x it matched
705
+ no row). It is the one key every app with lists honours.
579
706
  - A job's view may declare a filter (`week=this&list={list}`; `{list}`
580
707
  and `{since}` are filled in per request). Its keys are query params
581
708
  the surface ALREADY reads: declare one only for a param the surface
@@ -608,9 +735,14 @@ action="/…">`. A real external URL (`https://`, `mailto:`) is fine.
608
735
 
609
736
  ### 4b. Writes are rejected — "Invalid Server Actions request"
610
737
 
738
+ A server app only. A browser app has no Server Actions: its writes go
739
+ to the data door on the page's own origin, and nothing here applies to
740
+ them. Its preview still runs on `next dev`, so the dev server's
741
+ `allowedDevOrigins` below holds for it too.
742
+
611
743
  Already handled. Do not fix it in `next.config.mjs`.
612
744
 
613
- Next 15 refuses a Server Action whose `Origin` header disagrees with
745
+ Next refuses a Server Action whose `Origin` header disagrees with
614
746
  `x-forwarded-host`, and in this stack they always disagree: the sandbox
615
747
  ingress rewrites `x-forwarded-host` to its own vercel.run name while the
616
748
  browser is on the product host. Untreated, the app builds, loads and
@@ -620,7 +752,9 @@ button rather than a proxy policy.
620
752
  `next.config.mjs` already reads `CLAW_ALLOWED_ORIGINS`, which `product_run`
621
753
  derives per environment (`src/lib/products.ts`, `productAllowedOrigins`):
622
754
  the product's own FQDN in production, the dashboard's dev origin under a
623
- path prefix.
755
+ path prefix. The same hosts are the dev server's `allowedDevOrigins`:
756
+ Next 16's `next dev` refuses its own resources (the HMR socket, the error
757
+ overlay) to any other host, and says "Blocked cross-origin request".
624
758
 
625
759
  **Never hardcode an origin here.** `allowedOrigins: ["localhost:5173"]`
626
760
  passes every test you can run in development and refuses every write in
@@ -629,53 +763,205 @@ source — say so and stop; it is not an app-level fix.
629
763
 
630
764
  ## 5. API reference
631
765
 
766
+ ### Your tables — one schema, named bare
767
+
768
+ The app's tables live in its own schema, and no other app reads them.
769
+ In Makersclaw that is `app_<name>`, and `makersclaw.json` says
770
+ `"tables": "own"`. Besides its own tables, the app's database role
771
+ reaches only what the platform lays for it under `claw.` and
772
+ `claw_app.` (the activity door, `claw_app.activity`, below).
773
+
774
+ **Name every table bare: `deals`, with no schema in front — neither
775
+ `public.` nor `app_<name>.`.** The role's search path sends a bare name
776
+ to the app's schema. A schema written by hand breaks the app: its role
777
+ reaches nothing in `public`, and a second copy of the app lives in a
778
+ schema of another name. Bare holds everywhere a table is named: a
779
+ surface's `source`, every `lib/data.js` and `lib/actions.js` call, and
780
+ SQL of your own (`update deals set …`). `npm run check` refuses a
781
+ qualified name (§6, `tables`).
782
+
783
+ One schema per app, not one per copy of its code. A change build's
784
+ preview reads and writes the live app's tables, so rows saved while
785
+ testing are real rows. In a change to a live app, add what the change
786
+ needs (new tables, new columns that allow null or have a default);
787
+ remove or rename in a build after it ships.
788
+
789
+ **The company's things are not your tables.** A customer, a person, a
790
+ product — anything another app or a person would look up — is a record
791
+ in the company brain, read and written through `lib/brain.js` (Brain,
792
+ below). A row of yours points at a record by a `uuid` column named for
793
+ its kind (`contact_id`), declared in `links`. Never copy a record's
794
+ fields into a table of yours: the copy goes stale the day someone edits
795
+ the record.
796
+
797
+ `makersclaw init --tables own|public` records where the tables live;
798
+ Makersclaw passes it. It never changes afterwards: an app's tables move
799
+ from `public` to their own schema only with its workspace. An app laid
800
+ before 0.7.0 has no `tables` key: its tables are in `public`, and a
801
+ bare name reaches them there too. So write names bare in either.
802
+
632
803
  ### Reads — `lib/data.js` (server)
633
804
 
805
+ In a browser app `lib/data.js` is the door's (Door, below): these same
806
+ names, shapes and filters, called from `"use client"` files, with a hook
807
+ for each.
808
+
634
809
  Every read resolves `{ ..., error }` and never throws. `error` is text
635
810
  when the table is missing or the database unreachable; it is also
636
811
  `console.error`ed, so the log tail carries it to the platform agent.
637
- `source` is `"table"` or `"schema.table"`; names go through `ident()`.
812
+ `source` is the table's bare name (`"drafts"`); names go through
813
+ `ident()`. It still takes `"schema.table"`, for an app from before
814
+ 0.7.0 whose code names `public.<table>`.
638
815
 
639
816
  ```js
640
817
  loadRows({ source, idColumn = "id", orderBy, limit = 500, filter }) → { rows, error }
641
818
  loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none
642
819
  countRows({ source, filter }) → { count, error }
643
820
  countBy({ source, column, filter }) → { counts: { [value]: n }, error }
644
- loadActivity({ limit = 100, surface, since }) → { rows, error } // public.artifact_activity, this artifact
821
+ loadActivity({ limit = 100, surface, since }) → { rows, error } // claw_app.activity: this app's lines
645
822
  ```
646
823
 
647
- `filter` is `{ column: value }`: `null` → `IS NULL`, an array → `= ANY`,
648
- else equality. `orderBy` is `"updated_at desc, id"`; default `idColumn
649
- desc`. `limit` caps at 5000 (activity: 1000).
824
+ `filter` is an AND of `{ column: condition }`, at most 20: the
825
+ platform data door's grammar, so a filter means the same in a server
826
+ app and a browser one.
827
+
828
+ ```
829
+ value equals (text, a number, true or false, a Date)
830
+ [value, …] one of, at most 1,000
831
+ null empty
832
+ { not: value | null } anything else, empty rows included; or not empty
833
+ { gt | gte | lt | lte: v } a bound; one lower and one upper may combine
834
+ { has: value } an array column (text[], int[]…) holds it
835
+ { prefix: "text" } starts with, case ignored
836
+ ```
837
+
838
+ A column whose condition is `undefined` is left out. Anything else is
839
+ refused, and the read returns why as its `error`. `orderBy` is
840
+ `"updated_at desc, id"`; default `idColumn desc`. `limit` caps at 5000
841
+ (activity: 1000).
650
842
 
651
843
  ```js
652
- const { rows, error } = await loadRows({ source: "public.drafts", filter: { stage: ["draft", "approved"] } });
653
- const { counts } = await countBy({ source: "public.investors", column: "stage" });
844
+ const { rows, error } = await loadRows({ source: "drafts", filter: { stage: ["draft", "approved"] } });
845
+ const { rows: due } = await loadRows({ source: "tasks", filter: { done: false, due: { lte: today }, owner: { not: null } } });
846
+ const { counts } = await countBy({ source: "investors", column: "stage" });
654
847
  const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
655
848
  ```
656
849
 
657
850
  Helpers: `plain(row)` (timestamps → ISO, buffers → base64; a `date` is
658
851
  already its `"YYYY-MM-DD"`), `orderClause(orderBy,
659
- fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
852
+ fallback)` (validated `ORDER BY` text), `whereClause(filter, params)`
853
+ (the filter above as `WHERE` text), `markSample(sample, idColumn)`
660
854
  (stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
661
855
  key of the request, from `x-claw-surface`, unless one is passed).
662
856
 
663
857
  ### Writes — `lib/actions.js` (server actions, callable from either side)
664
858
 
665
- Each quotes names with `ident()`, writes one activity row through
666
- `logActivity`, calls `revalidatePath("/", "layout")`, and resolves
667
- `{ ok }` or `{ ok: false, error }`. Never throws. `surface` defaults to
668
- the request's surface (middleware sets it from `/s/<key>`).
859
+ In a browser app `lib/actions.js` is the door's (Door, below): these
860
+ same names, shapes and codes, as plain functions of the browser. The
861
+ door runs each write and writes its activity line.
862
+
863
+ In a server app each quotes names with `ident()`, runs in its own
864
+ transaction, writes one activity row through `logActivity` after it
865
+ commits, calls `revalidatePath("/", "layout")`, and resolves `{ ok }`
866
+ or `{ ok: false, error, code }`. Never throws. `code` is the data
867
+ door's (Door, below), so a surface branches on it the same way in both
868
+ modes: `not_found` (no such row), `no_such_table`, `no_such_action`,
869
+ `action_refused`, `conflict`, `bad_request`, `too_slow`, `try_again`,
870
+ else `failed`. `surface` defaults to the request's surface key
871
+ (the `<key>` of `/s/<key>`, which proxy.js reads).
669
872
 
670
873
  ```js
671
- updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data })
874
+ updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log })
672
875
  moveRow({ source, idColumn = "id", id, stateColumn, to, surface }) // action "moved"
673
876
  insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) → { ok, id }
674
877
  deleteRow({ source, idColumn = "id", id, surface, detail }) // action "removed"
675
878
  saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn = "value",
676
879
  column, idColumn = "id", id, key, value, surface })
880
+ runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
881
+ ```
882
+
883
+ `log: false` (`updateRow` and `runAction` only) writes no activity line:
884
+ for a save that is typing, not a decision, like an autosave every few
885
+ keystrokes. An insert and a delete always log. Name such a write "no
886
+ activity line" in the brief (`references/actions.md`), so the agent logs
887
+ none for it either. The door takes the same `log: false` in a browser
888
+ app.
889
+
890
+ **Who is asking.** Each write's transaction first sets two settings,
891
+ local to it: `claw.member_id` and `claw.member_email`, the member the
892
+ platform named on the request (`''` when none: a local run, a job).
893
+ The platform's data door sets the same two for a browser app, and the
894
+ agent's `sql` for the member it acts for. A default, a trigger or an
895
+ action reads them:
896
+
897
+ ```sql
898
+ nullif(current_setting('claw.member_email', true), '')
677
899
  ```
678
900
 
901
+ A column the page must not choose (`created_by`, `approved_by`) is set
902
+ by a `before insert` or `before update` trigger that reads it, never
903
+ sent from the page: a default alone keeps whatever the page sends.
904
+ Made with `sql`, once per table:
905
+
906
+ ```sql
907
+ create or replace function deals_created_by() returns trigger
908
+ language plpgsql as $$
909
+ begin
910
+ new.created_by := nullif(current_setting('claw.member_email', true), '');
911
+ return new;
912
+ end $$;
913
+ create trigger deals_created_by before insert on deals
914
+ for each row execute function deals_created_by();
915
+ ```
916
+
917
+ It is a label, not a permission: nothing checks it, and the app's own
918
+ agent can set it too.
919
+
920
+ **Actions.** A write of more than one row, or one that decides
921
+ something, is an action: a function in the app's own schema, `(args
922
+ jsonb) returns jsonb`, made with the `sql` verb. The screen calls it
923
+ with `runAction`, which runs `select <name>($1::jsonb)` with `args` as
924
+ JSON (`{}` when none); the app's agent runs the same function with
925
+ `sql`, so the two can never drift. Its name is one bare, lower-case
926
+ name (`close_cycle`): the data door calls only a function of the app's
927
+ own schema, so a name with a schema or capitals (`CloseCycle`) is
928
+ `no_such_action` in both modes. It refuses with `raise exception '<sentence>'`, which
929
+ comes back as `error`, code `action_refused`. It never writes
930
+ `claw_app.activity`: `runAction` logs the call (`action` defaults to the
931
+ function's name, `row_ref` is `rowRef` or the result's `id`), and the
932
+ agent logs its own.
933
+
934
+ ```sql
935
+ create or replace function close_cycle(args jsonb) returns jsonb
936
+ language plpgsql as $$
937
+ declare
938
+ c uuid := (args->>'cycle')::uuid;
939
+ begin
940
+ update cycles set status = 'closed', closed_at = now() where id = c and status = 'open';
941
+ if not found then raise exception 'that cycle is already closed'; end if;
942
+ update goals set status = 'carried' where cycle_id = c and status = 'open';
943
+ return jsonb_build_object('id', c);
944
+ end $$;
945
+ ```
946
+
947
+ ```js
948
+ const r = await runAction({ name: "close_cycle", args: { cycle: id }, action: "closed", detail: "Q3" });
949
+ if (!r.ok) setError(r.error); // "that cycle is already closed"
950
+ ```
951
+
952
+ **Name every action in the agent's brief, `references/actions.md`,
953
+ with its `select`**: `` `close_cycle` `` and `select
954
+ close_cycle($1::jsonb)`, its params and what it refuses. The agent
955
+ runs it from there, and `npm run check` refuses an action the screens
956
+ run that the brief does not name (§6, `agent`).
957
+
958
+ In a change to a live app, never change or drop a function the live app
959
+ runs, a helper one included: the preview writes the live schema, so the
960
+ live screens would run the new body at once, and a roll back would run
961
+ it too. Write a new one (`close_cycle_2`), call it from the new code and
962
+ name it in `references/actions.md`. Drop the old one once neither the
963
+ live build nor the one kept for roll back calls it.
964
+
679
965
  `saveSetting` table mode updates the `(key, value)` row and inserts one
680
966
  when missing — a plain `key text, value text` table is enough. JSON mode
681
967
  sets `key` inside the jsonb `column` of row `id`.
@@ -690,19 +976,288 @@ the column's default: `updated_at timestamptz not null default now()`.
690
976
  ```js
691
977
  "use client";
692
978
  import { moveRow } from "../lib/actions";
693
- const r = await moveRow({ source: "public.investors", id, stateColumn: "stage", to: "sent" });
979
+ const r = await moveRow({ source: "investors", id, stateColumn: "stage", to: "sent" });
694
980
  if (!r.ok) setError(r.error);
695
981
  ```
696
982
 
697
983
  Never write raw SQL from a surface. `lib/*.js` is fixed (`npm run check`
698
984
  refuses an edit, and `makersclaw update` lays the file again), so a write
699
- these cannot make goes in a server-action file of the app's own:
700
- `"use server"` on its first line, beside the components that call it
701
- (`components/deal-actions.js`). There it composes the actions above, or
702
- runs its UPDATE or DELETE through `query()` from `lib/db.js`, records it
703
- with `logActivity()` and ends with `revalidatePath("/", "layout")`, as
704
- `lib/actions.js` does. A new row still goes through `insertRow`: the
705
- check refuses a raw `INSERT INTO` anywhere outside `lib/`.
985
+ these cannot make is an action (above), called with `runAction`. In a
986
+ server app only, a server-action file of the app's own (`"use server"` on its first line,
987
+ beside the components that call it: `components/deal-actions.js`) may
988
+ compose the writes above. A new row still goes through `insertRow`:
989
+ the check refuses a raw `INSERT INTO` anywhere outside `lib/`, and, in
990
+ an app on its own tables, any SQL that writes in the app's code (§6,
991
+ `agent`).
992
+
993
+ ### Door — `lib/door.js` (browser, both modes)
994
+
995
+ The app's own tables, read and written from the browser through the
996
+ platform's data door, `_claw/data`. The gateway answers it under the
997
+ app's path, as the member whose cookie comes with the request. Each
998
+ write runs on the app's own database login, in one transaction that
999
+ knows who is asking (`claw.member_email`, above), and the door writes
1000
+ its activity line.
1001
+
1002
+ A browser app (`"mode": "browser"` in `makersclaw.json`) has no server of
1003
+ its own. Its `lib/data.js` and `lib/actions.js` are this file under
1004
+ their usual names, so a surface imports as it always did:
1005
+ `import { useRows } from "../lib/data"`, `import { moveRow } from
1006
+ "../lib/actions"`. In a server app a `"use client"` component may take
1007
+ the hooks from here.
1008
+
1009
+ ```js
1010
+ // Reads: lib/data.js's names and shapes
1011
+ loadRows({ source, idColumn = "id", orderBy, limit = 500, filter, offset, cursor }) → { rows, error, next }
1012
+ loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none, or no id
1013
+ countRows({ source, filter }) → { count, error }
1014
+ countBy({ source, column, filter }) → { counts, error }
1015
+ loadActivity({ limit = 100, surface, since }) → { rows, error }
1016
+
1017
+ // Writes: lib/actions.js's names and shapes
1018
+ insertRow({ source, idColumn, values, surface, action, detail }) → { ok, id }
1019
+ updateRow({ source, idColumn, id, patch, surface, action, detail, log }) → { ok }
1020
+ moveRow({ source, idColumn, id, stateColumn, to, surface }) → { ok }
1021
+ deleteRow({ source, idColumn, id, surface, detail }) → { ok }
1022
+ saveSetting({ mode, source, keyColumn, valueColumn, column, idColumn, id, key, value, surface })
1023
+ runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
1024
+
1025
+ // Hooks: a read on mount, and again on dataChanged()
1026
+ useRows(opts) useRow(opts) useCount(opts) useCountBy(opts) useActivity(opts)
1027
+ → { …the read's values, error, loading, reload }
1028
+ dataChanged() every hook on the page reads again
1029
+ whenLanded() resolves once those reads have landed
1030
+ ```
1031
+
1032
+ - `filter` is the grammar above (Reads), sent as it is. `orderBy` is
1033
+ `"updated_at desc"`. `cursor` is the last answer's `next`, which the
1034
+ door gives only when `orderBy` names one column; `offset` is at most
1035
+ 10,000.
1036
+ - Every function resolves and never throws. A read that fails keeps its
1037
+ empty values: `{ rows: [], error, code }`. A write that fails is
1038
+ `{ ok: false, error, code }`, never `undefined`. Branch on `code`;
1039
+ show `error`.
1040
+ - No door answers (a copy on its own, an older Makersclaw):
1041
+ `unavailable: true`, code `unavailable`. Show a quiet "not connected
1042
+ yet".
1043
+ - The codes a page meets: `bad_request` (a column, filter or order the
1044
+ table does not have), `no_such_table`, `no_such_action`, `not_found`,
1045
+ `read_only` (a view: write the table it reads), `action_refused` (an
1046
+ action's own `raise exception`: show its sentence), `conflict` (a
1047
+ unique or foreign key), `too_slow`, `try_again`, `rate_limited`.
1048
+ - Writes go one at a time, in the order they were called, as Server
1049
+ Actions did. A slow save never lands after the newer one.
1050
+ - A write that answers ok calls `dataChanged()`: every hook on the page
1051
+ reads again, the brain's too. When the next step needs the new rows
1052
+ (open the row just made), `await whenLanded()` first.
1053
+ - `loading` is true until a hook's first answer, and again when its
1054
+ options change. A re-read keeps the last rows, and `loading` false,
1055
+ until the new ones land; each answer is a new array. A failed re-read
1056
+ keeps the rows and sets `error`.
1057
+ - A hook compares its options by value on every render: a new object
1058
+ with the same values reads nothing. A server component read once per
1059
+ request; a hook's own answer is a render. So a time in a bound
1060
+ (`filter`, the brain's `where`, `since`; a Date or ISO text) is
1061
+ compared to the minute: `useActivity({ since: new Date(Date.now() - 7
1062
+ * 864e5) })` reads once, and again only after the minute has moved
1063
+ on, keeping its rows meanwhile. The read sends the time as given.
1064
+ Anything else worked out on each render (a `Date.now()` number) is new
1065
+ options every time, and reads every time: work it out once
1066
+ (`useMemo`, `useState`).
1067
+ - `useRow` with no `id` asks nothing: `row` is null.
1068
+ - `surface` defaults to the page's surface key (the `<key>` of
1069
+ `/s/<key>` in its path). `data` on `updateRow` is taken and never
1070
+ sent: the door never puts a value in a line. `log: false` as in
1071
+ Writes.
1072
+ - The door writes the activity line. Nothing in the browser logs.
1073
+
1074
+ ```jsx
1075
+ // surfaces/deals.jsx (a browser app)
1076
+ "use client";
1077
+ import { useSearchParams } from "next/navigation";
1078
+ import { useRows, whenLanded } from "../lib/data";
1079
+ import { insertRow, moveRow } from "../lib/actions";
1080
+ import { Notice } from "../components/ui";
1081
+ import { Board } from "../components/blocks/board";
1082
+
1083
+ export default function Deals() {
1084
+ const opened = useSearchParams().get("row");
1085
+ const { rows, error, loading } = useRows({ source: "deals", orderBy: "updated_at desc" });
1086
+ if (error) return <Notice tone="danger">{error}</Notice>;
1087
+ return (
1088
+ <Board
1089
+ rows={loading ? undefined : rows}
1090
+ groupBy="stage"
1091
+ columns={["lead", "call", "won"]}
1092
+ onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}
1093
+ onCreate={async (name, stage) => {
1094
+ const r = await insertRow({ source: "deals", values: { name, stage } });
1095
+ if (r.ok) await whenLanded();
1096
+ return r;
1097
+ }}
1098
+ />
1099
+ );
1100
+ }
1101
+ ```
1102
+
1103
+ ### Brain — `lib/brain.js` (browser, both modes)
1104
+
1105
+ The company brain is the workspace's shared records: contacts,
1106
+ companies, deals, whatever kinds the workspace keeps. Each record sits
1107
+ in an area (Everyone, Finance…). Every app sees the same Jane Doe.
1108
+
1109
+ An app reaches the brain only through the data door, `_claw/data`. The
1110
+ gateway answers it under the app's path, so it works while the app's
1111
+ machine sleeps. It hands the app only the kinds `makersclaw.json`
1112
+ declares, and only the records the member may read. Never SQL: the
1113
+ app's database login holds none of the brain.
1114
+
1115
+ **What the app declares: `brain` in `makersclaw.json`.** Write it
1116
+ before the first call. The platform grants it when the app ships; a
1117
+ ship that widens it waits for the member's Yes.
1118
+
1119
+ ```json
1120
+ "brain": {
1121
+ "kinds": [
1122
+ { "kind": "contact", "label": "Contact", "plural": "Contacts",
1123
+ "fields": [{ "name": "name", "type": "text" }, { "name": "email", "type": "email" }],
1124
+ "key": ["email"], "filters": [] }
1125
+ ],
1126
+ "reads": ["note"],
1127
+ "writes": ["contact"],
1128
+ "links": [{ "table": "tasks", "column": "contact_id", "kind": "contact", "on_erase": "null" }]
1129
+ }
1130
+ ```
1131
+
1132
+ - `kinds`: the full def of every kind the app reads or writes, since
1133
+ the workspace may not have it yet. `note`, the brain's own kind, needs
1134
+ none.
1135
+ - `reads` and `writes`: kinds by name. Writing implies reading. The door
1136
+ answers any other kind `not_declared`.
1137
+ - `links`: a `uuid` column of one of the app's own tables (named bare,
1138
+ `"tasks"`) that holds a record's id, and what erasing that record does
1139
+ to the row (`null` or `delete`). Own tables only (`"tables": "own"`).
1140
+
1141
+ `npm run check` checks the block's shape and that every call names a
1142
+ kind it declares (§6). The rest (field types, keys, each link against
1143
+ your tables) the platform checks when the app ships.
1144
+
1145
+ Call these from `"use client"` files, in a server app too. The grant
1146
+ cookie is who is asking, and it makes a write the member's press:
1147
+ confirmed, and able to overwrite a confirmed value, erase or merge. A
1148
+ write from the app's server would be app code acting: unconfirmed, and
1149
+ skipped over a confirmed value. So there is no Server Action for the
1150
+ brain.
1151
+
1152
+ ```js
1153
+ import { listRecords, writeRecord } from "../lib/brain";
1154
+
1155
+ // Reads
1156
+ kinds() → { kinds, brain, error }
1157
+ listRecords({ kind, where, order, limit, cursor, fields, prefix, state })
1158
+ → { records, next, error }
1159
+ getRecord({ id, links = false, state }) → { record, links, error }
1160
+ countRecords({ kind, by, where }) → { count, by, error }
1161
+ searchBrain({ q, kinds, limit, meaning }) → { results, meaning, error }
1162
+ recordHistory({ id, limit, cursor }) → { changes, next, error }
1163
+ brainChanges({ since, kinds }) → { changes, next, error }
1164
+ brainLists({ kind }) → { lists, error }
1165
+ listMembers({ list, limit, cursor }) → { list, records, next, error }
1166
+
1167
+ // Writes
1168
+ writeRecord({ kind, id, fields, area, from, ifVersion, owner, surface, detail })
1169
+ → { ok, id, created, changed, version, skipped }
1170
+ writeRecords({ writes }) → { ok, results } // at most 100, all or none
1171
+ confirmFields({ id, fields }) → { ok, confirmed, version }
1172
+ eraseRecord({ id, reason }) → { ok, erased } // gone for every app, no undo
1173
+ mergeRecords({ keep, drop }) → { ok, id, merged, filled, version }
1174
+ linkRecords({ from, to, type }) → { ok, created }
1175
+ unlinkRecords({ from, to, type }) → { ok, removed }
1176
+ createList({ name, kind, area }) → { ok, id }
1177
+ changeList({ list, add, remove, position }) → { ok, added, removed }
1178
+
1179
+ // Hooks: a read on mount, and again on dataChanged() (lib/door.js's rules)
1180
+ useRecords(args) useRecord(args) // no id: record null, nothing asked
1181
+ useRecordCounts(args) useBrainSearch(args) // a blank q asks nothing
1182
+ → { …the read's values, error, loading, reload }
1183
+ ```
1184
+
1185
+ Every function resolves and never throws:
1186
+
1187
+ - A read resolves the door's JSON with `error: null`.
1188
+ - A write resolves the door's JSON.
1189
+ - A refusal is `{ ok: false, error, code }`. A read keeps its empty
1190
+ values (`records: []`, `next: null`), so the surface renders it like
1191
+ no rows. Branch on `code`; show `error`.
1192
+ - A 404 with no code, or no network, is
1193
+ `{ ok: false, unavailable: true }`: a claw with no door yet. Show a
1194
+ quiet "not connected yet".
1195
+
1196
+ Arguments are camelCase (`ifVersion`). Results are the door's keys as
1197
+ they come (`label_field`, `updated_at`).
1198
+
1199
+ A record is `{ id, kind, area, label, fields, state, unconfirmed, owner,
1200
+ source, created_at, updated_at, version }`. `fields` holds the values;
1201
+ `state` says, field by field, who wrote it and whether a person
1202
+ confirmed it.
1203
+
1204
+ - `where` is an AND of conditions on the kind's fields:
1205
+ `{ stage: "lead" }` equals, `{ stage: ["lead", "won"] }` one of,
1206
+ `{ phone: null }` empty, `{ not: v }`, `{ gt | gte | lt | lte: v }`,
1207
+ `{ has: v }` (a choices field), `{ prefix: "ja" }`. Also `area`,
1208
+ `owner_id`, `unconfirmed`, `updated_at`. A kind's declared `filters` are fast;
1209
+ another field scans the kind and may answer `too_slow`.
1210
+ - `order` is `-updated_at` (the default), `updated_at`, `created_at`,
1211
+ `-created_at`, `label`, `-label`, `<field>` or `-<field>`.
1212
+ - `limit` is 50 by default, at most 200. `cursor` is the last answer's
1213
+ `next`; `next` is null at the end.
1214
+ - `state: false` leaves each field's state out. `fields: ["name"]`
1215
+ returns only those.
1216
+ - `searchBrain` looks by words and by meaning. `meaning: false` is words
1217
+ only: nothing is embedded and nothing is charged. Use it for a picker
1218
+ that searches as the member types. A blank `q` asks nothing.
1219
+ - `brainChanges` is what changed in the member's areas and the app's
1220
+ kinds since `since`, the last answer's `next`.
1221
+ - `ifVersion` refuses a write with `changed` when someone saved the
1222
+ record after the page read it.
1223
+ - `surface` defaults to the page's surface key (the `<key>` of
1224
+ `/s/<key>` in its path: `contacts`, not `/s/contacts`); `detail` is
1225
+ one sentence.
1226
+
1227
+ The door writes the activity line for every write it takes (Activity,
1228
+ below). Never call `logActivity` for a brain write. Writes go one at a
1229
+ time, in the same turn as `lib/door.js`'s. One that answers ok calls
1230
+ `dataChanged()`, so every hook on the page reads again (these four and
1231
+ the own tables'); a server app may also `router.refresh()`.
1232
+
1233
+ The codes a page meets: `not_declared` (the app did not declare that
1234
+ kind, or not for writing), `not_granted` (the workspace has not granted
1235
+ the app's areas yet), `area_not_granted`, `unknown_kind`, `not_found`,
1236
+ `bad_field`, `bad_request`, `changed`, `hidden_match` (a record with
1237
+ that key is in an area the member cannot read), `key_taken`,
1238
+ `not_a_person`, `rate_limited`, `too_slow`, `not_signed_in`.
1239
+
1240
+ **Server reads — `lib/brain-server.js`.** The same nine reads, same
1241
+ arguments and results, for a server component in a server app (a
1242
+ browser app has no such file: its brain reads are the hooks above). They go
1243
+ with the door grant the product proxy stamps on each member's request
1244
+ (`x-claw-door`), to `CLAW_DOOR_URL`. It has no writes. With no grant (a
1245
+ render outside a member's request, or a machine whose proxy does not
1246
+ stamp it yet) each read answers
1247
+ `{ ok: false, unavailable: true, code: "no_door" }`: read in the browser
1248
+ instead.
1249
+
1250
+ ```js
1251
+ // surfaces/contacts.jsx (server)
1252
+ import { listRecords } from "../lib/brain-server";
1253
+ import ContactList from "../components/ContactList";
1254
+
1255
+ export default async function Contacts({ searchParams }) {
1256
+ const first = await listRecords({ kind: "contact", order: "label" });
1257
+ // no_door: ContactList reads with lib/brain.js in the browser.
1258
+ return <ContactList first={first.code === "no_door" ? null : first} highlight={searchParams.row ?? null} />;
1259
+ }
1260
+ ```
706
1261
 
707
1262
  ### Platform — `lib/platform.js` (browser)
708
1263
 
@@ -789,10 +1344,10 @@ while the App tab is on screen (behind the window's other tabs it asks
789
1344
  nothing; coming back looks at once); a start through `useJob` on the
790
1345
  same page makes it look at once. Hand it to `LiveEmpty` (§3a).
791
1346
 
792
- There is no chat, runs or activity hook: the OS draws those tabs.
793
- Activity has no hook either — it is a Postgres table, read server-side
794
- with `loadActivity` by any domain surface that wants a slice of it (a
795
- row's history beside the row, say).
1347
+ There is no chat or runs hook: the OS draws those tabs. A domain
1348
+ surface that wants a slice of the activity (a row's history beside the
1349
+ row, say) reads it with `loadActivity`: on the server in a server app,
1350
+ or with `useActivity` (`lib/door.js`, §5 Door) from the browser.
796
1351
 
797
1352
  ### Commissioning work — `useJob()`
798
1353
 
@@ -951,12 +1506,18 @@ request, desk_origin } }`) or refused it; it never waits for the work.
951
1506
 
952
1507
  ### Activity — the contract
953
1508
 
954
- `public.artifact_activity` is how the founder sees the work. The OS's
955
- **Activity tab** on the app window reads it, newest first — and so can
956
- any domain surface that wants to (`loadActivity`). Every
957
- `lib/actions.js` write inserts a row through `logActivity()` in
958
- `lib/db.js`; the artifact's agent writes the same table with the `sql`
959
- verb, `by = 'agent'` or `'run:<id>'`.
1509
+ `claw_app.activity` is how the founder sees the work. It is the
1510
+ platform's view of the workspace's activity table, and it holds only
1511
+ this app's lines. The OS's **Activity tab** on the app window reads
1512
+ them, newest first — and so can any domain surface that wants to
1513
+ (`loadActivity`). In a server app every `lib/actions.js` write inserts a
1514
+ line through `logActivity()` in `lib/db.js` (but a `log: false` patch or
1515
+ action). In a browser app the door writes it, for every write it takes
1516
+ (`lib/door.js`), with the same `log: false`. The app's agent writes the
1517
+ same view
1518
+ with the `sql` verb, `by = 'agent'` or `'run:<id>'`. A brain write's
1519
+ line is the door's: it writes one for every write it takes (`lib/brain.js`
1520
+ never logs, and neither does the page).
960
1521
 
961
1522
  ```js
962
1523
  await logActivity({
@@ -969,13 +1530,22 @@ await logActivity({
969
1530
  });
970
1531
  ```
971
1532
 
972
- Columns: `id, artifact_id, at, action, surface, row_ref, detail, data
973
- (jsonb), by`. `loadActivity` returns them without `artifact_id` — every
974
- row is this artifact's. The OS Activity tab shows the surface's label
975
- on each line and does not link into the app yet; the convention for
976
- anything that does (a domain surface's own history slice today, the tab
977
- later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`, so stamp
978
- both on every line and make the surface honour `?row=`.
1533
+ The view fills in `artifact_id` (the app whose role writes the line)
1534
+ and `id`, so an insert names neither; one that names another app's id
1535
+ is refused. Columns: `id, artifact_id, at, action, surface, row_ref,
1536
+ detail, data (jsonb), by`. `loadActivity` returns them without
1537
+ `artifact_id` — every line is this app's. The OS Activity tab shows the
1538
+ surface's label on each line and does not link into the app yet; the
1539
+ convention for anything that does (a domain surface's own history slice
1540
+ today, the tab later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`,
1541
+ so stamp both on every line and make the surface honour `?row=`.
1542
+
1543
+ Up to 0.6.x the lines went into the `artifact_activity` table in
1544
+ `public`, named with `CLAW_ARTIFACT_ID`. An app on its own schema can
1545
+ reach nothing in `public`, so since 0.7.0 both functions go through the
1546
+ view; the lines are the same. Outside Makersclaw there is no view:
1547
+ `logActivity` warns and returns false, and `loadActivity` returns the
1548
+ database's `error`.
979
1549
 
980
1550
  ### Errors
981
1551
 
@@ -996,9 +1566,9 @@ files has to). The platform agent reads them as console events. Call
996
1566
  line per rule, under twenty lines, exit 1 on a failure that names the
997
1567
  file and line and what to use instead. It builds for itself, into
998
1568
  `.next-check`, and only when a source changed since the last check —
999
- never run `npm run build` first, and never build into `.next`: the dev
1000
- server product_run started owns that folder as another user, and a
1001
- second build there dies with `EACCES: .next/trace`.
1569
+ never run `npm run build` first, and never build into `.next`: it is not
1570
+ the check's folder. In a working copy the preview's dev server owns it;
1571
+ in a live app it holds the build the platform made and serves.
1002
1572
 
1003
1573
  ```
1004
1574
  check: 5 files · 2 surfaces
@@ -1009,10 +1579,20 @@ ok manifest 2 surfaces: drafts, sent
1009
1579
  ok icons every icon name exists
1010
1580
  ok kit 70 fixed files match the template
1011
1581
  ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
1582
+ ok agent no SQL write in app code; the agent's brief names every action with its call (1), every table written (2) and every server action that writes (0)
1583
+ ok tables its own schema (makersclaw.json): every table named bare, and each bare name one of the 2 its schema makes (../install/schema.sql); claw. and claw_app. are the platform's
1584
+ ok brain no brain block in makersclaw.json; no lib/brain.js call names a kind
1012
1585
  ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
1013
1586
  all clear
1014
1587
  ```
1015
1588
 
1589
+ A browser app's check has one more line, and its smoke serves files:
1590
+
1591
+ ```
1592
+ ok browser files and a door: 2 client surfaces; no server code, route, proxy or segment config; env only by the names a page may print
1593
+ ok smoke built in 38s (copy at ~/.claw-check/app) · export 2.1 MB in 64 files · / → 200 to /s/drafts · /s/drafts 200 · /s/sent 200 (0.1s)
1594
+ ```
1595
+
1016
1596
  `kit` refuses a fixed file that differs from the template (they are
1017
1597
  hashed in `check/pins.json`) and any file of yours under `components/ui`
1018
1598
  or `components/parts`. `depictions` reads both directions: nothing of
@@ -1020,8 +1600,117 @@ the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
1020
1600
  utility), nothing of the depiction outside its frame (a named import, a
1021
1601
  picture in the shell or a `Card`, a dead file nobody renders).
1022
1602
 
1603
+ `agent` holds the app's agent to what the screens do. Its brief beside
1604
+ the app (`../AGENTS.md`, `../references/*.md`) names, in backticks, every
1605
+ server action that writes. In an app on its own tables (`"tables":
1606
+ "own"`) the screens write through two doors only, so it also refuses:
1607
+
1608
+ - SQL that writes in the app's code: `insert into`, `update … set` or
1609
+ `delete from`, in any case, in a string of any `.js`, `.jsx` or `.mjs`
1610
+ but the fixed `lib/`, where it reads as a statement (a `where`, a
1611
+ `values`, a `set col =`, a `;` after it; handed to `query()`; or the
1612
+ whole string). Both modes, brief or none. A one-row write is
1613
+ `insertRow`, `updateRow`, `moveRow`, `deleteRow` or `saveSetting`;
1614
+ anything else is an action (§5). Prose in JSX text and a label in an
1615
+ attribute (`title="Delete from board"`) are not SQL; the same words
1616
+ alone in a string of code are, so word such a label otherwise.
1617
+ - The data door by its path: a string naming `_claw/data`,
1618
+ `data/actions/` or `data/tables/`, or `claw("data/…")`. The screens
1619
+ reach the door through its functions, which this rule and the ship
1620
+ can read.
1621
+ - An action the brief does not name with its call. For `runAction({
1622
+ name: "close_cycle" })` the brief says `` `close_cycle` `` and `select
1623
+ close_cycle(`, so the agent runs the same function:
1624
+ ```
1625
+ - `close_cycle`: the Close cycle button. Run `select close_cycle($1::jsonb)`
1626
+ with params `[{"cycle": "<id>"}]` (sql execute). It carries the open goals
1627
+ over and refuses a closed cycle. Then log `closed` on `cycles`.
1628
+ ```
1629
+ - A table a one-row write names (`source: "deals"`) that the brief does
1630
+ not name in backticks, with the one statement that does the same.
1631
+ - Brief or none: a name the code decides at run time (`runAction({ name:
1632
+ kind })`: name it in the code, so the brief can), `runAction("x")`
1633
+ (it takes one object) and a name that is not one bare, lower-case name.
1634
+
1635
+ A name or a source is read where it is written: a literal, or a `const`
1636
+ string of the same file that the call sees; a surface's `source:
1637
+ surface.source` is its entry's in `surfaces.js`. A call counts however
1638
+ the file reaches the function: imported, renamed, through `import * as`,
1639
+ with `?.(`, or through an app file that re-exports it. A one-row write
1640
+ whose table the code decides at run time (a prop) is not checked, and
1641
+ the ok line says how many. `node check/run.mjs
1642
+ --actions` prints what the screens run and write, `{"actions": […],
1643
+ "tables": […]}`, and exits 0: the platform's ship checks that every
1644
+ action is a function in the app's schema before it builds. An app with
1645
+ no `tables` key keeps the first sentence of this rule only.
1646
+
1647
+ `tables` reads every table name in the app's code when `makersclaw.json`
1648
+ says `"tables": "own"`: a surface's `source`, the `source` of every
1649
+ `lib/actions.js` and `lib/data.js` call (a `const` string of the same
1650
+ file counts, where the call sees it), and the name after `from`, `join`,
1651
+ `into` and `update` in SQL (not a column's `from`: `extract(epoch from
1652
+ t.at)`, `is distinct from`). Your tables are named bare (`deals`): the app's own schema answers
1653
+ them, and a second copy of the app lives in another. It refuses
1654
+ `public.<t>` (an app on its own schema reaches nothing there),
1655
+ `app_<x>.<t>` (a schema by name, even this app's), and `brain.<t>` or
1656
+ `brain_api.<t>` (the company brain goes through `lib/brain.js`, §5).
1657
+ `claw.` and `claw_app.` are the platform's (`claw_app.activity`, the
1658
+ activity view) and pass. An app laid before 0.7.0 has no `tables` key:
1659
+ its tables are in `public`, and this rule leaves its names alone.
1660
+ `"tables": "moved"` is refused: that is the platform's word for an old
1661
+ app whose tables its workspace moved, never a manifest's.
1662
+
1663
+ And each bare name must be a table or view the app's own schema makes,
1664
+ so none leans on something else. The list is the first found of:
1665
+ `CLAW_APP_TABLES` (the platform passes it when it ships the app, read
1666
+ from the live schema), `../install/schema.sql` (a play folder's), and
1667
+ `../../schema.sql` (a store package's), from their `create table` and
1668
+ `create view`. The names read: a surface's `source` in `surfaces.js`
1669
+ (a literal or a const), the `source` of a `lib/data.js`, `lib/door.js`
1670
+ or `lib/actions.js` call by its name (`loadRows`, `useRows`,
1671
+ `insertRow`…, either side of a ternary), and in SQL — strings joined
1672
+ with `+` read as one, handed to `query()` or starting as a statement —
1673
+ the name after `from` (and after its commas), `join`, `insert into`,
1674
+ `update … set`, a delete's or merge's `using`, and `table`, `truncate`
1675
+ or `copy`. A CTE, a function (`from unnest(…)`), a SQL string or
1676
+ comment and `pg_*` are not tables. Not read: a call through an import
1677
+ alias, options held in a variable (`loadRows(opts)`), SQL a helper takes
1678
+ that does not start as a statement, and SQL built at run time. With no
1679
+ list here, the ok line says "no table list here". The same SQL is read
1680
+ for `public.`, `app_<x>.` and `brain.` names that only it shows: after a
1681
+ comma, in a `using`, with spaces around the dot.
1682
+
1683
+ `brain` reads the `brain` block of `makersclaw.json` (§5) and every
1684
+ call of `lib/brain.js` or `lib/brain-server.js` whose kind is written in
1685
+ the code (`kind: "contact"`, or a `const` string the call sees).
1686
+ The block's `kinds`, `reads`, `writes` and `links` are arrays, and each
1687
+ kind has `kind`, `label`, `plural` and `fields`. A read's kind
1688
+ (`listRecords`, `countRecords`, `brainLists`; the `kinds` of
1689
+ `searchBrain` and `brainChanges`) is in `reads` or `writes`. A write's
1690
+ (`writeRecord`, each entry of `writeRecords`, `createList`) is in
1691
+ `writes`. A kind that only `kinds` defines is refused too: the door
1692
+ hands the app only what `reads` and `writes` name. A kind held in a
1693
+ variable is not read here; the door refuses it on the member's screen.
1694
+
1695
+ `browser` reads a browser app only (`"mode": "browser"`): its pages are
1696
+ files built once, and everything they read or write goes through the
1697
+ platform's data door. So it refuses, file and line, a `"use server"`
1698
+ module or function; an import of `next/headers`, `next/cache`,
1699
+ `next/server`, `pg`, `lib/db` or `lib/brain-server`; a `route.js` under
1700
+ `app/`; a `proxy.js` or `middleware.js` at the root (under export Next
1701
+ ignores it); an export of `dynamic`, `revalidate`, `fetchCache`,
1702
+ `runtime` or `dynamicParams`; a surface whose first line is not `"use
1703
+ client"`; and a `process.env` name other than `CLAW_BASE_PATH`,
1704
+ `CLAW_PRODUCT`, `NEXT_PUBLIC_*` and `NODE_ENV` — a prerendered page
1705
+ prints what it reads into a file anyone who opens the app can fetch.
1706
+
1023
1707
  The smoke test starts that build on a loopback port and loads every
1024
- surface; `--static` skips the build and the smoke. Then, by eye:
1708
+ surface; `--no-build` skips the build and the smoke. A browser app's smoke
1709
+ builds in a copy of the app (`~/.claw-check/<app>`), never in it, with
1710
+ no `.env`; serves the export by the platform's rules; and checks that
1711
+ `/` forwards to a surface and every `/s/<key>` answers 200 with no error
1712
+ page. That proves every page prerenders, not that a surface renders
1713
+ with data: the rows come in the browser. Then, by eye:
1025
1714
 
1026
1715
  - Renders at 360px, 768px and 1280px without horizontal scroll.
1027
1716
  - In a workspace, an empty table shows `LiveEmpty`: the work in flight,
@@ -1030,48 +1719,95 @@ surface; `--static` skips the build and the smoke. Then, by eye:
1030
1719
  - Error state shows the text from `{ error }`.
1031
1720
  - Keyboard focus visible on every control.
1032
1721
  - Numbers tabular (`data-nums`).
1033
- - Every write is a server action — `lib/actions.js`, or the app's own
1034
- `"use server"` file (§5) — never raw SQL in a surface.
1722
+ - Every write goes through `lib/actions.js` (a one-row write or
1723
+ `runAction`), or in a server app the app's own `"use server"` file
1724
+ (§5) — never raw SQL in a surface.
1035
1725
  - Every surface reachable from the nav.
1036
1726
 
1037
1727
  ## 7. Run
1038
1728
 
1039
1729
  1. `makersclaw init` laid this into the artifact's `app/` (in Makersclaw,
1040
- `artifact_scaffold {dir}` runs it and makes `public.artifact_activity`
1041
- in the workspace database). `makersclaw.json` records the version and
1042
- the blocks; `makersclaw update` moves the fixed files and blocks to a
1043
- newer version and never touches your own files.
1044
- 2. `app_database {name, dir}` → `DATABASE_URL` in `.env`. The app's role
1045
- reads and writes every table in `public`.
1046
- 3. Make the domain tables with the `sql` verb (they land in `public`).
1730
+ `artifact_scaffold {dir}` runs it). `makersclaw.json` records the
1731
+ version, the blocks, where the tables live (`"tables"`, from
1732
+ `init --tables`; §5, Your tables) and the mode (`"mode"`, from `init
1733
+ --mode`; §2). `makersclaw update` moves the fixed files and blocks
1734
+ to a newer version; it never touches your own files, and keeps every
1735
+ other key of `makersclaw.json` (`tables`, `mode`, `brain`). Only
1736
+ `makersclaw mode` changes the mode (§4). `pnpm-lock.yaml` is cut from the lock this version ships
1737
+ (by init, add and update), so every app on the version installs the
1738
+ same packages; `makersclaw install` installs from it. Never edit it
1739
+ by hand.
1740
+ 2. `app_database {name, dir}` gives the app its own schema and role.
1741
+ A server app gets `DATABASE_URL` in its environment; a browser app
1742
+ gets none, since its pages reach the tables through the door and its
1743
+ agent through `sql`. The role reads and writes the app's own tables,
1744
+ by bare name. The platform lays the activity door,
1745
+ `claw_app.activity`, with the database (§5, Activity); the app makes
1746
+ no activity table of its own.
1747
+ 3. Make the domain tables with the `sql` verb, bare
1748
+ (`create table if not exists deals (…)`): they land in the app's own
1749
+ schema. An app's first tables are often there before its first
1750
+ build, made by the chat that created it (`artifact_manage
1751
+ install_tables`): read them with `sql schema` first. An app from
1752
+ before 0.7.0 makes them in `public`, with the same bare names.
1047
1753
  4. `product_run` with this directory. While building, run it as a dev
1048
1754
  server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
1049
1755
  edit shows in the founder's live preview without a rebuild.
1050
1756
  `product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
1051
1757
  `CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID` — the last is what turns
1052
- examples off (§3a).
1758
+ examples off (§3a). A browser app previews the same way, on `next
1759
+ dev`; its surfaces read through the door there too.
1760
+ 5. `npm run check` (§6) before `surface_built` and before launch. For a
1761
+ browser app its smoke builds the export as the platform will: in a
1762
+ copy of the app (`~/.claw-check/<app>`), never in it; with no `.env`;
1763
+ served by the platform's rules. It proves every page prerenders and
1764
+ `/` forwards to a surface, not that a surface renders with data: look
1765
+ at the preview for that.
1053
1766
 
1054
1767
  Every `artifact_files` write under `app/` comes back with the console's
1055
1768
  state; a 5xx carries the log tail. Fix it before moving on — the founder
1056
1769
  is watching the preview.
1057
1770
 
1058
- Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
1059
- build never needs the database. `app/auto-refresh.jsx` re-renders the
1060
- page from the server while it is shown, so the agent's writes appear:
1061
- one refresh at a time, 5 s after the last one landed plus what that one
1062
- took (a minute at most), and only while the App tab is on screen in a
1063
- visible browser tab (hidden, it asks nothing; back, it refreshes at
1064
- once), nobody is typing (never while a sheet is open, a field they typed
1065
- in holds focus, or within 10 s of a keystroke), and the app answers
1066
- `/claw-alive` (a fixed route) with a 204.
1771
+ In a server app every page is `dynamic = "force-dynamic"`: nothing is
1772
+ prerendered, so a build never needs the database.
1773
+
1774
+ A browser app's pages are the opposite: `next build` prerenders each once
1775
+ (`output: "export"`), `/` as a page that forwards to the home surface
1776
+ (a script keeping the query, and a meta refresh) and
1777
+ `/s/<key>` as one file per surface, whose surface renders in the browser
1778
+ inside `<Suspense>`. Its theme is set by a script in `<head>` before the
1779
+ page paints: `?theme=`, then the `claw_theme` cookie, then dark. The
1780
+ platform builds it with `CLAW_BASE_PATH`, `CLAW_PRODUCT` and
1781
+ `CLAW_ARTIFACT_ID` and nothing else of the app's environment, and
1782
+ serves the files it leaves; no code of the app's runs on the workspace's
1783
+ machine.
1784
+
1785
+ `app/auto-refresh.jsx` re-renders the page from the server while it is
1786
+ shown, so the agent's writes appear: one refresh at a time, 5 s after
1787
+ the last one landed plus what that one took (a minute at most), and only
1788
+ while the App tab is on screen in a visible browser tab (hidden, it asks
1789
+ nothing; back, it refreshes at once), nobody is typing (never while a
1790
+ sheet is open, a field they typed in holds focus, or within 10 s of a
1791
+ keystroke), and the app answers `/claw-alive` (a fixed route) with a
1792
+ 204.
1793
+
1794
+ A browser app's `app/auto-refresh.jsx` keeps every one of those rules,
1795
+ but a tick does not re-render anything from a server. It asks the data
1796
+ door what changed (`_claw/data/changes`: the brain's changes in the
1797
+ app's kinds, and a stamp of the app's own tables), and only when
1798
+ something did, every hook on the page (`useRows` and the rest, the
1799
+ brain's too) reads again. A brain change shows on the next tick. A
1800
+ write the agent or a job makes to the app's own tables shows within
1801
+ about 11 s: the stamp comes from Postgres's counters, which lag.
1067
1802
 
1068
1803
  **A surface that must not be refreshed opts out with
1069
1804
  `data-claw-refresh="off"` on its root element**: one whose rows only its
1070
1805
  own writes change, or a table too big to send every few seconds. While
1071
1806
  any element on the page carries it, nothing is refreshed and nothing is
1072
1807
  probed; the surface's own writes still re-render it (`lib/actions.js`
1073
- revalidates), and so does every navigation. Set it from state to pause
1074
- only while it matters: `data-claw-refresh={importing ? "off" : undefined}`.
1808
+ revalidates; in a browser app every write reads the hooks again), and so
1809
+ does every navigation. Set it from state to pause only while it matters:
1810
+ `data-claw-refresh={importing ? "off" : undefined}`.
1075
1811
 
1076
1812
  **A page left open stops refreshing after 30 minutes with no sign of
1077
1813
  anyone** in the app — a key, the pointer, a touch, a wheel, focus. The