@terreno/agent-plugin 57.21.0 → 58.0.0-beta.2

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.
@@ -28,5 +28,5 @@
28
28
  "name": "terreno",
29
29
  "repository": "https://github.com/TerrenoLabs/terreno",
30
30
  "skills": "./skills/",
31
- "version": "57.21.0"
31
+ "version": "58.0.0-beta.2"
32
32
  }
package/CHANGELOG.md CHANGED
@@ -20,6 +20,95 @@ Upgrade notes for consumer action live in [`mcp-server/src/docs/upgrades/`](mcp-
20
20
 
21
21
  Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/). Add one Markdown file per feature (see that directory's README) instead of editing this section.
22
22
 
23
+ ## [58.0.0-beta.2] - 2026-11-03
24
+
25
+ Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.2.md`](mcp-server/src/docs/upgrades/58.0.0-beta.2.md).
26
+
27
+ ### Breaking
28
+
29
+ - Terreno 58 targets Expo SDK 58 (`~58.0.14`), React Native `0.88.0`, and React `19.3.0`. `@terreno/ui` peers `react-native` `0.88.0`. Native catalog moves with the SDK: `react-native-gesture-handler` `~3.3.0`, `react-native-reanimated` `4.7.1` + `react-native-worklets` `0.13.0`, `react-native-screens` `~4.30.0`, `react-native-webview` `14.0.1`, `@sentry/react-native` `~8.28.0`, `@shopify/flash-list` `2.3.2`, `@shopify/react-native-skia` `2.14.0`. Rebuild dev clients and store binaries.
30
+ - Expo Router 58: set `export const unstable_settings = {initialRouteName}` on the layout instead of passing `initialRouteName` to `<Slot>` / navigators. `SidebarNavigation` ignores the prop.
31
+ - Expo Router 58 static rendering: a custom `app/+html.tsx` must render `headNodes` and `bodyNodes` from `useServerDocumentContext()` (`expo-router/html`), or exported pages ship without their JavaScript. `example-frontend` is updated.
32
+ - React Native 0.88 generated types: type refs as `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance` (`Box.scrollRef`, `TextField.inputRef`, `Text` `ref`).
33
+ - `create-terreno-app` scaffolds Expo `~58.0.14`, React Native `0.88.0`, and React `19.3.0`.
34
+ - The Terreno 58 native module baseline lands in `example-frontend` and `demo`: `@stripe/stripe-react-native` 0.81.0, `react-native-purchases` and `react-native-purchases-ui` 10.12.2, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, and `expo-build-properties` (Android Kotlin 2.2.21 and `compileSdkVersion` 37 pins). Rebuild every dev client and store binary. `react-native-otp-verify` is left out because it does not support the New Architecture. The demo has a hidden **Native Modules** page that shows each module's version and whether the running binary links it. See the 58.0.0 upgrade note.
35
+ - `TerrenoApp` turns HTTP rate limiting on by default. Omitting `rateLimit` now mounts the limiter with the memory store (auth 20 / 15 min, api 600 / 15 min). Pass `rateLimit: false` to turn it off. On Cloud Run pass `rateLimit: {trustProxy: 1}` so unauthenticated traffic keys by client IP. See the 58.0.0 upgrade note.
36
+ - `@terreno/admin-frontend` no longer takes an RTK `api`. The `api` prop is removed from every admin component (`AdminProvider`, `AdminShellLayout`, `AdminHome`, `AdminModelList`, `AdminModelTable`, `AdminModelForm`, `AdminScreenRouter`, `ConfigurationScreen`, `DocumentStorageBrowser`, `ConsentHistory`, comms, jobs, org, consent, and announcement screens, and widget props), and admin hooks drop their `api` argument (`useAdminConfig(apiBase)`, `useAdminScripts(apiBase)`, `useAdminRoles(apiBase)`, `useConfigurationApi({basePath})`, `useDocumentStorageApi(basePath)`, `useConsentHistory(baseUrl)`, `useOrganizationsApi(basePath)`, `useCommsDashboardApi()`, `useJobsDashboardApi()`, and the `useAiObservability*Api()` hooks).
37
+ - `useAdminApi(api, routePath, modelName)` is removed. Use `useAdminModelApi(routePath, modelName)`, which keeps the same hook names and calls the same `/admin/{routePath}` URLs through the admin fetch client. `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are gone; `@reduxjs/toolkit` and `react-redux` are no longer peers of admin-frontend.
38
+ - `AdminProvider` is now the required auth binding for every admin request, including ObjectId and API-only model CRUD. Cookie SPAs pass `credentials="same-origin"`; embedded apps pass `apiOrigin` and `getAuthHeaders`. Product screens that reuse admin components wrap them in `AdminProvider`. See the 58.0.0 upgrade guide.
39
+ - Removed the legacy RTK realtime path. Use `sync` on `modelRouter` with `@terreno/syncdb` for live collection updates. `RealtimeApp`, `SyncApp`, socket auth, `sync:delta`, and admin `admin:model.changed` events are unchanged. See the 58.0.0 upgrade note.
40
+ - `@terreno/api`: `modelRouter` no longer accepts `realtime` (now a TypeScript error), and the change stream no longer emits the `sync` websocket event. Removed `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, `QuerySubscription`, `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`. Sockets no longer handle `subscribe:model`, `subscribe:document`, or `subscribe:query`.
41
+ - `@terreno/rtk`: removed `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and the `RealtimeEvent` type. The rest of `@terreno/rtk` (including `useSocketConnection` and `useRealtimeDebug`) is removed in the same release; see `remove-rtk.md`.
42
+ - Removed `@terreno/rtk` and every Redux dependency (`@reduxjs/toolkit`, `react-redux`, `redux-persist`, `@rtk-query/codegen-openapi`). `@terreno/rtk` is not published for 58. `@terreno/syncdb` is the only frontend data, request, and session layer. Follow `docs/how-to/migrate-rtk-to-syncdb.md`, which maps every removed export to its replacement, and the 58.0.0 upgrade note.
43
+ - `@terreno/syncdb` adds `@terreno/syncdb/api` (`createApiClient`, `ApiError`), request hooks in `@terreno/syncdb/react` (`ApiClientProvider`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`), `@terreno/syncdb/config` (`baseUrl`, `baseWebsocketsUrl`, `appHeaders`), `@terreno/syncdb/auth` (`createBetterAuthClient`, `SessionProvider`, `useSession`, `jwtAuthAdapter`, `useJwtAuth`, token-expiry helpers), `@terreno/syncdb/flags` (`useFeatureFlags`, `useTerrenoFeatureFlags` on the sync socket), `@terreno/syncdb/app` (`useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`, `useMCPTools`), and `@terreno/syncdb/buildNumber`.
44
+ - `terreno-syncdb-codegen --api-out` generates typed hooks for every non-synced OpenAPI operation (same names as the RTK codegen), replacing `@rtk-query/codegen-openapi`. `bun run sdk` writes `store/apiSdk.ts`.
45
+ - `@terreno/ui` `ConsentNavigator`, `AnnouncementNavigator`, `useConsentForms`, `useSubmitConsent`, `useAnnouncements`, and `useAcknowledgeAnnouncement` take a request `client` instead of an RTK `api`.
46
+ - `GptHistory` (`gpt/histories`) is a synced collection and keeps its ObjectId `_id` (no migration). `McpServiceToken` (`mcp/service-tokens`, read-only) is a synced collection with String ids; run `mcpServiceTokenStringIdsMigration` (`@terreno/api`) before deploying 58 frontends.
47
+ - `create-terreno-app`, the `terreno_bootstrap` prompt, and the `terreno_generate_screen` tool scaffold apps without Redux or `@terreno/rtk`. The terreno-mcp-local `get_rtk_state` tool, the MCP resource `terreno://docs/rtk`, and `terreno state --slice rtk` are removed; use `get_syncdb_state` and `terreno state`.
48
+
49
+ ### Added
50
+
51
+ - `DataTable` columns size themselves when `width` is omitted. Each auto column starts at the wider of its header and its content (sampled from at most 50 rows, measured with a canvas on web and estimated on native), clamped to 80–360px, and auto columns grow to fill the table's container. Columns that set `width` keep it. `DataTableColumn.width` is now optional.
52
+ - `@terreno/syncdb` `createSyncDb({idFormats})` mints 24-hex ObjectIds for a collection set to `"objectId"`, so a model that keeps Mongoose's default ObjectId `_id` can sync without migrating its ids. `mutate()` throws on an explicit non-hex id for such a collection. `generateObjectId` and the `SyncIdFormat` type are exported.
53
+ - Sync `validation` nacks carry an optional `reason`: `invalid_id` when an ObjectId-keyed collection gets a non-hex id (rejected before the mutation is claimed), and `id_collision` when a create names an `_id` another document holds. A collision never touches the existing document and its message never names the holder. The ledger records the reason (`SyncMutation.nackReason`) so a redelivery replays it.
54
+ - `GET /sync/entities` leaves out non-hex ids for an ObjectId-keyed model instead of answering 400, so a rejected create queued for repair cannot fail the rest of the repair batch.
55
+
56
+ ### Changed
57
+
58
+ - The repository's agent rules now generate with rulesync 17 (up from 9). The root rule `.rulesync/rules/00-root.md` now generates `AGENTS.md`, which Devin and other AGENTS.md readers load, in place of `.devin/rules/00-root.md`. Never edit `AGENTS.md` by hand: edit the root rule and run `bun run rules`. The examples that existed only in the old hand-edited `AGENTS.md` (syncdb and RTK SDK usage) moved into the root rule, so `CLAUDE.md`, the Cursor root rule, and Copilot instructions get them too. Links in the root rule now resolve from the repository root. Generated Copilot hooks use the portable `command` field. Generated Copilot subagents no longer list `agent/runSubagent` as their only tool, so they get every default tool. Generated skills carry their `license`, `disable-model-invocation`, and Devin `triggers` frontmatter.
59
+ - `@terreno/syncdb/auth` `SessionProvider`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`useSession().isSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
60
+
61
+ ### Removed
62
+
63
+ - `@terreno/ui` no longer exports the `Filter` and `FilterProps` aliases deprecated in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps` instead. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names.
64
+
65
+ ### Fixed
66
+
67
+ - `@terreno/syncdb`: logging out while `syncDb.start()` is still running no longer loses local writes or logs errors. `start()` now resolves the web encryption key from `GET /sync/key` before it loads the store, so the flush in `stop()` never needs the network after the session has ended. A `stop()` or `signOut()` call now cancels a `start()` that is running or queued ahead of it, so the transport never connects without a session. A failed key fetch is now retried on the next save instead of failing every save for the rest of the session.
68
+
69
+ ## [58.0.0-beta.1] - 2026-11-02
70
+
71
+ Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.1.md`](mcp-server/src/docs/upgrades/58.0.0-beta.1.md).
72
+
73
+ ### Breaking
74
+
75
+ - Terreno 58 targets Expo SDK 58 (`~58.0.14`), React Native `0.88.0`, and React `19.3.0`. `@terreno/ui` peers `react-native` `0.88.0`. Native catalog moves with the SDK: `react-native-gesture-handler` `~3.3.0`, `react-native-reanimated` `4.7.1` + `react-native-worklets` `0.13.0`, `react-native-screens` `~4.30.0`, `react-native-webview` `14.0.1`, `@sentry/react-native` `~8.28.0`, `@shopify/flash-list` `2.3.2`, `@shopify/react-native-skia` `2.14.0`. Rebuild dev clients and store binaries.
76
+ - Expo Router 58: set `export const unstable_settings = {initialRouteName}` on the layout instead of passing `initialRouteName` to `<Slot>` / navigators. `SidebarNavigation` ignores the prop.
77
+ - Expo Router 58 static rendering: a custom `app/+html.tsx` must render `headNodes` and `bodyNodes` from `useServerDocumentContext()` (`expo-router/html`), or exported pages ship without their JavaScript. `example-frontend` is updated.
78
+ - React Native 0.88 generated types: type refs as `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance` (`Box.scrollRef`, `TextField.inputRef`, `Text` `ref`).
79
+ - `create-terreno-app` scaffolds Expo `~58.0.14`, React Native `0.88.0`, and React `19.3.0`.
80
+ - The Terreno 58 native module baseline lands in `example-frontend` and `demo`: `@stripe/stripe-react-native` 0.81.0, `react-native-purchases` and `react-native-purchases-ui` 10.12.2, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, and `expo-build-properties` (Android Kotlin 2.2.21 and `compileSdkVersion` 37 pins). Rebuild every dev client and store binary. `react-native-otp-verify` is left out because it does not support the New Architecture. The demo has a hidden **Native Modules** page that shows each module's version and whether the running binary links it. See the 58.0.0 upgrade note.
81
+ - `TerrenoApp` turns HTTP rate limiting on by default. Omitting `rateLimit` now mounts the limiter with the memory store (auth 20 / 15 min, api 600 / 15 min). Pass `rateLimit: false` to turn it off. On Cloud Run pass `rateLimit: {trustProxy: 1}` so unauthenticated traffic keys by client IP. See the 58.0.0 upgrade note.
82
+ - `@terreno/admin-frontend` no longer takes an RTK `api`. The `api` prop is removed from every admin component (`AdminProvider`, `AdminShellLayout`, `AdminHome`, `AdminModelList`, `AdminModelTable`, `AdminModelForm`, `AdminScreenRouter`, `ConfigurationScreen`, `DocumentStorageBrowser`, `ConsentHistory`, comms, jobs, org, consent, and announcement screens, and widget props), and admin hooks drop their `api` argument (`useAdminConfig(apiBase)`, `useAdminScripts(apiBase)`, `useAdminRoles(apiBase)`, `useConfigurationApi({basePath})`, `useDocumentStorageApi(basePath)`, `useConsentHistory(baseUrl)`, `useOrganizationsApi(basePath)`, `useCommsDashboardApi()`, `useJobsDashboardApi()`, and the `useAiObservability*Api()` hooks).
83
+ - `useAdminApi(api, routePath, modelName)` is removed. Use `useAdminModelApi(routePath, modelName)`, which keeps the same hook names and calls the same `/admin/{routePath}` URLs through the admin fetch client. `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are gone; `@reduxjs/toolkit` and `react-redux` are no longer peers of admin-frontend.
84
+ - `AdminProvider` is now the required auth binding for every admin request, including ObjectId and API-only model CRUD. Cookie SPAs pass `credentials="same-origin"`; embedded apps pass `apiOrigin` and `getAuthHeaders`. Product screens that reuse admin components wrap them in `AdminProvider`. See the 58.0.0 upgrade guide.
85
+ - Removed the legacy RTK realtime path. Use `sync` on `modelRouter` with `@terreno/syncdb` for live collection updates. `RealtimeApp`, `SyncApp`, socket auth, `sync:delta`, and admin `admin:model.changed` events are unchanged. See the 58.0.0 upgrade note.
86
+ - `@terreno/api`: `modelRouter` no longer accepts `realtime` (now a TypeScript error), and the change stream no longer emits the `sync` websocket event. Removed `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, `QuerySubscription`, `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`. Sockets no longer handle `subscribe:model`, `subscribe:document`, or `subscribe:query`.
87
+ - `@terreno/rtk`: removed `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and the `RealtimeEvent` type. The rest of `@terreno/rtk` (including `useSocketConnection` and `useRealtimeDebug`) is removed in the same release; see `remove-rtk.md`.
88
+ - Removed `@terreno/rtk` and every Redux dependency (`@reduxjs/toolkit`, `react-redux`, `redux-persist`, `@rtk-query/codegen-openapi`). `@terreno/rtk` is not published for 58. `@terreno/syncdb` is the only frontend data, request, and session layer. Follow `docs/how-to/migrate-rtk-to-syncdb.md`, which maps every removed export to its replacement, and the 58.0.0 upgrade note.
89
+ - `@terreno/syncdb` adds `@terreno/syncdb/api` (`createApiClient`, `ApiError`), request hooks in `@terreno/syncdb/react` (`ApiClientProvider`, `useApiQuery`, `useLazyApiQuery`, `useApiMutation`, `useSocketEvent`), `@terreno/syncdb/config` (`baseUrl`, `baseWebsocketsUrl`, `appHeaders`), `@terreno/syncdb/auth` (`createBetterAuthClient`, `SessionProvider`, `useSession`, `jwtAuthAdapter`, `useJwtAuth`, token-expiry helpers), `@terreno/syncdb/flags` (`useFeatureFlags`, `useTerrenoFeatureFlags` on the sync socket), `@terreno/syncdb/app` (`useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`, `useMCPTools`), and `@terreno/syncdb/buildNumber`.
90
+ - `terreno-syncdb-codegen --api-out` generates typed hooks for every non-synced OpenAPI operation (same names as the RTK codegen), replacing `@rtk-query/codegen-openapi`. `bun run sdk` writes `store/apiSdk.ts`.
91
+ - `@terreno/ui` `ConsentNavigator`, `AnnouncementNavigator`, `useConsentForms`, `useSubmitConsent`, `useAnnouncements`, and `useAcknowledgeAnnouncement` take a request `client` instead of an RTK `api`.
92
+ - `GptHistory` (`gpt/histories`) and `McpServiceToken` (`mcp/service-tokens`, read-only) are synced collections with String ids. Run the `gptHistoryStringIdsMigration` (`@terreno/ai`) and `mcpServiceTokenStringIdsMigration` (`@terreno/api`) before deploying 58 frontends.
93
+ - `create-terreno-app`, the `terreno_bootstrap` prompt, and the `terreno_generate_screen` tool scaffold apps without Redux or `@terreno/rtk`. The terreno-mcp-local `get_rtk_state` tool, the MCP resource `terreno://docs/rtk`, and `terreno state --slice rtk` are removed; use `get_syncdb_state` and `terreno state`.
94
+
95
+ ### Added
96
+
97
+ - `DataTable` columns size themselves when `width` is omitted. Each auto column starts at the wider of its header and its content (sampled from at most 50 rows, measured with a canvas on web and estimated on native), clamped to 80–360px, and auto columns grow to fill the table's container. Columns that set `width` keep it. `DataTableColumn.width` is now optional.
98
+
99
+ ### Changed
100
+
101
+ - The repository's agent rules now generate with rulesync 17 (up from 9). The root rule `.rulesync/rules/00-root.md` now generates `AGENTS.md`, which Devin and other AGENTS.md readers load, in place of `.devin/rules/00-root.md`. Never edit `AGENTS.md` by hand: edit the root rule and run `bun run rules`. The examples that existed only in the old hand-edited `AGENTS.md` (syncdb and RTK SDK usage) moved into the root rule, so `CLAUDE.md`, the Cursor root rule, and Copilot instructions get them too. Links in the root rule now resolve from the repository root. Generated Copilot hooks use the portable `command` field. Generated Copilot subagents no longer list `agent/runSubagent` as their only tool, so they get every default tool. Generated skills carry their `license`, `disable-model-invocation`, and Devin `triggers` frontmatter.
102
+ - `@terreno/syncdb/auth` `SessionProvider`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`useSession().isSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
103
+
104
+ ### Removed
105
+
106
+ - `@terreno/ui` no longer exports the `Filter` and `FilterProps` aliases deprecated in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps` instead. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names.
107
+
108
+ ### Fixed
109
+
110
+ - `@terreno/syncdb`: logging out while `syncDb.start()` is still running no longer loses local writes or logs errors. `start()` now resolves the web encryption key from `GET /sync/key` before it loads the store, so the flush in `stop()` never needs the network after the session has ended. A `stop()` or `signOut()` call now cancels a `start()` that is running or queued ahead of it, so the transport never connects without a session. A failed key fetch is now retried on the next save instead of failing every save for the rest of the session.
111
+
23
112
  ## [57.21.0] - 2026-11-01
24
113
 
25
114
  Upgrade note: [`mcp-server/src/docs/upgrades/57.21.0.md`](mcp-server/src/docs/upgrades/57.21.0.md).
package/package.json CHANGED
@@ -30,5 +30,5 @@
30
30
  "type": "git",
31
31
  "url": "git+https://github.com/TerrenoLabs/terreno.git"
32
32
  },
33
- "version": "57.21.0"
33
+ "version": "58.0.0-beta.2"
34
34
  }
@@ -37,9 +37,10 @@ and [Build admin screens](../../../../docs/how-to/build-admin-screens.md).
37
37
  no per-model copy of the table exists.
38
38
  5. UI: `@terreno/ui` + `AdminScreenPage` for custom screens; `color="transparent"`
39
39
  inside the shell. String-`_id` models with `adminBroadcast` use windowed
40
- syncdb; framework RPC uses host-bound `adminRequest`; ObjectId compatibility
41
- CRUD keeps `useAdminApi` in Terreno 57. Done when there is no app-owned
42
- `fetch`/`axios` or new admin `injectEndpoints`.
40
+ syncdb; ObjectId/API-only model CRUD uses `useAdminModelApi`; custom RPC uses
41
+ `useAdminRpc` + `useAdminRpcQuery`. All of it goes through the `AdminProvider`
42
+ fetch client. Done when there is no app-owned `fetch`/`axios` and no `api`
43
+ prop on admin components.
43
44
  6. Verify with `verify-ui-changes` on the example admin or admin-spa. Done when
44
45
  home, sidebar, and the new flow are exercised as an admin user.
45
46
 
@@ -56,11 +57,13 @@ references/
56
57
 
57
58
  - Do not hand-roll a DataTable per model. Use `AdminModelTable` / `AdminScreenRouter`.
58
59
  - Do not put a second sidebar inside a screen. `AdminShell` owns nav.
59
- - Do not add new admin `injectEndpoints`. Terreno 58 removes `useAdminApi` and
60
- the required `api` prop; use windowed syncdb for eligible model CRUD.
60
+ - Do not pass an `api` prop to admin components (removed in Terreno 58).
61
+ `AdminProvider` (`credentials` for cookie SPAs, `apiOrigin` = `baseUrl` from
62
+ `@terreno/syncdb/config` + `getAuthHeaders` reading the app's
63
+ `authProvider.getToken()` for embedded apps) is the only auth binding.
61
64
  - Do not invent sidebar entries. Config (filtered by RBAC) is the nav source.
62
65
  - Do not use `router.back()` as the custom-screen back action; use `AdminScreenPage`.
63
- - Do not edit `openApiSdk.ts` by hand.
66
+ - Do not edit generated `store/apiSdk.ts` / `store/syncDbSdk.ts` by hand.
64
67
 
65
68
  ## Decision tree
66
69
 
@@ -38,5 +38,5 @@ relocates those two into Platform.
38
38
  ## Auth gate
39
39
 
40
40
  `admin:access` opens the page (`GET /admin/config` 403 without it). Hosts call
41
- `canOpenAdminPage({admin, permissions})` from `@terreno/rtk` and hide product
41
+ `canOpenAdminPage({admin, permissions})` from `@terreno/syncdb/app` and hide product
42
42
  nav to admin with the same check.
@@ -43,10 +43,11 @@ Reference: [admin-config](../../../../../docs/reference/admin-config.md).
43
43
  ## Data and MCP
44
44
 
45
45
  - Admin String-`_id` collection CRUD: windowed syncdb with `adminBroadcast`.
46
- - Framework admin RPC: host-bound `adminRequest`; do not add `injectEndpoints`.
47
- - ObjectId/API-only compatibility CRUD: `useAdminApi` until Terreno 58.
46
+ - ObjectId/API-only model CRUD: `useAdminModelApi(routePath, modelName)`.
47
+ - Framework and custom admin RPC: `useAdminRpc` + `useAdminRpcQuery` /
48
+ `useAdminRpcMutation` over the `AdminProvider` client.
48
49
  - After backend route changes: `cd example-frontend && bun run sdk` (non-synced
49
- only). Do not generate RTK CRUD for synced product collections.
50
+ routes, `store/apiSdk.ts`) and `bun run sync-sdk` (synced collections).
50
51
  - MCP: `terreno_install_admin` scaffolds wiring; still follow this skill for
51
52
  screens and nav. `terreno_search_docs` / `terreno_get_component_docs` for APIs.
52
53
 
@@ -2,12 +2,12 @@
2
2
  name: building-terreno-apps
3
3
  description: >-
4
4
  Complete guide for building Terreno full-stack Expo apps with @terreno/ui,
5
- Expo Router, and @terreno/rtk. Covers layout, forms, navigation, theming,
5
+ Expo Router, and @terreno/syncdb. Covers layout, forms, navigation, theming,
6
6
  screen patterns, and when to use Expo Go vs custom builds.
7
7
  ---
8
8
  # Building Terreno Apps
9
9
 
10
- Use this skill when building or refactoring screens in a Terreno Expo app (`example-frontend`, `admin-spa`, or a consumer app using `@terreno/ui` + `@terreno/rtk`).
10
+ Use this skill when building or refactoring screens in a Terreno Expo app (`example-frontend`, `admin-spa`, or a consumer app using `@terreno/ui` + `@terreno/syncdb`). Terreno 58 apps have no Redux and no `@terreno/rtk`; upgrade old apps with `docs/how-to/migrate-rtk-to-syncdb.md`.
11
11
 
12
12
  **Related skills:** `terreno-ui` (component selection), `terreno-data-fetching` (API hooks), `verify-ui-changes` (mandatory UI verification), `generate-sdk` (after backend changes), `building-admin-interfaces` (admin shell, custom screens, sidebar).
13
13
 
@@ -42,29 +42,43 @@ Admin CRUD, sidebar, custom screens, and `apiBase`/`routeBase`: use skill
42
42
 
43
43
  ## Provider Stack
44
44
 
45
- Every Terreno app root layout should wrap children in this order (outer → inner):
45
+ Every Terreno app root layout wraps children in this order (outer → inner). There is no
46
+ Redux store.
46
47
 
47
48
  ```tsx
48
- import {Provider} from "react-redux";
49
- import {PersistGate} from "redux-persist/integration/react";
49
+ import {SessionProvider} from "@terreno/syncdb/auth";
50
+ import {ApiClientProvider, SyncDbProvider} from "@terreno/syncdb/react";
50
51
  import {TerrenoProvider} from "@terreno/ui";
51
- // Import from your app's store bootstrap module (see example-frontend/store/index.ts)
52
- import store, {persistor} from "../store/index.ts";
52
+ import {betterAuthClient} from "@/lib/betterAuth";
53
+ import {apiClient} from "@/store/apiClient";
54
+ import {syncDb} from "@/store/syncdb";
53
55
 
54
56
  const RootLayout: React.FC = () => {
55
57
  return (
56
- <Provider store={store}>
57
- <PersistGate loading={null} persistor={persistor}>
58
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
59
+ <ApiClientProvider client={apiClient}>
58
60
  <TerrenoProvider>
59
- <Stack />
61
+ <SyncDbProvider client={syncDb}>
62
+ <Stack />
63
+ </SyncDbProvider>
60
64
  </TerrenoProvider>
61
- </PersistGate>
62
- </Provider>
65
+ </ApiClientProvider>
66
+ </SessionProvider>
63
67
  );
64
68
  };
65
69
  ```
66
70
 
67
- See `example-frontend/app/_layout.tsx` for the full pattern including auth routing, sockets, and feature flags.
71
+ | File | Holds |
72
+ |------|-------|
73
+ | `lib/betterAuth.ts` | `createBetterAuthClient` (`@terreno/syncdb/auth`) |
74
+ | `store/syncdb.ts` | Shared `authProvider` + `createSyncDb` client |
75
+ | `store/apiClient.ts` | `createApiClient` (`@terreno/syncdb/api`) for non-synced routes |
76
+ | `store/apiSdk.ts` | Generated request hooks — never edit |
77
+ | `store/syncDbSdk.ts` | Generated `SYNC_COLLECTIONS` + collection hooks — never edit |
78
+
79
+ See `example-frontend/app/_layout.tsx` for the full pattern including auth routing,
80
+ `syncDb.start()` on login, feature flags, consents, and announcements. App-wide UI state
81
+ uses React context or `useStoredState` from `@terreno/ui`.
68
82
 
69
83
  ## Code Style
70
84
 
@@ -119,20 +133,23 @@ Use the spacing scale via props (`padding={4}` → 16px). Prefer `gap` over marg
119
133
 
120
134
  ## Data on Screens
121
135
 
122
- Never call `fetch` or `axios` in screen components. Use generated RTK Query hooks:
136
+ Never call `fetch` or `axios` in screen components. Synced collections use syncdb hooks;
137
+ every other route uses the generated hooks in `store/apiSdk.ts`:
123
138
 
124
139
  ```tsx
125
- import {useGetTodosQuery, usePostTodosMutation} from "@/store/sdk";
140
+ import {useMutate, useQuery} from "@terreno/syncdb/react";
141
+ import {useGetAuthMeQuery} from "@/store/apiSdk";
126
142
 
127
- const {data, isLoading, error, refetch} = useGetTodosQuery({});
128
- const [createTodo, {isLoading: isCreating}] = usePostTodosMutation();
143
+ const todos = useQuery<Todo>("todos", {sort: "-created"});
144
+ const {create, update, remove} = useMutate("todos");
145
+ const {data: me, isLoading, error, refetch} = useGetAuthMeQuery();
129
146
  ```
130
147
 
131
148
  See `terreno-data-fetching` for auth, caching, and error handling.
132
149
 
133
150
  ## Forms
134
151
 
135
- Use `@terreno/ui` form fields with local state and RTK mutations:
152
+ Use `@terreno/ui` form fields with local state. Synced writes apply locally and never throw:
136
153
 
137
154
  ```tsx
138
155
  import {Button, TextField} from "@terreno/ui";
@@ -145,13 +162,9 @@ const handleSubmit = useCallback(async (): Promise<void> => {
145
162
  setTitleError("Title is required");
146
163
  return;
147
164
  }
148
- try {
149
- await createTodo({title: title.trim(), completed: false}).unwrap();
150
- setTitle("");
151
- } catch (err) {
152
- console.error("Failed to create todo", err);
153
- }
154
- }, [createTodo, title]);
165
+ create({data: {_id: generateMutationId(), title: title.trim(), completed: false}});
166
+ setTitle("");
167
+ }, [create, title]);
155
168
 
156
169
  <TextField
157
170
  title="Title"
@@ -159,7 +172,7 @@ const handleSubmit = useCallback(async (): Promise<void> => {
159
172
  onChange={setTitle}
160
173
  errorText={titleError}
161
174
  />
162
- <Button text="Create" onClick={handleSubmit} loading={isCreating} />
175
+ <Button text="Create" onClick={handleSubmit} />
163
176
  ```
164
177
 
165
178
  ## Navigation
@@ -173,7 +186,8 @@ See `./references/navigation.md` and `./references/route-structure.md`.
173
186
 
174
187
  ## Loading, Error, and Empty States
175
188
 
176
- Every data-driven screen must handle all three:
189
+ Every screen that loads data over the request client must handle all three (synced
190
+ collections are local after bootstrap, so they need only the empty state):
177
191
 
178
192
  ```tsx
179
193
  if (isLoading) {
@@ -187,7 +201,7 @@ if (error) {
187
201
  </Page>
188
202
  );
189
203
  }
190
- if (!data?.data?.length) {
204
+ if (!data?.length) {
191
205
  return (
192
206
  <Page title="Todos">
193
207
  <Text>No todos yet</Text>
@@ -216,10 +230,10 @@ Building or changing a screen?
216
230
  | \-- Page for full screens, Box for sections, Card for grouped content
217
231
  |
218
232
  |-- Need data from API?
219
- | \-- terreno-data-fetching (RTK Query hooks, never raw fetch)
233
+ | \-- terreno-data-fetching (syncdb + generated apiSdk hooks, never raw fetch)
220
234
  |
221
235
  |-- Need a form?
222
- | \-- @terreno/ui TextField/SelectField/DateTimeField + local state + mutation
236
+ | \-- @terreno/ui TextField/SelectField/DateTimeField + local state + useMutate / generated mutation
223
237
  |
224
238
  |-- Need navigation?
225
239
  | \-- references/navigation.md (Expo Router + auth gating)
@@ -261,16 +275,16 @@ useEffect(() => {
261
275
  }, []);
262
276
  ```
263
277
 
264
- **Right: Generated RTK Query hook**
278
+ **Right: syncdb hook for a synced collection**
265
279
 
266
280
  ```tsx
267
- const {data} = useGetTodosQuery({});
281
+ const todos = useQuery<Todo>("todos");
268
282
  ```
269
283
 
270
- **Wrong: Editing openApiSdk.ts by hand**
284
+ **Wrong: Editing store/apiSdk.ts by hand**
271
285
 
272
286
  ```tsx
273
- // Manually adding a hook to store/openApiSdk.ts
287
+ // Manually adding a hook to store/apiSdk.ts
274
288
  ```
275
289
 
276
290
  **Right: Regenerate SDK after backend changes**
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Building Terreno Apps"
3
- short_description: "Build Terreno Expo apps with @terreno/ui layout, RTK Query data, Expo Router navigation, and mandatory UI verification"
4
- default_prompt: "Use $building-terreno-apps to build or refactor a Terreno screen with @terreno/ui components, generated RTK Query hooks, and Expo Router navigation."
3
+ short_description: "Build Terreno Expo apps with @terreno/ui layout, @terreno/syncdb data and session, Expo Router navigation, and mandatory UI verification"
4
+ default_prompt: "Use $building-terreno-apps to build or refactor a Terreno screen with @terreno/ui components, @terreno/syncdb hooks, generated store/apiSdk.ts hooks, and Expo Router navigation."
@@ -6,34 +6,40 @@ Terreno-specific navigation patterns.
6
6
 
7
7
  ## Auth flow
8
8
 
9
- 1. Root `_layout.tsx` checks `useSelectCurrentUserId()` from `@terreno/rtk`.
9
+ 1. Root `_layout.tsx` reads `useSession()` from `@terreno/syncdb/auth`.
10
10
  2. Unauthenticated users redirect to `/login`.
11
- 3. Login screen calls `useEmailLoginMutation()` (from generated SDK via `generateAuthSlice`).
12
- 4. On success, tokens are stored automatically and `setUserId` is dispatched.
13
- 5. Root layout redirects to `/(tabs)` or the intended deep link.
11
+ 3. Login calls `betterAuthClient.signIn.email`, then `useSession().refresh()`.
12
+ 4. Once `userId` is set, the root layout starts syncdb and redirects to `/(tabs)` or the
13
+ intended deep link.
14
14
 
15
15
  ```tsx
16
- import {useEmailLoginMutation} from "@/store/sdk";
17
- import {Button, TextField, Page, Box} from "@terreno/ui";
16
+ import {useSession} from "@terreno/syncdb/auth";
17
+ import {Box, Button, Page, TextField} from "@terreno/ui";
18
+ import {betterAuthClient} from "@/lib/betterAuth";
18
19
 
19
20
  const LoginScreen: React.FC = () => {
21
+ const {refresh} = useSession();
20
22
  const [email, setEmail] = useState<string>("");
21
23
  const [password, setPassword] = useState<string>("");
22
- const [login, {isLoading, error}] = useEmailLoginMutation();
24
+ const [isLoading, setIsLoading] = useState<boolean>(false);
25
+ const [error, setError] = useState<string | undefined>();
23
26
 
24
27
  const handleLogin = useCallback(async (): Promise<void> => {
25
- try {
26
- await login({email, password}).unwrap();
27
- } catch (err) {
28
- console.error("Login failed", err);
28
+ setIsLoading(true);
29
+ const result = await betterAuthClient.signIn.email({email, password});
30
+ setIsLoading(false);
31
+ if (result.error) {
32
+ setError(result.error.message ?? "Login failed");
33
+ return;
29
34
  }
30
- }, [email, login, password]);
35
+ await refresh();
36
+ }, [email, password, refresh]);
31
37
 
32
38
  return (
33
39
  <Page title="Login" scroll>
34
40
  <Box gap={3} padding={4}>
35
41
  <TextField title="Email" value={email} onChange={setEmail} type="email" />
36
- <TextField title="Password" value={password} onChange={setPassword} type="password" />
42
+ <TextField title="Password" value={password} onChange={setPassword} type="password" errorText={error} />
37
43
  <Button text="Sign in" onClick={handleLogin} loading={isLoading} fullWidth />
38
44
  </Box>
39
45
  </Page>
@@ -87,7 +93,7 @@ For confirmation dialogs and action sheets, prefer `@terreno/ui` `Modal` or `Act
87
93
 
88
94
  ## Deep linking
89
95
 
90
- Configure `scheme` in `app.json`. Terreno RTK resolves API base URLs from `EXPO_PUBLIC_API_URL` or `expo.extra.BASE_URL` — keep deep link schemes separate from API URLs.
96
+ Configure `scheme` in `app.json`. `@terreno/syncdb/config` resolves API base URLs from `expo.extra.BASE_URL` or `EXPO_PUBLIC_API_URL` — keep deep link schemes separate from API URLs.
91
97
 
92
98
  ## Programmatic navigation
93
99
 
@@ -28,10 +28,13 @@ app/
28
28
  [id].tsx # AdminModelForm (edit)
29
29
  components/ # Screen components (NOT in app/)
30
30
  hooks/ # Custom hooks
31
+ lib/
32
+ betterAuth.ts # createBetterAuthClient (@terreno/syncdb/auth)
31
33
  store/
32
- index.ts # Redux store + generateAuthSlice
33
- sdk.ts # Hand-maintained SDK extensions
34
- openApiSdk.ts # Generated — never edit manually
34
+ syncdb.ts # Shared authProvider + createSyncDb client
35
+ apiClient.ts # createApiClient for non-synced routes
36
+ apiSdk.ts # Generated request hooks — never edit manually
37
+ syncDbSdk.ts # Generated collection hooks — never edit manually
35
38
  ```
36
39
 
37
40
  ### Rules
@@ -49,12 +52,15 @@ store/
49
52
 
50
53
  ```tsx
51
54
  import {Redirect, Stack, useSegments} from "expo-router";
52
- import {useSelectCurrentUserId} from "@terreno/rtk";
55
+ import {useSession} from "@terreno/syncdb/auth";
53
56
 
54
57
  const RootLayoutNav: React.FC = () => {
55
- const userId = useSelectCurrentUserId();
58
+ const {userId, isLoading} = useSession();
56
59
  const segments = useSegments();
57
60
 
61
+ if (isLoading) {
62
+ return null;
63
+ }
58
64
  if (!userId && segments[0] !== "login") {
59
65
  return <Redirect href="/login" />;
60
66
  }
@@ -71,21 +77,28 @@ const RootLayoutNav: React.FC = () => {
71
77
  Wrap admin routes once in `app/admin/_layout.tsx`:
72
78
 
73
79
  ```tsx
74
- import {AdminShellLayout} from "@terreno/admin-frontend";
80
+ import {AdminProvider, AdminShellLayout} from "@terreno/admin-frontend";
81
+ import {baseUrl} from "@terreno/syncdb/config";
75
82
  import {Stack} from "expo-router";
76
- import {terrenoApi} from "@/store/sdk";
83
+ import {getAdminAuthHeaders} from "@/store/syncdb"; // Bearer header from authProvider.getToken()
77
84
 
78
85
  export default function AdminLayout(): React.ReactElement {
79
86
  return (
80
- <AdminShellLayout
81
- api={terrenoApi}
87
+ <AdminProvider
82
88
  apiBase="/admin"
83
- configurationPath="/admin/configuration"
89
+ apiOrigin={baseUrl}
90
+ getAuthHeaders={getAdminAuthHeaders}
84
91
  routeBase="/admin"
85
- versionConfigPath="/version-config"
86
92
  >
87
- <Stack screenOptions={{headerShown: false, contentStyle: {flex: 1}}} />
88
- </AdminShellLayout>
93
+ <AdminShellLayout
94
+ apiBase="/admin"
95
+ configurationPath="/admin/configuration"
96
+ routeBase="/admin"
97
+ versionConfigPath="/version-config"
98
+ >
99
+ <Stack screenOptions={{headerShown: false, contentStyle: {flex: 1}}} />
100
+ </AdminShellLayout>
101
+ </AdminProvider>
89
102
  );
90
103
  }
91
104
  ```