@terreno/mcp 57.21.0 → 58.0.0-beta.1

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 (244) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +57 -3
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +17 -16
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/prompts/debug.md +6 -4
  9. package/dist/docs/prompts/screen.md +11 -17
  10. package/dist/docs/resources/overview.md +5 -3
  11. package/dist/docs/resources/patterns.md +157 -238
  12. package/dist/docs/resources/syncdb.md +44 -16
  13. package/dist/docs/templates/bootstrap/CLAUDE.template.md +2 -2
  14. package/dist/docs/templates/bootstrap/cursorrules.template.md +7 -5
  15. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
  16. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
  17. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
  18. package/dist/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
  19. package/dist/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
  20. package/dist/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
  21. package/dist/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
  22. package/dist/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
  23. package/dist/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
  24. package/dist/docs/upgrades/58.0.0-beta.1.md +504 -0
  25. package/dist/docs/upgrades/58.0.0.md +495 -4
  26. package/dist/docs/versioned/next/README.md +2 -2
  27. package/dist/docs/versioned/next/approval-rubric.md +2 -2
  28. package/dist/docs/versioned/next/explanation/admin-interface.md +6 -5
  29. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
  30. package/dist/docs/versioned/next/explanation/ai-workflows.md +5 -3
  31. package/dist/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
  32. package/dist/docs/versioned/next/explanation/authentication.md +62 -43
  33. package/dist/docs/versioned/next/explanation/dependency-management.md +1 -1
  34. package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  35. package/dist/docs/versioned/next/explanation/example-coverage.md +2 -2
  36. package/dist/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
  37. package/dist/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
  38. package/dist/docs/versioned/next/explanation/local-first-data.md +11 -9
  39. package/dist/docs/versioned/next/explanation/model-router-actions.md +1 -1
  40. package/dist/docs/versioned/next/explanation/modular-api-design.md +2 -2
  41. package/dist/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
  42. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
  43. package/dist/docs/versioned/next/explanation/source-rules.md +2 -2
  44. package/dist/docs/versioned/next/explanation/typescript-configuration.md +2 -4
  45. package/dist/docs/versioned/next/explanation/versioning-policy.md +3 -3
  46. package/dist/docs/versioned/next/how-to/README.md +3 -2
  47. package/dist/docs/versioned/next/how-to/add-feature-flags.md +50 -25
  48. package/dist/docs/versioned/next/how-to/add-organizations.md +2 -1
  49. package/dist/docs/versioned/next/how-to/admin-custom-home.md +8 -2
  50. package/dist/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
  51. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
  52. package/dist/docs/versioned/next/how-to/build-admin-screens.md +10 -8
  53. package/dist/docs/versioned/next/how-to/build-for-web.md +1 -1
  54. package/dist/docs/versioned/next/how-to/call-external-apis.md +2 -2
  55. package/dist/docs/versioned/next/how-to/circleci.md +12 -6
  56. package/dist/docs/versioned/next/how-to/configure-better-auth.md +114 -60
  57. package/dist/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
  58. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
  59. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
  60. package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
  61. package/dist/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
  62. package/dist/docs/versioned/next/how-to/github-actions-ci.md +2 -2
  63. package/dist/docs/versioned/next/how-to/in-app-notifications.md +1 -1
  64. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
  65. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  66. package/dist/docs/versioned/next/how-to/product-announcements.md +6 -3
  67. package/dist/docs/versioned/next/how-to/rate-limiting.md +38 -16
  68. package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
  69. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -3
  70. package/dist/docs/versioned/next/how-to/set-up-social-login.md +1 -1
  71. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
  72. package/dist/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
  73. package/dist/docs/versioned/next/how-to/upgrade-banner.md +5 -3
  74. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
  75. package/dist/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
  76. package/dist/docs/versioned/next/how-to/websocket-integration.md +113 -402
  77. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  78. package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
  79. package/dist/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
  80. package/dist/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
  81. package/dist/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
  82. package/dist/docs/versioned/next/reference/README.md +1 -1
  83. package/dist/docs/versioned/next/reference/admin-frontend.md +85 -70
  84. package/dist/docs/versioned/next/reference/admin-spa.md +10 -4
  85. package/dist/docs/versioned/next/reference/ai.md +36 -4
  86. package/dist/docs/versioned/next/reference/announcements.md +3 -3
  87. package/dist/docs/versioned/next/reference/api.md +39 -24
  88. package/dist/docs/versioned/next/reference/cli.md +3 -4
  89. package/dist/docs/versioned/next/reference/environment-variables.md +16 -16
  90. package/dist/docs/versioned/next/reference/feature-flags.md +48 -7
  91. package/dist/docs/versioned/next/reference/jobs.md +2 -2
  92. package/dist/docs/versioned/next/reference/legacy/rtk.md +25 -600
  93. package/dist/docs/versioned/next/reference/mcp-server.md +441 -1158
  94. package/dist/docs/versioned/next/reference/syncdb.md +426 -16
  95. package/dist/docs/versioned/next/reference/test.md +28 -3
  96. package/dist/docs/versioned/next/reference/ui.md +132 -19
  97. package/dist/docs/versioned/next/tasks/native-module-baseline.md +8 -4
  98. package/dist/docs/versioned/next/tasks/rate-limiting.md +9 -2
  99. package/dist/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
  100. package/dist/docs/versioned/next/tasks/remove-rtk.md +155 -0
  101. package/dist/local/localTools.d.ts.map +1 -1
  102. package/dist/local/localTools.js +3 -54
  103. package/dist/local/localTools.js.map +1 -1
  104. package/dist/local/tools/runtime.d.ts +0 -21
  105. package/dist/local/tools/runtime.d.ts.map +1 -1
  106. package/dist/local/tools/runtime.js +0 -176
  107. package/dist/local/tools/runtime.js.map +1 -1
  108. package/dist/local/tools/syncDb.d.ts +0 -11
  109. package/dist/local/tools/syncDb.d.ts.map +1 -1
  110. package/dist/local/tools/syncDb.js +1 -5
  111. package/dist/local/tools/syncDb.js.map +1 -1
  112. package/dist/packageGuidelines.d.ts +3 -2
  113. package/dist/packageGuidelines.d.ts.map +1 -1
  114. package/dist/packageGuidelines.js +4 -11
  115. package/dist/packageGuidelines.js.map +1 -1
  116. package/dist/prompts.d.ts.map +1 -1
  117. package/dist/prompts.js +17 -18
  118. package/dist/prompts.js.map +1 -1
  119. package/dist/resources.d.ts.map +1 -1
  120. package/dist/resources.js +1 -8
  121. package/dist/resources.js.map +1 -1
  122. package/dist/search/chunker.d.ts +1 -1
  123. package/dist/search/chunker.d.ts.map +1 -1
  124. package/dist/search/inferPackages.d.ts +1 -1
  125. package/dist/search/inferPackages.js +5 -5
  126. package/dist/search/inferPackages.js.map +1 -1
  127. package/dist/tools.d.ts +6 -0
  128. package/dist/tools.d.ts.map +1 -1
  129. package/dist/tools.js +126 -119
  130. package/dist/tools.js.map +1 -1
  131. package/package.json +6 -6
  132. package/src/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
  133. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  134. package/src/docs/prompts/debug.md +6 -4
  135. package/src/docs/prompts/screen.md +11 -17
  136. package/src/docs/resources/overview.md +5 -3
  137. package/src/docs/resources/patterns.md +157 -238
  138. package/src/docs/resources/syncdb.md +44 -16
  139. package/src/docs/templates/bootstrap/CLAUDE.template.md +2 -2
  140. package/src/docs/templates/bootstrap/cursorrules.template.md +7 -5
  141. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
  142. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
  143. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
  144. package/src/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
  145. package/src/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
  146. package/src/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
  147. package/src/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
  148. package/src/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
  149. package/src/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
  150. package/src/docs/upgrades/58.0.0-beta.1.md +504 -0
  151. package/src/docs/upgrades/58.0.0.md +495 -4
  152. package/src/docs/versioned/next/README.md +2 -2
  153. package/src/docs/versioned/next/approval-rubric.md +2 -2
  154. package/src/docs/versioned/next/explanation/admin-interface.md +6 -5
  155. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
  156. package/src/docs/versioned/next/explanation/ai-workflows.md +5 -3
  157. package/src/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
  158. package/src/docs/versioned/next/explanation/authentication.md +62 -43
  159. package/src/docs/versioned/next/explanation/dependency-management.md +1 -1
  160. package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  161. package/src/docs/versioned/next/explanation/example-coverage.md +2 -2
  162. package/src/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
  163. package/src/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
  164. package/src/docs/versioned/next/explanation/local-first-data.md +11 -9
  165. package/src/docs/versioned/next/explanation/model-router-actions.md +1 -1
  166. package/src/docs/versioned/next/explanation/modular-api-design.md +2 -2
  167. package/src/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
  168. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
  169. package/src/docs/versioned/next/explanation/source-rules.md +2 -2
  170. package/src/docs/versioned/next/explanation/typescript-configuration.md +2 -4
  171. package/src/docs/versioned/next/explanation/versioning-policy.md +3 -3
  172. package/src/docs/versioned/next/how-to/README.md +3 -2
  173. package/src/docs/versioned/next/how-to/add-feature-flags.md +50 -25
  174. package/src/docs/versioned/next/how-to/add-organizations.md +2 -1
  175. package/src/docs/versioned/next/how-to/admin-custom-home.md +8 -2
  176. package/src/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
  177. package/src/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
  178. package/src/docs/versioned/next/how-to/build-admin-screens.md +10 -8
  179. package/src/docs/versioned/next/how-to/build-for-web.md +1 -1
  180. package/src/docs/versioned/next/how-to/call-external-apis.md +2 -2
  181. package/src/docs/versioned/next/how-to/circleci.md +12 -6
  182. package/src/docs/versioned/next/how-to/configure-better-auth.md +114 -60
  183. package/src/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
  184. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
  185. package/src/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
  186. package/src/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
  187. package/src/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
  188. package/src/docs/versioned/next/how-to/github-actions-ci.md +2 -2
  189. package/src/docs/versioned/next/how-to/in-app-notifications.md +1 -1
  190. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
  191. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  192. package/src/docs/versioned/next/how-to/product-announcements.md +6 -3
  193. package/src/docs/versioned/next/how-to/rate-limiting.md +38 -16
  194. package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
  195. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -3
  196. package/src/docs/versioned/next/how-to/set-up-social-login.md +1 -1
  197. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
  198. package/src/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
  199. package/src/docs/versioned/next/how-to/upgrade-banner.md +5 -3
  200. package/src/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
  201. package/src/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
  202. package/src/docs/versioned/next/how-to/websocket-integration.md +113 -402
  203. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  204. package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
  205. package/src/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
  206. package/src/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
  207. package/src/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
  208. package/src/docs/versioned/next/reference/README.md +1 -1
  209. package/src/docs/versioned/next/reference/admin-frontend.md +85 -70
  210. package/src/docs/versioned/next/reference/admin-spa.md +10 -4
  211. package/src/docs/versioned/next/reference/ai.md +36 -4
  212. package/src/docs/versioned/next/reference/announcements.md +3 -3
  213. package/src/docs/versioned/next/reference/api.md +39 -24
  214. package/src/docs/versioned/next/reference/cli.md +3 -4
  215. package/src/docs/versioned/next/reference/environment-variables.md +16 -16
  216. package/src/docs/versioned/next/reference/feature-flags.md +48 -7
  217. package/src/docs/versioned/next/reference/jobs.md +2 -2
  218. package/src/docs/versioned/next/reference/legacy/rtk.md +25 -600
  219. package/src/docs/versioned/next/reference/mcp-server.md +441 -1158
  220. package/src/docs/versioned/next/reference/syncdb.md +426 -16
  221. package/src/docs/versioned/next/reference/test.md +28 -3
  222. package/src/docs/versioned/next/reference/ui.md +132 -19
  223. package/src/docs/versioned/next/tasks/native-module-baseline.md +8 -4
  224. package/src/docs/versioned/next/tasks/rate-limiting.md +9 -2
  225. package/src/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
  226. package/src/docs/versioned/next/tasks/remove-rtk.md +155 -0
  227. package/dist/docs/guidelines/rtk/guidelines/core.md +0 -26
  228. package/dist/docs/resources/rtk.md +0 -181
  229. package/dist/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
  230. package/dist/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
  231. package/dist/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
  232. package/dist/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
  233. package/dist/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
  234. package/dist/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
  235. package/dist/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
  236. package/src/docs/guidelines/rtk/guidelines/core.md +0 -26
  237. package/src/docs/resources/rtk.md +0 -181
  238. package/src/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
  239. package/src/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
  240. package/src/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
  241. package/src/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
  242. package/src/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
  243. package/src/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
  244. package/src/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
@@ -96,7 +96,8 @@ src/
96
96
  `getMCPTools`, `runBufferedChatTurn` (one chat turn to completion without HTTP: the same system
97
97
  prompt, tools, and block checks as `/gpt/histories/:id/turn`; takes `{body: {historyId, prompt},
98
98
  options, req}` where `req` carries `user` and `headers`)
99
- - **Models:** `AIRequest`, `GptHistory`, `FileAttachment`, `Project` (lazy; prefer `getProjectModel()`)
99
+ - **Models:** `AIRequest`, `AIDataset`, `GptHistory`, `FileAttachment`, `Project` (lazy; prefer `getProjectModel()`)
100
+ - **Migrations:** `gptHistoryStringIdsMigration` (`{slug, up}` for an app migration file), `migrateGptHistoryStringIds`, `GPT_HISTORY_STRING_IDS_MIGRATION`, type `GptHistoryStringIdsResult` ([GptHistory String ids](#gpthistory-string-ids))
100
101
  - **Routes:** `addGptRoutes`, `addGptHistoryRoutes`, `addAiRequestsExplorerRoutes`, `addFileRoutes`, `addProjectRoutes`, `addMcpRoutes`
101
102
  - **Structured output:** `parseAiJson`, `normalizeLlmJsonTextForStructuredOutput`, re-exported `Output`, `jsonSchema`, `JSONValue`, `FlexibleSchema` from `ai`
102
103
  - **Langfuse:** `initLangfuseClient`, `getLangfuseClient`, `shutdownLangfuseClient`, `compilePrompt`, `createPrompt`, `getPrompt`, `createTelemetryConfig`, `preparePromptForAI`, `initTracing`, `shutdownTracing`, `LangfuseCache`, cache helpers
@@ -224,10 +225,12 @@ Logs all AI calls for monitoring and admin explorer.
224
225
 
225
226
  ### GptHistory
226
227
 
227
- Conversation history with multi-modal prompts.
228
+ Conversation history with multi-modal prompts. A synced collection: `/gpt/histories` streams each
229
+ user's histories to `@terreno/syncdb` clients (see [addGptHistoryRoutes](#addgpthistoryroutesrouter-options)).
228
230
 
229
231
  | Field | Type | Description |
230
232
  |-------|------|-------------|
233
+ | `_id` | string | A UUID for new histories, so offline sync clients can mint ids. Histories saved before 58 keep their old ObjectId as a hex string after the [migration](#gpthistory-string-ids) |
231
234
  | `userId` | ObjectId | Owner (required) |
232
235
  | `title` | string? | Auto-generated on the first chat turn's reply (`/gpt/prompt` or `turn`) when empty |
233
236
  | `projectId` | ObjectId? | Optional project association |
@@ -238,6 +241,30 @@ Assistant replies from `/gpt/prompt` carry `streamId` and `status`: `streaming`
238
241
 
239
242
  **Virtual:** `ownerId` aliases `userId` for `Permissions.IsOwner`.
240
243
 
244
+ **Plugins:** `createdUpdatedPlugin`, `isDeletedPlugin`, `findOneOrNone`, `findExactlyOne`, `syncPlugin`. `syncPlugin` refuses `GptHistory.deleteMany`, `updateMany`, hard deletes, upserts, and query writes whose filter has no `_id`. Write one history at a time by `_id`; in tests, clear the collection with `GptHistory.collection.deleteMany({})`.
245
+
246
+ `AIDataset.historyId` is the history's String `_id`. `registerAiDataset` still accepts an ObjectId and stores its hex string.
247
+
248
+ #### GptHistory String ids
249
+
250
+ Terreno 58 changed `GptHistory._id` from ObjectId to String. Histories saved before the upgrade still have ObjectId `_id`s, which the String schema cannot find. The `gpt-history-string-ids` migration moves them. Add it to the app's `migrations/` directory and apply it with `terreno-migrate` before deploying 58 frontends:
251
+
252
+ ```typescript
253
+ // migrations/20261022130000-gpt-history-string-ids.ts
254
+ import {gptHistoryStringIdsMigration} from "@terreno/ai";
255
+
256
+ export const id = "20261022130000-gpt-history-string-ids";
257
+ export const up = gptHistoryStringIdsMigration.up;
258
+ ```
259
+
260
+ | Step | Effect |
261
+ |------|--------|
262
+ | 1 | Copy each ObjectId history (500 per batch) to `_id: oldId.toHexString()`, keeping every stored field. URLs and clients that hold the hex id keep working |
263
+ | 2 | Rewrite `AIDataset.historyId` refs from the ObjectId to the hex string, including refs to histories that no longer exist |
264
+ | 3 | Delete the ObjectId originals |
265
+
266
+ A dry run (`terreno-migrate up --dry`) only counts the histories and refs it would change. The migration writes through the MongoDB driver, so it does not stamp sync seqs; moved histories reach sync clients as unstamped legacy rows on the next snapshot. Running it again changes nothing, and a run stopped partway finishes on the next run. It has no `down`: new histories get UUIDs, which have no ObjectId to go back to. `migrateGptHistoryStringIds({connection, dryRun, batchSize?, logger?, checkCancellation?})` runs the same steps without a migration file and returns `{historiesMigrated, datasetsUpdated}`.
267
+
241
268
  ### FileAttachment
242
269
 
243
270
  Metadata for files stored in GCS.
@@ -412,6 +439,8 @@ CRUD at `/gpt/histories` via `modelRouter`:
412
439
  | Create, List | `IsAuthenticated` |
413
440
  | Read, Update, Delete | `IsOwner` |
414
441
 
442
+ Registered with the three-argument `modelRouter("/gpt/histories", GptHistory, {sync: {scope: {type: "owner", field: "userId"}}})`, so `SyncApp` serves the histories as the sync collection `gpt/histories` on each user's stream `gpt/histories|owner:{userId}`. A client reads them with `useQuery("gpt/histories")` and renames or deletes them with `useMutate("gpt/histories")`; sync payloads go through the same response handler as REST, so they also leave out `pendingAsk.promptIndex` and `pendingAsk.responseMessages`. Register `SyncApp` (and `RealtimeApp` for live deltas) in the app. REST CRUD stays for clients that do not sync. Every history route takes the String `_id`.
443
+
415
444
  Query filtered by `userId`; sort `-updated`; query fields `userId`, `projectId`. Create sets `userId` to the caller, so `{}` is a valid create body even when the app validates request bodies. Create and update bodies drop `pendingAsk` (including dotted `pendingAsk.*` paths), so only a chat turn writes it; the OpenAPI spec marks it `readOnly` on create and update. Create and update bodies also drop any `gcsKey` on `prompts[].content[]` parts (including dotted `prompts.N.content.M.gcsKey` paths) that the history does not already store, so a client can send stored prompts back but cannot add an attachment key.
416
445
 
417
446
  Pass `chat`, the options given to `addGptRoutes`, to add two headless actions for clients that do not read server-sent events, such as a watch app. Both exist only when `chat` turns `asks` on: without `chat`, or with `asks` off, neither action exists, so a host that never turned asks on gets no new endpoints. `AiApp` adds them when its `asks` option is set.
@@ -434,6 +463,9 @@ Bodies, results, and errors: [Headless endpoints](agent-ui-asks.md#headless-endp
434
463
  ### addFileRoutes(router, options)
435
464
 
436
465
  Requires `fileStorageService` and `gcsBucket` (registered by `AiApp` when both are set).
466
+ `fileStorageService` may also be a function that returns the current service or `undefined`.
467
+ Use it when storage is configured at runtime: the routes stay mounted and documented in
468
+ `/openapi.json`, and answer `503 File storage is not configured` while it returns `undefined`.
437
469
 
438
470
  | Endpoint | Method | Auth | Description |
439
471
  |----------|--------|------|-------------|
@@ -495,7 +527,7 @@ new AiApp({
495
527
  | `toolChoice` | `"auto"` \| `"none"` \| `"required"` (default `"auto"` when tools present) |
496
528
  | `maxSteps` | Max tool-calling steps (default 5) |
497
529
  | `titleModelId` | Cheaper model for conversation title generation |
498
- | `openApiOptions` | Passed to route OpenAPI builders |
530
+ | `openApiOptions` | Passed to route OpenAPI builders. Defaults to the OpenAPI instance `TerrenoApp` passes to `register` |
499
531
  | `projects` | Default `true`. `false` skips the `/gpt/projects` routes, so `AiApp` never registers the `Project` model |
500
532
 
501
533
  ## Cost and abuse limits
@@ -670,7 +702,7 @@ When `prompts.primary` is `local`, `ObservabilityApp.register` mounts admin-only
670
702
  | GET/POST | `/ai/observability/evaluators` | List / create. Create accepts `target: "full trace"` only (`generation span` and `dataset item` → 400). Seeded template install can still store other targets. `llm-judge` requires `judgePromptName`; create rejects when the judge prompt `outputSchema` omits a required dimension (400 names the key). `json-assert` supports `assertion` (`path` + `constraint`) or built-in output-schema mode. Human + `liveSampleRate > 0` → 400. Numeric dimension `range` is `min-max` (for example `0-1`); categorical `range` is `label|label` |
671
703
  | GET/PATCH/DELETE | `/ai/observability/evaluators/:id` | Read / update / soft-delete |
672
704
  | POST | `/ai/observability/traces/review` | Enqueue one or many traces against a human evaluator (`reason: "manual"`) |
673
- | GET | `/ai/observability/review` | Queue by `status` with counts; oldest-first. Response includes `more: false` so RTK preserves the count envelope. Rows include `traceName`, `promptName`, assignee, reason, and enqueue time |
705
+ | GET | `/ai/observability/review` | Queue by `status` with counts; oldest-first. Response includes `more: false` so request clients keep the count envelope instead of unwrapping `data`. Rows include `traceName`, `promptName`, assignee, reason, and enqueue time |
674
706
  | GET | `/ai/observability/review/:id` | Item + evaluator dimensions + `given` / `wrote` panels and `rawInput` / `rawOutput` for the Raw JSON disclosure |
675
707
  | POST | `/ai/observability/review/:id` | `submit` (scores via ScoreSinks, status `done`), `skip`, or `assign` |
676
708
 
@@ -82,7 +82,7 @@ The plugin applies `matchAudienceByType` before `matchAudience`. When `isStaff`
82
82
  | Method | Path | Description |
83
83
  |--------|------|-------------|
84
84
  | GET | `/announcements/pending` | Current interrupt (`modal` or `banner` only) + `remainingCount` (`platform` query: `ios` \| `android` \| `web`; optional `version` build number) |
85
- | GET | `/announcements/feed` | Paginated published changelog — all display modes (`platform` and optional `version` as above) |
85
+ | GET | `/announcements/feed` | Paginated published changelog — all display modes (`platform` and optional `version` as above; `limit` 1–100, default 20; `page` from 1). Returns `{data, more, page, total}`; `more` is true when a later page has items. |
86
86
  | POST | `/announcements/:id/acknowledge` | Record acknowledgement (idempotent per version). Same visibility as pending/feed; **404** when not visible. |
87
87
  | POST | `/announcements/:id/impression` | Record a view (`{ platform? }`). Same visibility as pending/feed; **404** when not visible. **400** for unknown keys or an invalid `platform`. |
88
88
  | POST | `/announcements/:id/click` | Record a primary-action click (`{ action: "primaryAction", platform? }`; optional `?version=` for min-build visibility). Returns **404** when not visible (checked before action/CTA validation). **400** when `action` is invalid, `primaryAction` is absent, or an explicit `platform` value is invalid. |
@@ -172,11 +172,11 @@ Set these environment variables on the MCP server to include live announcements:
172
172
 
173
173
  ### `useAcknowledgeAnnouncement`
174
174
 
175
- Injects RTK Query mutations for acknowledgement, impression, and primary-action click tracking:
175
+ Sends acknowledgement, impression, and primary-action click events through the `client` passed to `useAcknowledgeAnnouncement(client, baseUrl?)` (see [`TerrenoRequestClient`](ui.md#terrenorequestclient)):
176
176
 
177
177
  | Method | Route | Notes |
178
178
  |--------|-------|-------|
179
- | `acknowledge(id)` | `POST /announcements/:id/acknowledge` | Invalidates pending queue. Query includes `platform` and, when available, `version` (same visibility gate as pending/feed). |
179
+ | `acknowledge(id)` | `POST /announcements/:id/acknowledge` | Refetches the pending announcement in every mounted `useAnnouncements` on the same client. Query includes `platform` and, when available, `version` (same visibility gate as pending/feed). |
180
180
  | `recordImpression(id)` | `POST /announcements/:id/impression` | Sends current `platform` in body |
181
181
  | `recordClick(id)` | `POST /announcements/:id/click` | Body `{ action: "primaryAction", platform }`; query includes `platform` and, when available, `version` (same visibility gate as pending/feed) |
182
182
 
@@ -49,11 +49,16 @@ REST API framework built on Express and Mongoose. Provides modelRouter (CRUD end
49
49
 
50
50
  Two patterns for building Terreno APIs:
51
51
 
52
- ### Deprecated: modelRouter `realtime`
52
+ ### Removed: modelRouter `realtime`
53
53
 
54
- `modelRouter({ realtime })` (RTK cache-patching websocket events) is **deprecated** and **will be removed in Terreno 58**. Use `sync` with [`@terreno/syncdb`](syncdb.md). See [Migrate from RTK to syncdb](../how-to/migrate-rtk-to-syncdb.md).
54
+ Terreno 58 removed `modelRouter({ realtime })` and the legacy RTK `sync` websocket event. Passing `realtime` is now a TypeScript error. Use `sync` with [`@terreno/syncdb`](syncdb.md). See [Upgrade from @terreno/rtk to @terreno/syncdb](../how-to/migrate-rtk-to-syncdb.md).
55
55
 
56
- `RealtimeApp` is **not** deprecated. It still hosts Socket.io, change streams, and `sync:delta`.
56
+ Also removed: `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, `QuerySubscription`, the realtime registry (`registerRealtime`, `getRealtimeRegistry`, `findRegistryEntryByCollection`, `findRegistryEntryByRoutePath`, `updateRealtimeRegistryOptions`, `clearRealtimeRegistry`, `RealtimeRegistryEntry`), the query-subscription store (`addQuerySubscription`, `removeQuerySubscription`, `removeAllSocketQueries`, `getQuerySubscriptionsForCollection`, `computeQueryId`, `clearQueryStore`), and `MAX_MODEL_SUBSCRIPTIONS` / `MAX_DOCUMENT_SUBSCRIPTIONS` / `MAX_QUERY_SUBSCRIPTIONS`. The server no longer handles `subscribe:model`, `subscribe:document`, or `subscribe:query`.
57
+
58
+ `RealtimeApp` stays. It hosts Socket.io, socket auth, change streams, and `sync:delta`.
59
+ Before it sends a `sync:delta` to a socket, it runs the router's `read` permissions (or RBAC
60
+ read access) against the change's post-image hydrated as a model document, as REST does. So a
61
+ virtual owner field works: `GptHistory`'s `ownerId` alias of `userId` passes `Permissions.IsOwner`.
57
62
 
58
63
  Keep `admin.realtime` when you want admin `admin:model.changed` events.
59
64
 
@@ -99,15 +104,21 @@ new TerrenoApp({
99
104
 
100
105
  ### HTTP rate limiting
101
106
 
102
- Opt-in. Pass `rateLimit: {}` on `TerrenoApp` to enable (omitted = off; Terreno 58 defaults on).
107
+ On by default. Omitted, `undefined`, or `{}` mounts the limiter with the memory store and the defaults below. `rateLimit: false` turns it off. On Cloud Run pass `trustProxy: 1`.
103
108
 
104
109
  ```typescript
105
110
  new TerrenoApp({
106
111
  userModel: User,
107
- rateLimit: process.env.RATE_LIMIT_ENABLED === "true" ? {store: "memory"} : undefined,
112
+ rateLimit: {trustProxy: 1}, // or `false` to turn the limiter off
108
113
  });
109
114
  ```
110
115
 
116
+ | `rateLimit` | Result |
117
+ |-------------|--------|
118
+ | omitted / `undefined` | On: memory store, default limits |
119
+ | `{}` / `{store, limits, trustProxy, skip, betterAuthBasePath}` | On with those options |
120
+ | `false` | Off |
121
+
111
122
  | Option | Default | Meaning |
112
123
  |--------|---------|---------|
113
124
  | `store` | `"memory"` | `"memory"` \| `"redis"` \| `"mongo"` |
@@ -211,7 +222,7 @@ Both patterns create the same middleware stack (CORS, auth, logging, OpenAPI). H
211
222
 
212
223
  ## Collection catalog
213
224
 
214
- `modelRouter("/path", Model, options)` writes one catalog record per route path. MCP, realtime, and sync surfaces read that record; TerrenoApp calls `replaceCollectionOptions` once when access control is injected. Test helpers `clearMCPRegistry`, `clearRealtimeRegistry`, and `clearSyncRegistry` clear the entire catalog.
225
+ `modelRouter("/path", Model, options)` writes one catalog record per route path. MCP and sync surfaces read that record; TerrenoApp calls `replaceCollectionOptions` once when access control is injected. Test helpers `clearMCPRegistry` and `clearSyncRegistry` clear the entire catalog.
215
226
 
216
227
  ## modelRouter actions
217
228
 
@@ -382,7 +393,7 @@ Create, update, and delete tools share REST permission, hook, and persistence se
382
393
 
383
394
  ### MCP service token model
384
395
 
385
- `McpServiceToken` stores the hashed credential records used by the optional MCP service-token feature. It is not a `modelRouter` model: consumer-facing create, list, and revoke routes are registered only when `mcpServiceTokens` is enabled on `TerrenoApp`.
396
+ `McpServiceToken` stores the hashed credential records used by the optional MCP service-token feature. `_id` is a String (UUID for new rows; ObjectId hex for rows migrated from 57.x). The schema uses `isDeletedPlugin` and `syncPlugin`, so with `mcpServiceTokens` enabled the collection is synced read-only to its owner.
386
397
 
387
398
  ```typescript
388
399
  import {McpServiceToken} from "@terreno/api";
@@ -394,17 +405,23 @@ const {mcpServiceToken, token} = await McpServiceToken.issueFor(
394
405
  // Store or show `token` once. Only its SHA-256 hash is persisted.
395
406
  ```
396
407
 
397
- The token begins with `mcp_` followed by 32 random bytes encoded as hex. `issueFor` records the user's `tokenEpoch` (pass it on the user argument; default 0). `/mcp` refuses a token whose recorded epoch differs from the user's current `tokenEpoch` and sets its `revokedAt`, so a password reset, Better Auth password change, or session revocation ends every key minted before it. A row stored without `tokenEpoch` counts as epoch 0. `revokeOutdatedForUser(user)` revokes the user's keys from earlier epochs; the list and create routes call it so the list shows them as revoked. `verify(token)` returns `null` for unknown, expired, or revoked tokens. `revokeForUser(user, tokenId)` records `revokedAt`, and `countActiveForUser(userId)` excludes expired or revoked records. Document `deleteOne` (admin DELETE) also sets `revokedAt` and leaves the row for audit. `revokeAllForUser(userId)` revokes every active token the user holds and returns the count. Never return or log `tokenHash` or the plaintext token outside the initial issue result.
408
+ The token begins with `mcp_` followed by 32 random bytes encoded as hex. `issueFor` records the user's `tokenEpoch` (pass it on the user argument; default 0). `/mcp` refuses a token whose recorded epoch differs from the user's current `tokenEpoch` and sets its `revokedAt`, so a password reset, Better Auth password change, or session revocation ends every key minted before it. A row stored without `tokenEpoch` counts as epoch 0. `revokeOutdatedForUser(user)` revokes the user's keys from earlier epochs; the list and create routes call it so the list shows them as revoked. `verify(token)` returns `null` for unknown, expired, or revoked tokens. `revokeForUser(user, tokenId)` records `revokedAt`, and `countActiveForUser(userId)` excludes expired or revoked records. A delete never removes or tombstones the row: document `deleteOne` and a soft delete (admin DELETE sets `deleted: true`) both set `revokedAt` and keep `deleted: false`, so the owner's synced row shows the revoke. `revokeAllForUser(userId)` revokes every active token the user holds and returns the count. Never return or log `tokenHash` or the plaintext token outside the initial issue result.
398
409
 
399
- Self-serve HTTP routes live at `/mcp/service-tokens`. `TerrenoApp` mounts them when `mcpServiceTokens` is enabled, passing its OpenAPI bundle. You can also call `addMcpServiceTokenRoutes(app, {publicMcpUrl, openApi})` yourself. They require session or JWT auth and **reject** `Authorization: Bearer mcp_…` so a service token cannot mint or list tokens.
410
+ Self-serve routes are `modelRouter("/mcp/service-tokens", McpServiceToken, {sync: {scope: {type: "owner", field: "userId"}}})`. `TerrenoApp` mounts them when `mcpServiceTokens` is enabled, passing its OpenAPI bundle. You can also call `addMcpServiceTokenRoutes(app, {publicMcpUrl, openApi})` yourself, or build the registration with `createMcpServiceTokenRouter(options)`. They require session or JWT auth and **reject** `Authorization: Bearer mcp_…` so a service token cannot mint, list, or revoke tokens.
400
411
 
401
412
  | Method | Path | Body / params | Response |
402
413
  | --- | --- | --- | --- |
403
- | POST | `/mcp/service-tokens` | `{name, expiresAt?}` | `{data: {id, name, token, tokenPrefix, mcpUrl, expiresAt, created}}` |
404
- | GET | `/mcp/service-tokens` | `?page&limit` | `{data, page, limit, total, more}` — no `token` or `tokenHash` |
405
- | DELETE | `/mcp/service-tokens/:id` | — | `{data: {id, revokedAt}}` — owner only |
414
+ | POST | `/mcp/service-tokens/create` | `{name, expiresAt?}` | `{data: {id, name, token, tokenPrefix, mcpUrl, expiresAt, created}}` (operation `mcpServiceTokens_create`) |
415
+ | GET | `/mcp/service-tokens` | `?page&limit` | `{data, page, limit, more, total}` — the caller's rows, no `token` or `tokenHash` |
416
+ | GET | `/mcp/service-tokens/:id` | — | `{data}` — owner only (`403` otherwise) |
417
+ | POST | `/mcp/service-tokens/:id/revoke` | — | `{data}` — the revoked row without `tokenHash` (operation `mcpServiceTokens_revoke`) |
418
+ | POST / PATCH / DELETE | `/mcp/service-tokens[/:id]` | — | `405`: client writes are disabled |
419
+
420
+ `expiresAt` is an optional ISO-8601 datetime (Luxon `DateTime.fromISO`). Omit it for a token that does not expire. Create returns `mcpUrl` from `publicMcpUrl`, else `BETTER_AUTH_URL`, else the request host, with `/mcp` appended when missing. An 11th **active** token for the same user is `400`. Revoking an unknown, foreign, or already revoked token is `404`. List includes revoked rows for the owner so the UI can show history. Default and maximum page size is 100.
421
+
422
+ Sync: collection `mcp/service-tokens`, stream `mcp/service-tokens|owner:{userId}`. Snapshot entities, `/sync/entities` rows, and `sync:delta` payloads go through `toPublicMcpServiceToken`, which drops `tokenHash`. Sync `create`, `update`, and `delete` mutations are nacked `unauthorized`; mint and revoke through the actions above.
406
423
 
407
- `expiresAt` is an optional ISO-8601 datetime (Luxon `DateTime.fromISO`). Omit it for a token that does not expire. Create returns `mcpUrl` from `publicMcpUrl`, else `BETTER_AUTH_URL`, else the request host, with `/mcp` appended when missing. An 11th **active** token for the same user is `400`. Revoke and cross-owner access are `404`. List includes revoked rows for the owner so the UI can show history. Default page size is 100 (maximum 100).
424
+ Migration: `mcpServiceTokenStringIdsMigration.up` (slug `mcp-service-token-string-ids`) re-inserts each ObjectId row with `_id = oldId.toHexString()` and deletes the original. It is idempotent, resumes an interrupted run, and works without transactions. `migrateMcpServiceTokenStringIds(ctx)` returns `{converted, pending}`. Wire it into an app `migrations/` file as shown in [Run MongoDB migrations](../how-to/run-mongodb-migrations.md#framework-migrations).
408
425
 
409
426
  ### MCP OAuth
410
427
 
@@ -489,8 +506,7 @@ setupServer({
489
506
  - `POST /auth/refresh_token` — Refresh access token
490
507
  - `POST /auth/forgotPassword` — Always 202; mails a reset link only when the email exists (with
491
508
  `BetterAuthApp` registered, only when that user already has a JWT password)
492
- - `POST /auth/resetPassword` — `{token, password}`; also aliased at `POST /resetPassword` for the
493
- RTK client. With `BetterAuthApp` registered, a token for a user without a JWT password gets 400
509
+ - `POST /auth/resetPassword` — `{token, password}`; also aliased at `POST /resetPassword` (the `jwtAuthAdapter` `resetPassword` path in `@terreno/syncdb/auth`). With `BetterAuthApp` registered, a token for a user without a JWT password gets 400
494
510
  `legacy-password-reset-unavailable` and sets no password (`POST /auth/login` has no second
495
511
  factor); that user resets through Better Auth
496
512
  - `POST /auth/sendVerification` — Authenticated; 202 only after delivery succeeds; mails a verification link when `emailVerified` is not true
@@ -515,8 +531,8 @@ letter case, needs no password. `currentPassword` is never saved.
515
531
  | User has no password (for example GitHub-only or social-only) | 400 `Set a password on your account before changing your email. …` | Nothing |
516
532
  | Right `currentPassword` | 200 with the updated user | The whole body |
517
533
 
518
- The status is 403, not 401, so `@terreno/rtk` does not treat a wrong password as an expired
519
- session and log the user out.
534
+ The status is 403, not 401, so the syncdb request client does not treat a wrong password as an
535
+ expired session and log the user out.
520
536
 
521
537
  The password is checked against the passport-local-mongoose hash first. A wrong password
522
538
  counts toward the model's passport lockout when `limitAttempts` is on, the same counter
@@ -593,7 +609,7 @@ case instead of 202. Email lookup is case-insensitive. Successful reset calls `s
593
609
  and deletes Better Auth sessions. Better Auth password reset updates the JWT password of the
594
610
  matching app User when it already has one, and bumps its `tokenEpoch`. Access
595
611
  tokens stay valid until expiry (default 15m). `POST /resetPassword`
596
- matches the `@terreno/rtk` `resetPassword` mutation path (`password` or `newPassword`).
612
+ matches the `@terreno/syncdb/auth` `jwtAuthAdapter` `resetPassword` path (`password` or `newPassword`).
597
613
 
598
614
  For any other server-side password change, such as an admin "set password" action, call
599
615
  `changeUserPassword(user, password, {adminId, app: req.app})`. It does what reset does: sets the
@@ -821,7 +837,7 @@ Endpoints it adds, under `basePath` (default `/api/auth`):
821
837
  cookie, and redirects to the provider. A link that cannot start redirects to
822
838
  `errorCallbackURL` (or `callbackURL`) with `error=session_expired`, `error=provider_not_found`,
823
839
  or `error=link_failed`. The provider callback links the account and redirects to `callbackURL`
824
- with no `#ott`, since no session is created. rtk's `linkSocialWithHandoff` drives both steps;
840
+ with no `#ott`, since no session is created. `linkSocialWithHandoff` from `@terreno/syncdb/auth` drives both steps;
825
841
  see [Linking a provider in bearer mode](../how-to/configure-better-auth.md#linking-a-provider-in-bearer-mode).
826
842
 
827
843
  With the opt-in `rateLimit`, every path above is in the auth bucket. Better Auth's own limiter
@@ -832,7 +848,7 @@ allows three starts, three link intents, and three link starts per ten seconds p
832
848
  Every Better Auth server answers `GET {basePath}/terreno/methods` (default
833
849
  `/api/auth/terreno/methods`) with the sign-in methods it has enabled, so a login screen renders
834
850
  only those. It needs no session. Read it from a frontend with `useAuthMethods(authClient)` from
835
- `@terreno/rtk`.
851
+ `@terreno/syncdb/auth`.
836
852
 
837
853
  ``````json
838
854
  {
@@ -870,8 +886,7 @@ plugin-registered providers are reflected.
870
886
 
871
887
  Every Better Auth server serves `POST {basePath}/terreno/set-password` (default
872
888
  `/api/auth/terreno/set-password`) so a signed-in user who has no password, such as one who signed
873
- up with a provider, can add one. Better Auth's own `setPassword` is server-only. rtk's
874
- `useAccountSecurity` calls it from the password card's "Set a password" form.
889
+ up with a provider, can add one. Better Auth's own `setPassword` is server-only. `useAccountSecurity` from `@terreno/syncdb/auth` calls it from the password card's "Set a password" form.
875
890
 
876
891
  | Request | Response |
877
892
  | --- | --- |
@@ -903,8 +918,8 @@ means the cookie expired. Only values the response sets are carried. Such a sign
903
918
  answers with a two-factor challenge has no `set-auth-token`. The header is listed in
904
919
  `Access-Control-Expose-Headers`. On `{basePath}/two-factor/*`, `/passkey/*`, and `/sign-in/*`,
905
920
  the header's challenge cookies are merged into the request's cookies unless a real cookie of
906
- the same name is present; other names, including session cookies, are ignored. rtk's bearer mode
907
- uses it; see
921
+ the same name is present; other names, including session cookies, are ignored. The
922
+ `@terreno/syncdb/auth` bearer mode uses it; see
908
923
  [Challenges without cookies](../explanation/authentication.md#challenges-without-cookies).
909
924
 
910
925
  **Endpoints (when enabled):**
@@ -2,7 +2,7 @@
2
2
 
3
3
  Install: `bun add -g @terreno/cli` then run `terreno --help`.
4
4
 
5
- The `terreno` binary is the operator CLI for Terreno apps. It wraps documentation search, codegen (syncdb SDK, RTK OpenAPI SDK, Mongoose models, routes, screens, forms, admin install notes), app bootstrap, local project tools from `@terreno/mcp`, and **OpenAPI-driven REST calls**.
5
+ The `terreno` binary is the operator CLI for Terreno apps. It wraps documentation search, codegen (syncdb collection SDK, typed API request SDK, Mongoose models, routes, screens, forms, admin install notes), app bootstrap, local project tools from `@terreno/mcp`, and **OpenAPI-driven REST calls**.
6
6
 
7
7
  Generate an app-specific CLI from your backend spec:
8
8
 
@@ -18,7 +18,7 @@ terreno generate rest-cli --schema http://localhost:4000/openapi.json --out ./to
18
18
  | `docs component <Name>` | Print `@terreno/ui` component props |
19
19
  | `docs upgrade --from --to` | Lockstep upgrade notes |
20
20
  | `generate syncdb` | Typed syncdb hooks from OpenAPI (`--schema`, `--out`, `--collections`, `--config`, `--no-format`) |
21
- | `generate sdk` | Run `@rtk-query/codegen-openapi` with `--config` |
21
+ | `generate sdk` | Typed request hooks (`apiSdk.ts`) for every non-synced route via the syncdb codegen (`--schema`, `--out`, `--config`, `--no-format`); same output as `terreno-syncdb-codegen --api-out`. A legacy RTK `--config openapi-config.ts` alone is rejected |
22
22
  | `generate rest-cli` | Scaffold a bun CLI from OpenAPI (`--schema`, `--out`, `--name`, `--base-url`) |
23
23
  | `generate model` | Mongoose model (`--name`, `--field name:Type:required`, `--owner`, `--soft-delete`) |
24
24
  | `generate route` | `modelRouter` module (`--model-name`, `--route-path`, `--owner-filtered`, `--query-fields`, `--sort`) |
@@ -33,8 +33,7 @@ terreno generate rest-cli --schema http://localhost:4000/openapi.json --out ./to
33
33
  | `api request <METHOD> <path>` | Invoke by method + path template |
34
34
  | `info` | Local `@terreno/*` versions (via MCP local tools) |
35
35
  | `logs` | Merge backend/browser JSONL, Metro events, and app CDP console (`--entries`, `--level`, `--since`, `--sources`; `last-error`) |
36
- | `state` | Inspect client state with `get_client_state`: RTK Query (Better Auth via the `auth` alias or `betterAuth`; `--slice`, `--query`) and SyncDB, through the dev stores or Metro CDP. The result names the detected `layer` |
37
- | `eval` | Evaluate JavaScript in the app runtime through Metro CDP (`--code`; requires `TERRENO_MCP_EVAL=1`) |
36
+ | `state` | Inspect the live SyncDB client (`get_syncdb_state`): status, entities, outbox, conflicts, cursors, streams, debug events (`--collection`, `--entity-id`, `--limit`, `--name`, `--no-events`). Needs `createSyncDb({debug: true})`. The Terreno 57 `--slice` / `--query` Redux flags were removed in 58 and now exit 1 || `eval` | Evaluate JavaScript in the app runtime through Metro CDP (`--code`; requires `TERRENO_MCP_EVAL=1`) |
38
37
  | `navigate` | Navigate Expo Router through Metro CDP (requires `TERRENO_MCP_EVAL=1`) |
39
38
  | `web [url]` | Automate the web app with Bun 1.4 `Bun.WebView`; ordered `--action` JSON, `--snapshot`, and `--screenshot` |
40
39
  | `db schema` | Mongo collections (`--collection-filter`, `--summary`) |
@@ -85,10 +85,10 @@ Comprehensive guide to environment variables used across Terreno packages and ex
85
85
 
86
86
  | Variable | Read by | Required | Default | Secret | Scope |
87
87
  |----------|---------|----------|---------|--------|-------|
88
- | `EXPO_PUBLIC_API_URL` | `@terreno/rtk` | ✅ prod web | dev auto-detect | No | **client** |
89
- | `EXPO_PUBLIC_DEV_API_PORT` | `@terreno/rtk` | ❌ | `4000` | No | **client** |
90
- | `EXPO_PUBLIC_BUILD_NUMBER` | example-frontend | ❌ | — | No | **client** |
91
- | `OPENAPI_URL` | example-frontend `sdk` script | ❌ | `http://localhost:4000/openapi.json` | No | tooling |
88
+ | `EXPO_PUBLIC_API_URL` | `@terreno/syncdb/config` | ✅ prod web | dev auto-detect | No | **client** |
89
+ | `EXPO_PUBLIC_DEV_API_PORT` | `@terreno/syncdb/config` | ❌ | `4000` | No | **client** |
90
+ | `EXPO_PUBLIC_BUILD_NUMBER` | `@terreno/syncdb/buildNumber` (`app.config.ts`) | ❌ | git commit count | No | **client** |
91
+ | `OPENAPI_URL` | example-frontend `sdk` / `sync-sdk` scripts | ❌ | `http://localhost:4000/openapi.json` | No | tooling |
92
92
 
93
93
  ### `app.json` `extra` (not `process.env`)
94
94
 
@@ -96,18 +96,17 @@ Configured in `expo.extra` and read via `expo-constants`:
96
96
 
97
97
  | Key | Read by | Notes |
98
98
  |-----|---------|-------|
99
- | `BASE_URL` | `@terreno/rtk` | Production fallback when `EXPO_PUBLIC_API_URL` unset; use `__SAME_ORIGIN__` for same-origin admin SPA |
100
- | `DEV_API_PORT` | `@terreno/rtk` | Override dev API port (mirrors `EXPO_PUBLIC_DEV_API_PORT`) |
101
- | `AUTH_DEBUG` | `@terreno/rtk` | `"true"` enables auth debug logs |
102
- | `WEBSOCKETS_DEBUG` | `@terreno/rtk` | `"true"` enables socket debug logs |
103
- | `SOCKET_TRANSPORTS` | example-frontend (`store/socketTransports.ts`) | Comma-separated Socket.IO transports passed to `useSocketConnection({transports})` and `createSyncDb({socketTransports})`. Unset keeps `polling,websocket`; the Vercel build sets `websocket` |
104
- | `APP_ENV` | `@terreno/rtk` | Logged at startup |
99
+ | `BASE_URL` | `@terreno/syncdb/config` | Production fallback when `EXPO_PUBLIC_API_URL` unset; use `__SAME_ORIGIN__` for same-origin admin SPA |
100
+ | `DEV_API_PORT` | `@terreno/syncdb/config` | Override dev API port (mirrors `EXPO_PUBLIC_DEV_API_PORT`) |
101
+ | `buildNumber` | `@terreno/syncdb/app` `useUpgradeCheck` | Sent to `GET /version-check`; set it with `resolveBuildNumber` |
102
+ | `SOCKET_TRANSPORTS` | example-frontend (`store/socketTransports.ts`) | Comma-separated Socket.IO transports passed to `createSyncDb({socketTransports})`. Unset keeps `polling,websocket`; the Vercel build sets `websocket` |
105
103
 
106
- Resolution order for API base URL (`rtk/src/constants.ts`):
104
+ Resolution order for API base URL (`syncdb/src/config/resolveBaseUrls.ts`):
107
105
 
108
- 1. `EXPO_PUBLIC_API_URL` (production and dev)
109
- 2. `extra.BASE_URL` (production only, when env unset)
110
- 3. Dev fallbacks: `hostUri`, experience URL, `localhost`
106
+ 1. `extra.BASE_URL` = `__SAME_ORIGIN__` → the page origin (web)
107
+ 2. `EXPO_PUBLIC_API_URL` (production and dev)
108
+ 3. `extra.BASE_URL` (production only, when env unset)
109
+ 4. Dev fallbacks: `hostUri`, experience URL, `localhost` on the dev API port
111
110
 
112
111
  `EXPO_PUBLIC_API_URL` is inlined only into app source. Code under `node_modules`, including `@terreno/rtk`, does not see the inlined value in a production export and falls back to `extra.BASE_URL`, so set both for production web builds. `example-frontend/scripts/vercelBuild.ts` writes `extra.BASE_URL` from `EXPO_PUBLIC_API_URL` before `expo export`.
113
112
 
@@ -232,7 +231,7 @@ for authenticated test sends.
232
231
  | `VALKEY_URL` | `@terreno/api` realtime adapter **and** `rateLimit.store: "redis"` | ❌ | — | Yes | server |
233
232
  | `REDIS_URL` | `@terreno/api` fallback after `VALKEY_URL` (realtime + Redis rate-limit store) | ❌ | — | Yes | server |
234
233
 
235
- There is **no** `RATE_LIMIT_ENABLED` (or similar) read by `@terreno/api`. Apps that want an env toggle pass `rateLimit: process.env.RATE_LIMIT_ENABLED === "true" ? {store: "memory"} : undefined` themselves. See [Rate limiting](../how-to/rate-limiting.md).
234
+ There is **no** `RATE_LIMIT_ENABLED` (or similar) read by `@terreno/api`. The limiter is on by default. Apps that want an env toggle pass `rateLimit: process.env.RATE_LIMIT_DISABLED === "true" ? false : {trustProxy: 1}` themselves. See [Rate limiting](../how-to/rate-limiting.md).
236
235
 
237
236
  ## Webhooks & notifications
238
237
 
@@ -317,7 +316,8 @@ example-backend **test** setup only. They are not read by `@terreno/jobs`. Use
317
316
 
318
317
  `SEED_DEFAULTS=true` runs the example seed (default users, all with the published password
319
318
  `testpassword123`) when the server starts. A process with `NODE_ENV=production` logs a warning and
320
- skips it, so no deployed revision, PR previews included, holds those accounts.
319
+ skips it, so no deployed revision holds those accounts. The exception is a PR preview, whose
320
+ `MONGO_DB_NAME` starts with `terreno-example-pr-`: it seeds its own throwaway database.
321
321
 
322
322
  ## Testing & CI (tooling only)
323
323
 
@@ -41,14 +41,16 @@ new TerrenoApp({userModel: User})
41
41
 
42
42
  ### Frontend
43
43
 
44
- Use the `useFeatureFlags` hook from `@terreno/rtk` to evaluate flags for the current user:
44
+ Use `useFeatureFlags` from `@terreno/syncdb/flags` to read flags for the current user. It
45
+ loads `GET {basePath}/flagConfiguration` with the syncdb request client (the one from
46
+ `ApiClientProvider`) and refetches when the server pushes `featureFlagsChanged` on the
47
+ syncdb socket:
45
48
 
46
49
  ```typescript
47
- import {useFeatureFlags} from "@terreno/rtk";
48
- import {terrenoApi} from "@/store/sdk";
50
+ import {useFeatureFlags} from "@terreno/syncdb/flags";
49
51
 
50
- const MyComponent = () => {
51
- const {getFlag, getVariant, isLoading} = useFeatureFlags(terrenoApi);
52
+ const MyComponent: React.FC<{userId?: string}> = ({userId}) => {
53
+ const {getFlag, getVariant, isLoading} = useFeatureFlags({skip: !userId, userId});
52
54
 
53
55
  if (isLoading) return <Spinner />;
54
56
 
@@ -59,11 +61,41 @@ const MyComponent = () => {
59
61
  };
60
62
  ```
61
63
 
62
- New code can also use OpenFeature React hooks (`useBooleanFlagValue`, `useStringFlagValue`, `FeatureFlag`, …) after wiring `useTerrenoFeatureFlags` and `<OpenFeatureProvider domain="feature-flags">` — see `@terreno/rtk` and [how-to: add feature flags](../how-to/add-feature-flags.md).
64
+ New code can also use OpenFeature React hooks (`useBooleanFlagValue`, `useStringFlagValue`, `FeatureFlag`, …) after wiring `useTerrenoFeatureFlags` and `<OpenFeatureProvider domain="feature-flags">` — see [how-to: add feature flags](../how-to/add-feature-flags.md).
65
+
66
+ #### `@terreno/syncdb/flags`
67
+
68
+ | Export | Description |
69
+ |--------|-------------|
70
+ | `useTerrenoFeatureFlags(options?)` | Loads `/flagConfiguration` into OpenFeature's `TypedInMemoryProvider` for `domain`; returns `{client, flags, isLoading, error, refetch}` |
71
+ | `useFeatureFlags(options?)` | Flat accessors over `useTerrenoFeatureFlags`: `{flags, getFlag, getVariant, isLoading, error, refetch}` |
72
+ | `TerrenoFlagConfiguration`, `TerrenoFlagDefinition` | Wire shape of `/flagConfiguration` |
73
+ | `DEFAULT_FEATURE_FLAGS_BASE_PATH`, `DEFAULT_FEATURE_FLAGS_DOMAIN`, `DEFAULT_FEATURE_FLAGS_EVENT` | `"/feature-flags"`, `"feature-flags"`, `"featureFlagsChanged"` |
74
+
75
+ Options (both hooks):
76
+
77
+ | Option | Default | Description |
78
+ |--------|---------|-------------|
79
+ | `userId` | `null` | Current user. A change refetches and sets the OpenFeature `targetingKey` |
80
+ | `skip` | `false` | Do not fetch or listen (for example while signed out) |
81
+ | `client` | `ApiClientProvider` client | Request client from `@terreno/syncdb/api` |
82
+ | `socket` | `SyncDbProvider` client | Anything with `onSocketEvent` (a `SyncDb`). Pass your `syncDb` when the hook sits above `SyncDbProvider`; `null` turns live refresh off |
83
+ | `socketEventName` | `"featureFlagsChanged"` | Push event that triggers a refetch |
84
+ | `basePath` | `"/feature-flags"` | `FeatureFlagsApp` `basePath` |
85
+ | `domain` | `"feature-flags"` | OpenFeature domain; must match `<OpenFeatureProvider domain>` |
86
+
87
+ `isLoading` is `true` until the first response is installed in OpenFeature and again while a
88
+ refetch is in flight; it is `false` when skipped or after an error. Every hook reading the same
89
+ `basePath` shares one cache entry, so a single instance that listens to the socket (usually the
90
+ root `OpenFeatureProvider` bridge) is enough; other screens can pass `socket: null`. The last
91
+ unmounted `useTerrenoFeatureFlags` for a domain restores OpenFeature's NOOP provider.
92
+
93
+ `@openfeature/web-sdk` (and `@openfeature/react-sdk` for the React hooks) are optional peer
94
+ dependencies of `@terreno/syncdb`; add them to apps that import `@terreno/syncdb/flags`.
63
95
 
64
96
  ## OpenFeature integration
65
97
 
66
- `FeatureFlagsApp` registers an OpenFeature **server** provider (`MongoFeatureFlagProvider`) on a dedicated domain (default `"feature-flags"`) so the global default provider is unchanged. The authenticated bulk endpoint **`GET {basePath}/flagConfiguration`** returns a `FlagConfiguration`-compatible map (suitable for `TypedInMemoryProvider` on the client). Disabled **boolean** flags are included and resolve to `off`, so a client can tell a turned-off flag from one that was never created. Disabled variant flags and archived flags are omitted. The legacy **`GET {basePath}/evaluate`** response shape is unchanged but is **deprecated**: responses include `Deprecation: true` and a `Sunset` HTTP-date header (~90 days). Prefer `useTerrenoFeatureFlags` / `useFeatureFlags` from `@terreno/rtk`, or migrate direct HTTP callers to `/flagConfiguration`.
98
+ `FeatureFlagsApp` registers an OpenFeature **server** provider (`MongoFeatureFlagProvider`) on a dedicated domain (default `"feature-flags"`) so the global default provider is unchanged. The authenticated bulk endpoint **`GET {basePath}/flagConfiguration`** returns a `FlagConfiguration`-compatible map (suitable for `TypedInMemoryProvider` on the client). Disabled **boolean** flags are included and resolve to `off`, so a client can tell a turned-off flag from one that was never created. Disabled variant flags and archived flags are omitted. The legacy **`GET {basePath}/evaluate`** response shape is unchanged but is **deprecated**: responses include `Deprecation: true` and a `Sunset` HTTP-date header (~90 days). Prefer `useTerrenoFeatureFlags` / `useFeatureFlags` from `@terreno/syncdb/flags`, or migrate direct HTTP callers to `/flagConfiguration`. The server logs this advice once per process on the first `/evaluate` call.
67
99
 
68
100
  ### `defaultVariant` on `FeatureFlag`
69
101
 
@@ -81,6 +113,15 @@ Consumers may augment `@openfeature/core` with `BooleanFlagKey` / `StringFlagKey
81
113
 
82
114
  Pass `liveUpdates: { socketIoServer: io | () => io }` to broadcast on Mongoose change streams. **Requires MongoDB as a replica set** (single-node replset is fine). Emits a socket event (default `featureFlagsChanged`, payload `{ key }`) and `PROVIDER_CONFIGURATION_CHANGED` on the server provider. All authenticated subscribers receive the same event name; payload is only the flag key (same keys are already exposed per user via `/flagConfiguration`).
83
115
 
116
+ Emit on the Socket.io server that hosts the syncdb socket — `RealtimeApp.getIo()` — so clients
117
+ receive the push on the one socket they already hold:
118
+
119
+ ```typescript
120
+ const realtimeApp = new RealtimeApp({/* ... */});
121
+ app.register(realtimeApp);
122
+ app.register(new FeatureFlagsApp({liveUpdates: {socketIoServer: () => realtimeApp.getIo()}}));
123
+ ```
124
+
84
125
  ## Exports
85
126
 
86
127
  ### Classes
@@ -237,8 +237,8 @@ always hides payload regardless of the hook.
237
237
  Widgets are registered in the built-in registry — enable via `JobsApp.adminContribution()` only.
238
238
  The admin shell lists Jobs under **Platform** (not Screens). Hosts still contribute
239
239
  `name: "jobs"`; do not add a second sidebar link.
240
- Custom screens should pass the app's generated RTK `api` into `AdminProvider`; jobs screens wire
241
- admin HTTP internally (not via a public `useJobsDashboardApi` export).
240
+ Custom screens render inside the app's `AdminProvider` (auth binding); jobs screens wire
241
+ admin HTTP through its fetch client internally (not via a public `useJobsDashboardApi` export).
242
242
 
243
243
  ## GCP Cloud Tasks (`@terreno/jobs/runners/gcpCloudTasks`)
244
244