@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
@@ -1,6 +1,6 @@
1
1
  # @terreno/syncdb
2
2
 
3
- Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up. Supersedes `@terreno/rtk` for data-synchronization concerns.
3
+ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up. It is also the frontend's request client, session, feature-flag, and app-utility layer; Terreno 58 removed `@terreno/rtk` and Redux ([upgrade guide](../how-to/migrate-rtk-to-syncdb.md)).
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -11,6 +11,11 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
11
11
  - [SyncDb client methods](#syncdb-client-methods)
12
12
  - [React hooks](#react-hooks)
13
13
  - [Query windows](#query-windows)
14
+ - [API client](#api-client)
15
+ - [Config](#config)
16
+ - [Auth](#auth)
17
+ - [App utilities](#app-utilities)
18
+ - [Feature flags](#feature-flags)
14
19
  - [Codegen](#codegen)
15
20
  - [Conflict API](#conflict-api)
16
21
  - [Sync status API](#sync-status-api)
@@ -27,7 +32,13 @@ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on dev
27
32
  - `betterAuthAdapter`, `bridgeBetterAuthReactClient`, `AuthProvider`
28
33
  - `listConflicts`, `wipeLocalData`, `generateMutationId`
29
34
  - Queries: `QueryFieldNotAllowedError`, `ListRequestError`, `WhereFilter`, `SortSpec`, `WindowQuery`, `QueryWindowState`, `QueryWindows`, `compileWhere`, `DEFAULT_WINDOW_PAGE_SIZE`
30
- - React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`
35
+ - React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useWindowQuery`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`, `createCollectionHooks`, `ApiClientProvider`, `useApiClient`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`
36
+ - API client (`@terreno/syncdb/api`): `createApiClient`, `ApiClient`, `ApiError`, `isApiError`, `apiFetch`, `unwrapEnvelope`, `serializeParams`
37
+ - Config (`@terreno/syncdb/config`): `baseUrl`, `baseWebsocketsUrl`, `baseTasksUrl`, `appHeaders`, `isWeb`, `resolveBaseUrls`, `resolveDevApiPort`, `SAME_ORIGIN_SENTINEL`
38
+ - Auth (`@terreno/syncdb/auth`): `createBetterAuthClient`, `createStorageAdapter`, `repairCookieJar`, `withCookieJarRepair`, `SessionProvider`, `useSession`, `useSessionStore`, `createSessionStore`, `betterAuthSessionSource`, `betterAuthAdapter`, `bridgeBetterAuthReactClient`, Better Auth types; JWT (legacy): `jwtAuthAdapter`, `useJwtAuth`, `getTokenExpirationTimes`, `shouldShowStillThereModal`, `createJwtTokenStorage`, `readJwtTokens`, `decodeJwtClaims`
39
+ - App (`@terreno/syncdb/app`): `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `ADMIN_PAGE_PERMISSION`, `installDevConsoleLogger`, `useMCPTools`
40
+ - Flags (`@terreno/syncdb/flags`): `useFeatureFlags`, `useTerrenoFeatureFlags`, `TerrenoFlagConfiguration`
41
+ - Build number (`@terreno/syncdb/buildNumber`, node-only): `resolveBuildNumber`, `coerceBuildNumber`
31
42
  - CLI: `terreno-syncdb-codegen` (generates `SYNC_COLLECTIONS` + friendly hooks from OpenAPI)
32
43
  - Testing (`@terreno/syncdb/testing`): `createFakeTransport`
33
44
  - Dev console logger: `installTerrenoDevConsoleLogger({baseUrl})` from `@terreno/syncdb`, also
@@ -139,7 +150,7 @@ new TerrenoApp({userModel: User})
139
150
  ```
140
151
 
141
152
  - **`SyncApp`** — HTTP sync routes (`/sync/snapshot`, `/sync/mutate`, `/sync/mutate/batch`, `/sync/key`, `/sync/streams`, `/sync/entities`).
142
- - **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams). `modelRouter` `realtime` (RTK `sync` events) is deprecated and removed in Terreno 58; do not add it to new models.
153
+ - **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams). `modelRouter` `realtime` and its RTK `sync` events were removed in Terreno 58; `sync:delta` is the only collection event.
143
154
  - **`ensureSyncIndexes`** — `TerrenoApp.start()` binds the HTTP port first (or attaches to `httpServer` if the process already listened), then awaits index builds for snapshot queries and sync bookkeeping (`SyncMutation.mutationId` unique, `SyncCounter.stream` unique, etc.). Registration queues this work without contacting MongoDB, so models can load before the database connects. Hosts that build Express without `TerrenoApp.start()` should await `ensureSyncIndexes()` after connecting.
144
155
 
145
156
  Socket auth requires at least one configured authentication method. It enables legacy JWT
@@ -190,12 +201,12 @@ be passed to `betterAuthAdapter` directly.
190
201
  | `setIntervalFn` / `clearIntervalFn` | test hooks | global timers | Inject timers for deterministic tests |
191
202
 
192
203
  For a web app in Better Auth bearer mode (`createBetterAuthClient({webAuth: "bearer"})`), pass
193
- rtk's `getBetterAuthBearerToken` so every sync request reads the stored token without a
194
- `get-session` round trip. syncdb does not import rtk; the app wires the two together:
204
+ `getBetterAuthBearerToken` from `@terreno/syncdb/auth` so every sync and request-client call
205
+ reads the stored token without a `get-session` round trip:
195
206
 
196
207
  ```typescript
197
- import {getBetterAuthBearerToken} from "@terreno/rtk";
198
208
  import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
209
+ import {getBetterAuthBearerToken} from "@terreno/syncdb/auth";
199
210
 
200
211
  const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient), {
201
212
  getBearerToken: () => getBetterAuthBearerToken({storagePrefix: "myapp"}),
@@ -261,7 +272,7 @@ before anything else has confirmed one.
261
272
  | Method | Description |
262
273
  |--------|-------------|
263
274
  | `start()` | Resolve user, run wipe-on-user-change check, start persistence, connect transport, subscribe collections, start reconcile timer. Resolves even when offline. Idempotent while already started. |
264
- | `stop()` | Disconnect, stop persistence, clear timers and listeners. Auth changes that fired before the stop but had not run yet are dropped, so a stopped client never reconnects on its own (for example while the app signs out). A stream discovery (`GET /sync/streams`) that was still reading the token when `stop()` ran is not sent, so it never reaches the server with a session the app is signing out of. |
275
+ | `stop()` | Disconnect, flush and stop persistence, clear timers and listeners. Cancels a `start()` that is still running or queued ahead of it, so the transport never connects after logout. Auth changes that fired before the stop but had not run yet are dropped, so a stopped client never reconnects on its own (for example while the app signs out). A stream discovery (`GET /sync/streams`) that was still reading the token when `stop()` ran is not sent, so it never reaches the server with a session the app is signing out of. |
265
276
  | `goOffline()` | Simulated outage: disconnect transport, pause replay/reconcile/timer; local mutations keep queueing. |
266
277
  | `goOnline()` | End simulated outage; reconnect triggers reconcile + outbox replay. |
267
278
  | `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
@@ -272,9 +283,10 @@ before anything else has confirmed one.
272
283
  | `replayOutbox()` | Drain queued mutations for the current user now. |
273
284
  | `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
274
285
  | `retryFailed({entityId})` | Re-enable an entity's queued successors after a terminal validation failure. |
275
- | `signOut()` | Explicit teardown like `stop()`; wipes local data only when `wipeOnSignOut: true`. |
286
+ | `signOut()` | Explicit teardown like `stop()` (including cancelling an in-flight `start()`); wipes local data only when `wipeOnSignOut: true`. |
276
287
  | `getSyncStatus()` | Snapshot of aggregate sync state (see [Sync status API](#sync-status-api)). |
277
288
  | `onStatusChange(callback)` | Subscribe to connectivity/syncing changes; returns unsubscribe. |
289
+ | `onSocketEvent(event, handler)` | Listen for an app-level server event (for example `featureFlagsChanged`) on the sync socket; returns unsubscribe. Listeners survive `stop()`/`start()` and reconnects. `sync:*` and Socket.io lifecycle names throw. A transport without `onEvent` returns a no-op. |
278
290
  | `store` | Read surface over the local MergeableStore (`SyncStore`). |
279
291
  | `outbox` | Durable mutation outbox (`Outbox`). |
280
292
  | `debug` | `SyncDebugLog` when `debug` is enabled; otherwise `undefined`. |
@@ -442,6 +454,20 @@ Available when the client was created with `debug: true`. `log?.snapshot()` retu
442
454
 
443
455
  Returns the `SyncDb` instance from context (escape hatch for imperative calls like `forceResync()`).
444
456
 
457
+ ### `useSocketEvent(event, handler, options?)`
458
+
459
+ Binds `handler` to a server socket event while mounted, through `onSocketEvent` on the
460
+ `SyncDbProvider` client. The latest handler always runs; the listener is rebound only when
461
+ `event`, `client`, or `enabled` changes. Options: `client` (a `SyncDb`, or `null` to listen to
462
+ nothing; required outside a provider) and `enabled` (default `true`). See
463
+ [WebSocket integration](../how-to/websocket-integration.md).
464
+
465
+ ```typescript
466
+ useSocketEvent("announcementPublished", () => {
467
+ void refetch();
468
+ });
469
+ ```
470
+
445
471
  ### `createCollectionHooks`
446
472
 
447
473
  Factory used by `terreno-syncdb-codegen` and by hand-written custom collections. Returns six operation hooks (`useListQuery`, `useWindowQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`). Generated SDKs rename them to friendly names (`useTodos`, `useTodosWindow`, `useTodo`, `useCreateTodo`, …). `useWindowQuery` works only for collections listed in `queryCollections`. Mutation hooks return `[trigger]`; triggers apply locally and return `{mutationId, id}` synchronously. Optional `retries` maps to `maxAttempts` (`false` → 1, a number → that many, omitted → engine default).
@@ -453,6 +479,331 @@ export const {useListQuery: useNotes, useCreateMutation: useCreateNote} =
453
479
  });
454
480
  ```
455
481
 
482
+ ## API client
483
+
484
+ Synced collections read and write through the local store. Every other route —
485
+ `/auth/me`, modelRouter actions, AI, consents, announcements, version check — goes
486
+ through the request client in `@terreno/syncdb/api`. It uses the same `AuthProvider` as
487
+ `createSyncDb`, so both send the same token and refresh it the same way.
488
+
489
+ ```typescript
490
+ import {createApiClient} from "@terreno/syncdb/api";
491
+ import {appHeaders, baseUrl} from "@terreno/syncdb/config";
492
+
493
+ export const apiClient = createApiClient({
494
+ authProvider, // the provider passed to createSyncDb
495
+ baseUrl,
496
+ credentials: "include", // "same-origin" for cookie SPAs
497
+ headers: appHeaders, // App-Version + App-Platform
498
+ });
499
+ ```
500
+
501
+ Wrap the app in `<ApiClientProvider client={apiClient}>`, then:
502
+
503
+ ```typescript
504
+ import {useApiMutation, useApiQuery} from "@terreno/syncdb/react";
505
+
506
+ const {data: me, isLoading, refetch} = useApiQuery<Profile>(
507
+ {url: "/auth/me"},
508
+ {providesTags: ["profile"], skip: !userId}
509
+ );
510
+ const [updateMe, {isLoading: isSaving}] = useApiMutation<UpdateProfile, Profile>(
511
+ (body) => ({body, method: "PATCH", url: "/auth/me"}),
512
+ {invalidatesTags: ["profile"]}
513
+ );
514
+ await updateMe({name: "Ada"}).unwrap();
515
+ ```
516
+
517
+ | `createApiClient` option | Description |
518
+ |--------------------------|-------------|
519
+ | `baseUrl` | API origin; relative request URLs are prefixed with it |
520
+ | `authProvider` | `getToken` adds `Authorization: Bearer`; on a 401, `refresh` runs once and the request retries |
521
+ | `credentials` | Fetch credentials mode for cookie sessions |
522
+ | `headers` | Object or (async) function merged into every request |
523
+ | `timeoutMs` | Per-request timeout (default 30 000) |
524
+ | `onError` | Called with every `ApiError` after any retry (report to Sentry here) |
525
+ | `keepUnusedDataMs` | How long an unsubscribed cache entry survives (default 60 000) |
526
+
527
+ | Request arg | Description |
528
+ |-------------|-------------|
529
+ | `url`, `method` | Path (or absolute URL) and HTTP method (default `GET`) |
530
+ | `params` | Query params; arrays become repeated keys, nested objects bracket keys (`updated[$gte]`) |
531
+ | `body` | Plain objects are JSON-encoded; `FormData`, `Blob`, and strings pass through |
532
+ | `headers` | Per-request headers (part of the cache key) |
533
+ | `unwrap` | Unwrap `{data}` single-document envelopes (default `true`); list envelopes with `more` stay whole |
534
+ | `parseAs`, `timeoutMs`, `signal` | Response parsing, timeout, and cancellation |
535
+
536
+ | Client method | Description |
537
+ |---------------|-------------|
538
+ | `request(args)` | One uncached request |
539
+ | `fetchQuery(args, {providesTags, force})` | Cached request; identical in-flight calls share one fetch |
540
+ | `getQueryState(args)` / `subscribeQuery(args, listener)` | Read and watch one cache entry |
541
+ | `invalidateTags(tags)` | Refetch mounted queries with those tags; drop unmounted ones |
542
+ | `reset()` | Clear the cache; call when the signed-in user changes |
543
+
544
+ | Hook | Returns |
545
+ |------|---------|
546
+ | `useApiQuery(args \| null, {skip, providesTags, pollingInterval, transformResponse, client})` | `{data, error, isLoading, isFetching, isSuccess, isError, isUninitialized, refetch}` |
547
+ | `useLazyApiQuery({providesTags, client})` | `[trigger(args), state]`; each trigger fetches fresh data |
548
+ | `useApiMutation(toRequest, {invalidatesTags, client})` | `[trigger(arg), {data, error, isLoading, isSuccess, isError, reset}]` |
549
+
550
+ Triggers resolve `{data}` or `{error}`; `.unwrap()` resolves the data or throws.
551
+ Failures are `ApiError` (`status`, `data.title`, `data.detail`, `data.code`, `data.fields`,
552
+ `data.meta`); timeouts and network failures have `status: 0`. `data.disableExternalErrorTracking`
553
+ is `true` when the server marks the error as expected, so an `onError` reporter should skip
554
+ Sentry for it.
555
+
556
+ ## Config
557
+
558
+ `@terreno/syncdb/config` resolves the app's API origin once at import (needs
559
+ `expo-constants`):
560
+
561
+ 1. `extra.BASE_URL` set to `__SAME_ORIGIN__` resolves to the page origin (same-origin web SPAs).
562
+ 2. `EXPO_PUBLIC_API_URL`, in any build.
563
+ 3. `extra.BASE_URL` in `app.json` (production/staging builds only).
564
+ 4. Expo `hostUri` plus the dev API port (simulator/device).
565
+ 5. `http://localhost:<dev API port>`.
566
+
567
+ The dev API port is `4000` unless `EXPO_PUBLIC_DEV_API_PORT` or `extra.DEV_API_PORT` sets
568
+ it. `appHeaders()` returns `App-Version` (Expo config version) and `App-Platform`
569
+ (`web` or `mobile`).
570
+
571
+ ## Auth
572
+
573
+ `@terreno/syncdb/auth` holds the Better Auth client and the session state that replace
574
+ `@terreno/rtk`'s `createBetterAuthClient` and Better Auth Redux slice. It needs the
575
+ optional peers `better-auth`, `@better-auth/expo`, `expo-secure-store`,
576
+ `@react-native-async-storage/async-storage`, and `react`.
577
+
578
+ ```tsx
579
+ import {createBetterAuthClient, SessionProvider, useSession} from "@terreno/syncdb/auth";
580
+ import {baseUrl} from "@terreno/syncdb/config";
581
+
582
+ export const betterAuthClient = createBetterAuthClient({
583
+ baseURL: baseUrl,
584
+ scheme: "myapp",
585
+ storagePrefix: "myapp",
586
+ });
587
+
588
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
589
+ <App />
590
+ </SessionProvider>;
591
+
592
+ const {userId, user, isLoading, isAuthenticated, isSigningOut, error, logout, refresh} =
593
+ useSession();
594
+ ```
595
+
596
+ `createBetterAuthClient({baseURL, scheme, storagePrefix?})` returns the Better Auth React
597
+ client with the Expo plugin. Storage is `expo-secure-store` (synchronous API) on native and
598
+ AsyncStorage on web. Reads of the plugin's cookie jar (`<storagePrefix>_cookie`) go through
599
+ `repairCookieJar`, which drops entries the plugin cannot read, so one malformed entry never
600
+ blocks every native request. `storagePrefix` defaults to `"terreno"`.
601
+
602
+ `SessionProvider` props (read once on mount):
603
+
604
+ | Prop | Type | Description |
605
+ |------|------|-------------|
606
+ | `authClient` | Better Auth client | Wrapped with `betterAuthSessionSource`. Use this or `source`. |
607
+ | `source` | `SessionSource` | Any session source (`getSession`, `signOut`, optional `subscribe`). |
608
+ | `store` | `SessionStore` | A store from `createSessionStore`, instead of the props above. Share it with non-React code. |
609
+ | `syncDbs` | `{signOut}[]` | syncdb clients signed out on `logout`, after the auth server confirms the sign-out and while the session still names the user. Create them with `wipeOnSignOut: true` to wipe local data. |
610
+ | `onLogout` | `() => void \| Promise<void>` | Extra cleanup after the auth and syncdb sign-outs. |
611
+ | `signOutTimeoutMs` | `number` | How long `logout` waits for the server's sign-out, and for the session check after a failed one. Default `10000`. |
612
+ | `pollIntervalMs` | `number` | Poll cadence when the source cannot push changes. Default `60000`; `0` turns polling off. |
613
+
614
+ When the session syncs:
615
+
616
+ - **Start:** the provider reads the session on mount. `isLoading` is `true` until it settles.
617
+ - **Sign-in, sign-out, user switch:** Better Auth's session atom (`$store.atoms.session`)
618
+ pushes the change. Only settled emissions apply: while a read is in flight (`isPending` or
619
+ `isRefetching`) the atom still carries the previous user, so a sign-out never briefly
620
+ restores them. Call `refresh()` after a sign-in to resolve the user immediately.
621
+ - **Polling:** only when the client has no session atom.
622
+ - **Logout:** `logout()` signs out of the auth server while the session stays in the state
623
+ (`isSigningOut` is `true`, `isLoading` is untouched), then signs out each `syncDbs` client,
624
+ runs `onLogout`, and clears the session. The session stays until the server answers so a
625
+ login screen that opens at once never finds the live session and signs straight back in, and
626
+ so the sync clients still know the current user for their `wipeOnSignOut` wipe. A sign-out
627
+ that fails or outlasts `signOutTimeoutMs` triggers a session check: when the session is
628
+ still live (or the server cannot be asked and the client may still hold one), `logout()`
629
+ keeps the session, wipes nothing, and sets `error` to `SIGN_OUT_FAILED_MESSAGE`; a session
630
+ that is already gone clears normally. Session pushes during a logout are held, and a
631
+ different user who signed in meanwhile wins when it ends. Concurrent `logout()` calls share
632
+ one run. A failing `syncDbs` or `onLogout` step is logged and the rest still run.
633
+
634
+ A failed startup or `refresh()` read sets `error` to `"Failed to sync session"` and clears the
635
+ session. A failed background read (atom error, poll) keeps the current session, so a network
636
+ blip is never a logout. `useSession()` throws outside a `SessionProvider`.
637
+
638
+ `betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` turns the same client into
639
+ the `AuthProvider` for `createSyncDb` and `createApiClient`. Both are also exported from the
640
+ package root.
641
+
642
+ ### Social login, account linking, and account security
643
+
644
+ `@terreno/syncdb/auth` also exports the pieces behind the example app's login and security
645
+ screens. They take a client from `createBetterAuthClient` (pass `features` to turn on the
646
+ `twoFactor`, `passkey`, `multiSession`, `lastLoginMethod` and `oneTimeToken` client plugins) and
647
+ need no store: session state comes from `useSession()`. Setup: [Configure Better
648
+ Auth](../how-to/configure-better-auth.md) and [Account security](account-security.md).
649
+
650
+ - `BetterAuthClient` — The client type without `features` (also `ReturnType<typeof createBetterAuthClient>`). `BetterAuthClientWithFeatures<Features>` — the client type with them. `BetterAuthClientFor<Features>` — which of the two `createBetterAuthClient` returns. `BetterAuthHandoffClient` — what the OAuth handoff helpers accept: any client from `createBetterAuthClient`
651
+ - `BetterAuthOAuthProvider` — `"google" | "github" | "apple"` or any generic OAuth provider id, such as `"mock-oidc"`
652
+ - `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` — Reads the token bearer mode stored, or `null`
653
+ - `signInSocialWithHandoff({client, provider, callbackURL?, errorCallbackURL?})` — Starts a social or generic OAuth sign-in for a web bearer client: stores a random nonce in `sessionStorage` and navigates the page to the server's `{basePath}/oauth-handoff/start` with `handoff=<nonce>` on the callback URLs. Throws on native and in cookie mode ([how-to](../how-to/configure-better-auth.md#social-sign-in-in-bearer-mode))
654
+ - `completeBetterAuthOAuthHandoff({client}): Promise<boolean>` — On the return page, removes `#ott` and `handoff` from the URL first, then exchanges the token at `{basePath}/one-time-token/verify` only when `handoff` matches the stored nonce. Resolves `true` only when a bearer token was stored; never rejects. Await it at startup before session reads and Sentry
655
+ - `linkSocialWithHandoff({client, provider, callbackURL, errorCallbackURL?}): Promise<{error}>` — Links a provider to the signed-in user. On web in bearer mode it posts the intent to `{basePath}/oauth-handoff/link-intent` with the bearer, then submits the returned single-use token in a hidden POST form to `startURL`, which redirects to the provider; the page comes back to `callbackURL` with no `#ott`. Both URLs may be any page on a web origin (`callbackPaths` do not apply); `errorCallbackURL` defaults to `callbackURL`. In cookie mode and on native it calls `client.linkSocial`. Resolves `{error: null}` once the browser is on its way, or `{error: {status, code?, message?}}` (such as 403 `LINKING_DISABLED`) without submitting anything. `BetterAuthLinkClient` is its client type ([how-to](../how-to/configure-better-auth.md#linking-a-provider-in-bearer-mode))
656
+ - `useAuthMethods(authClient, {skip?}): {methods, isLoading, error, refetch}` — Loads the server's enabled sign-in methods (`BetterAuthMethods`: `providers`, `emailPassword`, `signUp`, `accountLinking`, and feature flags) once per client from `{basePath}/terreno/methods` through the client's `$fetch`, so bearer and cookie modes both work. `methods` is `undefined` while loading and after an error; render no provider buttons until it arrives. `refetch()` requests them again (for Try again); `skip: true` makes no request ([response](api.md#better-auth-authentication))
657
+ - `createRememberedAccountsStore({rememberIdentity, storageKey?, max?, storage?})` — On-device store of accounts that signed in here, for the login screen. Returns `{record(entry), forget(userId), clear(), list(), lastUsedMethod(), isHydrated(), ready, getSnapshot(), subscribe(listener), dispose()}`. Each change re-reads storage first, and on web the store follows `storage` events for its key until `dispose()`, so a stale tab cannot bring back a forgotten account. A failed change rejects to its caller without blocking later changes. Persists `{userId, method, lastUsedAt}` (newest first, at most `max`, default 5, and at most 2048 bytes) under `storageKey` (default `terreno_remembered_accounts`) in SecureStore on native (`WHEN_UNLOCKED_THIS_DEVICE_ONLY` on iOS) or localStorage on web. `rememberIdentity` is required: pass `false` to keep only the method. `name` (in clear), a masked `email`, and an `https://` `image` are kept only with `rememberIdentity: true`. Unreadable or oversized stored data resets to an empty list ([how-to](../how-to/configure-better-auth.md#4-remember-accounts-on-this-device))
658
+ - `useRememberedAccounts(store): {accounts, lastUsedMethod, isHydrated, forget, clear}` — Subscribes a component to the store. `isHydrated` is `false` until native storage has loaded
659
+ - `toLoginAccountOptions(accounts)` — Maps `RememberedAccount[]` to `@terreno/ui` `LoginAccountOption[]` (`id` is the `userId`) for `LoginScreen`'s `rememberedAccounts`
660
+ - `maskEmail(email)` — `josh@mail.flourish.health` → `j***@m***.health`: first character of the local part and domain, subdomains hidden, top-level domain kept; `undefined` for non-emails
661
+ - `getBetterAuthClientWebAuth(client): {webAuth, bearerToken}` — Whether a client from `createBetterAuthClient` is in web bearer mode, and the token it holds; `{webAuth: "cookie", bearerToken: null}` on native, in cookie mode, and for other clients. No request
662
+ - `useLoginController({client, rememberedAccounts, onSignedIn, onTwoFactorRequired?, rememberIdentity?, callbackURL?, errorCallbackURL?, error?, onSignUpPress?, onForgotPasswordPress?, providerLabels?, passkeyAutofill?, skipWhenSignedIn?, webAuth?, getBearerToken?, initialEmail?})` — Drives a login screen from a Better Auth client. Returns `{step, methods, isEmailPasswordEnabled, screenProps, twoFactor, signInEmail, signInSocial, signInPasskey, continueAs, forget}`: `step` is `"checking"`, `"login"`, `"twoFactor"` or `"signedIn"`; `isEmailPasswordEnabled` is `false` when the server has email and password off (render `LoginScreen` with `fields={[]}`); `screenProps` spreads into `@terreno/ui` `LoginScreen`; `twoFactor` feeds `TwoFactorChallengeScreen`. A live session calls `onSignedIn` without showing the screen (in bearer mode it first finishes a pending OAuth handoff; with no stored token it asks the server nothing). If `onSignedIn` throws, `step` returns to `"login"` with an error. The passkey button and autofill are web-only. Device sessions (`multiSession.listDeviceSessions`, never in bearer mode) become session cards and hide remembered entries for the same user. Social sign-in uses `signInSocialWithHandoff` in bearer mode and `signIn.social` otherwise. Every finished sign-in is recorded in the store. Server refusals become notices through `getLoginFeedback` ([how-to](../how-to/configure-better-auth.md#5-drop-in-login-screen))
663
+ - `TerrenoLoginScreen` — `LoginScreen` connected through `useLoginController`; takes its options plus `fields?` (default `DEFAULT_LOGIN_FIELDS`), `title?`, `logo?`, `trustDeviceDays?`, `initialTrustDevice?` (pass `false` to start "Trust this device" unchecked), `onStepChange?` (called with each new step: `checking`, `login`, `twoFactor`, `signedIn`), `testID?` (default `login-screen`; spinner `${testID}-checking`). Renders the inline two-factor challenge unless `onTwoFactorRequired` is set
664
+ - `useAccountSecurity({client, callbackURL, methods?, errorCallbackURL?, linkError?, providerLabels?, passkeySupported?, minPasswordLength?})` — Drives an account security screen from a Better Auth client. Returns `{screenProps, methods, user, refetch}`; `screenProps` (`linkedAccounts`, `password`, `passkeys`, `twoFactor`, `sessions`, `isLoading`, `error`, `onRetry`) spreads into `@terreno/ui` `AccountSecurityScreen`. A card's props are `undefined` when the server's methods (or the client's features) leave it off; passkeys are web-only. Loads `listAccounts`, `passkey.listUserPasskeys`, `getSession({query: {disableCookieCache: true}})` and `listSessions`; each Better Auth `{error}` becomes a thrown `Error` for the card, and after each change only the affected section reloads. Never calls `unlinkAccount` for the only account, or `deletePasskey` for the only sign-in method (`LAST_SIGN_IN_METHOD_MESSAGE`). Links through `linkSocialWithHandoff`; "Set a password" posts `{basePath}/terreno/set-password`. `SESSION_NOT_FRESH` becomes `SIGN_IN_AGAIN_MESSAGE`; a change refused with `CREDENTIAL_ACCOUNT_NOT_FOUND` switches the password card to set mode (`NO_PASSWORD_YET_MESSAGE`). `getLinkErrorMessage(code)` maps a link's `?error=` to safe text ([how-to](../how-to/configure-better-auth.md#6-drop-in-security-screen))
665
+ - `TerrenoAccountSecurityScreen` — `AccountSecurityScreen` connected through `useAccountSecurity`; takes its options plus `title?`, `description?`, `header?`, `children?`, `withPage?`, `maxWidth?`, `testID?`
666
+ - `recordRememberedSignIn({store, user, method, rememberIdentity?})` — Records a sign-in the controller did not finish, such as one completed on the app's own two-factor screen
667
+ - `getLoginFeedback({failure, mode?, originalMethod?}): {notice?, error?, isSignUpClosed}` — Maps `{code?, message?, source: "response" | "url"}` to login screen feedback: `account_not_linked` / `OAUTH_LINK_ERROR` / "account not linked" to guidance for the linking mode, `VERIFY_EMAIL_TO_USE_ACCOUNT` to "verify your email", `signup_disabled` / `EMAIL_PASSWORD_SIGN_UP_DISABLED` to "ask your administrator" (and `isSignUpClosed`), `LINKING_DISABLED`, `provider_not_found`; a response's own message otherwise, and never text from a URL
668
+
669
+ ### JWT auth (legacy)
670
+
671
+ For backends on `@terreno/api`'s JWT routes (`/auth/login`, `/auth/refresh_token`). Ported
672
+ from `@terreno/rtk`'s `generateAuthSlice` and `emptyApi` base query; no Redux.
673
+
674
+ ```tsx
675
+ import {jwtAuthAdapter, SessionProvider, useJwtAuth, useSession} from "@terreno/syncdb/auth";
676
+ import {createApiClient} from "@terreno/syncdb/api";
677
+ import {appHeaders, baseUrl} from "@terreno/syncdb/config";
678
+
679
+ export const jwtAuth = jwtAuthAdapter({baseUrl, headers: appHeaders});
680
+ export const apiClient = createApiClient({authProvider: jwtAuth, baseUrl, headers: appHeaders});
681
+ export const syncDb = createSyncDb({authProvider: jwtAuth, baseUrl, /* … */});
682
+
683
+ <SessionProvider source={jwtAuth} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
684
+ <App />
685
+ </SessionProvider>;
686
+
687
+ // In a screen:
688
+ const {login, signup, googleLogin, resetPassword, logout, isAuthenticating, error} =
689
+ useJwtAuth({adapter: jwtAuth});
690
+ await login({email, password});
691
+ const {userId, isAuthenticated} = useSession();
692
+ ```
693
+
694
+ `jwtAuthAdapter(options)` options:
695
+
696
+ | Option | Type | Description |
697
+ |--------|------|-------------|
698
+ | `baseUrl` | `string` | API origin. Required. |
699
+ | `storage` | `JwtTokenStorage` | Token storage. Default: `expo-secure-store` on native, AsyncStorage on web (keys `AUTH_TOKEN`, `REFRESH_TOKEN`; nothing is stored during SSR). |
700
+ | `paths` | `Partial<JwtAuthPaths>` | Route overrides. Defaults: `login` `/auth/login`, `signup` `/auth/signup`, `refreshToken` `/auth/refresh_token`, `googleLogin` `/auth/google`, `resetPassword` `/resetPassword`. |
701
+ | `headers` | object or function | Extra headers on auth requests (e.g. `appHeaders`). |
702
+ | `refreshBeforeExpirySecs` | `number` | Refresh when the access token has fewer seconds left. Default `130`. |
703
+ | `fetchImpl` | `FetchLike` | Custom `fetch`. |
704
+ | `timeoutMs` | `number` | Auth request timeout. Default `30000`. |
705
+
706
+ The adapter is an `AuthProvider` and a `SessionSource`:
707
+
708
+ | Member | Behaviour |
709
+ |--------|-----------|
710
+ | `getToken()` | Stored access token. Under `refreshBeforeExpirySecs` left (or expired), refreshes first. Both tokens expired: signs out and returns `null`. |
711
+ | `refresh()` | `POST /auth/refresh_token` with `{refreshToken}`; stores the new pair and resolves `true`. Concurrent calls share one request. `createApiClient` calls it on a 401 and retries once; `createSyncDb` calls it on an auth pause. |
712
+ | `getUserId()` / `getSession()` | The stored token's `id` claim; `getSession()` returns `{userId, user: {id}}` or `null`. |
713
+ | `onAuthChange(cb)` / `subscribe(listener)` | Fire on login, logout, and token rotation. |
714
+ | `login(body)` | `POST /auth/login` with `{email \| username, password}`; stores the tokens and signs in. |
715
+ | `signup(body)` | `POST /auth/signup` with `{email, password, ...extra}`; stores the tokens and signs in. |
716
+ | `googleLogin({idToken})` | `POST /auth/google`; stores the tokens and signs in. |
717
+ | `resetPassword(body)` | `POST /resetPassword` (`{token, password}`); returns `{token, refreshToken, userId}` without signing in. |
718
+ | `setTokens({token, refreshToken?})` | Store a pair from a custom route and sign in. |
719
+ | `logout()` / `signOut()` | Clear both tokens and notify listeners. JWTs are stateless, so no server call. |
720
+
721
+ Failed requests reject with `ApiError`. Its `message` is the server's `title`, or the
722
+ `message` the JWT routes send on a 401 (for example `"Password or username is incorrect"`).
723
+ Status `0` means the request never got a response (`data.code` `network-error` or
724
+ `request-timeout`).
725
+
726
+ When a refresh fails:
727
+
728
+ - The server rejects it (401, missing tokens in the response, any other HTTP error), or no
729
+ refresh token is stored: both tokens are cleared and the session signs out. syncdb clients
730
+ pause (they never wipe on a sign-out they did not request).
731
+ - Network error or timeout: the session and current token stay; the next request retries.
732
+
733
+ `useJwtAuth({adapter})` must render inside the `SessionProvider` whose `source` is `adapter`.
734
+ `login`, `signup`, `googleLogin`, and `resetPassword` set `isAuthenticating` while pending,
735
+ store the failure message in `error` (cleared when the next request starts), and reject with
736
+ the error. `logout` is the `SessionProvider` logout.
737
+
738
+ Token helpers (all take an optional `{storage, now}`):
739
+
740
+ | Function | Returns |
741
+ |----------|---------|
742
+ | `getTokenExpirationTimes()` | `{authRemainingSecs, refreshRemainingSecs}`; both `undefined` unless both tokens are stored. |
743
+ | `shouldShowStillThereModal()` | `true` when the refresh token has 65 seconds or less left. |
744
+ | `readJwtTokens(storage?)` | `{token, refreshToken}` from storage. |
745
+ | `decodeJwtClaims(token)` | The token's claims (unverified), or `null`. |
746
+
747
+ ## App utilities
748
+
749
+ `@terreno/syncdb/app` holds app-shell helpers that replace their `@terreno/rtk`
750
+ versions. It needs `react`, `react-native`, and `expo-constants`; `useMCPTools` also
751
+ needs `@modelcontextprotocol/client`. All are optional peer dependencies.
752
+
753
+ ```typescript
754
+ import {
755
+ canOpenAdminPage,
756
+ installDevConsoleLogger,
757
+ useMCPTools,
758
+ useUpgradeCheck,
759
+ } from "@terreno/syncdb/app";
760
+
761
+ installDevConsoleLogger(); // module scope; no-op outside __DEV__
762
+
763
+ const {isRequired, isWarning, onUpdate} = useUpgradeCheck({recheckOnForeground: true});
764
+ const isAdmin = canOpenAdminPage({admin: me?.admin, permissions: me?.permissions});
765
+ const {tools} = useMCPTools({authProvider}); // the provider passed to createSyncDb
766
+ ```
767
+
768
+ | Export | Description |
769
+ |--------|-------------|
770
+ | `useUpgradeCheck({pollingIntervalMs, recheckOnForeground, client})` | Calls `GET /version-check` through the request client (`ApiClientProvider` or `client`) with `extra.buildNumber`. Returns `{isRequired, isWarning, requiredMessage, warningMessage, warningCheckCount, canUpdate, onUpdate}`. Skips the check when there is no build number. The server's `pollingIntervalMs` replaces the local fallback after the first response. |
771
+ | `hasPermission(permissions, request)` | True when the `/auth/me` `permissions` map grants every requested action |
772
+ | `canOpenAdminPage({admin, permissions})` | With RBAC, only `admin:access` opens the admin UI; without it, falls back to `user.admin` |
773
+ | `installDevConsoleLogger({baseUrl})` | In `__DEV__`, batches `console.error` / `console.warn` and global errors to `POST /__terreno/browser-logs`. Safe to call more than once. |
774
+ | `useMCPTools({authProvider, baseURL})` | Lists the backend's `/mcp` tools with the official MCP client. The token comes from `authProvider.getToken()` on each fetch, so Better Auth and JWT sessions both work. Returns `{tools, isLoading, error, refetch}`. |
775
+
776
+ `@terreno/syncdb/buildNumber` is node-only (it runs `git`). Import it from
777
+ `app.config.ts`, never from app code:
778
+
779
+ ```typescript
780
+ import {resolveBuildNumber} from "@terreno/syncdb/buildNumber";
781
+
782
+ extra: {buildNumber: resolveBuildNumber({configValue: config.extra?.buildNumber})}
783
+ ```
784
+
785
+ `resolveBuildNumber` order: `override`, `configValue`, `EXPO_PUBLIC_BUILD_NUMBER` (or
786
+ `envVar`), `git rev-list --count HEAD`, then `undefined` (the check is skipped).
787
+
788
+ ## Feature flags
789
+
790
+ `@terreno/syncdb/flags` reads `@terreno/feature-flags` flags with the request client and
791
+ refreshes them when the server emits `featureFlagsChanged` on the sync socket — no second
792
+ socket. Requires the optional peers `@openfeature/web-sdk` (and `@openfeature/react-sdk` for
793
+ OpenFeature React hooks).
794
+
795
+ ```typescript
796
+ import {useFeatureFlags, useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
797
+
798
+ // Root, above SyncDbProvider: install flags into OpenFeature and listen for pushes.
799
+ useTerrenoFeatureFlags({skip: !userId, socket: syncDb, userId});
800
+
801
+ // Screens: flat accessors over the same cached query.
802
+ const {getFlag, getVariant, isLoading} = useFeatureFlags({skip: !userId, socket: null, userId});
803
+ ```
804
+
805
+ Options and results: [feature flags reference](./feature-flags.md#terrenosyncdbflags).
806
+
456
807
  ## Query windows
457
808
 
458
809
  Use query windows when a collection is too large to sync whole (messages, events, audit
@@ -567,6 +918,56 @@ terreno-syncdb-codegen \
567
918
 
568
919
  Do not edit the generated file. `--collections` filters when extensions exist. When they do not, it reads list/create/patch schemas from `GET /{name}` (or `/{name}/`). A missing path, a list response without `data.items`, a spec with no extensions and no `--collections` all exit non-zero.
569
920
 
921
+ A collection name is the route path without its leading slash, so a nested route keeps its
922
+ `/`: `modelRouter("/gpt/histories", …)` syncs as `gpt/histories`, and
923
+ `useQuery("gpt/histories")` reads it. Names are letters, digits, `_`, and `-` in
924
+ `/`-separated segments. Hooks drop the separators and singularize the last segment when the
925
+ list item schema has no `$ref` name: `gpt/histories` → `GptHistory`, `useGptHistories`.
926
+
927
+ ### API SDK (`--api-out`)
928
+
929
+ `--api-out <file>` writes typed request hooks for every operation that is **not** synced
930
+ collection CRUD: auth (`/auth/me`), modelRouter actions, AI, consents, announcements,
931
+ version check, and plain REST routes. It replaces `@rtk-query/codegen-openapi`. Pass
932
+ `--out`, `--api-out`, or both in one run.
933
+
934
+ ```bash
935
+ terreno-syncdb-codegen \
936
+ --schema http://localhost:4000/openapi.json \
937
+ --out ./store/syncDbSdk.ts \
938
+ --api-out ./store/apiSdk.ts \
939
+ --config ./syncdb-codegen.json
940
+ ```
941
+
942
+ | Generated per operation | Example (`GET /gpt/datasets/{id}?historyId=`) |
943
+ |-------------------------|-----------------------------------------------|
944
+ | `<Name>Args` | `{id: string; historyId: string}` |
945
+ | `<Name>Res` | Response after envelope unwrapping (`{data: X}` → `X`; lists keep `more`) |
946
+ | `<name>Request(arg)` | `ApiRequestArgs` for `apiClient.request(...)` |
947
+ | `use<Name>Query` / `useLazy<Name>Query` | GET operations; `providesTags` from the operation's OpenAPI tags |
948
+ | `use<Name>Mutation` | Other methods; `invalidatesTags` from the operation's OpenAPI tags |
949
+
950
+ Names follow the RTK codegen rule, so migrated screens keep their hook names: the
951
+ camelCased `operationId`, otherwise method + path with `{param}` read as `By<Param>`
952
+ (`PATCH /gpt/histories/{id}/rating` → `usePatchGptHistoriesByIdRatingMutation`). Arguments flatten
953
+ the same way: one path param, query param, or body is the argument itself; otherwise an
954
+ object of path params, query params, and `body`. A `multipart/form-data` body is
955
+ `FormData`. Two operations that would share a name exit non-zero; give one an
956
+ `operationId`. `excludePaths` in the config file (path prefixes, or regexes starting
957
+ with `^`) leaves routes out.
958
+
959
+ A paginated custom route must return `more` with its `data` (as `GET /announcements/feed`
960
+ does: `{data, more, page, total}`). Without `more`, the client unwraps the envelope to the
961
+ `data` array and drops `page` and `total`.
962
+
963
+ ```typescript
964
+ import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
965
+
966
+ const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
967
+ const [updateMe] = usePatchAuthMeMutation();
968
+ await updateMe({name: "Ada"}).unwrap();
969
+ ```
970
+
570
971
  ## Conflict API
571
972
 
572
973
  A **conflict** occurs when the client's `baseVersion` (last seen `_syncSeq`) does not match the server's current seq. The server responds with a `conflict` nack (HTTP **409** on `POST /sync/mutate`, or `sync:nack` with `code: "conflict"` on the socket) carrying the canonical server document.
@@ -702,6 +1103,7 @@ Conflict responses on mutate: **409** with `{nack}` body (`code: "conflict"`).
702
1103
  | `sync:nack` | server → client | `{mutationId, code, serverDoc?, serverSeq?, serverDeleted?, message?, retryAfterMs?}` |
703
1104
  | `sync:mutateBatch` | client → server | `{mutations: SyncMutateRequest[], batchId?}` |
704
1105
  | `sync:auth-expired` | server → client | `{reason: "expired" \| "disabled"}` — session re-validation sweep; client enters auth-pause |
1106
+ | any other name | server → client | App events (for example `featureFlagsChanged` `{key}`), delivered to `onSocketEvent` / `useSocketEvent` listeners |
705
1107
 
706
1108
  Limits: 50 collection subscriptions per socket; 100 mutations/second per socket (each batch mutation counts); batches capped at 100.
707
1109
 
@@ -730,6 +1132,12 @@ const syncDb = createSyncDb({
730
1132
  });
731
1133
  ```
732
1134
 
1135
+ `start()` resolves the key before it loads the store, while the session is still valid.
1136
+ The derived key is cached in IndexedDB, so the flush in `stop()` never fetches
1137
+ `/sync/key`. Logging out while `start()` is still running keeps pending outbox writes.
1138
+ If the key fetch fails (an offline cold start), `start()` still resolves and the next
1139
+ save retries the fetch.
1140
+
733
1141
  Saves are debounced (500 ms). The store is serialized once per write, not once per
734
1142
  transaction: autosave compares TinyBase's content hashes (O(1)) to skip saves the last
735
1143
  write already covered, so a bootstrap of many pages costs a few writes instead of one
@@ -758,15 +1166,16 @@ transport.respondWithAck();
758
1166
  transport.deliverDelta({...});
759
1167
  ```
760
1168
 
761
- `createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders.
1169
+ `createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders. `transport.emitEvent(name, ...args)` delivers an app event to `onSocketEvent` listeners; `eventListenerCount(name)` reports how many are bound.
762
1170
 
763
1171
  ## Environment variables
764
1172
 
765
- Syncdb reads no environment variables directly. Host apps typically set:
1173
+ `@terreno/syncdb/config` reads these at import; the core client reads none. Host apps typically set:
766
1174
 
767
1175
  | Variable | Used by | Description |
768
1176
  |----------|---------|-------------|
769
- | `EXPO_PUBLIC_API_URL` | `@terreno/rtk` `baseUrl` | Backend origin passed to `createSyncDb({baseUrl})` |
1177
+ | `EXPO_PUBLIC_API_URL` | `@terreno/syncdb/config` `baseUrl` | Backend origin passed to `createSyncDb({baseUrl})` and `createApiClient({baseUrl})` |
1178
+ | `EXPO_PUBLIC_DEV_API_PORT` | `@terreno/syncdb/config` | Local dev API port (default `4000`) |
770
1179
 
771
1180
  Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for `RealtimeApp` change streams.
772
1181
 
@@ -778,8 +1187,9 @@ Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for
778
1187
  - **No `bulkWrite` / `updateMany` / `deleteMany`** on synced models — use per-document loops.
779
1188
  - **Do not write reserved tables** (`_outbox`, `_cursors`, `_conflicts`) directly — use `client.mutate`, hooks, and `resolveConflict`.
780
1189
  - **User switch wipes local data** — confirmed different-user login clears the previous user's store; bare logout/401 does not (INV-2).
781
- - **Keep `@terreno/rtk` for non-synced routes** — generated OpenAPI hooks remain
782
- the right tool for custom REST endpoints, auth, feature flags, and ObjectId
783
- admin compatibility CRUD. Eligible admin String-`_id` collection CRUD uses
784
- windowed syncdb; framework admin RPC uses its host-bound fetch client. See
785
- [How to migrate from RTK to syncdb](../how-to/migrate-rtk-to-syncdb.md).
1190
+ - **Non-synced routes use the request client** — `/auth/me`, modelRouter actions, AI,
1191
+ and other request/response routes go through generated `store/apiSdk.ts` hooks
1192
+ (`terreno-syncdb-codegen --api-out`) or `useApiQuery` / `useApiMutation`. Eligible
1193
+ admin String-`_id` collection CRUD uses windowed syncdb; other admin requests use
1194
+ admin-frontend's host-bound fetch client. See
1195
+ [How to upgrade from @terreno/rtk to @terreno/syncdb](../how-to/migrate-rtk-to-syncdb.md).
@@ -282,6 +282,28 @@ per-file, per-rule counts so CI fails only on new violations. Suppress one line
282
282
  `test-hygiene-disable-next-line <rule>[, <rule>]` comment. Exit codes: 0 clean, 1 new
283
283
  violations, 2 usage error.
284
284
 
285
+ ### Documented-response checks (`@terreno/api/testing`)
286
+
287
+ `expectDocumentedResponse({spec, path, method, response})` validates a real response body
288
+ against the schema `/openapi.json` documents for that path, method, and status (AJV, with
289
+ `#/components/...` refs and `nullable` resolved). It throws listing every violation, or when
290
+ the route, status, or media type is not documented. `validateDocumentedResponse` returns
291
+ `{valid, errors}` instead of throwing. `path` is the OpenAPI template
292
+ (`/announcements/{id}/click`); pass the supertest response as `response`.
293
+
294
+ `@terreno/api/testing` also registers api's own test models (including `User`). A package
295
+ that defines its own `User` model imports the same two functions from the side-effect-free
296
+ `@terreno/api/testing/openApi` entry instead, so one test process never compiles `User` twice.
297
+
298
+ ```typescript
299
+ import {authAsUser, expectDocumentedResponse} from "@terreno/api/testing";
300
+
301
+ const spec = (await supertest(app).get("/openapi.json")).body;
302
+ const agent = await authAsUser(app, "notAdmin");
303
+ const res = await agent.get("/auth/me").expect(200);
304
+ expectDocumentedResponse({method: "get", path: "/auth/me", response: res, spec});
305
+ ```
306
+
285
307
  ## Smoke helpers
286
308
 
287
309
  A package smoke is a real-process check of the package's built output. It lives in
@@ -347,7 +369,7 @@ exits 0 on a 2-vCPU worker before it prints the `All files` row. When that
347
369
  row is still missing after one retry, the script uses `coverage/lcov.info`
348
370
  only if it lists every non-test source file under `src/`. Dedicated CircleCI
349
371
  jobs (`api-ci`, `ai-ci`, `blocks-ci`,
350
- `rtk-ci`, `ui-ci`, `syncdb-ci`, `comms-ci`, `mcp-server-ci`, `admin-spa-ci`) run
372
+ `ui-ci`, `syncdb-ci`, `comms-ci`, `mcp-server-ci`, `admin-spa-ci`) run
351
373
  that script. Published packages without a dedicated workflow
352
374
  (`admin-backend`, `admin-frontend`, `api-health`, `feature-flags`, `@terreno/test`)
353
375
  run the same lint, compile, and coverage commands via the parameterized
@@ -357,7 +379,7 @@ including `.github/workflows/packages-ci.yml`. Those twins run
357
379
  pass `secrets.CODECOV_TOKEN` into `upload-codecov`.
358
380
 
359
381
  Each of those jobs then uploads `coverage/lcov.info` to Codecov with a distinct
360
- flag (`api`, `ui`, `rtk`, …) via `scripts/upload-codecov.sh`. The script
382
+ flag (`api`, `ui`, `syncdb`, …) via `scripts/upload-codecov.sh`. The script
361
383
  downloads the linux uploader over HTTPS and refuses to run it unless the
362
384
  SHA-256 digest matches the pin in the script (`CODECOV_UPLOADER_SHA256`).
363
385
  `codecov.yml` sets per-package flags, `target: auto` with a 1% threshold so
@@ -394,7 +416,10 @@ registered `DemoConfig` demo and story through `renderWithTheme` from
394
416
  Pull requests also run the `New file coverage` workflow. Every newly added workspace
395
417
  `.ts` or `.tsx` implementation file must have at least 90% function coverage and 90%
396
418
  line coverage. Test, spec, story, generated OpenAPI SDK, `dist`, isolated-test,
397
- `src/types` type modules, demo `story-config/*.config.tsx`, and Expo Router route
419
+ `src/types` type modules, modules whose only exports are `interface` and `type`
420
+ aliases (comments and string literals do not count; TypeScript erases them, so
421
+ Bun writes no LCOV record), demo
422
+ `story-config/*.config.tsx`, and Expo Router route
398
423
  files (`index`, `_layout`, `+not-found`, `[param]`, plus example recovery screens
399
424
  `forgotPassword` / `resetPassword` / `verifyEmail`) are excluded. A new implementation
400
425
  file that is absent from LCOV is treated as 0% covered, except files listed in