@terreno/mcp 58.0.0-beta.2 → 58.0.0-beta.3
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.
- package/CHANGELOG.md +47 -0
- package/dist/docs/upgrades/58.0.0-beta.2.md +1 -1
- package/dist/docs/upgrades/58.0.0-beta.3.md +493 -0
- package/dist/docs/upgrades/58.0.0.md +1 -1
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +10 -3
- package/package.json +4 -4
- package/src/docs/upgrades/58.0.0-beta.2.md +1 -1
- package/src/docs/upgrades/58.0.0-beta.3.md +493 -0
- package/src/docs/upgrades/58.0.0.md +1 -1
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -20,6 +20,53 @@ 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.3] - 2026-11-03
|
|
24
|
+
|
|
25
|
+
Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.3.md`](mcp-server/src/docs/upgrades/58.0.0-beta.3.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
|
+
- Apps upgrading from `@terreno/rtk` must install `expo-constants` and `expo-network` themselves (rtk depended on them; `@terreno/syncdb` lists them as optional peers), plus the optional peers for the features they use: `better-auth`, `@better-auth/expo`, `@better-auth/passkey`, `@openfeature/web-sdk`, `@openfeature/react-sdk`, `@modelcontextprotocol/client`. See the upgrade note.
|
|
44
|
+
- `@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`.
|
|
45
|
+
- `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`.
|
|
46
|
+
- `@terreno/ui` `ConsentNavigator`, `AnnouncementNavigator`, `useConsentForms`, `useSubmitConsent`, `useAnnouncements`, and `useAcknowledgeAnnouncement` take a request `client` instead of an RTK `api`.
|
|
47
|
+
- `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.
|
|
48
|
+
- `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`.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `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.
|
|
53
|
+
- `@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.
|
|
54
|
+
- 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.
|
|
55
|
+
- `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.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- 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.
|
|
60
|
+
- `@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.
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
64
|
+
- `@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.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- `@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.
|
|
69
|
+
|
|
23
70
|
## [58.0.0-beta.2] - 2026-11-03
|
|
24
71
|
|
|
25
72
|
Upgrade note: [`mcp-server/src/docs/upgrades/58.0.0-beta.2.md`](mcp-server/src/docs/upgrades/58.0.0-beta.2.md).
|
|
@@ -316,7 +316,7 @@ If the app creates histories from the client with `useMutate("gpt/histories").cr
|
|
|
316
316
|
|
|
317
317
|
Steps:
|
|
318
318
|
|
|
319
|
-
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage`; rebuild native clients.
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
320
|
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
321
|
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
322
|
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# Upgrading to 58.0.0-beta.3
|
|
2
|
+
|
|
3
|
+
**Action required:** Yes
|
|
4
|
+
**Affected packages:** @terreno/rtk (removed), @terreno/syncdb, @terreno/ui, @terreno/admin-frontend, @terreno/admin-spa, @terreno/api, @terreno/ai, @terreno/feature-flags, @terreno/mcp, @terreno/cli, create-terreno-app
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
First release of the Expo SDK 58 line. Upgrade the app to Expo 58 and React Native 0.88, rebuild native binaries, then bump every `@terreno/*` package to `58.0.0` together. This major also deletes `@terreno/rtk` and Redux: `@terreno/syncdb` is the only frontend data, request, and session layer. Run the `McpServiceToken` data migration before deploying 58 frontends. Each removal has its own section below.
|
|
9
|
+
|
|
10
|
+
Order of work:
|
|
11
|
+
|
|
12
|
+
1. Upgrade Expo (next section) and rebuild native binaries.
|
|
13
|
+
2. Backend: remove `modelRouter` `realtime`, then run the [MCP service token](#mcp-service-tokens-are-a-synced-collection) migration. GPT histories keep their ObjectId ids and need no migration ([GPT histories](#gpt-histories-are-a-synced-collection)).
|
|
14
|
+
3. Frontend: [replace `@terreno/rtk` and Redux](#terrenortk-and-redux-are-removed) with `@terreno/syncdb`, then update [admin](#admin-no-longer-takes-an-rtk-api) and [ui navigators](#consent-and-announcement-navigators-take-a-client).
|
|
15
|
+
|
|
16
|
+
## Upgrade Expo before Terreno
|
|
17
|
+
|
|
18
|
+
| Package | Version |
|
|
19
|
+
|---------|---------|
|
|
20
|
+
| `expo` | `~58.0.14` |
|
|
21
|
+
| `react-native` | `0.88.0` |
|
|
22
|
+
| `react` / `react-dom` | `19.3.0` |
|
|
23
|
+
| `@types/react` | `~19.3.0` |
|
|
24
|
+
| `@sentry/react-native` | `~8.28.0` (major 7 → 8) |
|
|
25
|
+
| `react-native-gesture-handler` | `~3.3.0` (major 2 → 3) |
|
|
26
|
+
| `react-native-webview` | `14.0.1` (major 13 → 14) |
|
|
27
|
+
|
|
28
|
+
Run `bunx expo install expo@^58.0.0 --fix` at the app root, rebuild dev clients and store binaries (the native fingerprint changes), then `bun run compile` / `bun run lint`.
|
|
29
|
+
|
|
30
|
+
### Expo Router `initialRouteName`
|
|
31
|
+
|
|
32
|
+
**What changed**
|
|
33
|
+
|
|
34
|
+
Expo Router 58 no longer accepts `initialRouteName` as a `<Slot>` / `<Stack>` / `<Navigator>` prop. `SidebarNavigation` ignores it.
|
|
35
|
+
|
|
36
|
+
**Migration**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Before
|
|
40
|
+
<Slot initialRouteName="home" />
|
|
41
|
+
|
|
42
|
+
// After — in the same _layout.tsx
|
|
43
|
+
export const unstable_settings = {initialRouteName: "home"};
|
|
44
|
+
<Slot />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom `app/+html.tsx`
|
|
48
|
+
|
|
49
|
+
Expo Router 58 renders static web pages by streaming. The bundle `<script>` tags,
|
|
50
|
+
React Native Web styles, and route metadata now reach a custom `app/+html.tsx` through
|
|
51
|
+
`useServerDocumentContext()`. A `+html.tsx` that does not render them exports pages with
|
|
52
|
+
no JavaScript: the static HTML shows, but nothing hydrates or responds.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import {ScrollViewStyleReset, useServerDocumentContext} from "expo-router/html";
|
|
56
|
+
|
|
57
|
+
export default function Root({children}: {children: React.ReactNode}) {
|
|
58
|
+
const {bodyAttributes, bodyNodes, headNodes, htmlAttributes} = useServerDocumentContext();
|
|
59
|
+
return (
|
|
60
|
+
<html lang="en" {...htmlAttributes}>
|
|
61
|
+
<head>
|
|
62
|
+
<meta charSet="utf-8" />
|
|
63
|
+
<ScrollViewStyleReset />
|
|
64
|
+
{headNodes}
|
|
65
|
+
</head>
|
|
66
|
+
<body {...bodyAttributes}>
|
|
67
|
+
{children}
|
|
68
|
+
{bodyNodes}
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Apps without a custom `+html.tsx` need no change.
|
|
76
|
+
|
|
77
|
+
### React Native 0.88 instance types
|
|
78
|
+
|
|
79
|
+
**What changed**
|
|
80
|
+
|
|
81
|
+
React Native 0.87+ ships generated types without `react-native-legacy-deep-imports`. Component names such as `View`, `Text`, `ScrollView`, and `TextInput` are no longer usable as ref types. `Box.scrollRef`, `TextField.inputRef`, and the `Text` `ref` now use the instance types.
|
|
82
|
+
|
|
83
|
+
**Migration**
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// Before
|
|
87
|
+
const ref = useRef<TextInput>(null);
|
|
88
|
+
|
|
89
|
+
// After
|
|
90
|
+
import type {TextInputInstance} from "react-native";
|
|
91
|
+
const ref = useRef<TextInputInstance>(null);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance`. `TextStyle` is `Readonly`; build style objects instead of mutating them. `TextInput.State.currentlyFocusedInput()` replaces `currentlyFocusedField()`.
|
|
95
|
+
|
|
96
|
+
### `@terreno/ui` peer dependency
|
|
97
|
+
|
|
98
|
+
`@terreno/ui` peers `react-native` `0.88.0`. Installing `@terreno/ui@58` next to Expo 57 is unsupported.
|
|
99
|
+
|
|
100
|
+
### `@sentry/react-native` 8
|
|
101
|
+
|
|
102
|
+
Expo 58 bundles `@sentry/react-native` `~8.28.0`. Follow Sentry's 7 → 8 migration guide for your app's `Sentry.init` options.
|
|
103
|
+
|
|
104
|
+
## `Filter` and `FilterProps` are removed
|
|
105
|
+
|
|
106
|
+
`@terreno/ui` removes the `Filter` and `FilterProps` aliases added as deprecations in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps`. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names. See [57.7.0](57.7.0.md).
|
|
107
|
+
|
|
108
|
+
## HTTP rate limiting defaults on
|
|
109
|
+
|
|
110
|
+
`TerrenoApp` now mounts the HTTP rate limiter when `rateLimit` is omitted: memory store, auth 20 / 15 min (login, signup, refresh, GitHub OAuth, Better Auth credential routes), api 600 / 15 min (everything else). `/health`, `/healthz`, `/openapi.json`, and `/swagger` are never limited. Over the limit returns 429 `rate-limit-exceeded` with `Retry-After`.
|
|
111
|
+
|
|
112
|
+
Steps:
|
|
113
|
+
|
|
114
|
+
1. **Cloud Run (or any load balancer): pass `trustProxy: 1`.** Without it every unauthenticated request keys by the front end's address, so the whole service shares one auth bucket (20 logins per 15 minutes for all users).
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
new TerrenoApp({userModel: User, rateLimit: {trustProxy: 1}});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. **If you set `app.set("trust proxy", …)` before `build()`** (for example in `beforeJsonSetup`), move the value to `rateLimit.trustProxy`. The limiter sets Express `trust proxy` when it mounts and defaults it to `false`.
|
|
121
|
+
3. **More than one replica:** the memory store is per process. Pass `store: "redis"` (`VALKEY_URL` / `REDIS_URL`) or `store: "mongo"` for one shared bucket.
|
|
122
|
+
4. **To keep HTTP unlimited, pass `rateLimit: false`:**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
new TerrenoApp({userModel: User, rateLimit: false});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
5. **Tests:** each `build()` gets a fresh memory store. A suite that logs in more than 20 times or sends more than 600 requests against one app instance should pass `rateLimit: false` in its shared app helper.
|
|
129
|
+
|
|
130
|
+
Apps that already pass `rateLimit: {…}` see no change. `@terreno/api` still does not read `RATE_LIMIT_ENABLED`; an app-owned toggle should now produce `false` to turn the limiter off. See [Rate limiting](../../../docs/how-to/rate-limiting.md).
|
|
131
|
+
|
|
132
|
+
## Admin no longer takes an RTK `api`
|
|
133
|
+
|
|
134
|
+
`@terreno/admin-frontend` stopped injecting endpoints into your RTK Query API. Every admin request — model CRUD (including ObjectId and API-only models), `/admin/config`, scripts, roles, configuration, documents, comms, jobs, organizations, consent, announcements, and AI observability — now goes through one native `fetch` client that `AdminProvider` binds to your auth. The `api` prop, `useAdminApi`, `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are removed. String-`_id` models with `adminBroadcast` keep the windowed syncdb path from 57.
|
|
135
|
+
|
|
136
|
+
Before:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<AdminProvider api={terrenoApi} apiBase="/admin" apiOrigin={baseUrl} getAuthHeaders={getAdminAuthHeaders} routeBase="/admin">
|
|
140
|
+
<AdminShellLayout api={terrenoApi} apiBase="/admin" routeBase="/admin">
|
|
141
|
+
<Stack />
|
|
142
|
+
</AdminShellLayout>
|
|
143
|
+
</AdminProvider>
|
|
144
|
+
|
|
145
|
+
<AdminScreenRouter api={terrenoApi} apiBase="/admin" name={model} routeBase="/admin" />
|
|
146
|
+
<AdminModelTable api={terrenoApi} baseUrl="/admin" modelName="User" />
|
|
147
|
+
|
|
148
|
+
const {useListQuery} = useAdminApi(terrenoApi, "/admin/users", "User");
|
|
149
|
+
const {config} = useAdminConfig(terrenoApi, "/admin");
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
After:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<AdminProvider apiBase="/admin" apiOrigin={baseUrl} getAuthHeaders={getAdminAuthHeaders} routeBase="/admin">
|
|
156
|
+
<AdminShellLayout apiBase="/admin" routeBase="/admin">
|
|
157
|
+
<Stack />
|
|
158
|
+
</AdminShellLayout>
|
|
159
|
+
</AdminProvider>
|
|
160
|
+
|
|
161
|
+
<AdminScreenRouter apiBase="/admin" name={model} routeBase="/admin" />
|
|
162
|
+
<AdminModelTable baseUrl="/admin" modelName="User" />
|
|
163
|
+
|
|
164
|
+
const {useListQuery} = useAdminModelApi("/admin/users", "User");
|
|
165
|
+
const {config} = useAdminConfig("/admin");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Same-origin cookie SPAs (like `@terreno/admin-spa`) pass `credentials="same-origin"` on `AdminProvider` and omit `apiOrigin` / `getAuthHeaders`. `getAdminAuthHeaders` is your host's `() => ({Authorization: "Bearer …"})` (example-frontend reads the Better Auth session token).
|
|
169
|
+
|
|
170
|
+
Product screens that reuse admin components outside the admin tree (for example `DocumentStorageBrowser` or `ConsentHistory` on a user tab) must render inside an `AdminProvider` with the same binding. Without one, admin hooks fall back to an unauthenticated same-origin `fetch` and warn once in development.
|
|
171
|
+
|
|
172
|
+
Checklist:
|
|
173
|
+
|
|
174
|
+
- [ ] Remove `api={…}` from every `@terreno/admin-frontend` component and from custom admin widget props.
|
|
175
|
+
- [ ] Drop the `api` first argument from `useAdminConfig`, `useAdminScripts`, `useAdminRoles`, `useConfigurationApi`, `useDocumentStorageApi`, `useConsentHistory`, `useOrganizationsApi`, `useCommsDashboardApi`, `useJobsDashboardApi`, and the `useAiObservability*Api` hooks. `useOrganizationsApi` also drops its unused `organizationId` argument.
|
|
176
|
+
- [ ] Query `refetch()` now returns a Promise; wrap fire-and-forget calls as `void refetch()`.
|
|
177
|
+
- [ ] Replace `useAdminApi(api, routePath, name)` with `useAdminModelApi(routePath, name)`. Hook names, `.unwrap()`, and response shapes are unchanged.
|
|
178
|
+
- [ ] Make sure one `AdminProvider` wraps the admin tree with `credentials` (cookie) or `apiOrigin` + `getAuthHeaders` (Bearer).
|
|
179
|
+
- [ ] Wrap any product-side reuse of admin components in `AdminProvider`.
|
|
180
|
+
- [ ] Delete type-erased casts such as `terrenoApi as unknown as AdminScreenProps["api"]` and any admin-only RTK tag types.
|
|
181
|
+
- [ ] Replace custom admin `injectEndpoints` with `useAdminRpc` + `useAdminRpcQuery` / `useAdminRpcMutation`.
|
|
182
|
+
|
|
183
|
+
## New native modules (rebuild dev clients)
|
|
184
|
+
|
|
185
|
+
Terreno 58 adds every native module the B2B features need in one release, so later features ship as JavaScript and OTA updates against the same binary. A dev client or store build made before 58 does not contain these modules. Apps that call them on an old binary crash or get `null` modules.
|
|
186
|
+
|
|
187
|
+
| Package | Version | Config plugin |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `@stripe/stripe-react-native` | `0.81.0` | Yes: `merchantIdentifier`, `enableGooglePay` |
|
|
190
|
+
| `react-native-purchases` | `10.12.2` | None |
|
|
191
|
+
| `react-native-purchases-ui` | `10.12.2` | None (keep the same version as `react-native-purchases`) |
|
|
192
|
+
| `expo-device` | `~58.0.5` | None |
|
|
193
|
+
| `expo-crypto` | `~58.0.2` (unchanged pin) | None |
|
|
194
|
+
| `expo-local-authentication` | `~58.0.2` | Yes: `faceIDPermission` |
|
|
195
|
+
| `expo-system-ui` | `~58.0.6` | Applied automatically |
|
|
196
|
+
| `expo-build-properties` | `~58.0.11` | Yes: Android Kotlin and `compileSdkVersion` pins |
|
|
197
|
+
|
|
198
|
+
`react-native-otp-verify` is not in the baseline. It does not support the New Architecture, the only architecture React Native 0.88 runs. For one-time codes, use `TextInput` with `textContentType="oneTimeCode"` (iOS) and `autoComplete="sms-otp"` (Android), which need no native module.
|
|
199
|
+
|
|
200
|
+
Checklist:
|
|
201
|
+
|
|
202
|
+
1. Add the packages to the app with `bunx expo install @stripe/stripe-react-native react-native-purchases@10.12.2 react-native-purchases-ui@10.12.2 expo-device expo-crypto expo-local-authentication expo-system-ui expo-build-properties`. In the Terreno monorepo they are root catalog entries; reference them with `"catalog:"`.
|
|
203
|
+
2. Add the config plugins to `app.json` (or `app.config.ts`). Replace the merchant id with your own Apple Pay merchant id; register it in the Apple Developer portal before the iOS build.
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
[
|
|
207
|
+
"expo-build-properties",
|
|
208
|
+
{"android": {"compileSdkVersion": 37, "kotlinVersion": "2.2.21"}}
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
"@stripe/stripe-react-native",
|
|
212
|
+
{"enableGooglePay": true, "merchantIdentifier": "merchant.com.yourcompany.app"}
|
|
213
|
+
],
|
|
214
|
+
[
|
|
215
|
+
"expo-local-authentication",
|
|
216
|
+
{"faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to confirm it is you."}
|
|
217
|
+
]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
React Native 0.88 already compiles against Android SDK 37. Stripe needs at least SDK 36 and Kotlin 2.x; the pins keep both explicit. Kotlin `2.2.21` matches the Stripe Android build default and Expo's KSP table.
|
|
221
|
+
3. Rebuild every dev client and store binary (`eas build --profile development` for iOS and Android). Expo fingerprints change, so OTA updates from 57 binaries do not apply to 58 binaries.
|
|
222
|
+
4. Open the demo's hidden **Native Modules** page (`/demo/Native%20Modules`) on the new dev client. Each native module must show **Linked**. Web shows **Native only**.
|
|
223
|
+
|
|
224
|
+
What still works in Expo Go:
|
|
225
|
+
|
|
226
|
+
| Flow | Expo Go |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `expo-device`, `expo-crypto`, `expo-system-ui` | Works (bundled in Expo Go) |
|
|
229
|
+
| `expo-local-authentication` | Works, except Face ID on iOS (Expo Go has no Face ID usage string); use a dev build |
|
|
230
|
+
| `react-native-purchases` / `-ui` | Runs in RevenueCat Preview API mode: mock offerings and paywalls, no real purchases |
|
|
231
|
+
| `@stripe/stripe-react-native` | Card flows work (Expo Go 58 bundles `0.81.0`). Apple Pay and Google Pay need a dev build, because the merchant id is set by the config plugin |
|
|
232
|
+
| Web | Unchanged. Native-only calls need a `Platform.OS` guard |
|
|
233
|
+
|
|
234
|
+
## Legacy RTK realtime is removed
|
|
235
|
+
|
|
236
|
+
Terreno 58 deletes the RTK cache-patching realtime path. Collection live updates go through `@terreno/syncdb`: `sync` on `modelRouter` plus `sync:delta` events. `RealtimeApp` stays: it still hosts Socket.io, socket auth, change streams, and `sync:delta`. `SyncApp` and `admin.realtime` (`admin:model.changed` events) are unchanged. The rest of `@terreno/rtk`, including `useSocketConnection` and `useRealtimeDebug`, is removed too; see [`@terreno/rtk` and Redux are removed](#terrenortk-and-redux-are-removed).
|
|
237
|
+
|
|
238
|
+
Removed from `@terreno/api`:
|
|
239
|
+
|
|
240
|
+
- `modelRouter` option `realtime`. Passing it is now a TypeScript error.
|
|
241
|
+
- The `sync` websocket event. The change stream emits only `sync:delta` for models registered with `sync`.
|
|
242
|
+
- The `subscribe:model`, `subscribe:document`, and `subscribe:query` socket events (and their `unsubscribe:*` pairs, plus `query:subscribed`).
|
|
243
|
+
- Types `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, and `QuerySubscription`.
|
|
244
|
+
- Registry helpers `registerRealtime`, `getRealtimeRegistry`, `findRegistryEntryByCollection`, `findRegistryEntryByRoutePath`, `updateRealtimeRegistryOptions`, `clearRealtimeRegistry`, and type `RealtimeRegistryEntry`.
|
|
245
|
+
- Query store helpers `addQuerySubscription`, `removeQuerySubscription`, `removeAllSocketQueries`, `getQuerySubscriptionsForCollection`, `computeQueryId`, and `clearQueryStore`.
|
|
246
|
+
- Constants `MAX_MODEL_SUBSCRIPTIONS`, `MAX_DOCUMENT_SUBSCRIPTIONS`, and `MAX_QUERY_SUBSCRIPTIONS`.
|
|
247
|
+
|
|
248
|
+
Removed from `@terreno/rtk`:
|
|
249
|
+
|
|
250
|
+
- `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and type `RealtimeEvent`.
|
|
251
|
+
|
|
252
|
+
To upgrade:
|
|
253
|
+
|
|
254
|
+
1. Backend: delete `realtime: {...}` from every `modelRouter` config. For each collection that needs live updates, add `syncPlugin` + `isDeletedPlugin` to the schema and a `sync` config to the router (three-argument form), then register `SyncApp` next to `RealtimeApp`.
|
|
255
|
+
2. Frontend: delete `setRealtimeSocket(socket)`, `useSyncConnection(...)`, and every `onCacheEntryAdded: realtimeList(...)` / `realtimeDocument(...)` override. Read and write synced collections with `useQuery` / `useMutate` from `@terreno/syncdb/react`.
|
|
256
|
+
3. Tests: replace `clearRealtimeRegistry()` with `clearSyncRegistry()`, which clears the whole collection catalog.
|
|
257
|
+
|
|
258
|
+
See [How to upgrade from @terreno/rtk to @terreno/syncdb](../../../docs/how-to/migrate-rtk-to-syncdb.md).
|
|
259
|
+
|
|
260
|
+
## MCP service tokens are a synced collection
|
|
261
|
+
|
|
262
|
+
`McpServiceToken` now has a String `_id` (UUID for new rows), `isDeletedPlugin`, and `syncPlugin`. With `mcpServiceTokens` enabled, `/mcp/service-tokens` is a `modelRouter` synced read-only to its owner (collection `mcp/service-tokens`, owner scope on `userId`). Sync snapshots and deltas never include `tokenHash`. `McpServiceToken.verify` and `/mcp` authentication are unchanged.
|
|
263
|
+
|
|
264
|
+
| 57.x | 58.0.0 |
|
|
265
|
+
|------|--------|
|
|
266
|
+
| `POST /mcp/service-tokens` | `POST /mcp/service-tokens/create` (same body; same one-time `token` response) |
|
|
267
|
+
| `DELETE /mcp/service-tokens/:id` | `POST /mcp/service-tokens/:id/revoke` (response is the token row without `tokenHash`) |
|
|
268
|
+
| `GET /mcp/service-tokens` rows `{id, name, tokenPrefix, …}` with `null` dates | Standard `modelRouter` list rows (`_id`, `id`, unset dates omitted); or `useQuery("mcp/service-tokens")` |
|
|
269
|
+
| `revokeForUser(user, ObjectId)` | `revokeForUser(user, string)` |
|
|
270
|
+
|
|
271
|
+
`POST`, `PATCH`, and `DELETE` on the collection return `405`, and sync client writes are nacked `unauthorized`. Admin delete still revokes: a soft delete is stored as `revokedAt`, not a tombstone. `McpServiceToken.deleteMany` now throws once the collection is synced (use `McpServiceToken.collection.deleteMany` in tests).
|
|
272
|
+
|
|
273
|
+
**Run the data migration before deploying 58.** Existing rows keep their ids as hex strings. Add this file to your `migrations/` directory, dry-run it, then apply it:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// migrations/20261022120000-mcp-service-token-string-ids.ts
|
|
277
|
+
import {mcpServiceTokenStringIdsMigration} from "@terreno/api";
|
|
278
|
+
|
|
279
|
+
export const id = "20261022120000-mcp-service-token-string-ids";
|
|
280
|
+
export const up = mcpServiceTokenStringIdsMigration.up;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The migration is idempotent and has no `down` (new rows use UUIDs). See [Connect an MCP client with a service token](../../../docs/how-to/connect-mcp-service-token.md).
|
|
284
|
+
|
|
285
|
+
## GPT histories are a synced collection
|
|
286
|
+
|
|
287
|
+
`GptHistory` now has `isDeletedPlugin` and `syncPlugin`, and keeps its ObjectId `_id` (no data migration). `/gpt/histories` is registered with `sync: {scope: {type: "owner", field: "userId"}}`, so `SyncApp` serves each user's histories as the collection `gpt/histories`. REST CRUD, `/gpt/prompt`, `/gpt/histories/:id/stream`, the `turn` and `pendingAsks` actions, ratings, and `/gpt/actions` are unchanged.
|
|
288
|
+
|
|
289
|
+
| 57.x | 58.0.0 |
|
|
290
|
+
|------|--------|
|
|
291
|
+
| `useGetGptHistoriesQuery` + `updateQueryData` / `invalidateTags` | `useQuery("gpt/histories")` and `useMutate("gpt/histories")` from `@terreno/syncdb/react` |
|
|
292
|
+
| `GptHistory.deleteMany(...)` / `updateMany(...)` in app code or tests | Write one history at a time by `_id`; tests use `GptHistory.collection.deleteMany({})` |
|
|
293
|
+
|
|
294
|
+
If the app creates histories from the client with `useMutate("gpt/histories").create`, pass `idFormats: {"gpt/histories": "objectId"}` to `createSyncDb` so the client mints ObjectIds instead of UUIDs. See [@terreno/ai reference](../../../docs/reference/ai.md#gpthistory-ids).
|
|
295
|
+
|
|
296
|
+
## `@terreno/rtk` and Redux are removed
|
|
297
|
+
|
|
298
|
+
`@terreno/rtk` is not published for 58, and no Terreno package uses Redux. `@terreno/syncdb` takes over every job it did. The [upgrade guide](../../../docs/how-to/migrate-rtk-to-syncdb.md) walks through each step and maps **every** removed export to its replacement; this section is the summary.
|
|
299
|
+
|
|
300
|
+
| `@terreno/rtk` 57.x | 58.0.0 |
|
|
301
|
+
|---------------------|--------|
|
|
302
|
+
| `emptySplitApi`, `getBaseQuery`, `@rtk-query/codegen-openapi` → `store/openApiSdk.ts` | `createApiClient` (`@terreno/syncdb/api`) + `ApiClientProvider`; `terreno-syncdb-codegen --api-out` → `store/apiSdk.ts` (same hook names) |
|
|
303
|
+
| RTK Query hooks for collection CRUD | `useQuery` / `useMutate` (`@terreno/syncdb/react`) |
|
|
304
|
+
| `api.injectEndpoints(...)` | Document the route in OpenAPI and regenerate, or `useApiQuery` / `useApiMutation` |
|
|
305
|
+
| `createBetterAuthClient` | `createBetterAuthClient` (`@terreno/syncdb/auth`) |
|
|
306
|
+
| `generateBetterAuthSlice` + `selectBetterAuth*` selectors | `SessionProvider` + `useSession()` (`@terreno/syncdb/auth`) |
|
|
307
|
+
| `generateAuthSlice` (JWT) + `useSelectCurrentUserId` | `jwtAuthAdapter` + `useJwtAuth` + `useSession()` (`@terreno/syncdb/auth`) |
|
|
308
|
+
| `getTokenExpirationTimes`, `shouldShowStillThereModal` | Same names (`@terreno/syncdb/auth`) |
|
|
309
|
+
| `baseUrl`, `baseWebsocketsUrl`, `resolveBaseUrls` | Same names (`@terreno/syncdb/config`), plus `appHeaders()` |
|
|
310
|
+
| `useSocketConnection` | The syncdb socket: `useSocketEvent` (`@terreno/syncdb/react`) / `syncDb.onSocketEvent` |
|
|
311
|
+
| `useFeatureFlags(api, …)`, `useTerrenoFeatureFlags(api, …)` | `useFeatureFlags({client, socket, …})`, `useTerrenoFeatureFlags({client, socket, …})` (`@terreno/syncdb/flags`) |
|
|
312
|
+
| `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installTerrenoDevConsoleLogger`, `useMCPTools` | `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`, `useMCPTools({authProvider})` (`@terreno/syncdb/app`) |
|
|
313
|
+
| `@terreno/rtk/buildNumber` | `@terreno/syncdb/buildNumber` |
|
|
314
|
+
| Offline middleware, `useOfflineStatus`, `useServerStatus` | The syncdb outbox; `useSyncStatus()` |
|
|
315
|
+
| `useCan`, `useSelectPermissions`, `createSessionApi`, `useTerrenoChat`, `generateTags`, `populateId`, `registerTerrenoDevStore` | Removed (no replacement needed; see the guide) |
|
|
316
|
+
|
|
317
|
+
Steps:
|
|
318
|
+
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
|
+
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
|
+
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
|
+
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
323
|
+
5. Remove `useSocketConnection`; pass `socket: syncDb` to the flag hooks and use `useSocketEvent` for other server pushes. The app opens one socket.
|
|
324
|
+
6. Verify: `rg -l "@terreno/rtk|@reduxjs/toolkit|react-redux|redux-persist|rtk-query" --glob '!**/node_modules/**' .` returns nothing; compile, lint, and test pass.
|
|
325
|
+
|
|
326
|
+
The feature-flags `GET /evaluate` deprecation warning now points at `@terreno/syncdb/flags`.
|
|
327
|
+
|
|
328
|
+
### Consent and announcement navigators take a `client`
|
|
329
|
+
|
|
330
|
+
`@terreno/ui` no longer calls into an RTK Query API. `ConsentNavigator`, `AnnouncementNavigator`, and their hooks take a request `client` (`TerrenoRequestClient`; a syncdb `ApiClient` satisfies it).
|
|
331
|
+
|
|
332
|
+
| 57.x | 58.0.0 |
|
|
333
|
+
|------|--------|
|
|
334
|
+
| `<ConsentNavigator api={terrenoApi}>` | `<ConsentNavigator client={apiClient}>` |
|
|
335
|
+
| `<AnnouncementNavigator api={terrenoApi} …>` | `<AnnouncementNavigator client={apiClient} …>` |
|
|
336
|
+
| `useConsentForms(api, baseUrl)`, `useSubmitConsent(api, baseUrl)` | `useConsentForms(client, baseUrl)`, `useSubmitConsent(client, baseUrl)` |
|
|
337
|
+
| `useAnnouncements(api, …)`, `useAcknowledgeAnnouncement(api, …)` | `useAnnouncements(client, …)`, `useAcknowledgeAnnouncement(client, …)` |
|
|
338
|
+
|
|
339
|
+
See [@terreno/ui reference](../../../docs/reference/ui.md).
|
|
340
|
+
|
|
341
|
+
### Scaffolds, MCP tools, and the CLI
|
|
342
|
+
|
|
343
|
+
`create-terreno-app` and the `terreno_bootstrap` prompt now scaffold a frontend with no Redux and no `@terreno/rtk`: `lib/betterAuth.ts` (`createBetterAuthClient` from `@terreno/syncdb/auth`), `store/syncdb.ts` (shared `authProvider` + `syncDb`), `store/apiClient.ts` (`createApiClient` from `@terreno/syncdb/api`), and `store/apiSdk.ts` (generated hooks). `app/_layout.tsx` mounts `ApiClientProvider` and `SessionProvider`.
|
|
344
|
+
|
|
345
|
+
| 57.x | 58.0.0 |
|
|
346
|
+
|------|--------|
|
|
347
|
+
| `bun run sdk` → `scripts/generate-sdk.ts` + `openapi-config.ts` (`@rtk-query/codegen-openapi`) → `store/openApiSdk.ts` | `bun run sdk` → `api-sdk` script: `terreno-syncdb-codegen --schema <openapi.json> --api-out ./store/apiSdk.ts` |
|
|
348
|
+
| `store/index.ts`, `store/appState.ts`, `store/errors.ts`, `store/sdk.ts`, `store/betterAuthApi.ts`, `tsconfig.codegen.json` | Deleted. Session: `useSession()`; request errors: `createApiClient({onError})` |
|
|
349
|
+
| `useGetMeQuery` / `usePatchMeMutation` from `@/store/sdk` | `useGetAuthMeQuery` / `usePatchAuthMeMutation` from `@/store/apiSdk` |
|
|
350
|
+
| `getAdminAuthHeaders` from `@/store/betterAuthApi` | `getAdminAuthHeaders` from `@/store/apiClient` (reads `authProvider.getToken()`) |
|
|
351
|
+
| MCP `get_rtk_state` (terreno-mcp-local) | `get_syncdb_state`, `syncdb_snapshot`, `syncdb_action` |
|
|
352
|
+
| `terreno state --slice rtk` | `terreno state` (reads `get_syncdb_state`; `--collection`, `--id`, `--limit`) |
|
|
353
|
+
| `terreno generate sdk --config openapi-config.ts` | `terreno generate sdk --schema <openapi.json> --out store/apiSdk.ts` |
|
|
354
|
+
| MCP resource `terreno://docs/rtk` | `terreno://docs/syncdb` |
|
|
355
|
+
| `terreno_generate_screen` list/detail/form used `@/store/openApiSdk` hooks | Uses `useQuery` / `useEntity` / `useMutate` from `@terreno/syncdb/react` |
|
|
356
|
+
|
|
357
|
+
## Better Auth 1.7 and native sign-in
|
|
358
|
+
|
|
359
|
+
Terreno 58 pins `better-auth` and `@better-auth/expo` to `1.7.4` (the root catalog). `@better-auth/passkey` needs 1.7, and the 1.6 and 1.7 Expo plugins differ on native.
|
|
360
|
+
|
|
361
|
+
### Pin Better Auth 1.7.4
|
|
362
|
+
|
|
363
|
+
**What changed**
|
|
364
|
+
|
|
365
|
+
`@terreno/syncdb` and `@terreno/api` type-check against Better Auth 1.7. Mixing 1.6 and 1.7 between `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` fails to compile (client plugin types disagree) and breaks passkey sign-in at runtime. Better Auth 1.7 serves provider callbacks at `/callback/:id`; check that any redirect URI you registered with a provider still matches.
|
|
366
|
+
|
|
367
|
+
**Migration**
|
|
368
|
+
|
|
369
|
+
Set all three to the same version in the app (`"catalog:"` in the Terreno monorepo):
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"dependencies": {
|
|
374
|
+
"@better-auth/expo": "1.7.4",
|
|
375
|
+
"@better-auth/passkey": "1.7.4",
|
|
376
|
+
"better-auth": "1.7.4"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Native storage needs `getItemAsync` and `setItemAsync`
|
|
382
|
+
|
|
383
|
+
**What changed**
|
|
384
|
+
|
|
385
|
+
The 1.7 Expo client plugin reads and writes its session cookie through `storage.getItemAsync` / `storage.setItemAsync`. A storage object with only `getItem` / `setItem` makes every auth request on iOS and Android throw `undefined is not a function`. Web does not exercise this path, so web e2e passes while a device build is broken.
|
|
386
|
+
|
|
387
|
+
`createBetterAuthClient` from `@terreno/syncdb/auth` supplies both methods through its `StorageAdapter`. Apps that build their own Better Auth client with `expoClient({storage})` must provide them too (`getItemAsync?: (key) => Promise<string | null>`, `setItemAsync?: (key, value) => Promise<void>`).
|
|
388
|
+
|
|
389
|
+
### Native sign-out needs `nativeApps: true` on the server
|
|
390
|
+
|
|
391
|
+
**What changed**
|
|
392
|
+
|
|
393
|
+
Native requests carry `expo-origin` instead of `Origin`. Without the server-side Expo plugin, `POST /auth/sign-out` answers `403 Missing or null Origin` on iOS and Android, so the user can never sign out.
|
|
394
|
+
|
|
395
|
+
**Migration**
|
|
396
|
+
|
|
397
|
+
Set `nativeApps: true` in the Better Auth server config (`create-terreno-app` and `example-backend` already do):
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const config: BetterAuthConfig = {enabled: true, nativeApps: true, /* ... */};
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Logout waits for the server and keeps the session on failure
|
|
404
|
+
|
|
405
|
+
`useSession().logout()` signs out of the server first, with the session held. While it runs, `useSession().isSigningOut` is `true` and `isLoading` is unchanged, so a navigator gated on `isLoading` keeps its screen. A sign-out that fails or times out (`signOutTimeoutMs`, default 10 seconds) first checks the session. If it is still live, the session and local data stay and `error` is `SIGN_OUT_FAILED_MESSAGE`; if it is gone, local state is cleared. Do not treat `logout()` as instant, and show `isSigningOut` on the sign-out button.
|
|
406
|
+
|
|
407
|
+
### Keep `SyncDbProvider` mounted across sign-out
|
|
408
|
+
|
|
409
|
+
Mounting `<SyncDbProvider>` only while `userId` is set unmounts every `useQuery` / `useMutate` consumer in the middle of logout, and screens that are still on screen throw. Mount it unconditionally and let `SessionProvider` (`syncDbs={[syncDb]}`) start and stop the client:
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// Before
|
|
413
|
+
{userId ? <SyncDbProvider client={syncDb}>{stack}</SyncDbProvider> : stack}
|
|
414
|
+
|
|
415
|
+
// After
|
|
416
|
+
<SyncDbProvider client={syncDb}>{stack}</SyncDbProvider>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Also keep the navigator on screen while a refresh or sign-out runs with a user present: gate the loading screen on `isLoading && !userId`, not on `isLoading` alone.
|
|
420
|
+
|
|
421
|
+
### Social login, passkeys, two-factor, and account security live in `@terreno/syncdb/auth`
|
|
422
|
+
|
|
423
|
+
57.x apps that adopted the screens and hooks from `@terreno/rtk` change the import path. Names are unchanged.
|
|
424
|
+
|
|
425
|
+
| 57.x (`@terreno/rtk`) | 58.0.0 (`@terreno/syncdb/auth`) |
|
|
426
|
+
|-----------------------|---------------------------------|
|
|
427
|
+
| `TerrenoLoginScreen`, `useLoginController` | Same |
|
|
428
|
+
| `TerrenoAccountSecurityScreen`, `useAccountSecurity` | Same |
|
|
429
|
+
| `useAuthMethods(authClient)` | Same |
|
|
430
|
+
| `createRememberedAccountsStore`, `signInSocialWithHandoff`, `completeBetterAuthOAuthHandoff` | Same |
|
|
431
|
+
| `createBetterAuthClient({features})` | Same; `features: {multiSession, passkey, twoFactor}` |
|
|
432
|
+
| `selectBetterAuthIsSigningOut` | `useSession().isSigningOut` |
|
|
433
|
+
|
|
434
|
+
`@terreno/syncdb` now has `@better-auth/passkey` and `@terreno/ui` as optional peers (the auth screens render `@terreno/ui`). Add `@terreno/ui` to apps that import `TerrenoLoginScreen` or `TerrenoAccountSecurityScreen`.
|
|
435
|
+
|
|
436
|
+
### Provisional consent hooks take a `client`
|
|
437
|
+
|
|
438
|
+
`useProvisionalConsent` and `ProvisionalConsentSigner` (`@terreno/ui`) take `client` (a `TerrenoRequestClient`) like the other consent hooks, not `api`.
|
|
439
|
+
|
|
440
|
+
### `PATCH /auth/me` documents `currentPassword`
|
|
441
|
+
|
|
442
|
+
The OpenAPI body of `PATCH /auth/me` now lists `currentPassword`. Regenerate `store/apiSdk.ts` (`bun run sdk`) so `usePatchAuthMeMutation` types it. The server still refuses a `password` field on that route; use the password-change flow.
|
|
443
|
+
|
|
444
|
+
### Admin lists: `useAdminRpcQuery` `keepPreviousData`
|
|
445
|
+
|
|
446
|
+
`useAdminRpcQuery` clears its data when the `url` changes, so a body is never shown under the wrong URL. Custom admin screens that page through a list pass `keepPreviousData: true` to keep the previous page visible while the next one loads; without it the list goes blank on every page.
|
|
447
|
+
|
|
448
|
+
## Example app changes to copy
|
|
449
|
+
|
|
450
|
+
`example-frontend` and `example-backend` are the reference app. Apply these to your own app; each maps to a section above.
|
|
451
|
+
|
|
452
|
+
**Native build config** (needed for EAS and local dev client builds):
|
|
453
|
+
|
|
454
|
+
| File | Change | Why |
|
|
455
|
+
|------|--------|-----|
|
|
456
|
+
| `app.json` | `"disableSPM": true` on the `@stripe/stripe-react-native` plugin | iOS `pod install` fails when Expo's precompiled modules turn Stripe's SPM dynamic framework into a static library. `useFrameworks: "dynamic"` does not fix it |
|
|
457
|
+
| `eas.json` | On the Android `development` build profile: `"resourceClass": "large"` and `"gradleCommand": ":app:assembleDebug -PreactNativeArchitectures=arm64-v8a"` | The default Android worker kills the Gradle daemon (container out of memory); building one ABI also halves the native compile |
|
|
458
|
+
| `package.json` | `"eas-build-post-install"` compiles workspace dependencies in dependency order, not `bun run compile` | A clean EAS checkout has no `dist/`. `--filter '*'` does not order dev and peer dependencies, so `@terreno/test` compiles before `@terreno/ui`. Outside the Terreno monorepo, publish the packages and this hook is not needed |
|
|
459
|
+
| `package.json` | `@terreno/ui` is a dependency whenever you import `@terreno/syncdb/auth` screens | `syncdb` compiles against it |
|
|
460
|
+
|
|
461
|
+
**Auth and session wiring:**
|
|
462
|
+
|
|
463
|
+
| File | Change |
|
|
464
|
+
|------|--------|
|
|
465
|
+
| `lib/betterAuth.ts` | `createBetterAuthClient({features: {multiSession: true, passkey: true, twoFactor: true}})`; `createRememberedAccountsStore`; `getLoginCallbackURL` / `getSecurityCallbackURL` helpers; `brandedOAuthProviders()` driven by the server's `/terreno/methods` instead of `EXPO_PUBLIC_MOCK_OIDC_SIGN_IN` |
|
|
466
|
+
| `app/_layout.tsx` | `SyncDbProvider` always mounted; loading screen only while `isAuthLoading && !userId`; a signed-in user may stay on `/login` (the login screen records the method and navigates itself); only `/signup` redirects signed-in users |
|
|
467
|
+
| `app/login.tsx` | `TerrenoLoginScreen` with `rememberIdentity`, two-factor inline challenge, passkey sign-in, "Continue as" cards |
|
|
468
|
+
| `app/settings/security.tsx` (new) | `TerrenoAccountSecurityScreen`: password, two-factor, passkeys, linked providers, device sessions |
|
|
469
|
+
| `app/(tabs)/profile.tsx` | Changing the email asks for the current password (`currentPassword` on `PATCH /auth/me`); there is no "New Password" field, because `PATCH /auth/me` refuses `password`; the sign-out button shows `isSigningOut` |
|
|
470
|
+
| `app/consent-sign/[userId].tsx` (new) | Provisional consent signing on a member's behalf, via `useProvisionalConsent({client, userId})` |
|
|
471
|
+
| `utils/userSecurityAccess.ts` and the admin user and org member security screens | Admin recovery and per-member security views |
|
|
472
|
+
| `store/apiSdk.ts`, `store/syncDbSdk.ts` | Regenerate: `bun run sdk && bun run sync-sdk` |
|
|
473
|
+
|
|
474
|
+
**Backend (`example-backend`):** `betterAuthConfig.ts` turns on `nativeApps`, `multiSession`, `twoFactor`, and passkeys (passkeys stay off in production while `FRONTEND_URL` is a loopback URL; a browser only accepts a passkey on the `rpID` host or a subdomain of it). It also registers a mock OIDC provider only when `MOCK_OIDC_ISSUER` is set, never in production.
|
|
475
|
+
|
|
476
|
+
**Phone layouts.** The web layouts these screens had do not fit a phone; the example fixes show what to check in your own screens:
|
|
477
|
+
|
|
478
|
+
| Screen | Problem | Fix |
|
|
479
|
+
|--------|---------|-----|
|
|
480
|
+
| `app/(tabs)/ai.tsx` | The fixed chat-history sidebar left no room for the conversation on a phone | Show the sidebar at desktop width only (`isSupportedDesktopWidth`). Below it, add Chats and New chat buttons, a history sheet, and safe-area padding |
|
|
481
|
+
| `components/SyncTodosScreen.tsx` | The header's Browse button squeezed the title into a one-character column | Stack the header: `direction="column" lgDirection="row"`. Use `lg`, not `md`: native breakpoints are sm 320 / md 375 / lg 600 / xl 1024, so every phone is at least `md` |
|
|
482
|
+
|
|
483
|
+
**Verify a device build, not only web.** The native-only breakages in this section (async storage, `nativeApps`, `SyncDbProvider` unmount) pass web e2e. Run the dev client on iOS and Android, sign in, sign out, and sign back in as a second user before shipping.
|
|
484
|
+
|
|
485
|
+
### Verification
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
rg -l "@terreno/rtk|@reduxjs/toolkit|react-redux|redux-persist|rtk-query" --glob '!**/node_modules/**' . # nothing
|
|
489
|
+
bun pm ls 2>/dev/null | rg "better-auth|@better-auth" # all 1.7.4
|
|
490
|
+
bun run compile && bun run lint && bun test
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
On a dev client: sign in, open Profile, tap Sign out (it shows progress, then lands on login with no error screen), and confirm `POST /auth/sign-out` returns 200.
|
|
@@ -316,7 +316,7 @@ If the app creates histories from the client with `useMutate("gpt/histories").cr
|
|
|
316
316
|
|
|
317
317
|
Steps:
|
|
318
318
|
|
|
319
|
-
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage`; rebuild native clients.
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
320
|
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
321
|
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
322
|
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
@@ -41,11 +41,18 @@ on a MongoDB replica set. See [Backend setup](../reference/syncdb.md#backend-set
|
|
|
41
41
|
```bash
|
|
42
42
|
bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi
|
|
43
43
|
bun add @terreno/syncdb
|
|
44
|
-
bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage
|
|
44
|
+
bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
`@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively.
|
|
48
|
+
`@terreno/syncdb` lists them only as optional peers, and `baseUrl` imports `expo-constants`, so
|
|
49
|
+
install both explicitly.
|
|
50
|
+
|
|
51
|
+
Keep or add the other optional peers of `@terreno/syncdb` for the features the app uses:
|
|
52
|
+
|
|
53
|
+
- `better-auth`, `@better-auth/expo`, `@better-auth/passkey`: `createBetterAuthClient`
|
|
54
|
+
- `@openfeature/web-sdk`, `@openfeature/react-sdk`: feature flags
|
|
55
|
+
- `@modelcontextprotocol/client`: `useMCPTools` Rebuild native clients
|
|
49
56
|
after adding `expo-sqlite`.
|
|
50
57
|
|
|
51
58
|
Pin `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` to the same `1.7.4`.
|
package/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
12
12
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
13
13
|
"@sentry/bun": "^11.0.0",
|
|
14
|
-
"@terreno/api": "58.0.0-beta.
|
|
15
|
-
"@terreno/blocks": "58.0.0-beta.
|
|
14
|
+
"@terreno/api": "58.0.0-beta.3",
|
|
15
|
+
"@terreno/blocks": "58.0.0-beta.3",
|
|
16
16
|
"express": "^5.2.1",
|
|
17
17
|
"luxon": "^3.7.2",
|
|
18
18
|
"minisearch": "^7.2.0",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@biomejs/biome": "^2.3.6",
|
|
24
24
|
"@modelcontextprotocol/client": "^2.0.0",
|
|
25
|
-
"@terreno/test": "58.0.0-beta.
|
|
25
|
+
"@terreno/test": "58.0.0-beta.3",
|
|
26
26
|
"@types/bun": "^1.4.0",
|
|
27
27
|
"@types/luxon": "^3.7.1",
|
|
28
28
|
"@types/node": "^25.3.0",
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
"typecheck": "NODE_OPTIONS=--max-old-space-size=8192 tsc -p tsconfig.typecheck.json"
|
|
110
110
|
},
|
|
111
111
|
"type": "module",
|
|
112
|
-
"version": "58.0.0-beta.
|
|
112
|
+
"version": "58.0.0-beta.3"
|
|
113
113
|
}
|
|
@@ -316,7 +316,7 @@ If the app creates histories from the client with `useMutate("gpt/histories").cr
|
|
|
316
316
|
|
|
317
317
|
Steps:
|
|
318
318
|
|
|
319
|
-
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage`; rebuild native clients.
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
320
|
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
321
|
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
322
|
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# Upgrading to 58.0.0-beta.3
|
|
2
|
+
|
|
3
|
+
**Action required:** Yes
|
|
4
|
+
**Affected packages:** @terreno/rtk (removed), @terreno/syncdb, @terreno/ui, @terreno/admin-frontend, @terreno/admin-spa, @terreno/api, @terreno/ai, @terreno/feature-flags, @terreno/mcp, @terreno/cli, create-terreno-app
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
First release of the Expo SDK 58 line. Upgrade the app to Expo 58 and React Native 0.88, rebuild native binaries, then bump every `@terreno/*` package to `58.0.0` together. This major also deletes `@terreno/rtk` and Redux: `@terreno/syncdb` is the only frontend data, request, and session layer. Run the `McpServiceToken` data migration before deploying 58 frontends. Each removal has its own section below.
|
|
9
|
+
|
|
10
|
+
Order of work:
|
|
11
|
+
|
|
12
|
+
1. Upgrade Expo (next section) and rebuild native binaries.
|
|
13
|
+
2. Backend: remove `modelRouter` `realtime`, then run the [MCP service token](#mcp-service-tokens-are-a-synced-collection) migration. GPT histories keep their ObjectId ids and need no migration ([GPT histories](#gpt-histories-are-a-synced-collection)).
|
|
14
|
+
3. Frontend: [replace `@terreno/rtk` and Redux](#terrenortk-and-redux-are-removed) with `@terreno/syncdb`, then update [admin](#admin-no-longer-takes-an-rtk-api) and [ui navigators](#consent-and-announcement-navigators-take-a-client).
|
|
15
|
+
|
|
16
|
+
## Upgrade Expo before Terreno
|
|
17
|
+
|
|
18
|
+
| Package | Version |
|
|
19
|
+
|---------|---------|
|
|
20
|
+
| `expo` | `~58.0.14` |
|
|
21
|
+
| `react-native` | `0.88.0` |
|
|
22
|
+
| `react` / `react-dom` | `19.3.0` |
|
|
23
|
+
| `@types/react` | `~19.3.0` |
|
|
24
|
+
| `@sentry/react-native` | `~8.28.0` (major 7 → 8) |
|
|
25
|
+
| `react-native-gesture-handler` | `~3.3.0` (major 2 → 3) |
|
|
26
|
+
| `react-native-webview` | `14.0.1` (major 13 → 14) |
|
|
27
|
+
|
|
28
|
+
Run `bunx expo install expo@^58.0.0 --fix` at the app root, rebuild dev clients and store binaries (the native fingerprint changes), then `bun run compile` / `bun run lint`.
|
|
29
|
+
|
|
30
|
+
### Expo Router `initialRouteName`
|
|
31
|
+
|
|
32
|
+
**What changed**
|
|
33
|
+
|
|
34
|
+
Expo Router 58 no longer accepts `initialRouteName` as a `<Slot>` / `<Stack>` / `<Navigator>` prop. `SidebarNavigation` ignores it.
|
|
35
|
+
|
|
36
|
+
**Migration**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Before
|
|
40
|
+
<Slot initialRouteName="home" />
|
|
41
|
+
|
|
42
|
+
// After — in the same _layout.tsx
|
|
43
|
+
export const unstable_settings = {initialRouteName: "home"};
|
|
44
|
+
<Slot />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Custom `app/+html.tsx`
|
|
48
|
+
|
|
49
|
+
Expo Router 58 renders static web pages by streaming. The bundle `<script>` tags,
|
|
50
|
+
React Native Web styles, and route metadata now reach a custom `app/+html.tsx` through
|
|
51
|
+
`useServerDocumentContext()`. A `+html.tsx` that does not render them exports pages with
|
|
52
|
+
no JavaScript: the static HTML shows, but nothing hydrates or responds.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import {ScrollViewStyleReset, useServerDocumentContext} from "expo-router/html";
|
|
56
|
+
|
|
57
|
+
export default function Root({children}: {children: React.ReactNode}) {
|
|
58
|
+
const {bodyAttributes, bodyNodes, headNodes, htmlAttributes} = useServerDocumentContext();
|
|
59
|
+
return (
|
|
60
|
+
<html lang="en" {...htmlAttributes}>
|
|
61
|
+
<head>
|
|
62
|
+
<meta charSet="utf-8" />
|
|
63
|
+
<ScrollViewStyleReset />
|
|
64
|
+
{headNodes}
|
|
65
|
+
</head>
|
|
66
|
+
<body {...bodyAttributes}>
|
|
67
|
+
{children}
|
|
68
|
+
{bodyNodes}
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Apps without a custom `+html.tsx` need no change.
|
|
76
|
+
|
|
77
|
+
### React Native 0.88 instance types
|
|
78
|
+
|
|
79
|
+
**What changed**
|
|
80
|
+
|
|
81
|
+
React Native 0.87+ ships generated types without `react-native-legacy-deep-imports`. Component names such as `View`, `Text`, `ScrollView`, and `TextInput` are no longer usable as ref types. `Box.scrollRef`, `TextField.inputRef`, and the `Text` `ref` now use the instance types.
|
|
82
|
+
|
|
83
|
+
**Migration**
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// Before
|
|
87
|
+
const ref = useRef<TextInput>(null);
|
|
88
|
+
|
|
89
|
+
// After
|
|
90
|
+
import type {TextInputInstance} from "react-native";
|
|
91
|
+
const ref = useRef<TextInputInstance>(null);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use `ViewInstance`, `TextInstance`, `ScrollViewInstance`, and `TextInputInstance`. `TextStyle` is `Readonly`; build style objects instead of mutating them. `TextInput.State.currentlyFocusedInput()` replaces `currentlyFocusedField()`.
|
|
95
|
+
|
|
96
|
+
### `@terreno/ui` peer dependency
|
|
97
|
+
|
|
98
|
+
`@terreno/ui` peers `react-native` `0.88.0`. Installing `@terreno/ui@58` next to Expo 57 is unsupported.
|
|
99
|
+
|
|
100
|
+
### `@sentry/react-native` 8
|
|
101
|
+
|
|
102
|
+
Expo 58 bundles `@sentry/react-native` `~8.28.0`. Follow Sentry's 7 → 8 migration guide for your app's `Sentry.init` options.
|
|
103
|
+
|
|
104
|
+
## `Filter` and `FilterProps` are removed
|
|
105
|
+
|
|
106
|
+
`@terreno/ui` removes the `Filter` and `FilterProps` aliases added as deprecations in 57.7.0. Import `DropdownPanel` and `DropdownPanelProps`. `FilterSelectMenu`, `FilterBoolean`, `FilterAccordion`, and `FilterChangesBadge` keep their names. See [57.7.0](57.7.0.md).
|
|
107
|
+
|
|
108
|
+
## HTTP rate limiting defaults on
|
|
109
|
+
|
|
110
|
+
`TerrenoApp` now mounts the HTTP rate limiter when `rateLimit` is omitted: memory store, auth 20 / 15 min (login, signup, refresh, GitHub OAuth, Better Auth credential routes), api 600 / 15 min (everything else). `/health`, `/healthz`, `/openapi.json`, and `/swagger` are never limited. Over the limit returns 429 `rate-limit-exceeded` with `Retry-After`.
|
|
111
|
+
|
|
112
|
+
Steps:
|
|
113
|
+
|
|
114
|
+
1. **Cloud Run (or any load balancer): pass `trustProxy: 1`.** Without it every unauthenticated request keys by the front end's address, so the whole service shares one auth bucket (20 logins per 15 minutes for all users).
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
new TerrenoApp({userModel: User, rateLimit: {trustProxy: 1}});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. **If you set `app.set("trust proxy", …)` before `build()`** (for example in `beforeJsonSetup`), move the value to `rateLimit.trustProxy`. The limiter sets Express `trust proxy` when it mounts and defaults it to `false`.
|
|
121
|
+
3. **More than one replica:** the memory store is per process. Pass `store: "redis"` (`VALKEY_URL` / `REDIS_URL`) or `store: "mongo"` for one shared bucket.
|
|
122
|
+
4. **To keep HTTP unlimited, pass `rateLimit: false`:**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
new TerrenoApp({userModel: User, rateLimit: false});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
5. **Tests:** each `build()` gets a fresh memory store. A suite that logs in more than 20 times or sends more than 600 requests against one app instance should pass `rateLimit: false` in its shared app helper.
|
|
129
|
+
|
|
130
|
+
Apps that already pass `rateLimit: {…}` see no change. `@terreno/api` still does not read `RATE_LIMIT_ENABLED`; an app-owned toggle should now produce `false` to turn the limiter off. See [Rate limiting](../../../docs/how-to/rate-limiting.md).
|
|
131
|
+
|
|
132
|
+
## Admin no longer takes an RTK `api`
|
|
133
|
+
|
|
134
|
+
`@terreno/admin-frontend` stopped injecting endpoints into your RTK Query API. Every admin request — model CRUD (including ObjectId and API-only models), `/admin/config`, scripts, roles, configuration, documents, comms, jobs, organizations, consent, announcements, and AI observability — now goes through one native `fetch` client that `AdminProvider` binds to your auth. The `api` prop, `useAdminApi`, `AdminApi`, `EndpointBuilder`, and admin `injectEndpoints` are removed. String-`_id` models with `adminBroadcast` keep the windowed syncdb path from 57.
|
|
135
|
+
|
|
136
|
+
Before:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<AdminProvider api={terrenoApi} apiBase="/admin" apiOrigin={baseUrl} getAuthHeaders={getAdminAuthHeaders} routeBase="/admin">
|
|
140
|
+
<AdminShellLayout api={terrenoApi} apiBase="/admin" routeBase="/admin">
|
|
141
|
+
<Stack />
|
|
142
|
+
</AdminShellLayout>
|
|
143
|
+
</AdminProvider>
|
|
144
|
+
|
|
145
|
+
<AdminScreenRouter api={terrenoApi} apiBase="/admin" name={model} routeBase="/admin" />
|
|
146
|
+
<AdminModelTable api={terrenoApi} baseUrl="/admin" modelName="User" />
|
|
147
|
+
|
|
148
|
+
const {useListQuery} = useAdminApi(terrenoApi, "/admin/users", "User");
|
|
149
|
+
const {config} = useAdminConfig(terrenoApi, "/admin");
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
After:
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<AdminProvider apiBase="/admin" apiOrigin={baseUrl} getAuthHeaders={getAdminAuthHeaders} routeBase="/admin">
|
|
156
|
+
<AdminShellLayout apiBase="/admin" routeBase="/admin">
|
|
157
|
+
<Stack />
|
|
158
|
+
</AdminShellLayout>
|
|
159
|
+
</AdminProvider>
|
|
160
|
+
|
|
161
|
+
<AdminScreenRouter apiBase="/admin" name={model} routeBase="/admin" />
|
|
162
|
+
<AdminModelTable baseUrl="/admin" modelName="User" />
|
|
163
|
+
|
|
164
|
+
const {useListQuery} = useAdminModelApi("/admin/users", "User");
|
|
165
|
+
const {config} = useAdminConfig("/admin");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Same-origin cookie SPAs (like `@terreno/admin-spa`) pass `credentials="same-origin"` on `AdminProvider` and omit `apiOrigin` / `getAuthHeaders`. `getAdminAuthHeaders` is your host's `() => ({Authorization: "Bearer …"})` (example-frontend reads the Better Auth session token).
|
|
169
|
+
|
|
170
|
+
Product screens that reuse admin components outside the admin tree (for example `DocumentStorageBrowser` or `ConsentHistory` on a user tab) must render inside an `AdminProvider` with the same binding. Without one, admin hooks fall back to an unauthenticated same-origin `fetch` and warn once in development.
|
|
171
|
+
|
|
172
|
+
Checklist:
|
|
173
|
+
|
|
174
|
+
- [ ] Remove `api={…}` from every `@terreno/admin-frontend` component and from custom admin widget props.
|
|
175
|
+
- [ ] Drop the `api` first argument from `useAdminConfig`, `useAdminScripts`, `useAdminRoles`, `useConfigurationApi`, `useDocumentStorageApi`, `useConsentHistory`, `useOrganizationsApi`, `useCommsDashboardApi`, `useJobsDashboardApi`, and the `useAiObservability*Api` hooks. `useOrganizationsApi` also drops its unused `organizationId` argument.
|
|
176
|
+
- [ ] Query `refetch()` now returns a Promise; wrap fire-and-forget calls as `void refetch()`.
|
|
177
|
+
- [ ] Replace `useAdminApi(api, routePath, name)` with `useAdminModelApi(routePath, name)`. Hook names, `.unwrap()`, and response shapes are unchanged.
|
|
178
|
+
- [ ] Make sure one `AdminProvider` wraps the admin tree with `credentials` (cookie) or `apiOrigin` + `getAuthHeaders` (Bearer).
|
|
179
|
+
- [ ] Wrap any product-side reuse of admin components in `AdminProvider`.
|
|
180
|
+
- [ ] Delete type-erased casts such as `terrenoApi as unknown as AdminScreenProps["api"]` and any admin-only RTK tag types.
|
|
181
|
+
- [ ] Replace custom admin `injectEndpoints` with `useAdminRpc` + `useAdminRpcQuery` / `useAdminRpcMutation`.
|
|
182
|
+
|
|
183
|
+
## New native modules (rebuild dev clients)
|
|
184
|
+
|
|
185
|
+
Terreno 58 adds every native module the B2B features need in one release, so later features ship as JavaScript and OTA updates against the same binary. A dev client or store build made before 58 does not contain these modules. Apps that call them on an old binary crash or get `null` modules.
|
|
186
|
+
|
|
187
|
+
| Package | Version | Config plugin |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `@stripe/stripe-react-native` | `0.81.0` | Yes: `merchantIdentifier`, `enableGooglePay` |
|
|
190
|
+
| `react-native-purchases` | `10.12.2` | None |
|
|
191
|
+
| `react-native-purchases-ui` | `10.12.2` | None (keep the same version as `react-native-purchases`) |
|
|
192
|
+
| `expo-device` | `~58.0.5` | None |
|
|
193
|
+
| `expo-crypto` | `~58.0.2` (unchanged pin) | None |
|
|
194
|
+
| `expo-local-authentication` | `~58.0.2` | Yes: `faceIDPermission` |
|
|
195
|
+
| `expo-system-ui` | `~58.0.6` | Applied automatically |
|
|
196
|
+
| `expo-build-properties` | `~58.0.11` | Yes: Android Kotlin and `compileSdkVersion` pins |
|
|
197
|
+
|
|
198
|
+
`react-native-otp-verify` is not in the baseline. It does not support the New Architecture, the only architecture React Native 0.88 runs. For one-time codes, use `TextInput` with `textContentType="oneTimeCode"` (iOS) and `autoComplete="sms-otp"` (Android), which need no native module.
|
|
199
|
+
|
|
200
|
+
Checklist:
|
|
201
|
+
|
|
202
|
+
1. Add the packages to the app with `bunx expo install @stripe/stripe-react-native react-native-purchases@10.12.2 react-native-purchases-ui@10.12.2 expo-device expo-crypto expo-local-authentication expo-system-ui expo-build-properties`. In the Terreno monorepo they are root catalog entries; reference them with `"catalog:"`.
|
|
203
|
+
2. Add the config plugins to `app.json` (or `app.config.ts`). Replace the merchant id with your own Apple Pay merchant id; register it in the Apple Developer portal before the iOS build.
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
[
|
|
207
|
+
"expo-build-properties",
|
|
208
|
+
{"android": {"compileSdkVersion": 37, "kotlinVersion": "2.2.21"}}
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
"@stripe/stripe-react-native",
|
|
212
|
+
{"enableGooglePay": true, "merchantIdentifier": "merchant.com.yourcompany.app"}
|
|
213
|
+
],
|
|
214
|
+
[
|
|
215
|
+
"expo-local-authentication",
|
|
216
|
+
{"faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to confirm it is you."}
|
|
217
|
+
]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
React Native 0.88 already compiles against Android SDK 37. Stripe needs at least SDK 36 and Kotlin 2.x; the pins keep both explicit. Kotlin `2.2.21` matches the Stripe Android build default and Expo's KSP table.
|
|
221
|
+
3. Rebuild every dev client and store binary (`eas build --profile development` for iOS and Android). Expo fingerprints change, so OTA updates from 57 binaries do not apply to 58 binaries.
|
|
222
|
+
4. Open the demo's hidden **Native Modules** page (`/demo/Native%20Modules`) on the new dev client. Each native module must show **Linked**. Web shows **Native only**.
|
|
223
|
+
|
|
224
|
+
What still works in Expo Go:
|
|
225
|
+
|
|
226
|
+
| Flow | Expo Go |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `expo-device`, `expo-crypto`, `expo-system-ui` | Works (bundled in Expo Go) |
|
|
229
|
+
| `expo-local-authentication` | Works, except Face ID on iOS (Expo Go has no Face ID usage string); use a dev build |
|
|
230
|
+
| `react-native-purchases` / `-ui` | Runs in RevenueCat Preview API mode: mock offerings and paywalls, no real purchases |
|
|
231
|
+
| `@stripe/stripe-react-native` | Card flows work (Expo Go 58 bundles `0.81.0`). Apple Pay and Google Pay need a dev build, because the merchant id is set by the config plugin |
|
|
232
|
+
| Web | Unchanged. Native-only calls need a `Platform.OS` guard |
|
|
233
|
+
|
|
234
|
+
## Legacy RTK realtime is removed
|
|
235
|
+
|
|
236
|
+
Terreno 58 deletes the RTK cache-patching realtime path. Collection live updates go through `@terreno/syncdb`: `sync` on `modelRouter` plus `sync:delta` events. `RealtimeApp` stays: it still hosts Socket.io, socket auth, change streams, and `sync:delta`. `SyncApp` and `admin.realtime` (`admin:model.changed` events) are unchanged. The rest of `@terreno/rtk`, including `useSocketConnection` and `useRealtimeDebug`, is removed too; see [`@terreno/rtk` and Redux are removed](#terrenortk-and-redux-are-removed).
|
|
237
|
+
|
|
238
|
+
Removed from `@terreno/api`:
|
|
239
|
+
|
|
240
|
+
- `modelRouter` option `realtime`. Passing it is now a TypeScript error.
|
|
241
|
+
- The `sync` websocket event. The change stream emits only `sync:delta` for models registered with `sync`.
|
|
242
|
+
- The `subscribe:model`, `subscribe:document`, and `subscribe:query` socket events (and their `unsubscribe:*` pairs, plus `query:subscribed`).
|
|
243
|
+
- Types `RealtimeConfig`, `RealtimeEvent`, `DocumentSubscription`, and `QuerySubscription`.
|
|
244
|
+
- Registry helpers `registerRealtime`, `getRealtimeRegistry`, `findRegistryEntryByCollection`, `findRegistryEntryByRoutePath`, `updateRealtimeRegistryOptions`, `clearRealtimeRegistry`, and type `RealtimeRegistryEntry`.
|
|
245
|
+
- Query store helpers `addQuerySubscription`, `removeQuerySubscription`, `removeAllSocketQueries`, `getQuerySubscriptionsForCollection`, `computeQueryId`, and `clearQueryStore`.
|
|
246
|
+
- Constants `MAX_MODEL_SUBSCRIPTIONS`, `MAX_DOCUMENT_SUBSCRIPTIONS`, and `MAX_QUERY_SUBSCRIPTIONS`.
|
|
247
|
+
|
|
248
|
+
Removed from `@terreno/rtk`:
|
|
249
|
+
|
|
250
|
+
- `realtimeList`, `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`, `useSyncConnection`, and type `RealtimeEvent`.
|
|
251
|
+
|
|
252
|
+
To upgrade:
|
|
253
|
+
|
|
254
|
+
1. Backend: delete `realtime: {...}` from every `modelRouter` config. For each collection that needs live updates, add `syncPlugin` + `isDeletedPlugin` to the schema and a `sync` config to the router (three-argument form), then register `SyncApp` next to `RealtimeApp`.
|
|
255
|
+
2. Frontend: delete `setRealtimeSocket(socket)`, `useSyncConnection(...)`, and every `onCacheEntryAdded: realtimeList(...)` / `realtimeDocument(...)` override. Read and write synced collections with `useQuery` / `useMutate` from `@terreno/syncdb/react`.
|
|
256
|
+
3. Tests: replace `clearRealtimeRegistry()` with `clearSyncRegistry()`, which clears the whole collection catalog.
|
|
257
|
+
|
|
258
|
+
See [How to upgrade from @terreno/rtk to @terreno/syncdb](../../../docs/how-to/migrate-rtk-to-syncdb.md).
|
|
259
|
+
|
|
260
|
+
## MCP service tokens are a synced collection
|
|
261
|
+
|
|
262
|
+
`McpServiceToken` now has a String `_id` (UUID for new rows), `isDeletedPlugin`, and `syncPlugin`. With `mcpServiceTokens` enabled, `/mcp/service-tokens` is a `modelRouter` synced read-only to its owner (collection `mcp/service-tokens`, owner scope on `userId`). Sync snapshots and deltas never include `tokenHash`. `McpServiceToken.verify` and `/mcp` authentication are unchanged.
|
|
263
|
+
|
|
264
|
+
| 57.x | 58.0.0 |
|
|
265
|
+
|------|--------|
|
|
266
|
+
| `POST /mcp/service-tokens` | `POST /mcp/service-tokens/create` (same body; same one-time `token` response) |
|
|
267
|
+
| `DELETE /mcp/service-tokens/:id` | `POST /mcp/service-tokens/:id/revoke` (response is the token row without `tokenHash`) |
|
|
268
|
+
| `GET /mcp/service-tokens` rows `{id, name, tokenPrefix, …}` with `null` dates | Standard `modelRouter` list rows (`_id`, `id`, unset dates omitted); or `useQuery("mcp/service-tokens")` |
|
|
269
|
+
| `revokeForUser(user, ObjectId)` | `revokeForUser(user, string)` |
|
|
270
|
+
|
|
271
|
+
`POST`, `PATCH`, and `DELETE` on the collection return `405`, and sync client writes are nacked `unauthorized`. Admin delete still revokes: a soft delete is stored as `revokedAt`, not a tombstone. `McpServiceToken.deleteMany` now throws once the collection is synced (use `McpServiceToken.collection.deleteMany` in tests).
|
|
272
|
+
|
|
273
|
+
**Run the data migration before deploying 58.** Existing rows keep their ids as hex strings. Add this file to your `migrations/` directory, dry-run it, then apply it:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// migrations/20261022120000-mcp-service-token-string-ids.ts
|
|
277
|
+
import {mcpServiceTokenStringIdsMigration} from "@terreno/api";
|
|
278
|
+
|
|
279
|
+
export const id = "20261022120000-mcp-service-token-string-ids";
|
|
280
|
+
export const up = mcpServiceTokenStringIdsMigration.up;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The migration is idempotent and has no `down` (new rows use UUIDs). See [Connect an MCP client with a service token](../../../docs/how-to/connect-mcp-service-token.md).
|
|
284
|
+
|
|
285
|
+
## GPT histories are a synced collection
|
|
286
|
+
|
|
287
|
+
`GptHistory` now has `isDeletedPlugin` and `syncPlugin`, and keeps its ObjectId `_id` (no data migration). `/gpt/histories` is registered with `sync: {scope: {type: "owner", field: "userId"}}`, so `SyncApp` serves each user's histories as the collection `gpt/histories`. REST CRUD, `/gpt/prompt`, `/gpt/histories/:id/stream`, the `turn` and `pendingAsks` actions, ratings, and `/gpt/actions` are unchanged.
|
|
288
|
+
|
|
289
|
+
| 57.x | 58.0.0 |
|
|
290
|
+
|------|--------|
|
|
291
|
+
| `useGetGptHistoriesQuery` + `updateQueryData` / `invalidateTags` | `useQuery("gpt/histories")` and `useMutate("gpt/histories")` from `@terreno/syncdb/react` |
|
|
292
|
+
| `GptHistory.deleteMany(...)` / `updateMany(...)` in app code or tests | Write one history at a time by `_id`; tests use `GptHistory.collection.deleteMany({})` |
|
|
293
|
+
|
|
294
|
+
If the app creates histories from the client with `useMutate("gpt/histories").create`, pass `idFormats: {"gpt/histories": "objectId"}` to `createSyncDb` so the client mints ObjectIds instead of UUIDs. See [@terreno/ai reference](../../../docs/reference/ai.md#gpthistory-ids).
|
|
295
|
+
|
|
296
|
+
## `@terreno/rtk` and Redux are removed
|
|
297
|
+
|
|
298
|
+
`@terreno/rtk` is not published for 58, and no Terreno package uses Redux. `@terreno/syncdb` takes over every job it did. The [upgrade guide](../../../docs/how-to/migrate-rtk-to-syncdb.md) walks through each step and maps **every** removed export to its replacement; this section is the summary.
|
|
299
|
+
|
|
300
|
+
| `@terreno/rtk` 57.x | 58.0.0 |
|
|
301
|
+
|---------------------|--------|
|
|
302
|
+
| `emptySplitApi`, `getBaseQuery`, `@rtk-query/codegen-openapi` → `store/openApiSdk.ts` | `createApiClient` (`@terreno/syncdb/api`) + `ApiClientProvider`; `terreno-syncdb-codegen --api-out` → `store/apiSdk.ts` (same hook names) |
|
|
303
|
+
| RTK Query hooks for collection CRUD | `useQuery` / `useMutate` (`@terreno/syncdb/react`) |
|
|
304
|
+
| `api.injectEndpoints(...)` | Document the route in OpenAPI and regenerate, or `useApiQuery` / `useApiMutation` |
|
|
305
|
+
| `createBetterAuthClient` | `createBetterAuthClient` (`@terreno/syncdb/auth`) |
|
|
306
|
+
| `generateBetterAuthSlice` + `selectBetterAuth*` selectors | `SessionProvider` + `useSession()` (`@terreno/syncdb/auth`) |
|
|
307
|
+
| `generateAuthSlice` (JWT) + `useSelectCurrentUserId` | `jwtAuthAdapter` + `useJwtAuth` + `useSession()` (`@terreno/syncdb/auth`) |
|
|
308
|
+
| `getTokenExpirationTimes`, `shouldShowStillThereModal` | Same names (`@terreno/syncdb/auth`) |
|
|
309
|
+
| `baseUrl`, `baseWebsocketsUrl`, `resolveBaseUrls` | Same names (`@terreno/syncdb/config`), plus `appHeaders()` |
|
|
310
|
+
| `useSocketConnection` | The syncdb socket: `useSocketEvent` (`@terreno/syncdb/react`) / `syncDb.onSocketEvent` |
|
|
311
|
+
| `useFeatureFlags(api, …)`, `useTerrenoFeatureFlags(api, …)` | `useFeatureFlags({client, socket, …})`, `useTerrenoFeatureFlags({client, socket, …})` (`@terreno/syncdb/flags`) |
|
|
312
|
+
| `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installTerrenoDevConsoleLogger`, `useMCPTools` | `useUpgradeCheck`, `hasPermission`, `canOpenAdminPage`, `installDevConsoleLogger`, `useMCPTools({authProvider})` (`@terreno/syncdb/app`) |
|
|
313
|
+
| `@terreno/rtk/buildNumber` | `@terreno/syncdb/buildNumber` |
|
|
314
|
+
| Offline middleware, `useOfflineStatus`, `useServerStatus` | The syncdb outbox; `useSyncStatus()` |
|
|
315
|
+
| `useCan`, `useSelectPermissions`, `createSessionApi`, `useTerrenoChat`, `generateTags`, `populateId`, `registerTerrenoDevStore` | Removed (no replacement needed; see the guide) |
|
|
316
|
+
|
|
317
|
+
Steps:
|
|
318
|
+
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
|
+
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
|
+
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
|
+
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
323
|
+
5. Remove `useSocketConnection`; pass `socket: syncDb` to the flag hooks and use `useSocketEvent` for other server pushes. The app opens one socket.
|
|
324
|
+
6. Verify: `rg -l "@terreno/rtk|@reduxjs/toolkit|react-redux|redux-persist|rtk-query" --glob '!**/node_modules/**' .` returns nothing; compile, lint, and test pass.
|
|
325
|
+
|
|
326
|
+
The feature-flags `GET /evaluate` deprecation warning now points at `@terreno/syncdb/flags`.
|
|
327
|
+
|
|
328
|
+
### Consent and announcement navigators take a `client`
|
|
329
|
+
|
|
330
|
+
`@terreno/ui` no longer calls into an RTK Query API. `ConsentNavigator`, `AnnouncementNavigator`, and their hooks take a request `client` (`TerrenoRequestClient`; a syncdb `ApiClient` satisfies it).
|
|
331
|
+
|
|
332
|
+
| 57.x | 58.0.0 |
|
|
333
|
+
|------|--------|
|
|
334
|
+
| `<ConsentNavigator api={terrenoApi}>` | `<ConsentNavigator client={apiClient}>` |
|
|
335
|
+
| `<AnnouncementNavigator api={terrenoApi} …>` | `<AnnouncementNavigator client={apiClient} …>` |
|
|
336
|
+
| `useConsentForms(api, baseUrl)`, `useSubmitConsent(api, baseUrl)` | `useConsentForms(client, baseUrl)`, `useSubmitConsent(client, baseUrl)` |
|
|
337
|
+
| `useAnnouncements(api, …)`, `useAcknowledgeAnnouncement(api, …)` | `useAnnouncements(client, …)`, `useAcknowledgeAnnouncement(client, …)` |
|
|
338
|
+
|
|
339
|
+
See [@terreno/ui reference](../../../docs/reference/ui.md).
|
|
340
|
+
|
|
341
|
+
### Scaffolds, MCP tools, and the CLI
|
|
342
|
+
|
|
343
|
+
`create-terreno-app` and the `terreno_bootstrap` prompt now scaffold a frontend with no Redux and no `@terreno/rtk`: `lib/betterAuth.ts` (`createBetterAuthClient` from `@terreno/syncdb/auth`), `store/syncdb.ts` (shared `authProvider` + `syncDb`), `store/apiClient.ts` (`createApiClient` from `@terreno/syncdb/api`), and `store/apiSdk.ts` (generated hooks). `app/_layout.tsx` mounts `ApiClientProvider` and `SessionProvider`.
|
|
344
|
+
|
|
345
|
+
| 57.x | 58.0.0 |
|
|
346
|
+
|------|--------|
|
|
347
|
+
| `bun run sdk` → `scripts/generate-sdk.ts` + `openapi-config.ts` (`@rtk-query/codegen-openapi`) → `store/openApiSdk.ts` | `bun run sdk` → `api-sdk` script: `terreno-syncdb-codegen --schema <openapi.json> --api-out ./store/apiSdk.ts` |
|
|
348
|
+
| `store/index.ts`, `store/appState.ts`, `store/errors.ts`, `store/sdk.ts`, `store/betterAuthApi.ts`, `tsconfig.codegen.json` | Deleted. Session: `useSession()`; request errors: `createApiClient({onError})` |
|
|
349
|
+
| `useGetMeQuery` / `usePatchMeMutation` from `@/store/sdk` | `useGetAuthMeQuery` / `usePatchAuthMeMutation` from `@/store/apiSdk` |
|
|
350
|
+
| `getAdminAuthHeaders` from `@/store/betterAuthApi` | `getAdminAuthHeaders` from `@/store/apiClient` (reads `authProvider.getToken()`) |
|
|
351
|
+
| MCP `get_rtk_state` (terreno-mcp-local) | `get_syncdb_state`, `syncdb_snapshot`, `syncdb_action` |
|
|
352
|
+
| `terreno state --slice rtk` | `terreno state` (reads `get_syncdb_state`; `--collection`, `--id`, `--limit`) |
|
|
353
|
+
| `terreno generate sdk --config openapi-config.ts` | `terreno generate sdk --schema <openapi.json> --out store/apiSdk.ts` |
|
|
354
|
+
| MCP resource `terreno://docs/rtk` | `terreno://docs/syncdb` |
|
|
355
|
+
| `terreno_generate_screen` list/detail/form used `@/store/openApiSdk` hooks | Uses `useQuery` / `useEntity` / `useMutate` from `@terreno/syncdb/react` |
|
|
356
|
+
|
|
357
|
+
## Better Auth 1.7 and native sign-in
|
|
358
|
+
|
|
359
|
+
Terreno 58 pins `better-auth` and `@better-auth/expo` to `1.7.4` (the root catalog). `@better-auth/passkey` needs 1.7, and the 1.6 and 1.7 Expo plugins differ on native.
|
|
360
|
+
|
|
361
|
+
### Pin Better Auth 1.7.4
|
|
362
|
+
|
|
363
|
+
**What changed**
|
|
364
|
+
|
|
365
|
+
`@terreno/syncdb` and `@terreno/api` type-check against Better Auth 1.7. Mixing 1.6 and 1.7 between `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` fails to compile (client plugin types disagree) and breaks passkey sign-in at runtime. Better Auth 1.7 serves provider callbacks at `/callback/:id`; check that any redirect URI you registered with a provider still matches.
|
|
366
|
+
|
|
367
|
+
**Migration**
|
|
368
|
+
|
|
369
|
+
Set all three to the same version in the app (`"catalog:"` in the Terreno monorepo):
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"dependencies": {
|
|
374
|
+
"@better-auth/expo": "1.7.4",
|
|
375
|
+
"@better-auth/passkey": "1.7.4",
|
|
376
|
+
"better-auth": "1.7.4"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Native storage needs `getItemAsync` and `setItemAsync`
|
|
382
|
+
|
|
383
|
+
**What changed**
|
|
384
|
+
|
|
385
|
+
The 1.7 Expo client plugin reads and writes its session cookie through `storage.getItemAsync` / `storage.setItemAsync`. A storage object with only `getItem` / `setItem` makes every auth request on iOS and Android throw `undefined is not a function`. Web does not exercise this path, so web e2e passes while a device build is broken.
|
|
386
|
+
|
|
387
|
+
`createBetterAuthClient` from `@terreno/syncdb/auth` supplies both methods through its `StorageAdapter`. Apps that build their own Better Auth client with `expoClient({storage})` must provide them too (`getItemAsync?: (key) => Promise<string | null>`, `setItemAsync?: (key, value) => Promise<void>`).
|
|
388
|
+
|
|
389
|
+
### Native sign-out needs `nativeApps: true` on the server
|
|
390
|
+
|
|
391
|
+
**What changed**
|
|
392
|
+
|
|
393
|
+
Native requests carry `expo-origin` instead of `Origin`. Without the server-side Expo plugin, `POST /auth/sign-out` answers `403 Missing or null Origin` on iOS and Android, so the user can never sign out.
|
|
394
|
+
|
|
395
|
+
**Migration**
|
|
396
|
+
|
|
397
|
+
Set `nativeApps: true` in the Better Auth server config (`create-terreno-app` and `example-backend` already do):
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const config: BetterAuthConfig = {enabled: true, nativeApps: true, /* ... */};
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Logout waits for the server and keeps the session on failure
|
|
404
|
+
|
|
405
|
+
`useSession().logout()` signs out of the server first, with the session held. While it runs, `useSession().isSigningOut` is `true` and `isLoading` is unchanged, so a navigator gated on `isLoading` keeps its screen. A sign-out that fails or times out (`signOutTimeoutMs`, default 10 seconds) first checks the session. If it is still live, the session and local data stay and `error` is `SIGN_OUT_FAILED_MESSAGE`; if it is gone, local state is cleared. Do not treat `logout()` as instant, and show `isSigningOut` on the sign-out button.
|
|
406
|
+
|
|
407
|
+
### Keep `SyncDbProvider` mounted across sign-out
|
|
408
|
+
|
|
409
|
+
Mounting `<SyncDbProvider>` only while `userId` is set unmounts every `useQuery` / `useMutate` consumer in the middle of logout, and screens that are still on screen throw. Mount it unconditionally and let `SessionProvider` (`syncDbs={[syncDb]}`) start and stop the client:
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// Before
|
|
413
|
+
{userId ? <SyncDbProvider client={syncDb}>{stack}</SyncDbProvider> : stack}
|
|
414
|
+
|
|
415
|
+
// After
|
|
416
|
+
<SyncDbProvider client={syncDb}>{stack}</SyncDbProvider>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Also keep the navigator on screen while a refresh or sign-out runs with a user present: gate the loading screen on `isLoading && !userId`, not on `isLoading` alone.
|
|
420
|
+
|
|
421
|
+
### Social login, passkeys, two-factor, and account security live in `@terreno/syncdb/auth`
|
|
422
|
+
|
|
423
|
+
57.x apps that adopted the screens and hooks from `@terreno/rtk` change the import path. Names are unchanged.
|
|
424
|
+
|
|
425
|
+
| 57.x (`@terreno/rtk`) | 58.0.0 (`@terreno/syncdb/auth`) |
|
|
426
|
+
|-----------------------|---------------------------------|
|
|
427
|
+
| `TerrenoLoginScreen`, `useLoginController` | Same |
|
|
428
|
+
| `TerrenoAccountSecurityScreen`, `useAccountSecurity` | Same |
|
|
429
|
+
| `useAuthMethods(authClient)` | Same |
|
|
430
|
+
| `createRememberedAccountsStore`, `signInSocialWithHandoff`, `completeBetterAuthOAuthHandoff` | Same |
|
|
431
|
+
| `createBetterAuthClient({features})` | Same; `features: {multiSession, passkey, twoFactor}` |
|
|
432
|
+
| `selectBetterAuthIsSigningOut` | `useSession().isSigningOut` |
|
|
433
|
+
|
|
434
|
+
`@terreno/syncdb` now has `@better-auth/passkey` and `@terreno/ui` as optional peers (the auth screens render `@terreno/ui`). Add `@terreno/ui` to apps that import `TerrenoLoginScreen` or `TerrenoAccountSecurityScreen`.
|
|
435
|
+
|
|
436
|
+
### Provisional consent hooks take a `client`
|
|
437
|
+
|
|
438
|
+
`useProvisionalConsent` and `ProvisionalConsentSigner` (`@terreno/ui`) take `client` (a `TerrenoRequestClient`) like the other consent hooks, not `api`.
|
|
439
|
+
|
|
440
|
+
### `PATCH /auth/me` documents `currentPassword`
|
|
441
|
+
|
|
442
|
+
The OpenAPI body of `PATCH /auth/me` now lists `currentPassword`. Regenerate `store/apiSdk.ts` (`bun run sdk`) so `usePatchAuthMeMutation` types it. The server still refuses a `password` field on that route; use the password-change flow.
|
|
443
|
+
|
|
444
|
+
### Admin lists: `useAdminRpcQuery` `keepPreviousData`
|
|
445
|
+
|
|
446
|
+
`useAdminRpcQuery` clears its data when the `url` changes, so a body is never shown under the wrong URL. Custom admin screens that page through a list pass `keepPreviousData: true` to keep the previous page visible while the next one loads; without it the list goes blank on every page.
|
|
447
|
+
|
|
448
|
+
## Example app changes to copy
|
|
449
|
+
|
|
450
|
+
`example-frontend` and `example-backend` are the reference app. Apply these to your own app; each maps to a section above.
|
|
451
|
+
|
|
452
|
+
**Native build config** (needed for EAS and local dev client builds):
|
|
453
|
+
|
|
454
|
+
| File | Change | Why |
|
|
455
|
+
|------|--------|-----|
|
|
456
|
+
| `app.json` | `"disableSPM": true` on the `@stripe/stripe-react-native` plugin | iOS `pod install` fails when Expo's precompiled modules turn Stripe's SPM dynamic framework into a static library. `useFrameworks: "dynamic"` does not fix it |
|
|
457
|
+
| `eas.json` | On the Android `development` build profile: `"resourceClass": "large"` and `"gradleCommand": ":app:assembleDebug -PreactNativeArchitectures=arm64-v8a"` | The default Android worker kills the Gradle daemon (container out of memory); building one ABI also halves the native compile |
|
|
458
|
+
| `package.json` | `"eas-build-post-install"` compiles workspace dependencies in dependency order, not `bun run compile` | A clean EAS checkout has no `dist/`. `--filter '*'` does not order dev and peer dependencies, so `@terreno/test` compiles before `@terreno/ui`. Outside the Terreno monorepo, publish the packages and this hook is not needed |
|
|
459
|
+
| `package.json` | `@terreno/ui` is a dependency whenever you import `@terreno/syncdb/auth` screens | `syncdb` compiles against it |
|
|
460
|
+
|
|
461
|
+
**Auth and session wiring:**
|
|
462
|
+
|
|
463
|
+
| File | Change |
|
|
464
|
+
|------|--------|
|
|
465
|
+
| `lib/betterAuth.ts` | `createBetterAuthClient({features: {multiSession: true, passkey: true, twoFactor: true}})`; `createRememberedAccountsStore`; `getLoginCallbackURL` / `getSecurityCallbackURL` helpers; `brandedOAuthProviders()` driven by the server's `/terreno/methods` instead of `EXPO_PUBLIC_MOCK_OIDC_SIGN_IN` |
|
|
466
|
+
| `app/_layout.tsx` | `SyncDbProvider` always mounted; loading screen only while `isAuthLoading && !userId`; a signed-in user may stay on `/login` (the login screen records the method and navigates itself); only `/signup` redirects signed-in users |
|
|
467
|
+
| `app/login.tsx` | `TerrenoLoginScreen` with `rememberIdentity`, two-factor inline challenge, passkey sign-in, "Continue as" cards |
|
|
468
|
+
| `app/settings/security.tsx` (new) | `TerrenoAccountSecurityScreen`: password, two-factor, passkeys, linked providers, device sessions |
|
|
469
|
+
| `app/(tabs)/profile.tsx` | Changing the email asks for the current password (`currentPassword` on `PATCH /auth/me`); there is no "New Password" field, because `PATCH /auth/me` refuses `password`; the sign-out button shows `isSigningOut` |
|
|
470
|
+
| `app/consent-sign/[userId].tsx` (new) | Provisional consent signing on a member's behalf, via `useProvisionalConsent({client, userId})` |
|
|
471
|
+
| `utils/userSecurityAccess.ts` and the admin user and org member security screens | Admin recovery and per-member security views |
|
|
472
|
+
| `store/apiSdk.ts`, `store/syncDbSdk.ts` | Regenerate: `bun run sdk && bun run sync-sdk` |
|
|
473
|
+
|
|
474
|
+
**Backend (`example-backend`):** `betterAuthConfig.ts` turns on `nativeApps`, `multiSession`, `twoFactor`, and passkeys (passkeys stay off in production while `FRONTEND_URL` is a loopback URL; a browser only accepts a passkey on the `rpID` host or a subdomain of it). It also registers a mock OIDC provider only when `MOCK_OIDC_ISSUER` is set, never in production.
|
|
475
|
+
|
|
476
|
+
**Phone layouts.** The web layouts these screens had do not fit a phone; the example fixes show what to check in your own screens:
|
|
477
|
+
|
|
478
|
+
| Screen | Problem | Fix |
|
|
479
|
+
|--------|---------|-----|
|
|
480
|
+
| `app/(tabs)/ai.tsx` | The fixed chat-history sidebar left no room for the conversation on a phone | Show the sidebar at desktop width only (`isSupportedDesktopWidth`). Below it, add Chats and New chat buttons, a history sheet, and safe-area padding |
|
|
481
|
+
| `components/SyncTodosScreen.tsx` | The header's Browse button squeezed the title into a one-character column | Stack the header: `direction="column" lgDirection="row"`. Use `lg`, not `md`: native breakpoints are sm 320 / md 375 / lg 600 / xl 1024, so every phone is at least `md` |
|
|
482
|
+
|
|
483
|
+
**Verify a device build, not only web.** The native-only breakages in this section (async storage, `nativeApps`, `SyncDbProvider` unmount) pass web e2e. Run the dev client on iOS and Android, sign in, sign out, and sign back in as a second user before shipping.
|
|
484
|
+
|
|
485
|
+
### Verification
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
rg -l "@terreno/rtk|@reduxjs/toolkit|react-redux|redux-persist|rtk-query" --glob '!**/node_modules/**' . # nothing
|
|
489
|
+
bun pm ls 2>/dev/null | rg "better-auth|@better-auth" # all 1.7.4
|
|
490
|
+
bun run compile && bun run lint && bun test
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
On a dev client: sign in, open Profile, tap Sign out (it shows progress, then lands on login with no error screen), and confirm `POST /auth/sign-out` returns 200.
|
|
@@ -316,7 +316,7 @@ If the app creates histories from the client with `useMutate("gpt/histories").cr
|
|
|
316
316
|
|
|
317
317
|
Steps:
|
|
318
318
|
|
|
319
|
-
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage`; rebuild native clients.
|
|
319
|
+
1. Remove the packages: `bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi`. Add `@terreno/syncdb` and `bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network`; rebuild native clients. `@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively; `@terreno/syncdb` only lists them as optional peers, and `baseUrl` imports `expo-constants`. Keep or add these optional peers for the features you use: `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` (`createBetterAuthClient`); `@openfeature/web-sdk` and `@openfeature/react-sdk` (flags); `@modelcontextprotocol/client` (`useMCPTools`).
|
|
320
320
|
2. Create one `authProvider` (`betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))`, or `jwtAuthAdapter`) and pass it to both `createSyncDb` and `createApiClient`.
|
|
321
321
|
3. Replace `<Provider store>` / `<PersistGate>` with `<SessionProvider>`, `<ApiClientProvider>`, and `<SyncDbProvider>`. Replace `useSelector` session reads with `useSession()`; move app-state slices to React context or `useStoredState`.
|
|
322
322
|
4. Point `bun run sdk` at `terreno-syncdb-codegen --api-out ./store/apiSdk.ts`, regenerate, and change imports from `@/store/openApiSdk` / `@/store/sdk` to `@/store/apiSdk` (`useGetMeQuery` → `useGetAuthMeQuery`).
|
|
@@ -41,11 +41,18 @@ on a MongoDB replica set. See [Backend setup](../reference/syncdb.md#backend-set
|
|
|
41
41
|
```bash
|
|
42
42
|
bun remove @terreno/rtk @reduxjs/toolkit react-redux redux-persist @rtk-query/codegen-openapi
|
|
43
43
|
bun add @terreno/syncdb
|
|
44
|
-
bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage
|
|
44
|
+
bunx expo install expo-sqlite expo-secure-store @react-native-async-storage/async-storage expo-constants expo-network
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
`@terreno/rtk` depended on `expo-constants` and `expo-network`, so apps got them transitively.
|
|
48
|
+
`@terreno/syncdb` lists them only as optional peers, and `baseUrl` imports `expo-constants`, so
|
|
49
|
+
install both explicitly.
|
|
50
|
+
|
|
51
|
+
Keep or add the other optional peers of `@terreno/syncdb` for the features the app uses:
|
|
52
|
+
|
|
53
|
+
- `better-auth`, `@better-auth/expo`, `@better-auth/passkey`: `createBetterAuthClient`
|
|
54
|
+
- `@openfeature/web-sdk`, `@openfeature/react-sdk`: feature flags
|
|
55
|
+
- `@modelcontextprotocol/client`: `useMCPTools` Rebuild native clients
|
|
49
56
|
after adding `expo-sqlite`.
|
|
50
57
|
|
|
51
58
|
Pin `better-auth`, `@better-auth/expo`, and `@better-auth/passkey` to the same `1.7.4`.
|