@terreno/mcp 57.19.2 → 57.20.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 (197) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +32 -269
  3. package/dist/bootstrap.d.ts +12 -0
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +77 -122
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/resources/overview.md +4 -2
  9. package/dist/docs/resources/patterns.md +237 -176
  10. package/dist/docs/resources/ui.md +48 -17
  11. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  12. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  13. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  14. package/dist/docs/upgrades/57.20.0.md +238 -0
  15. package/dist/docs/versioned/next/approval-rubric.md +30 -12
  16. package/dist/docs/versioned/next/explanation/README.md +5 -0
  17. package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
  18. package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
  19. package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  20. package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  21. package/dist/docs/versioned/next/explanation/authentication.md +45 -1
  22. package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
  23. package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
  24. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
  25. package/dist/docs/versioned/next/explanation/organizations.md +37 -0
  26. package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
  27. package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
  28. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  29. package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
  30. package/dist/docs/versioned/next/how-to/README.md +7 -2
  31. package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
  32. package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  33. package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  34. package/dist/docs/versioned/next/how-to/circleci.md +15 -5
  35. package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  36. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  37. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  38. package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  39. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  40. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  41. package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  42. package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  43. package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  44. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  45. package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  46. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  47. package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  48. package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  49. package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  50. package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  51. package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  52. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  53. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  54. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  55. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  56. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  57. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  58. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  59. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  60. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  61. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  62. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  63. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  64. package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  65. package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  66. package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  67. package/dist/docs/versioned/next/reference/README.md +1 -0
  68. package/dist/docs/versioned/next/reference/account-security.md +700 -0
  69. package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
  70. package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
  71. package/dist/docs/versioned/next/reference/api.md +461 -35
  72. package/dist/docs/versioned/next/reference/cli.md +1 -1
  73. package/dist/docs/versioned/next/reference/comms.md +8 -0
  74. package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
  75. package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
  76. package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
  77. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  78. package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
  79. package/dist/docs/versioned/next/reference/syncdb.md +8 -1
  80. package/dist/docs/versioned/next/reference/ui.md +1271 -27
  81. package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  82. package/dist/docs/versioned/next/tutorials/README.md +4 -0
  83. package/dist/httpApp.d.ts +14 -0
  84. package/dist/httpApp.d.ts.map +1 -0
  85. package/dist/httpApp.js +99 -0
  86. package/dist/httpApp.js.map +1 -0
  87. package/dist/index.js +8 -90
  88. package/dist/index.js.map +1 -1
  89. package/dist/local/localTools.d.ts.map +1 -1
  90. package/dist/local/localTools.js +44 -14
  91. package/dist/local/localTools.js.map +1 -1
  92. package/dist/local/metro/metroDevSession.d.ts +18 -0
  93. package/dist/local/metro/metroDevSession.d.ts.map +1 -1
  94. package/dist/local/metro/metroDevSession.js +42 -3
  95. package/dist/local/metro/metroDevSession.js.map +1 -1
  96. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  97. package/dist/local/tools/databaseQuery.js +3 -13
  98. package/dist/local/tools/databaseQuery.js.map +1 -1
  99. package/dist/local/tools/databaseSchema.d.ts.map +1 -1
  100. package/dist/local/tools/databaseSchema.js +3 -13
  101. package/dist/local/tools/databaseSchema.js.map +1 -1
  102. package/dist/local/tools/localMongo.d.ts +10 -0
  103. package/dist/local/tools/localMongo.d.ts.map +1 -0
  104. package/dist/local/tools/localMongo.js +30 -0
  105. package/dist/local/tools/localMongo.js.map +1 -0
  106. package/dist/local/tools/runtime.d.ts +18 -2
  107. package/dist/local/tools/runtime.d.ts.map +1 -1
  108. package/dist/local/tools/runtime.js +75 -20
  109. package/dist/local/tools/runtime.js.map +1 -1
  110. package/dist/local/tools/syncDb.d.ts +11 -0
  111. package/dist/local/tools/syncDb.d.ts.map +1 -1
  112. package/dist/local/tools/syncDb.js +15 -3
  113. package/dist/local/tools/syncDb.js.map +1 -1
  114. package/dist/rateLimit.d.ts +35 -0
  115. package/dist/rateLimit.d.ts.map +1 -0
  116. package/dist/rateLimit.js +112 -0
  117. package/dist/rateLimit.js.map +1 -0
  118. package/dist/tools.d.ts.map +1 -1
  119. package/dist/tools.js +42 -60
  120. package/dist/tools.js.map +1 -1
  121. package/package.json +4 -3
  122. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  123. package/src/docs/resources/overview.md +4 -2
  124. package/src/docs/resources/patterns.md +237 -176
  125. package/src/docs/resources/ui.md +48 -17
  126. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  127. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  128. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  129. package/src/docs/upgrades/57.20.0.md +238 -0
  130. package/src/docs/versioned/next/approval-rubric.md +30 -12
  131. package/src/docs/versioned/next/explanation/README.md +5 -0
  132. package/src/docs/versioned/next/explanation/account-linking.md +169 -0
  133. package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
  134. package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  135. package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  136. package/src/docs/versioned/next/explanation/authentication.md +45 -1
  137. package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
  138. package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
  139. package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
  140. package/src/docs/versioned/next/explanation/organizations.md +37 -0
  141. package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
  142. package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
  143. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  144. package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
  145. package/src/docs/versioned/next/how-to/README.md +7 -2
  146. package/src/docs/versioned/next/how-to/audit-log.md +22 -3
  147. package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  148. package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  149. package/src/docs/versioned/next/how-to/circleci.md +15 -5
  150. package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  151. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  152. package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  153. package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  154. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  155. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  156. package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  157. package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  158. package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  159. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  160. package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  161. package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  162. package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  163. package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  164. package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  165. package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  166. package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  167. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  168. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  169. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  170. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  171. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  172. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  173. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  174. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  175. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  176. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  177. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  178. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  179. package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  180. package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  181. package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  182. package/src/docs/versioned/next/reference/README.md +1 -0
  183. package/src/docs/versioned/next/reference/account-security.md +700 -0
  184. package/src/docs/versioned/next/reference/admin-backend.md +3 -1
  185. package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
  186. package/src/docs/versioned/next/reference/api.md +461 -35
  187. package/src/docs/versioned/next/reference/cli.md +1 -1
  188. package/src/docs/versioned/next/reference/comms.md +8 -0
  189. package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
  190. package/src/docs/versioned/next/reference/environment-variables.md +9 -3
  191. package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
  192. package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  193. package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
  194. package/src/docs/versioned/next/reference/syncdb.md +8 -1
  195. package/src/docs/versioned/next/reference/ui.md +1271 -27
  196. package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  197. package/src/docs/versioned/next/tutorials/README.md +4 -0
@@ -0,0 +1,173 @@
1
+ # Agent guidelines
2
+
3
+ An agent writes Terreno-shaped code only when Terreno's conventions are in its context.
4
+ Those conventions reach it through four channels. Each one has a single source of truth,
5
+ and every other copy is generated from it.
6
+
7
+ | Channel | Source of truth | How the agent gets it | Status |
8
+ | --- | --- | --- | --- |
9
+ | Hosted MCP docs | `docs/`, `website/versioned_docs/`, `mcp-server/src/docs/resources/` | `terreno_search_docs`, `terreno_get_component_docs`, `terreno://docs/*` resources | Shipped |
10
+ | Per-package guidelines | `<package>/.ai/guidelines/core.md` | Merged into the files `terreno_bootstrap_ai_rules` returns | Shipped |
11
+ | App rule files | The app's `.rulesync/rules/` | Generated per editor by `rulesync generate` | Shipped |
12
+ | Agent skills | `plugins/terreno-planning/skills/`, `plugins/terreno-scan/skills/`, `.rulesync/skills/` | `npx skills add TerrenoLabs/terreno` or a host plugin | Shipped |
13
+
14
+ The runtime side of the loop, meaning what the agent can see in a running app, is covered
15
+ in [AI development loop](ai-development-loop.md). This page covers only conventions.
16
+
17
+ ## Hosted docs and search
18
+
19
+ The hosted server (`terreno-mcp`) bundles the docs at build time. `sync-versioned-docs`
20
+ copies `docs/` as the `next` version and each `website/versioned_docs/version-*` tree as a
21
+ released version. `sync-ui-docs` copies the component type data.
22
+
23
+ `terreno_search_docs` runs BM25 over that corpus and the resource pages. It takes several
24
+ queries in one call, an optional package filter, and an optional lockstep `version`. With a
25
+ version, it answers from the nearest retained snapshot, so an app on an older release gets
26
+ that release's guidance.
27
+
28
+ Resources are the fixed entry points: `terreno://docs/overview`, `api`, `ui`, `syncdb`,
29
+ `rtk`, `patterns`, plus one page per `@terreno/ui` component. A client can read them
30
+ without a tool call.
31
+
32
+ Search is pull-based. The agent sees nothing until it asks. That is why the search tool's
33
+ own description tells the agent to call it before guessing at Terreno APIs.
34
+
35
+ ## Per-package `.ai/` guidelines
36
+
37
+ Each core package keeps a short `.ai/guidelines/core.md` next to its code, so a change to
38
+ the package and a change to its guidance land in the same PR. Two packages also carry skill
39
+ copies under `.ai/skills/`.
40
+
41
+ | Package | `.ai/` contents | In the npm tarball |
42
+ | --- | --- | --- |
43
+ | `@terreno/api` | `guidelines/core.md`, `skills/mongoose-schema-safety/` | Yes (no `files` field) |
44
+ | `@terreno/ui` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
45
+ | `@terreno/rtk` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
46
+ | `@terreno/syncdb` | `guidelines/core.md`, `skills/debug-syncdb-with-mcp/` | No (`files` is `dist` and `src` only) |
47
+ | `@terreno/admin-backend` | `guidelines/core.md` | Yes (no `files` field) |
48
+ | `@terreno/admin-frontend` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
49
+
50
+ The path that matters today runs through the MCP server, not `node_modules`.
51
+ `mcp-server/scripts/sync-package-guidelines.ts` copies every `.ai/` tree into
52
+ `mcp-server/src/docs/guidelines/<package>/`. `@terreno/mcp` publishes that directory, and
53
+ `terreno_bootstrap_ai_rules` reads it.
54
+
55
+ Nothing in Terreno reads `node_modules/@terreno/*/.ai/` in a consumer project yet; the
56
+ tarball copies are available to any tool that looks for vendor guidelines there.
57
+
58
+ Planned (from the per-package guidelines phase of the MCP parity plan, not built):
59
+
60
+ - Versioned guidelines (`.ai/guidelines/<major>/core.md`) picked by the app's installed
61
+ version. Guidelines are a single flat `core.md` per package today.
62
+ - Shipping `@terreno/syncdb`'s `.ai/` in its tarball. Its `files` field leaves it out.
63
+
64
+ ## `terreno_bootstrap_ai_rules`
65
+
66
+ This hosted tool turns the package guidelines into rule files for one app. It does not
67
+ write files. It returns their contents and instructions, and the agent writes them.
68
+
69
+ | File | Contents |
70
+ | --- | --- |
71
+ | `.rulesync/rules/00-root.md` | The one root rule, for Cursor, Claude Code, Copilot, and `AGENTS.md`: app description, code style, and the merged `core.md` of `api`, `ui`, `rtk`, `syncdb` |
72
+ | `backend/AGENTS.md`, `backend/CLAUDE.md` | Backend conventions, plus the `admin-backend` guidelines when requested |
73
+ | `frontend/AGENTS.md`, `frontend/CLAUDE.md` | Frontend conventions, the MCP servers to use, plus the `admin-frontend` guidelines when requested |
74
+ | `rulesync.jsonc` | rulesync config: `rules` feature, targets `cursor`, `claudecode`, `copilot`, `agentsmd`, `delete: true` |
75
+
76
+ `packages` narrows the merge. Pass the app's `@terreno/*` dependencies so a project without
77
+ the admin panel gets no admin guidance. Unknown ids are dropped, and an empty result falls
78
+ back to all six packages.
79
+
80
+ The root rules go through rulesync. The `backend/` and `frontend/` files do not: they are
81
+ direct output, written as-is.
82
+
83
+ `bunx create-terreno-app` writes its own `.cursorrules` and `CLAUDE.md`. The
84
+ `terreno_bootstrap` prompt tells the agent to run `create-terreno-app` first and this tool
85
+ second.
86
+
87
+ The returned instructions run `bunx rulesync@9 generate`, the major this repository pins.
88
+ A test in `mcp-server` runs `rulesync generate` on the returned files. From `00-root.md`,
89
+ rulesync writes `.cursor/rules/00-root.mdc`, `CLAUDE.md`, `.github/copilot-instructions.md`,
90
+ and `AGENTS.md`. Windsurf, Codex, and other agents read
91
+ `AGENTS.md`; rulesync 9 has no `windsurf` target. The generated `CLAUDE.md` replaces the
92
+ one `create-terreno-app` wrote, and `.cursor/rules/00-root.mdc` supersedes its
93
+ `.cursorrules`, so delete that file. The root rule contains everything both files said,
94
+ including the "Adding Features" steps.
95
+
96
+ Earlier versions of the tool also returned `.rulesync/rules/01-claudecode-root.md`, a
97
+ `localRoot` rule that rulesync 9 rejects without a Claude Code `root` rule. Delete that file
98
+ when you re-bootstrap. The root rule now covers Claude Code.
99
+
100
+ ## The `.rulesync/` pattern
101
+
102
+ This repository and the apps the tool bootstraps follow the same rule. Agent instructions
103
+ are written once under `.rulesync/`, and every editor-specific file is generated from them.
104
+
105
+ In this repository:
106
+
107
+ | Source | Generated targets |
108
+ | --- | --- |
109
+ | `.rulesync/rules/**` | `CLAUDE.md`, `.claude/rules/`, `AGENTS.md`, `.cursor/rules/`, `.github/copilot-instructions.md`, `.github/instructions/`, `.devin/rules/` |
110
+ | `.rulesync/skills/`, `.rulesync/subagents/`, `.rulesync/hooks.json` | Per-host skill, subagent, and hook directories (`.claude/`, `.cursor/`, `.github/`, `.devin/`, `.agents/`) |
111
+ | `plugins/terreno-planning/skills/`, `plugins/terreno-scan/skills/`, plus `.rulesync/skills/` | `skills/` (installable set), `plugins/terreno-claude/` and `plugins/terreno-scan-claude/` (Claude Code plugins) |
112
+
113
+ `rulesync.jsonc` maps each target to its features. `bun run rules` regenerates the rulesync
114
+ targets. `bun run skills:sync` regenerates `skills/` and the Claude plugin. The
115
+ `rulesync-check` workflow regenerates and fails on any diff, and `bun run skills:check` does
116
+ the same for skills.
117
+
118
+ The config sets `delete: true`. rulesync removes stale generated files before it writes, so
119
+ a generated file holds only what its source says. A hand edit to a generated file is lost
120
+ on the next run.
121
+
122
+ ## Extending the rules in your app
123
+
124
+ Regeneration replaces two kinds of file. rulesync replaces every generated editor file. A
125
+ re-run of `terreno_bootstrap_ai_rules` returns fresh copies of everything in its table,
126
+ including `00-root.md`, `rulesync.jsonc`, and the `backend/`/`frontend/` agent files. Anything you add to those files is lost.
127
+
128
+ `rulesync generate` runs with `delete: true`. It overwrites the root `CLAUDE.md`,
129
+ `AGENTS.md`, and `.github/copilot-instructions.md`. It deletes every file under
130
+ `.claude/rules/`, `.cursor/rules/`, `.github/instructions/`, and `.agents/memories/` that
131
+ it did not generate. Before the first `rulesync generate`, and before every later one,
132
+ move any edits to those root files and any hand-written files in those directories into
133
+ `.rulesync/rules/10-project.md` or another `.rulesync/rules/*.md` source. Otherwise they
134
+ are lost.
135
+
136
+ Keep your conventions in rulesync sources the tool never emits:
137
+
138
+ 1. Leave `00-root.md` untouched. Treat it as Terreno-owned.
139
+ 2. Add one or more files such as `.rulesync/rules/10-project.md`. Use a number above `00`
140
+ and a name the tool does not use.
141
+ 3. Scope with `globs` instead of editing `backend/AGENTS.md` or `frontend/AGENTS.md`. A rule
142
+ with `globs: ["backend/**"]` applies to backend files in every editor.
143
+ 4. Run `bunx rulesync@9 generate` and commit the sources and the output together.
144
+
145
+ ```markdown
146
+ ---
147
+ targets: ["*"]
148
+ description: "Acme backend conventions"
149
+ globs: ["backend/**"]
150
+ ---
151
+
152
+ # Acme backend
153
+
154
+ - Store money as integer cents in fields named `*Cents`.
155
+ - Every tenant-scoped model has an `orgId` index.
156
+ ```
157
+
158
+ From one source, rulesync 9 writes `.cursor/rules/10-project.mdc` (`globs`),
159
+ `.claude/rules/10-project.md` (`paths`), `.github/instructions/10-project.instructions.md`
160
+ (`applyTo`), and `.agents/memories/10-project.md` (linked from `AGENTS.md`). A re-bootstrap
161
+ followed by `rulesync generate` leaves these files in place and
162
+ replaces only the Terreno-owned ones.
163
+
164
+ If you changed `rulesync.jsonc`, for example to add a target, reapply that change after a
165
+ re-bootstrap. The tool returns the default config.
166
+
167
+ ## Related
168
+
169
+ - [AI development loop](ai-development-loop.md): what the agent sees at runtime
170
+ - [Set up Terreno MCP](../how-to/set-up-terreno-mcp.md): connect both servers to your editor
171
+ - [MCP server reference](../reference/mcp-server.md#terreno_bootstrap_ai_rules): tool parameters
172
+ - [Install agent skills](../how-to/install-agent-skills.md): the skills channel
173
+ - [Planning plugins](planning-plugins.md): how the lifecycle skills are packaged
@@ -0,0 +1,196 @@
1
+ # AI development loop
2
+
3
+ A coding agent working on a Terreno app runs the same five-stage loop on every change:
4
+ it asks how something should be done, generates the code, runs the app, observes what the
5
+ app did, and fixes what it finds. Terreno's two MCP servers supply a tool for each stage.
6
+
7
+ - **`terreno-mcp`** is the hosted server. It holds the docs, the code generators, and the
8
+ convention validators. It knows Terreno but not your app.
9
+ - **`terreno-mcp-local`** runs in your project over stdio. It reads your app's logs,
10
+ client state, and database, and drives the running app. It knows your app but not the
11
+ docs.
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ ASK["1. Ask<br/>terreno_search_docs<br/>terreno_get_component_docs<br/>application_info"]
16
+ GEN["2. Generate<br/>terreno_generate_*<br/>terreno_validate_model_schema"]
17
+ RUN["3. Run<br/>backend dev server<br/>Metro (Expo)"]
18
+ OBS["4. Observe<br/>read_logs (backend / browser / metro / app)<br/>last_error<br/>get_client_state"]
19
+ FIX["5. Fix<br/>edit, then navigate<br/>browser snapshot + screenshot"]
20
+ ASK --> GEN --> RUN --> OBS --> FIX
21
+ FIX -->|"iterate"| ASK
22
+ ```
23
+
24
+ This page covers what the agent can see and do at each stage, and where that stops. How
25
+ Terreno's conventions reach the agent in the first place is in
26
+ [Agent guidelines](agent-guidelines.md).
27
+
28
+ ## The five stages
29
+
30
+ ### 1. Ask
31
+
32
+ The agent looks up the current way to do something before it writes code.
33
+
34
+ | Tool | Server | Contributes |
35
+ | --- | --- | --- |
36
+ | `application_info` | local | The app's `@terreno/*`, Expo, React Native, and Mongoose versions, so later answers match the installed release |
37
+ | `terreno_search_docs` | hosted | BM25 search over the bundled docs, several queries per call, filtered by package and lockstep `version` |
38
+ | `terreno_get_component_docs` | hosted | The full props table for one `@terreno/ui` component |
39
+ | `terreno_get_upgrade_guide`, `terreno_search_update_notes`, `terreno_get_update_note`, `terreno_ask_update_help` | hosted | Upgrade notes for a version range, and questions against them |
40
+ | `terreno://docs/*` resources | hosted | Fixed entry pages a client can read without a tool call |
41
+
42
+ Version matters here. An agent that passes the app's version to `terreno_search_docs` gets
43
+ the guidance for the release the app is on, not for the newest one.
44
+
45
+ ### 2. Generate
46
+
47
+ The agent produces code in Terreno's shape instead of inventing it.
48
+
49
+ | Tool | Server | Contributes |
50
+ | --- | --- | --- |
51
+ | `terreno_generate_model`, `terreno_generate_route`, `terreno_generate_screen`, `terreno_generate_form_fields` | hosted | A Mongoose model with the standard plugins and types, a `modelRouter` with permissions, an Expo screen, form fields |
52
+ | `terreno_validate_model_schema` | hosted | Checks a schema against the conventions, such as a `description` on every field |
53
+ | `terreno_validate_ui_blocks` | hosted | Checks an agent reply document against the blocks catalog |
54
+ | `terreno_install_admin` | hosted | Admin panel integration snippets |
55
+ | `terreno_bootstrap_ai_rules` | hosted | Rule files for the app's editors |
56
+
57
+ The generators return file contents and instructions. They do not write files, so the agent
58
+ stays in control of where code lands. A new app is scaffolded by `create-terreno-app`, not by
59
+ an MCP tool.
60
+
61
+ ### 3. Run
62
+
63
+ The app runs the way a developer runs it: the backend dev server, and Expo's Metro dev
64
+ server for the web, simulator, or device build. There is no special agent runtime. The
65
+ local server attaches to what is already running.
66
+
67
+ Two pieces of development-only instrumentation make the running app observable:
68
+
69
+ - The backend's logger writes JSONL to `.terreno/logs/app.log`. By default this is on
70
+ whenever `NODE_ENV` is not `production`.
71
+ - On web, `installTerrenoDevConsoleLogger` (from `@terreno/syncdb`) forwards `console.error`,
72
+ `console.warn`, and uncaught errors and rejections to the backend's `POST /__terreno/browser-logs` route, which writes
73
+ `.terreno/logs/browser.log`. The route exists only in development and is never registered
74
+ in production. See [Development browser-log ingestion](../reference/api.md#development-browser-log-ingestion).
75
+
76
+ ### 4. Observe
77
+
78
+ The agent reads what the app did.
79
+
80
+ | Tool | Contributes |
81
+ | --- | --- |
82
+ | `read_logs` | One merged, time-ordered tail from four sources: `backend` (server JSONL), `browser` (web errors and warnings JSONL), `metro` (bundler events), and `app` (the Hermes console over the Chrome DevTools Protocol). Its `connection` field says which live sources are attached. |
83
+ | `last_error` | The newest error-level entry across the same sources, as the raw line. It includes a stack when the entry carries one. |
84
+ | `get_client_state` | The client data layer in the running app. It reports which `layer` it found (`rtk`, `syncdb`, both, or `none`) and returns the RTK Query cache and auth state, the SyncDB state, or both. `get_rtk_state` is a deprecated alias. |
85
+ | `get_syncdb_state`, `syncdb_snapshot` | SyncDB entities, outbox, conflicts, cursors, and debug events, plus snapshots to compare before and after a change |
86
+ | `database_schema`, `database_query` | The app's MongoDB collections and documents, through a read-only allowlist |
87
+
88
+ All of these are reads. None of them needs an opt-in.
89
+
90
+ ### 5. Fix
91
+
92
+ The agent edits code in its own editor, then reproduces the path that failed and checks the
93
+ result.
94
+
95
+ | Tool | Contributes |
96
+ | --- | --- |
97
+ | `navigate` | Moves a native app to an expo-router path over the same CDP connection the logs use |
98
+ | `browser` | Drives the web app in a headless WebView: open, click, type, scroll, then `snapshot` the rendered page and save a `screenshot` as proof |
99
+ | `evaluate` | Runs a JavaScript expression in the app's Hermes runtime |
100
+ | `syncdb_action` | Changes a live SyncDB client: mutate, flush, resync, resolve conflicts, go offline |
101
+
102
+ `evaluate`, `navigate`, `syncdb_action`, and the `browser` tool's `evaluate` action run code
103
+ or change state inside the app, so they refuse unless the local server was started with
104
+ `TERRENO_MCP_EVAL=1`. Generated editor configuration never sets it. The other `browser`
105
+ actions do not need it.
106
+
107
+ After the fix, the loop goes back to Observe. A change is done when the logs and state show
108
+ the new behavior, not when the edit compiles.
109
+
110
+ ## Tool layer versus process layer
111
+
112
+ The loop is half of how Terreno works with agents. This page covers the tool layer. The
113
+ `/terreno-*` lifecycle skills are the process layer, described in
114
+ [Planning plugins](planning-plugins.md).
115
+
116
+ | Layer | Answers | Delivered by |
117
+ | --- | --- | --- |
118
+ | **Tool** | What can the agent see and do? | MCP servers: docs search, codegen, merged logs, client state, navigation |
119
+ | **Process** | In what order should the agent work, and who checks it? | `/terreno-*`: question-first planning, TDD, fresh-context independent review, evidence gates, reactive review loop |
120
+
121
+ The two compose. The lifecycle's build and prove stages use the loop's tools to gather their
122
+ evidence, and the tools are most useful when the lifecycle decides when to call them.
123
+
124
+ ## What makes this different
125
+
126
+ The agent observes what the app actually did, not a description of it.
127
+
128
+ Without these tools, an agent learns about a failure from a person's summary, a pasted stack
129
+ trace, or its own guess about what the code should do. With them, it reads the 500 from the
130
+ server log, the uncaught error from the browser console, and the cache entry the screen
131
+ rendered from. It reads them from the same run it just changed.
132
+
133
+ The rest is table stakes. Docs search, generators, and rule files exist for other frameworks
134
+ too, and Terreno does not claim otherwise.
135
+
136
+ ## What the agent cannot see
137
+
138
+ The loop ends at the edge of the local development session, and some sources have gaps
139
+ inside it.
140
+
141
+ - **Production.** The backend log file is off in production by default, the browser-log
142
+ route is never registered there, and no tool reads a hosted log service. Production errors
143
+ reach the agent only if a person brings them in.
144
+ - **Native crash reports.** A native crash outside the dev loop, such as an iOS crash report
145
+ or an Android tombstone, never reaches the JavaScript console, so no log source carries it.
146
+ - **Devices not attached to a dev server.** The `app` source, `navigate`, and `evaluate` reach
147
+ a runtime only through Metro's inspector. A phone running a release build, or one not
148
+ connected to Metro, is invisible.
149
+ - **The `app` source on web.** The web build has no Hermes target, so `app` returns nothing
150
+ there. On web, errors, warnings, and uncaught exceptions arrive through the `browser` source
151
+ instead; `console.log` and `console.info` reach no source. `app` needs a native
152
+ development build or Expo Go attached to Metro.
153
+ - **Metro events under Expo CLI.** The `metro` source reads Metro's `/events` socket. In Expo
154
+ CLI 57 that socket accepts connections but never sends events, so `metro` stays empty even
155
+ when its status reads connected.
156
+ - **The Hermes debugger connection.** The local server holds one CDP session to the app. On a
157
+ default `expo start` with current React Native, that session and React Native DevTools
158
+ coexist. On a target that allows a single debugger, or when Expo runs headless, opening
159
+ DevTools disconnects the local server. The next call that reads the app reconnects and
160
+ disconnects DevTools in turn, and console entries logged in between are lost. The
161
+ `connection` field reports the close reason.
162
+ - **Missing stacks.** `last_error` returns the line as it was logged. If the code logged a
163
+ message without a stack, there is no stack to return.
164
+
165
+ ## How this composes
166
+
167
+ Terreno does not build its own simulator or browser automation. It composes with three tools
168
+ that already do it well, and keeps to the part they lack: reading the app's logs, client
169
+ state, and data.
170
+
171
+ | Tool | Use it for | Lives |
172
+ | --- | --- | --- |
173
+ | `expo-mcp` | Interacting with the iOS simulator or Android emulator: screenshots and taps on the native app | Agent session |
174
+ | Playwright MCP | Interacting with the web app in a full browser while working | Agent session |
175
+ | Maestro | Flows worth keeping, such as login, signup, and creating a todo, committed and run in CI | Repository |
176
+
177
+ The division of labor:
178
+
179
+ - **Interaction is the composed servers' job.** `expo-mcp` and Playwright MCP move the app
180
+ and show what is on screen. Terreno's `browser` tool covers the same ground on web when a
181
+ lightweight headless session is enough.
182
+ - **Observation is Terreno's job.** After a tap or a click, `read_logs`, `last_error`, and
183
+ `get_client_state` show what the app did behind the screen.
184
+ - **Durability is Maestro's job.** An agent session ends. A flow that proved a fix and should
185
+ keep proving it becomes a Maestro flow, so CI repeats the check on every change.
186
+
187
+ `create-terreno-app` writes `expo` and `playwright` entries next to the two Terreno servers
188
+ in the app's `.cursor/mcp.json` and `.mcp.json`, so a new app starts with all four.
189
+
190
+ ## Related
191
+
192
+ - [Set up Terreno MCP](../how-to/set-up-terreno-mcp.md): connect the servers to your editor
193
+ - [Debug a Terreno app with MCP](../how-to/debug-with-mcp.md): the loop on a real failure
194
+ - [MCP server reference](../reference/mcp-server.md): every tool, prompt, resource, and variable
195
+ - [Agent guidelines](agent-guidelines.md): how Terreno's conventions reach the agent
196
+ - [Planning plugins](planning-plugins.md): the process layer
@@ -37,11 +37,55 @@ Modern session-based authentication with built-in social OAuth support.
37
37
  3. Backend redirects to OAuth provider or validates credentials
38
38
  4. Better Auth creates session in MongoDB
39
39
  5. Frontend receives a session cookie (web, default), a bearer session token (native), or, on web with `webAuth: "bearer"`, a bearer token from the `set-auth-token` header. Bearer on web is the option when the web app and the API are on different sites, where browsers block the third-party session cookie. See [Frontend and API on different sites](../how-to/configure-better-auth.md#frontend-and-api-on-different-sites)
40
- 6. On the first authenticated request or socket handshake, Terreno looks up the app `User` by `betterAuthId` and creates it if missing (`syncBetterAuthUser`). An email alone does not prove someone owns an existing account, so an existing app `User` with the same email is linked only after Better Auth verifies that email, and never when the row already belongs to another Better Auth account. Linking removes a legacy password the row never proved with a verified email. `RealtimeApp` uses the registered `BetterAuthApp` instance for socket sessions without extra wiring
40
+ 6. As the session is created, Terreno looks up the app `User` by `betterAuthId` and creates it if missing (`syncBetterAuthUser`), recording the OAuth provider in `oauthProvider`. An email alone does not prove someone owns an existing account, so a Better Auth user claims an existing `User` with the same email only when Better Auth verified that email or its sign-up password matches that `User`'s passport hash; otherwise the sign-up answers 409 "Verify your email to use this account" before any Better Auth user is created. A `User` that already belongs to another Better Auth account is never relinked (409 `ACCOUNT_ALREADY_EXISTS`). Linking through a verified email removes a legacy password the row never proved with a verified email, and bumps `tokenEpoch`. See [Account linking](account-linking.md). The first authenticated request or socket handshake retries the sync if it did not run. `RealtimeApp` uses the registered `BetterAuthApp` instance for socket sessions without extra wiring
41
41
  7. `req.user` is that app document for subsequent `modelRouter` permissions. A session whose app `User` has `disabled: true` no longer authenticates: `req.user` stays unset, so protected routes answer 401 and disabling an account ends its live sessions. Better Auth's own routes, such as sign-out, still work
42
42
 
43
43
  Email/password create omits `oauthProvider`. Apps that never use social login can keep `strict: "throw"` without declaring that field. OAuth create sets `oauthProvider` and the User schema must declare it.
44
44
 
45
+ #### Challenges without cookies
46
+
47
+ Bearer mode sends no cookies, but Better Auth keeps three short-lived values in signed cookies:
48
+ the two-factor challenge set when a sign-in needs a second factor, the trust-device token set
49
+ when the user ticks "trust this device", and the passkey challenge set by the WebAuthn options
50
+ endpoints. A cross-site bearer client never sends them back, so without help it could not finish
51
+ a 2FA or passkey sign-in.
52
+
53
+ When `twoFactor` or `passkey` is configured, Terreno adds a plugin that carries those cookies in
54
+ the `X-Terreno-Auth-Challenge` header instead:
55
+
56
+ 1. A bearer client sends the header on every `{basePath}/two-factor/*`, `/passkey/*`, and
57
+ `/sign-in/*` call. It sends `-` when it holds nothing, which opts it in.
58
+ 2. For a request that sent the header, the response copies each challenge cookie it sets into
59
+ the header as `name=value` pairs (URL search params), still signed with the auth secret. Only
60
+ values that response sets are copied, never cookies the request already had. An expired
61
+ cookie comes back with an empty value. A `rememberMe: false` sign-in's `dont_remember` cookie
62
+ rides along with the two-factor challenge, so the verified session stays short. The header is
63
+ added to `Access-Control-Expose-Headers`, the same way `bearer()` exposes `set-auth-token`.
64
+ A sign-in answered with a two-factor challenge loses its `set-auth-token`, which named the
65
+ session two-factor had just deleted.
66
+ 3. On those paths the server turns the header back into cookies before Better Auth reads them.
67
+ A real cookie always wins, and only the challenge cookies (plus `dont_remember`) are accepted. The header can
68
+ never set a session cookie, and a tampered value fails Better Auth's signature check (the 2FA
69
+ verify answers 401, a passkey verify 400 `CHALLENGE_NOT_FOUND`, and a tampered trust value
70
+ just asks for the second factor).
71
+ 4. rtk's bearer mode stores the header under `${storagePrefix}_auth_challenge` in the same
72
+ `bearerStorage` as the bearer token. It never stores a token from a response that asks for a
73
+ second factor. It drops the 2FA challenge and `dont_remember` after a successful verify, and
74
+ everything but the trust-device value on sign-out. The trust-device value stays until the server
75
+ expires it or `trustDeviceDays` runs out, so the device stays trusted across sign-outs.
76
+
77
+ Clients that do not send the header (cookie mode, native) never receive it, so their challenge
78
+ cookies stay httpOnly. That holds only for requests the page's own code does not send with the
79
+ header: a script on the page that adds it to a sign-in or verify call can read the challenge
80
+ values that call's response sets.
81
+
82
+ Exposure: in bearer mode the trust-device token lives in web storage beside the bearer token.
83
+ Any XSS on the frontend's origin can read both, and a stolen trust value skips the second factor
84
+ for that account until it expires (it is still useless without the password). Apps that handle
85
+ PHI should pass `bearerStorage: window.sessionStorage`, which ends trust with the tab, and serve
86
+ a strict Content-Security-Policy. See
87
+ [Frontend and API on different sites](../how-to/configure-better-auth.md#frontend-and-api-on-different-sites).
88
+
45
89
  **Key properties:**
46
90
  - Session-based (cookies / bearer session) vs. stateless JWT
47
91
  - A session cookie is sent with requests other sites start, so a cookie-authenticated write counts only when it comes from a trusted origin. Bearer tokens are never sent automatically, so they need no such check
@@ -0,0 +1,105 @@
1
+ # Consent forms
2
+
3
+ `ConsentApp` gives an app versioned consent forms (terms, privacy, HIPAA, research, and
4
+ custom agreements), a record of every response, and a gate that keeps a user out of the app
5
+ until they have answered every form that applies to them.
6
+
7
+ ## Forms and versions
8
+
9
+ A `ConsentForm` has a `slug`, a `version`, localized markdown `content`, and options such as
10
+ required checkboxes, a drawn signature, and whether the user may decline. Publishing
11
+ (`POST /consent-forms/:id/publish`) copies the form to `version + 1`, activates the copy, and
12
+ deactivates every other version of the same slug. Only active forms are shown to users. Publish a new version rather than editing
13
+ a form users have already answered, so each response still points at the text the user saw.
14
+
15
+ An app can narrow which active forms apply to a user with `resolveConsentForms(user, forms)`,
16
+ for example to show a research consent only to enrolled members. Every consent endpoint that
17
+ asks "which forms apply to this user" runs the same resolver.
18
+
19
+ ## Responses
20
+
21
+ A `ConsentResponse` records one answer to one form version: `agreed`, `agreedAt`, the locale
22
+ shown, checkbox values, an optional signature, and `formVersionSnapshot`. With
23
+ `auditTrail: true`, it also keeps the user's IP address and user agent and a snapshot of the
24
+ exact content they saw. Responses are append-only. A user who answers again gets a new
25
+ record, so the history of what they agreed to, and when, is never rewritten.
26
+
27
+ ## The gate
28
+
29
+ `GET /consents/pending` returns the forms that apply to the user and that the user has not
30
+ answered at the form's current version. The client (`ConsentNavigator` in `@terreno/ui`)
31
+ shows them one at a time until the list is empty. A decline also clears the form from the
32
+ list: the user has answered it.
33
+
34
+ When a form is republished, every user's earlier answer is for an older version, so the new
35
+ version reappears in their gate.
36
+
37
+ ## Provisional signatures
38
+
39
+ Some consent is given before the user ever opens the app. A guardian may agree on the phone
40
+ at intake, or staff may record verbal consent during a visit. For compliance the form must
41
+ count as signed from that moment, but the user should still read the form and sign it
42
+ themselves.
43
+
44
+ A provisional signature is a `ConsentResponse` with `provisional: true`. Staff create it with
45
+ `POST /consent-responses/provisional`, naming the target user, the form, and a required note
46
+ that explains how consent was obtained. The response records:
47
+
48
+ - `agreed: true` and `agreedAt` (server time)
49
+ - `provisionalSignedBy`: the staff member who recorded it
50
+ - `provisionalNote`: the note (trimmed, at most 2000 characters)
51
+ - `formVersionSnapshot`, plus `contentSnapshot`, `provisionalSignerIp`, and
52
+ `provisionalSignerUserAgent` when `auditTrail` is on
53
+
54
+ `ipAddress`, `userAgent`, `signature`, and `signedAt` stay empty. Those fields describe the
55
+ user's own device and signature, and the user did not sign.
56
+
57
+ ### Why a provisional signature does not clear the gate
58
+
59
+ Only the user's own responses remove a form from `/consents/pending`. A provisional signature
60
+ is a record that someone else vouched for the user's consent. It is not the user's consent in
61
+ their own words. Keeping the form in the gate means the user still meets it, reads it, and
62
+ agrees or declines. The gate tells them who signed on their behalf and when, but not the
63
+ signer's note: that stays in the admin audit history. The user's own answer is stored as a separate response, so both records
64
+ stay for audit.
65
+
66
+ A separate `ProvisionalConsent` model was rejected. A field on `ConsentResponse` keeps one
67
+ audit trail per user and reuses the existing admin screens, populate paths, and PDF code.
68
+
69
+ ### Voiding a provisional signature
70
+
71
+ A provisional signature recorded in error (the wrong member, the wrong form) is voided with
72
+ `POST /consent-responses/:id/void` and a required reason. Voiding sets `voidedAt`,
73
+ `voidedBy`, and `voidReason`; it never deletes the record, so the audit history shows both
74
+ the signature and its void. A voided signature no longer counts as consent, and the user's
75
+ gate no longer mentions it. Only provisional signatures can be voided: the user's own
76
+ answers are theirs to change by answering again.
77
+
78
+ ### What counts as signed
79
+
80
+ `getConsentStatuses` (and `GET /consent-responses/status`) is the "considered signed"
81
+ contract. For each form that applies to the user, only non-voided responses to the current
82
+ version count. The user's own latest answer always wins: agreeing makes the form `signed`, declining
83
+ makes it `declined`. Without one, a provisional signature makes it `provisional`, which
84
+ counts as consented. Otherwise it is `pending`. App code that needs to know whether a user
85
+ may proceed (for example before scheduling a visit) checks `isConsented` on these rows
86
+ instead of reading responses directly.
87
+
88
+ ### Who may sign for another user
89
+
90
+ Pass `accessControl` to `ConsentApp` to use RBAC. Any authenticated user whose roles grant
91
+ `consentResponse:signOnBehalf` may then sign for others. The built-in `admin` role includes
92
+ that action, and existing unsealed admin roles gain it the next time roles are seeded. The
93
+ merge runs on every seed, so removing `signOnBehalf` from the `admin` role does not stick.
94
+ To keep admins from signing, give them a custom role instead of `admin`.
95
+ Without `accessControl`, only users with `admin: true` may sign, and `ConsentApp` logs a
96
+ warning at register.
97
+
98
+ There is no relationship check: a permitted signer may sign for any user. A signer cannot
99
+ sign for themselves, and the form must be active and must apply to the target user under
100
+ `resolveConsentForms`. The same permission covers reading another user's consent statuses.
101
+ When `AuditApp` is registered, every provisional signature, void, and status read writes an
102
+ `AuditEvent` naming the actor and the target user. Audit events gain a `read` operation and
103
+ a `viewed` verb for these reads.
104
+
105
+ Reference: [ConsentApp](../reference/api.md#consent-forms-consentapp).
@@ -27,6 +27,7 @@ that file (and its imports) to a working example. Gaps stay explicit.
27
27
  | File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts`, `file-uploads` flag in `example-backend/src/api/fileUploads.ts` | yes — `example-frontend/app/(tabs)/documents.tsx` (Files section, every signed-in user, 1 upload / IP / min), `example-frontend/app/(tabs)/ai.tsx`, `example-frontend/app/gcs-settings.tsx` | The `file-uploads` feature flag hides upload controls and rejects uploads when off. A missing flag leaves uploads enabled. Deployed bucket is terraform-managed (`google_storage_bucket.example_documents`). |
28
28
  | Better Auth | yes — `example-backend/src/utils/betterAuthConfig.ts`, `BetterAuthApp` in `example-backend/src/server.ts` | yes — `example-frontend/app/login.tsx`, `example-frontend/lib/betterAuth.ts` | Default `AUTH_PROVIDER` is `better-auth`. |
29
29
  | syncdb local-first | yes — `SyncApp` in `example-backend/src/server.ts` | yes — `example-frontend/store/syncdb.ts`, `example-frontend/app/(tabs)/index.tsx` | PR #869 closed without merge; `@terreno/syncdb` shipped on the launch line and is the example todos path. |
30
+ | List/detail layout | n/a — reuses the synced todos | yes — `example-frontend/app/todo-browser/` (`ListDetailLayout` group, opened from the todos screen's Browse button), list in `example-frontend/components/TodoBrowserList.tsx` | Phones push each todo; at 768px and wider the list stays on the left. e2e: `example-frontend/e2e/todo-browser.spec.ts` (phone, tablet, deep link, resize, keyboard). The demo inbox at `demo/app/demo/list-detail/` shows the same layout with static data. Steps: [build-list-detail-screens.md](../how-to/build-list-detail-screens.md). |
30
31
  | SyncDB diagnostics | n/a | yes — `debug` registration in `example-frontend/store/syncdb.ts`, debugger UI at `example-frontend/app/syncdb-debug.tsx` | `terreno-mcp-local` can inspect, snapshot/compare/merge, mutate, flush, reconcile/resync, and exercise offline/conflict paths against the running development client. |
31
32
  | RBAC | yes — `example-backend/src/rbacRoles.ts` (including optional `aiObservabilityViewer` / `aiObservabilityOperator` roles), `access.ts`, `accessControl: access` on `ObservabilityApp` / `AdminApp`, `access: {resource: "todo"}` on the todo router | yes — `example-frontend/app/admin/roles.tsx` | Shipped. Plan: [rbac-permissions.md](../implementationPlans/rbac-permissions.md). |
32
33
  | Organizations | yes — `createAccess({organizations: true})`, `organizationSettingsSchema`, `orgScopedPlugin` on projects | yes — admin org directory/settings at `example-frontend/app/admin/orgs/` | Settings live on `Organization.settings`; example registers a nested timezone schema. |
@@ -35,7 +35,7 @@ Terreno 57.
35
35
  ## What you start doing
36
36
 
37
37
  1. **Conflict resolution UI** — When the server rejects a mutation (409 / `sync:nack`), the user must choose a strategy (`useServer` or `keepMine`). Use `ConflictSheet` from `@terreno/ui` or build your own with `useConflicts`.
38
- 2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`).
38
+ 2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`, which waits `offlineGraceMs` — 3s by default — before showing "Offline" so startup and brief socket drops do not flash it).
39
39
  3. **Encryption at rest (web)** — Web persistence uses AES-GCM via the default `createServerKeyProvider`; understand key fetch and `onDecryptFailure` for support flows.
40
40
  4. **Key lifecycle on logout** — Call `syncDb.stop()` and consider `wipeLocalData` when the authenticated user changes so one account does not read another's local store.
41
41
  5. **Backend sync registration** — Every synced collection needs `syncPlugin`, `isDeletedPlugin`, and a `sync` block on `modelRouter`, plus `SyncApp` and `RealtimeApp` on the server. Optional `sync.adminBroadcast: true` also emits `sync:delta` to `{collection}|admin`. Admin clients subscribe with `mode: "window"` (`createSyncDb({windowCollections})`) so they do not snapshot or reconcile that collection; they call `hydrateWindow` (REST membership ids + `GET /sync/entities`). Pass the same `accessControl` into `SyncApp` that `AdminApp` uses so `{collection}|admin` requires `admin:access`. `AdminApp.register` also installs that model's admin list/read/`queryFilter` on the admin window so hydrate and live `|admin` deltas match `/admin` REST. Without RBAC, `user.admin` is enough for the shell gate. Live deltas on that stream apply only to ids already in the window; Refresh is a REST re-query plus `hydrateWindow`.
@@ -47,6 +47,13 @@ Tenant isolation uses multiple checks rather than one convention:
47
47
  admin models whose schema contains `organizationId`. Models without that path
48
48
  remain platform-scoped.
49
49
 
50
+ Adding, changing, or removing a membership through `POST` / `PATCH` / `DELETE
51
+ /orgs/:id/members` clears that user's cached permissions, so a new org-admin can open the
52
+ admin page (and a demoted or removed one loses it) on their next request instead of after
53
+ the permission cache's 30-second TTL. Disabling (`PATCH /orgs/:id` with `disabled: true`) or
54
+ deleting (`DELETE /orgs/:id`) an organization suspends all of its memberships and clears the
55
+ cached permissions of every member the same way.
56
+
50
57
  ## Disabled organizations
51
58
 
52
59
  When `Organization.disabled` is `true`, tenant context and membership mutations
@@ -64,6 +71,35 @@ stop, but operators can still manage the organization record itself:
64
71
  Disabling or deleting an organization suspends its memberships. Re-enabling does
65
72
  not automatically reactivate them.
66
73
 
74
+ ## Member recovery
75
+
76
+ When the app registers `AccountSecurityApp`, an org-admin can help a locked-out member without
77
+ platform staff. `OrgsApp` mounts the same recovery handlers the platform uses at
78
+ `/orgs/:orgId/members/:userId/security/*`, behind an org authorizer.
79
+
80
+ - **Reach.** An org-admin may act on any active member of their org, even one who also belongs
81
+ to other orgs (D2). A suspended member, or a user outside the org, answers `404`, so the routes
82
+ do not reveal who exists elsewhere.
83
+ - **Guards.** Platform staff (`superadmin`, `operator`) and `admin: true` users are never reachable
84
+ through an org (D3). Neither is anyone who holds a permission the org-admin lacks, nor the
85
+ org-admin themselves (D9). Disabled orgs answer `403`.
86
+ - **Disable means suspend.** One org must not lock a user out of every other org, so the org
87
+ path has no account disable. It offers `suspend-membership` instead, which suspends the
88
+ membership in that org only (Q8). The user still signs in and keeps their other orgs.
89
+ `User.disabled` and `issue-sign-in-link` stay platform-only.
90
+ - **Attach-then-act.** An org-admin can attach any existing user by email
91
+ (`POST /orgs/:id/members`). Every org-admin action is audited with the org's id and emails the
92
+ user, so they learn about it ([notifications](../reference/account-security.md#notifications);
93
+ the org is named only with `notifications.includeOrgName`). Apps that want more can set
94
+ `orgRecovery.minMembershipDays` to refuse members who joined too recently.
95
+ - **Platform staff** holding `user:manageSecurity` pass the org checks. The target must still be
96
+ an active member of the org in the path. Their reach check is the platform one: both sides are
97
+ resolved outside the organization, so an org-only grant does not put the member out of reach.
98
+
99
+ See [Recover a locked-out user](../how-to/recover-locked-out-user.md) for who can do what, and
100
+ the [account security reference](../reference/account-security.md#org-admin-routes) for every
101
+ response.
102
+
67
103
  ## Why organizations are optional
68
104
 
69
105
  Automatically registering routes, roles, or Mongoose models would change
@@ -83,6 +119,7 @@ multi-tenant-ready apps.
83
119
  ## Related
84
120
 
85
121
  - [Add organizations](../how-to/add-organizations.md)
122
+ - [Recover a locked-out user](../how-to/recover-locked-out-user.md)
86
123
  - [API reference](../reference/api.md#organizations)
87
124
  - [Admin backend reference](../reference/admin-backend.md#organization-scoping)
88
125
  - [Admin frontend reference](../reference/admin-frontend.md#orgdirectoryscreen)
@@ -112,3 +112,7 @@ Install both from the marketplaces described in
112
112
  [install agent skills](../how-to/install-agent-skills.md). Claude Code resolves a plugin
113
113
  skill's command from its frontmatter `name`, which is why the two `*-claude/` trees are
114
114
  generated with shortened names by `bun run skills:sync`.
115
+
116
+ ## Related
117
+
118
+ - [AI development loop](ai-development-loop.md): the tool layer these stages use to see the running app