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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,18 +2,21 @@
2
2
  name: terreno-data-fetching
3
3
  description: >-
4
4
  Use when implementing or debugging ANY network request, API call, or data
5
- fetching in a Terreno app. Covers syncdb collection CRUD, RTK Query generated
6
- hooks for non-synced routes, admin's host-bound fetch client, auth, caching,
7
- and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
5
+ fetching in a Terreno app. Covers syncdb collection CRUD, generated request
6
+ hooks (store/apiSdk.ts) for non-synced routes, session auth, socket pushes,
7
+ feature flags, admin's host-bound fetch client, and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
8
8
  application code.
9
9
  Lifecycle composition: Grow for data-flow shape, Pick for implementation, Roast
10
10
  for independent network/cache behavior proof.
11
11
  ---
12
12
  # Terreno Data Fetching
13
13
 
14
- **You MUST use this skill for ANY networking work in Terreno apps.** Use syncdb
15
- for synced collection CRUD and generated RTK Query hooks for non-synced routes.
14
+ **You MUST use this skill for ANY networking work in Terreno apps.** Everything goes
15
+ through `@terreno/syncdb`: `useQuery` / `useMutate` for synced collection CRUD, and
16
+ generated hooks on the `@terreno/syncdb/api` request client for every other route.
16
17
  Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
18
+ `@terreno/rtk` and Redux were removed in Terreno 58; upgrade old code with
19
+ `docs/how-to/migrate-rtk-to-syncdb.md`.
17
20
 
18
21
  **Related skills:** `generate-sdk` (regenerate hooks after backend changes), `terreno-backend-api` (create backend routes), `building-terreno-apps` (screen integration).
19
22
 
@@ -28,9 +31,9 @@ Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
28
31
 
29
32
  ```
30
33
  references/
31
- auth-and-tokens.md generateAuthSlice, login/logout, token refresh
32
- realtime.md Sockets, feature flags, server status
33
- sdk-customization.md injectEndpoints, cache tags, hand-maintained sdk.ts
34
+ auth-and-tokens.md SessionProvider/useSession, login/logout, token refresh, JWT
35
+ realtime.md Socket events, feature flags, sync status
36
+ sdk-customization.md Generated apiSdk.ts, ad-hoc useApiQuery/useApiMutation, cache tags
34
37
  ```
35
38
 
36
39
  ## When to Use
@@ -44,15 +47,19 @@ references/
44
47
 
45
48
  ## Core Rules
46
49
 
47
- 1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useMutate`) for synced collection CRUD.
48
- 2. **Use generated hooks** from `store/openApiSdk.ts` (or `store/sdk.ts`) for non-synced routes.
49
- 3. **Never edit `openApiSdk.ts` manually** — regenerate with `generate-sdk`.
50
- 4. **Never use `axios` or raw `fetch`** in app code. Framework-owned admin RPC
51
- is the exception: `@terreno/admin-frontend` binds `adminRequest` from host
52
- `credentials` / `getAuthHeaders`.
53
- 5. **Do not add new admin `injectEndpoints`.** String-`_id` models with
54
- `adminBroadcast` use windowed syncdb; ObjectId compatibility CRUD remains RTK
55
- until Terreno 58 removes `useAdminApi` and the required `api` prop.
50
+ 1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useWindowQuery`, `useMutate`) for synced collection CRUD.
51
+ 2. **Use generated hooks** from `store/apiSdk.ts` (`terreno-syncdb-codegen --api-out`) for every
52
+ non-synced route: `/auth/me`, modelRouter actions, AI, consents, announcements.
53
+ 3. **Never edit `store/apiSdk.ts` or `store/syncDbSdk.ts`** — regenerate with `generate-sdk`.
54
+ 4. **Ad-hoc routes not in OpenAPI** use `useApiQuery` / `useLazyApiQuery` / `useApiMutation`
55
+ from `@terreno/syncdb/react`. Prefer documenting the route in OpenAPI and regenerating.
56
+ 5. **Never use `axios` or raw `fetch`** in app code. Exceptions: SSE streams (`/gpt/prompt`)
57
+ authenticated with `authProvider.getToken()`, and framework-owned admin requests
58
+ (`@terreno/admin-frontend` binds `adminRequest` from `AdminProvider` `credentials` /
59
+ `getAuthHeaders` / `apiOrigin`).
60
+ 6. **Admin uses its own fetch client.** String-`_id` models with `adminBroadcast` use
61
+ windowed syncdb; ObjectId/API-only CRUD uses `useAdminModelApi`; admin RPC uses
62
+ `useAdminRpc`. Admin components take no `api` prop (removed in Terreno 58).
56
63
 
57
64
  ## Quick Start
58
65
 
@@ -71,12 +78,13 @@ const TodosScreen: React.FC = () => {
71
78
  ### Synced collection write
72
79
 
73
80
  ```tsx
81
+ import {generateMutationId} from "@terreno/syncdb";
74
82
  import {useMutate} from "@terreno/syncdb/react";
75
83
 
76
84
  const {create, update} = useMutate("todos");
77
85
 
78
86
  const handleCreate = useCallback((): void => {
79
- create({data: {title: "New todo", completed: false}});
87
+ create({data: {_id: generateMutationId(), title: "New todo", completed: false}});
80
88
  }, [create]);
81
89
 
82
90
  const handleToggle = useCallback((id: string, completed: boolean): void => {
@@ -84,47 +92,48 @@ const handleToggle = useCallback((id: string, completed: boolean): void => {
84
92
  }, [update]);
85
93
  ```
86
94
 
87
- ### Auth
95
+ ### Non-synced route
88
96
 
89
97
  ```tsx
90
- import {generateAuthSlice} from "@terreno/rtk";
91
- import {terrenoApi} from "@/store/sdk";
98
+ import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
99
+
100
+ const {data: me, isLoading} = useGetAuthMeQuery(undefined, {skip: !userId});
101
+ const [updateMe, {isLoading: isSaving}] = usePatchAuthMeMutation();
102
+ await updateMe({name}).unwrap();
103
+ ```
92
104
 
93
- const {authReducer, logout, middleware} = generateAuthSlice(terrenoApi);
105
+ ### Auth
94
106
 
95
- // In a component:
96
- import {useEmailLoginMutation} from "@/store/sdk";
97
- import {useSelectCurrentUserId} from "@terreno/rtk";
107
+ ```tsx
108
+ import {useSession} from "@terreno/syncdb/auth";
98
109
 
99
- const [login] = useEmailLoginMutation();
100
- const userId = useSelectCurrentUserId();
110
+ const {userId, isAuthenticated, isLoading, logout} = useSession();
101
111
  ```
102
112
 
103
113
  See `./references/auth-and-tokens.md` for the full auth flow.
104
114
 
105
- ## Redux Store Setup
115
+ ## Client Setup
116
+
117
+ One `authProvider` feeds the sync client and the request client, so both send the same
118
+ token and refresh on a 401 the same way. Reference: `example-frontend/store/syncdb.ts`,
119
+ `store/apiClient.ts`, `app/_layout.tsx`.
106
120
 
107
121
  ```tsx
108
- import {configureStore} from "@reduxjs/toolkit";
109
- import {generateAuthSlice} from "@terreno/rtk";
110
- import {terrenoApi} from "./sdk";
111
-
112
- const authSlice = generateAuthSlice(terrenoApi);
113
-
114
- const store = configureStore({
115
- reducer: {
116
- auth: authSlice.authReducer,
117
- [terrenoApi.reducerPath]: terrenoApi.reducer,
118
- },
119
- middleware: (getDefault) =>
120
- getDefault().concat(terrenoApi.middleware, ...authSlice.middleware),
121
- });
122
-
123
- export const {logout} = authSlice;
122
+ // store/syncdb.ts
123
+ export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient));
124
+ export const syncDb = createSyncDb({authProvider, baseUrl, collections: [...SYNC_COLLECTIONS], name: "myapp", wipeOnSignOut: true});
125
+
126
+ // store/apiClient.ts
127
+ export const apiClient = createApiClient({authProvider, baseUrl, credentials: "include", headers: appHeaders});
128
+
129
+ // app/_layout.tsx — no Redux store
130
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
131
+ <ApiClientProvider client={apiClient}>
132
+ <SyncDbProvider client={syncDb}>{children}</SyncDbProvider>
133
+ </ApiClientProvider>
134
+ </SessionProvider>
124
135
  ```
125
136
 
126
- See `example-frontend/store/index.ts` for persist and Sentry integration.
127
-
128
137
  ## Environment Variables
129
138
 
130
139
  | Variable | Purpose |
@@ -133,7 +142,7 @@ See `example-frontend/store/index.ts` for persist and Sentry integration.
133
142
  | `EXPO_PUBLIC_DEV_API_PORT` | Dev API port when using Expo host URI (default `4000`) |
134
143
  | `expo.extra.BASE_URL` | Production/staging API URL in `app.json` |
135
144
 
136
- Base URL resolution priority (from `@terreno/rtk`):
145
+ Base URL resolution priority (`baseUrl` from `@terreno/syncdb/config`):
137
146
 
138
147
  1. `expo.extra.BASE_URL`
139
148
  2. `EXPO_PUBLIC_API_URL`
@@ -144,13 +153,13 @@ Never put server secrets in `EXPO_PUBLIC_*` variables.
144
153
 
145
154
  ## Error Handling
146
155
 
147
- RTK Query exposes errors on the hook result:
156
+ Request hooks expose `ApiError` (`@terreno/syncdb/api`) on the result:
148
157
 
149
158
  ```tsx
150
- const {error, isError} = useGetMeQuery();
159
+ const {error, isError} = useGetAuthMeQuery();
151
160
 
152
161
  if (isError) {
153
- // error is FetchBaseQueryError | SerializedError
162
+ // error.status, error.data.title, error.data.detail; status 0 = network error or timeout
154
163
  console.error("Query failed", error);
155
164
  }
156
165
  ```
@@ -160,27 +169,29 @@ For mutations, use `.unwrap()`:
160
169
  ```tsx
161
170
  try {
162
171
  await updateMe({name}).unwrap();
163
- } catch (err) {
164
- // Handle APIError-shaped responses from @terreno/api
165
- console.error("Mutation failed", err);
172
+ } catch (error) {
173
+ if (isApiError(error)) {
174
+ console.error("Mutation failed", error.data.title);
175
+ }
166
176
  }
167
177
  ```
168
178
 
169
- Use `isNetworkFetchError` from `@terreno/rtk` to detect connectivity issues in error UI.
179
+ Report every request failure once with `createApiClient({onError})` (skip Sentry when
180
+ `error.data.disableExternalErrorTracking` is `true`). Synced writes never throw: rejected
181
+ writes surface in `useConflicts()` and `useSyncStatus().failedCount`.
170
182
 
171
183
  ## SDK Regeneration
172
184
 
173
- After a non-synced backend API change, run the `generate-sdk` skill. Synced
174
- collection CRUD does not belong in the OpenAPI SDK.
185
+ After a backend API change, run the `generate-sdk` skill.
175
186
 
176
187
  ```bash
177
188
  # Backend running on :4000, then:
178
- cd example-frontend && bun run sdk
189
+ cd example-frontend && bun run sdk # store/apiSdk.ts (non-synced routes)
190
+ cd example-frontend && bun run sync-sdk # store/syncDbSdk.ts (synced collections)
179
191
  ```
180
192
 
181
- Triggers: custom routes, non-synced `modelRouter` routes, and OpenAPI builder
182
- edits. Regenerate syncdb collection types/hooks with its codegen when a synced
183
- schema changes.
193
+ Triggers: custom routes, modelRouter actions, non-synced `modelRouter` routes, and OpenAPI
194
+ edits (`sdk`); synced schema or `sync` config changes (`sync-sdk`).
184
195
 
185
196
  ## Decision Tree
186
197
 
@@ -190,22 +201,22 @@ Need data in a Terreno app?
190
201
  | \-- @terreno/syncdb hooks
191
202
  |
192
203
  |-- Non-synced API route?
193
- | \-- useGet*/usePost* from generated SDK
204
+ | \-- useGet*/usePost* from store/apiSdk.ts
194
205
  |
195
206
  |-- Built-in admin?
196
207
  | |-- String id + adminBroadcast -> windowed syncdb
197
- | |-- Framework RPC -> host-bound adminRequest
198
- | \-- ObjectId compatibility CRUD -> useAdminApi until Terreno 58
208
+ | |-- Framework RPC -> AdminProvider adminRequest (useAdminRpc)
209
+ | \-- ObjectId / API-only CRUD -> useAdminModelApi
199
210
  |
200
211
  |-- Login/logout?
201
212
  | \-- references/auth-and-tokens.md
202
213
  |
203
- |-- Realtime updates?
204
- | \-- references/realtime.md (useSocketConnection)
214
+ |-- Server push / feature flags?
215
+ | \-- references/realtime.md (useSocketEvent, @terreno/syncdb/flags)
205
216
  |
206
217
  |-- Custom endpoint not in SDK?
207
218
  | |-- Backend exists? -> generate-sdk first
208
- | \-- App-only extension? -> references/sdk-customization.md
219
+ | \-- Route outside OpenAPI? -> references/sdk-customization.md (useApiQuery)
209
220
  |
210
221
  |-- Backend doesn't exist?
211
222
  | \-- terreno-backend-api, then generate-sdk
@@ -237,10 +248,10 @@ fetch(url, {headers: {Authorization: `Bearer ${token}`}});
237
248
  **Right: a generated hook handles auth for a non-synced route**
238
249
 
239
250
  ```tsx
240
- const {data: profile} = useGetMeQuery();
251
+ const {data: profile} = useGetAuthMeQuery();
241
252
  ```
242
253
 
243
- **Wrong: editing openApiSdk.ts**
254
+ **Wrong: editing store/apiSdk.ts**
244
255
 
245
256
  ```tsx
246
257
  // Adding a new endpoint by hand
@@ -269,8 +280,9 @@ throw new APIError({status: 422, title: "Not a member"});
269
280
  ## Example Invocations
270
281
 
271
282
  - "How do I fetch todos?" → `useQuery("todos")` from `@terreno/syncdb/react`
272
- - "How do I handle login?" → `useEmailLoginMutation` + `generateAuthSlice`
273
- - "Should I use React Query?" → No — syncdb for collections, generated RTK hooks for non-synced routes
274
- - "API calls return 401" → Check token refresh; see auth-and-tokens.md
283
+ - "How do I handle login?" → `betterAuthClient.signIn.email` then `useSession().refresh()`
284
+ - "Should I use React Query?" → No — syncdb for collections, generated `store/apiSdk.ts` hooks for non-synced routes
285
+ - "API calls return 401" → Check the shared `authProvider`; see auth-and-tokens.md
286
+ - "Old code imports `@terreno/rtk`" → `docs/how-to/migrate-rtk-to-syncdb.md`
275
287
  - "Hooks are missing after backend change" → Run `generate-sdk` skill
276
288
  - "How do I add a custom endpoint?" → Backend route + OpenAPI, then regenerate the SDK
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Terreno Data Fetching"
3
- short_description: "RTK Query generated hooks, auth tokens, and realtime sockets for Terreno apps — never axios or raw fetch"
4
- default_prompt: "Use $terreno-data-fetching to implement API calls with generated RTK Query hooks, auth, and caching in a Terreno app."
3
+ short_description: "syncdb collection hooks, generated request hooks, session auth, and socket events for Terreno apps — never axios or raw fetch"
4
+ default_prompt: "Use $terreno-data-fetching to implement API calls with @terreno/syncdb collection hooks, generated store/apiSdk.ts hooks, and session auth in a Terreno app."
@@ -1,93 +1,89 @@
1
1
  # Auth and Tokens — Terreno
2
2
 
3
- ## generateAuthSlice
3
+ Session state lives in `@terreno/syncdb/auth`. There is no Redux store. Reference:
4
+ `docs/reference/syncdb.md#auth`, `example-frontend/lib/betterAuth.ts`, `app/login.tsx`.
4
5
 
5
- `generateAuthSlice(api)` from `@terreno/rtk` creates:
6
-
7
- - `authReducer` — tracks `userId`, `error`, `lastTokenRefreshTimestamp`
8
- - `logout` action — clears tokens from secure storage
9
- - `middleware` — stores tokens on login, removes on logout
10
- - Built-in endpoints: `emailLogin`, `emailSignUp`, `googleLogin`, `resetPassword`
11
-
12
- ## Login
6
+ ## Better Auth client
13
7
 
14
8
  ```tsx
15
- import {useEmailLoginMutation} from "@/store/sdk";
16
- import {useSelectCurrentUserId} from "@terreno/rtk";
17
-
18
- const [login, {isLoading, error}] = useEmailLoginMutation();
19
-
20
- await login({email, password}).unwrap();
21
- // Tokens stored automatically; setUserId dispatched
9
+ // lib/betterAuth.ts
10
+ import {createBetterAuthClient} from "@terreno/syncdb/auth";
11
+ import {baseUrl} from "@terreno/syncdb/config";
12
+
13
+ export const betterAuthClient = createBetterAuthClient({
14
+ baseURL: baseUrl,
15
+ scheme: "myapp",
16
+ storagePrefix: "myapp",
17
+ });
22
18
  ```
23
19
 
24
- ## Logout
20
+ Storage is `expo-secure-store` on native and AsyncStorage on web. Never store tokens in
21
+ plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
25
22
 
26
- ```tsx
27
- import {useAppDispatch, logout} from "../store/index.ts";
23
+ ## Session provider
28
24
 
29
- const dispatch = useAppDispatch();
30
- dispatch(logout());
31
- // Clears AUTH_TOKEN and REFRESH_TOKEN from storage
25
+ ```tsx
26
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
27
+ <App />
28
+ </SessionProvider>
32
29
  ```
33
30
 
34
- ## Token storage
35
-
36
- - **Native:** `expo-secure-store` (encrypted)
37
- - **Web:** `@react-native-async-storage/async-storage` (SSR-safe wrapper)
38
-
39
- Never store tokens in plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
31
+ ```tsx
32
+ import {useSession} from "@terreno/syncdb/auth";
40
33
 
41
- ## Token refresh
34
+ const {userId, user, isAuthenticated, isLoading, error, logout, refresh} = useSession();
35
+ ```
42
36
 
43
- `emptyApi` from `@terreno/rtk` automatically:
37
+ ## Login
44
38
 
45
- - Refreshes access token when < 2 minutes from expiry
46
- - Retries failed requests after 401 with refreshed token
47
- - Uses a mutex to prevent concurrent refresh storms
39
+ ```tsx
40
+ const {refresh} = useSession();
48
41
 
49
- No manual refresh logic needed in app code.
42
+ const result = await betterAuthClient.signIn.email({email, password});
43
+ if (!result.error) {
44
+ await refresh(); // resolve userId now instead of waiting for the session atom
45
+ }
46
+ ```
50
47
 
51
- ## Better Auth (optional)
48
+ Use `SocialLoginButton` from `@terreno/ui` with `betterAuthClient.signIn.social({provider})`
49
+ for OAuth.
52
50
 
53
- For apps using Better Auth instead of JWT/Passport:
51
+ ## Logout
54
52
 
55
53
  ```tsx
56
- import {createBetterAuthClient, generateBetterAuthSlice, setTokenSource} from "@terreno/rtk";
57
- import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
54
+ const {logout} = useSession();
55
+ await logout();
56
+ ```
58
57
 
59
- const authClient = createBetterAuthClient({
60
- baseURL: process.env.EXPO_PUBLIC_API_URL,
61
- scheme: "myapp", // deep-link scheme, required
62
- });
58
+ `logout()` signs out each `syncDbs` client first (wiping local rows when created with
59
+ `wipeOnSignOut: true`), then the auth server, then runs `onLogout`.
63
60
 
64
- // {reducer, middleware, actions, slice, syncSession}; mount reducer at `betterAuth`
65
- const betterAuth = generateBetterAuthSlice({authClient});
61
+ ## Token refresh
66
62
 
67
- // One token source for syncdb and every rtk consumer, registered once at startup
68
- export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient));
69
- setTokenSource(authProvider);
70
- ```
63
+ `betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` is the shared
64
+ `AuthProvider` for `createSyncDb` and `createApiClient`. The request client adds
65
+ `Authorization: Bearer`, and on a 401 calls `authProvider.refresh()` once and retries.
66
+ No manual refresh logic in app code. Code outside the client (SSE streams, `AdminProvider`
67
+ `getAuthHeaders`) reads `authProvider.getToken()`.
71
68
 
72
- With the source registered, `emptySplitApi` sends the Better Auth token, sends no
73
- `authorization` header when there is none, and logs out only after a 401 survives one
74
- `refresh()`. Do not fork the base query to read the session. Without `setTokenSource`, rtk
75
- uses the JWT tokens in storage (`jwtTokenSource`).
69
+ ## JWT backends (legacy)
76
70
 
77
- Use `SocialLoginButton` from `@terreno/ui` for OAuth providers.
71
+ Apps on `@terreno/api` JWT routes use `jwtAuthAdapter` as both the `authProvider` and the
72
+ `SessionProvider` `source`, plus `useJwtAuth({adapter})` for `login`, `signup`,
73
+ `googleLogin`, `resetPassword`. It refreshes when under 130 s remain and on a 401, with one
74
+ shared refresh in flight. See `docs/reference/syncdb.md#jwt-auth-legacy`.
78
75
 
79
76
  ## Current user
80
77
 
81
78
  ```tsx
82
- import {useSelectCurrentUserId} from "@terreno/rtk";
83
- import {useReadProfile} from "@/hooks/useReadProfile"; // app-specific
79
+ import {useGetAuthMeQuery} from "@/store/apiSdk";
84
80
 
85
- const userId = useSelectCurrentUserId();
86
- const {data: profile} = useReadProfile(); // GET /auth/me via sdk.ts
81
+ const {userId} = useSession();
82
+ const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
87
83
  ```
88
84
 
89
85
  ## Protected routes
90
86
 
91
- Gate navigation on `userId` in root `_layout.tsx`. Redirect unauthenticated users to `/login`.
92
-
93
- For admin routes, also check `user.admin === true` and redirect non-admins.
87
+ Gate navigation on `useSession().userId` in root `_layout.tsx`. Redirect unauthenticated
88
+ users to `/login`. For admin routes, check `canOpenAdminPage({admin: me?.admin, permissions:
89
+ me?.permissions})` from `@terreno/syncdb/app`.
@@ -1,45 +1,50 @@
1
1
  # Realtime — Terreno
2
2
 
3
- ## Realtime sockets
3
+ The syncdb client owns the app's only socket. Do not open a second one.
4
+
5
+ ## Server push events
4
6
 
5
7
  ```tsx
6
- import {useSocketConnection, getAuthToken} from "@terreno/rtk";
8
+ import {useSocketEvent} from "@terreno/syncdb/react";
7
9
 
8
- const {socket, isSocketConnected} = useSocketConnection({
9
- baseUrl: "ws://localhost:4000",
10
- shouldConnect: !!userId,
11
- getAuthToken: () => getAuthToken(),
10
+ useSocketEvent("announcementPublished", (payload) => {
11
+ console.info("announcement", payload);
12
12
  });
13
13
  ```
14
14
 
15
- Sockets auto-reconnect and refresh auth on token rotation.
15
+ Inside `SyncDbProvider` the hook uses that client; above it, pass `{client: syncDb}`, or
16
+ `{client: null}` to listen to nothing. Outside React: `syncDb.onSocketEvent(event, handler)`
17
+ returns an unsubscribe.
16
18
 
17
19
  ## Feature flags
18
20
 
19
21
  ```tsx
20
- import {useTerrenoFeatureFlags} from "@terreno/rtk";
22
+ import {OpenFeatureProvider} from "@openfeature/react-sdk";
23
+ import {useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
21
24
 
22
- useTerrenoFeatureFlags(terrenoApi, {
23
- skip: !userId,
24
- socket,
25
- userId,
26
- });
25
+ const {userId} = useSession();
26
+ useTerrenoFeatureFlags({client: apiClient, skip: !userId, socket: syncDb, userId});
27
+ return <OpenFeatureProvider domain="feature-flags">{children}</OpenFeatureProvider>;
27
28
  ```
28
29
 
29
- Wrap children in `OpenFeatureProvider` (see `example-frontend/app/_layout.tsx`).
30
+ Flags load through the request client and refetch when the server pushes
31
+ `featureFlagsChanged` on the sync socket. Screens can read flat accessors with
32
+ `useFeatureFlags({skip, socket, userId})`. See `example-frontend/app/_layout.tsx` and
33
+ `docs/reference/feature-flags.md`.
30
34
 
31
- ## Server status
35
+ ## Sync status
32
36
 
33
37
  ```tsx
34
- import {useServerStatus} from "@terreno/rtk";
38
+ import {useSyncStatus} from "@terreno/syncdb/react";
35
39
 
36
- const {isServerReachable} = useServerStatus();
40
+ const {isOnline, queuedCount, conflictCount, failedCount, paused} = useSyncStatus();
37
41
  ```
38
42
 
39
- Use to show degraded UI when the backend is unreachable.
43
+ Render `SyncStatusBanner` / `OfflineBanner` from `@terreno/ui` with these values.
40
44
 
41
45
  ## Realtime model updates
42
46
 
43
- `modelRouter` `realtime` (RTK cache-patching via `realtimeList` / `realtimeDocument`) is **deprecated** and **will be removed in Terreno 58**. Use `sync` on `modelRouter` with `@terreno/syncdb`. `RealtimeApp` stays as the socket host.
44
-
45
- Do not add new `realtimeList` / `realtimeDocument` wiring. After adding `sync` to a backend model, use syncdb hooks on the client.
47
+ Live collection updates come from `sync` on the backend `modelRouter`; read them with
48
+ `useQuery` / `useEntity`. `RealtimeApp` hosts the socket for `sync:delta` and app events.
49
+ Terreno 58 removed `modelRouter` `realtime` and every `@terreno/rtk` socket helper
50
+ (`useSocketConnection`, `realtimeList`, `realtimeDocument`, `useRealtimeDebug`).
@@ -4,81 +4,52 @@
4
4
 
5
5
  | File | Editable? | Purpose |
6
6
  |------|-----------|---------|
7
- | `store/openApiSdk.ts` | **Never** | Auto-generated hooks from OpenAPI |
8
- | `store/sdk.ts` | **Yes** | Extensions, cache tags, custom endpoints |
9
- | `openapi-config.ts` | **Yes** | Codegen configuration |
7
+ | `store/apiSdk.ts` | **Never** | Typed hooks for every non-synced OpenAPI operation (`--api-out`) |
8
+ | `store/syncDbSdk.ts` | **Never** | `SYNC_COLLECTIONS` + collection hooks (`--out`) |
9
+ | `store/apiClient.ts` | **Yes** | `createApiClient` (auth, headers, `onError`) |
10
+ | `syncdb-codegen.json` | **Yes** | Codegen config (`excludePaths`, per-collection `overrides`) |
10
11
 
11
- ## injectEndpoints
12
+ ## Generated hooks
12
13
 
13
- Add app-specific endpoints that are not in the OpenAPI spec (rare — prefer backend routes):
14
+ Names follow the old RTK codegen rule: camelCased `operationId`, otherwise method + path with
15
+ `{param}` as `By<Param>` (`PATCH /gpt/histories/{id}` → `usePatchGptHistoriesByIdMutation`).
16
+ Each operation also gets `<Name>Args`, `<Name>Res`, and `<name>Request(arg)` for
17
+ `apiClient.request(...)`. Tags come from the operation's OpenAPI tags.
14
18
 
15
19
  ```tsx
16
- import {openapi} from "./openApiSdk";
17
-
18
- export const terrenoApi = openapi
19
- .injectEndpoints({
20
- endpoints: (build) => ({
21
- getMe: build.query({
22
- query: () => "/auth/me",
23
- providesTags: ["profile"],
24
- }),
25
- patchMe: build.mutation({
26
- query: (body) => ({
27
- url: "/auth/me",
28
- method: "PATCH",
29
- body,
30
- }),
31
- invalidatesTags: ["profile"],
32
- }),
33
- }),
34
- })
35
- .enhanceEndpoints({
36
- addTagTypes: ["todos", "users", "profile"],
37
- });
20
+ import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
38
21
  ```
39
22
 
40
- Import hooks from `@/store/sdk`, not `@/store/openApiSdk` directly.
41
-
42
- ## Cache tags
23
+ ## Routes outside OpenAPI
43
24
 
44
- Use `enhanceEndpoints` to add tag types, then `providesTags` / `invalidatesTags` in injected endpoints:
25
+ Prefer documenting the route in OpenAPI and regenerating. When that is not possible, call it
26
+ directly through the request client:
45
27
 
46
28
  ```tsx
47
- import {invalidatesIdTags, providesIdTags} from "@terreno/rtk";
48
-
49
- // In injectEndpoints:
50
- listItems: build.query({
51
- query: (params) => ({url: "/items", params}),
52
- providesTags: providesIdTags("items"),
53
- }),
54
- createItem: build.mutation({
55
- query: (body) => ({url: "/items", method: "POST", body}),
56
- invalidatesTags: invalidatesIdTags("items"),
57
- }),
29
+ import {useApiMutation, useApiQuery, useLazyApiQuery} from "@terreno/syncdb/react";
30
+
31
+ const {data, isLoading, refetch} = useApiQuery<Report>(
32
+ {url: "/reports/summary", params: {range}},
33
+ {providesTags: ["reports"], skip: !userId}
34
+ );
35
+
36
+ const [rebuild, {isLoading: isRebuilding}] = useApiMutation<RebuildBody, Report>(
37
+ (body) => ({body, method: "POST", url: "/reports/rebuild"}),
38
+ {invalidatesTags: ["reports"]}
39
+ );
40
+ await rebuild({range}).unwrap();
58
41
  ```
59
42
 
60
- ## Codegen config
61
-
62
- ```typescript
63
- // openapi-config.ts
64
- const config: ConfigFile = {
65
- apiFile: "@terreno/rtk",
66
- apiImport: "emptySplitApi",
67
- outputFile: "./store/openApiSdk.ts",
68
- schemaFile: "http://localhost:4000/openapi.json",
69
- hooks: true,
70
- tag: true,
71
- flattenArg: true,
72
- };
73
- ```
43
+ ## Cache tags
74
44
 
75
- Override schema URL with `OPENAPI_URL` env var when needed.
45
+ Queries register `providesTags`; mutations `invalidatesTags`. Imperatively:
46
+ `apiClient.invalidateTags(["reports"])`. `apiClient.reset()` clears the cache (done on logout
47
+ via `SessionProvider` `onLogout`). Synced collections have no request cache; their data is local.
76
48
 
77
49
  ## Workflow after backend change
78
50
 
79
51
  1. Start backend (`bun run backend:dev`)
80
- 2. Run `cd example-frontend && bun run sdk`
81
- 3. Import new hooks from `@/store/sdk`
82
- 4. If the endpoint needs custom cache behavior, update `sdk.ts`
52
+ 2. Run `cd example-frontend && bun run sdk` (and `bun run sync-sdk` for synced schema changes)
53
+ 3. Import new hooks from `@/store/apiSdk` (or `@/store/syncDbSdk`)
83
54
 
84
55
  See the `generate-sdk` skill for the full procedure.
@@ -6,8 +6,8 @@ description: >-
6
6
  layout, forms, tables, modals, feedback, or navigation chrome. Covers
7
7
  component selection, Box/Page patterns, theme props, and when NOT to use raw
8
8
  React Native primitives. Lifecycle composition: Grow for UI shape, Pick for
9
- implementation, Roast for UI conformance review. Not for backend APIs, RTK
10
- Query, or Expo Router setup.
9
+ implementation, Roast for UI conformance review. Not for backend APIs, data
10
+ fetching (syncdb), or Expo Router setup.
11
11
  ---
12
12
  # @terreno/ui
13
13
 
@@ -42,6 +42,8 @@ Sizes: `sm`, `md`, `lg`, `xl`, `2xl` (responsive web/mobile values).
42
42
  />
43
43
  ```
44
44
 
45
+ Column `width` is optional. Omit it and the column sizes itself from its title and sampled content, then grows to fill the table; a set `width` is always kept. See `docs/reference/ui.md` (DataTable column widths).
46
+
45
47
  Column types: `text`, `date`, `number`, `boolean`, `custom`.
46
48
 
47
49
  For admin tables, prefer `AdminModelTable` from `@terreno/admin-frontend`.