@terreno/mcp 57.21.0 → 58.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +57 -3
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +17 -16
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/prompts/debug.md +6 -4
  9. package/dist/docs/prompts/screen.md +11 -17
  10. package/dist/docs/resources/overview.md +5 -3
  11. package/dist/docs/resources/patterns.md +157 -238
  12. package/dist/docs/resources/syncdb.md +44 -16
  13. package/dist/docs/templates/bootstrap/CLAUDE.template.md +2 -2
  14. package/dist/docs/templates/bootstrap/cursorrules.template.md +7 -5
  15. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
  16. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
  17. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
  18. package/dist/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
  19. package/dist/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
  20. package/dist/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
  21. package/dist/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
  22. package/dist/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
  23. package/dist/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
  24. package/dist/docs/upgrades/58.0.0-beta.1.md +504 -0
  25. package/dist/docs/upgrades/58.0.0.md +495 -4
  26. package/dist/docs/versioned/next/README.md +2 -2
  27. package/dist/docs/versioned/next/approval-rubric.md +2 -2
  28. package/dist/docs/versioned/next/explanation/admin-interface.md +6 -5
  29. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
  30. package/dist/docs/versioned/next/explanation/ai-workflows.md +5 -3
  31. package/dist/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
  32. package/dist/docs/versioned/next/explanation/authentication.md +62 -43
  33. package/dist/docs/versioned/next/explanation/dependency-management.md +1 -1
  34. package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  35. package/dist/docs/versioned/next/explanation/example-coverage.md +2 -2
  36. package/dist/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
  37. package/dist/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
  38. package/dist/docs/versioned/next/explanation/local-first-data.md +11 -9
  39. package/dist/docs/versioned/next/explanation/model-router-actions.md +1 -1
  40. package/dist/docs/versioned/next/explanation/modular-api-design.md +2 -2
  41. package/dist/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
  42. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
  43. package/dist/docs/versioned/next/explanation/source-rules.md +2 -2
  44. package/dist/docs/versioned/next/explanation/typescript-configuration.md +2 -4
  45. package/dist/docs/versioned/next/explanation/versioning-policy.md +3 -3
  46. package/dist/docs/versioned/next/how-to/README.md +3 -2
  47. package/dist/docs/versioned/next/how-to/add-feature-flags.md +50 -25
  48. package/dist/docs/versioned/next/how-to/add-organizations.md +2 -1
  49. package/dist/docs/versioned/next/how-to/admin-custom-home.md +8 -2
  50. package/dist/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
  51. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
  52. package/dist/docs/versioned/next/how-to/build-admin-screens.md +10 -8
  53. package/dist/docs/versioned/next/how-to/build-for-web.md +1 -1
  54. package/dist/docs/versioned/next/how-to/call-external-apis.md +2 -2
  55. package/dist/docs/versioned/next/how-to/circleci.md +12 -6
  56. package/dist/docs/versioned/next/how-to/configure-better-auth.md +114 -60
  57. package/dist/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
  58. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
  59. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
  60. package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
  61. package/dist/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
  62. package/dist/docs/versioned/next/how-to/github-actions-ci.md +2 -2
  63. package/dist/docs/versioned/next/how-to/in-app-notifications.md +1 -1
  64. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
  65. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  66. package/dist/docs/versioned/next/how-to/product-announcements.md +6 -3
  67. package/dist/docs/versioned/next/how-to/rate-limiting.md +38 -16
  68. package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
  69. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -3
  70. package/dist/docs/versioned/next/how-to/set-up-social-login.md +1 -1
  71. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
  72. package/dist/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
  73. package/dist/docs/versioned/next/how-to/upgrade-banner.md +5 -3
  74. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
  75. package/dist/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
  76. package/dist/docs/versioned/next/how-to/websocket-integration.md +113 -402
  77. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  78. package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
  79. package/dist/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
  80. package/dist/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
  81. package/dist/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
  82. package/dist/docs/versioned/next/reference/README.md +1 -1
  83. package/dist/docs/versioned/next/reference/admin-frontend.md +85 -70
  84. package/dist/docs/versioned/next/reference/admin-spa.md +10 -4
  85. package/dist/docs/versioned/next/reference/ai.md +36 -4
  86. package/dist/docs/versioned/next/reference/announcements.md +3 -3
  87. package/dist/docs/versioned/next/reference/api.md +39 -24
  88. package/dist/docs/versioned/next/reference/cli.md +3 -4
  89. package/dist/docs/versioned/next/reference/environment-variables.md +16 -16
  90. package/dist/docs/versioned/next/reference/feature-flags.md +48 -7
  91. package/dist/docs/versioned/next/reference/jobs.md +2 -2
  92. package/dist/docs/versioned/next/reference/legacy/rtk.md +25 -600
  93. package/dist/docs/versioned/next/reference/mcp-server.md +441 -1158
  94. package/dist/docs/versioned/next/reference/syncdb.md +426 -16
  95. package/dist/docs/versioned/next/reference/test.md +28 -3
  96. package/dist/docs/versioned/next/reference/ui.md +132 -19
  97. package/dist/docs/versioned/next/tasks/native-module-baseline.md +8 -4
  98. package/dist/docs/versioned/next/tasks/rate-limiting.md +9 -2
  99. package/dist/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
  100. package/dist/docs/versioned/next/tasks/remove-rtk.md +155 -0
  101. package/dist/local/localTools.d.ts.map +1 -1
  102. package/dist/local/localTools.js +3 -54
  103. package/dist/local/localTools.js.map +1 -1
  104. package/dist/local/tools/runtime.d.ts +0 -21
  105. package/dist/local/tools/runtime.d.ts.map +1 -1
  106. package/dist/local/tools/runtime.js +0 -176
  107. package/dist/local/tools/runtime.js.map +1 -1
  108. package/dist/local/tools/syncDb.d.ts +0 -11
  109. package/dist/local/tools/syncDb.d.ts.map +1 -1
  110. package/dist/local/tools/syncDb.js +1 -5
  111. package/dist/local/tools/syncDb.js.map +1 -1
  112. package/dist/packageGuidelines.d.ts +3 -2
  113. package/dist/packageGuidelines.d.ts.map +1 -1
  114. package/dist/packageGuidelines.js +4 -11
  115. package/dist/packageGuidelines.js.map +1 -1
  116. package/dist/prompts.d.ts.map +1 -1
  117. package/dist/prompts.js +17 -18
  118. package/dist/prompts.js.map +1 -1
  119. package/dist/resources.d.ts.map +1 -1
  120. package/dist/resources.js +1 -8
  121. package/dist/resources.js.map +1 -1
  122. package/dist/search/chunker.d.ts +1 -1
  123. package/dist/search/chunker.d.ts.map +1 -1
  124. package/dist/search/inferPackages.d.ts +1 -1
  125. package/dist/search/inferPackages.js +5 -5
  126. package/dist/search/inferPackages.js.map +1 -1
  127. package/dist/tools.d.ts +6 -0
  128. package/dist/tools.d.ts.map +1 -1
  129. package/dist/tools.js +126 -119
  130. package/dist/tools.js.map +1 -1
  131. package/package.json +6 -6
  132. package/src/docs/guidelines/admin-frontend/guidelines/core.md +6 -5
  133. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  134. package/src/docs/prompts/debug.md +6 -4
  135. package/src/docs/prompts/screen.md +11 -17
  136. package/src/docs/resources/overview.md +5 -3
  137. package/src/docs/resources/patterns.md +157 -238
  138. package/src/docs/resources/syncdb.md +44 -16
  139. package/src/docs/templates/bootstrap/CLAUDE.template.md +2 -2
  140. package/src/docs/templates/bootstrap/cursorrules.template.md +7 -5
  141. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +8 -12
  142. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +75 -52
  143. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +46 -108
  144. package/src/docs/templates/bootstrap/frontend/app/signup.template.tsx +53 -0
  145. package/src/docs/templates/bootstrap/frontend/lib/betterAuth.template.md +18 -0
  146. package/src/docs/templates/bootstrap/frontend/store/apiClient.template.md +33 -0
  147. package/src/docs/templates/bootstrap/frontend/store/apiSdk.template.md +76 -0
  148. package/src/docs/templates/bootstrap/frontend/store/syncdb.template.md +56 -0
  149. package/src/docs/templates/bootstrap/frontend/utils/index.template.md +0 -4
  150. package/src/docs/upgrades/58.0.0-beta.1.md +504 -0
  151. package/src/docs/upgrades/58.0.0.md +495 -4
  152. package/src/docs/versioned/next/README.md +2 -2
  153. package/src/docs/versioned/next/approval-rubric.md +2 -2
  154. package/src/docs/versioned/next/explanation/admin-interface.md +6 -5
  155. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +1 -1
  156. package/src/docs/versioned/next/explanation/ai-workflows.md +5 -3
  157. package/src/docs/versioned/next/explanation/announcements/rtk-deprecation.md +16 -11
  158. package/src/docs/versioned/next/explanation/authentication.md +62 -43
  159. package/src/docs/versioned/next/explanation/dependency-management.md +1 -1
  160. package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  161. package/src/docs/versioned/next/explanation/example-coverage.md +2 -2
  162. package/src/docs/versioned/next/explanation/explicit-any-policy.md +5 -6
  163. package/src/docs/versioned/next/explanation/gcp-hosting-architecture.md +1 -1
  164. package/src/docs/versioned/next/explanation/local-first-data.md +11 -9
  165. package/src/docs/versioned/next/explanation/model-router-actions.md +1 -1
  166. package/src/docs/versioned/next/explanation/modular-api-design.md +2 -2
  167. package/src/docs/versioned/next/explanation/no-barrel-imports.md +5 -5
  168. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +4 -3
  169. package/src/docs/versioned/next/explanation/source-rules.md +2 -2
  170. package/src/docs/versioned/next/explanation/typescript-configuration.md +2 -4
  171. package/src/docs/versioned/next/explanation/versioning-policy.md +3 -3
  172. package/src/docs/versioned/next/how-to/README.md +3 -2
  173. package/src/docs/versioned/next/how-to/add-feature-flags.md +50 -25
  174. package/src/docs/versioned/next/how-to/add-organizations.md +2 -1
  175. package/src/docs/versioned/next/how-to/admin-custom-home.md +8 -2
  176. package/src/docs/versioned/next/how-to/admin-custom-widget.md +0 -1
  177. package/src/docs/versioned/next/how-to/agent-ui-asks.md +6 -6
  178. package/src/docs/versioned/next/how-to/build-admin-screens.md +10 -8
  179. package/src/docs/versioned/next/how-to/build-for-web.md +1 -1
  180. package/src/docs/versioned/next/how-to/call-external-apis.md +2 -2
  181. package/src/docs/versioned/next/how-to/circleci.md +12 -6
  182. package/src/docs/versioned/next/how-to/configure-better-auth.md +114 -60
  183. package/src/docs/versioned/next/how-to/connect-mcp-service-token.md +10 -4
  184. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +14 -2
  185. package/src/docs/versioned/next/how-to/debug-with-mcp.md +8 -13
  186. package/src/docs/versioned/next/how-to/expose-mcp-tools.md +15 -4
  187. package/src/docs/versioned/next/how-to/generate-the-client-sdk.md +33 -0
  188. package/src/docs/versioned/next/how-to/github-actions-ci.md +2 -2
  189. package/src/docs/versioned/next/how-to/in-app-notifications.md +1 -1
  190. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +318 -447
  191. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  192. package/src/docs/versioned/next/how-to/product-announcements.md +6 -3
  193. package/src/docs/versioned/next/how-to/rate-limiting.md +38 -16
  194. package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +19 -0
  195. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -3
  196. package/src/docs/versioned/next/how-to/set-up-social-login.md +1 -1
  197. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +2 -2
  198. package/src/docs/versioned/next/how-to/track-upstream-expo.md +1 -1
  199. package/src/docs/versioned/next/how-to/upgrade-banner.md +5 -3
  200. package/src/docs/versioned/next/how-to/upgrade-terreno.md +5 -5
  201. package/src/docs/versioned/next/how-to/use-syncdb-correctly.md +1 -1
  202. package/src/docs/versioned/next/how-to/websocket-integration.md +113 -402
  203. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  204. package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +5 -0
  205. package/src/docs/versioned/next/implementationPlans/native-module-baseline.md +29 -5
  206. package/src/docs/versioned/next/implementationPlans/remove-legacy-realtime.md +1 -1
  207. package/src/docs/versioned/next/implementationPlans/remove-rtk.md +185 -0
  208. package/src/docs/versioned/next/reference/README.md +1 -1
  209. package/src/docs/versioned/next/reference/admin-frontend.md +85 -70
  210. package/src/docs/versioned/next/reference/admin-spa.md +10 -4
  211. package/src/docs/versioned/next/reference/ai.md +36 -4
  212. package/src/docs/versioned/next/reference/announcements.md +3 -3
  213. package/src/docs/versioned/next/reference/api.md +39 -24
  214. package/src/docs/versioned/next/reference/cli.md +3 -4
  215. package/src/docs/versioned/next/reference/environment-variables.md +16 -16
  216. package/src/docs/versioned/next/reference/feature-flags.md +48 -7
  217. package/src/docs/versioned/next/reference/jobs.md +2 -2
  218. package/src/docs/versioned/next/reference/legacy/rtk.md +25 -600
  219. package/src/docs/versioned/next/reference/mcp-server.md +441 -1158
  220. package/src/docs/versioned/next/reference/syncdb.md +426 -16
  221. package/src/docs/versioned/next/reference/test.md +28 -3
  222. package/src/docs/versioned/next/reference/ui.md +132 -19
  223. package/src/docs/versioned/next/tasks/native-module-baseline.md +8 -4
  224. package/src/docs/versioned/next/tasks/rate-limiting.md +9 -2
  225. package/src/docs/versioned/next/tasks/remove-legacy-realtime.md +5 -5
  226. package/src/docs/versioned/next/tasks/remove-rtk.md +155 -0
  227. package/dist/docs/guidelines/rtk/guidelines/core.md +0 -26
  228. package/dist/docs/resources/rtk.md +0 -181
  229. package/dist/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
  230. package/dist/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
  231. package/dist/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
  232. package/dist/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
  233. package/dist/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
  234. package/dist/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
  235. package/dist/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
  236. package/src/docs/guidelines/rtk/guidelines/core.md +0 -26
  237. package/src/docs/resources/rtk.md +0 -181
  238. package/src/docs/templates/bootstrap/frontend/openapi-config.template.md +0 -16
  239. package/src/docs/templates/bootstrap/frontend/scripts/generate-sdk.template.md +0 -49
  240. package/src/docs/templates/bootstrap/frontend/store/appState.template.md +0 -45
  241. package/src/docs/templates/bootstrap/frontend/store/errors.template.md +0 -51
  242. package/src/docs/templates/bootstrap/frontend/store/index.template.md +0 -86
  243. package/src/docs/templates/bootstrap/frontend/store/openApiSdk.template.md +0 -49
  244. package/src/docs/templates/bootstrap/frontend/store/sdk.template.md +0 -80
@@ -1,474 +1,185 @@
1
1
  # How to Add WebSocket Integration
2
2
 
3
- Step-by-step guide to adding real-time Socket.io connections to your Terreno application using `@terreno/rtk`.
3
+ Push server events to a Terreno app over the socket `@terreno/syncdb` already opens. One app
4
+ keeps one Socket.io connection: sync deltas, feature-flag pushes, and your own events share it.
4
5
 
5
6
  ## Prerequisites
6
7
 
7
- - Backend with Socket.io server configured
8
- - Frontend using `@terreno/rtk` and `@terreno/ui`
9
- - Redux store set up with `generateAuthSlice`
8
+ - Backend registers `RealtimeApp` (and `SyncApp`) from `@terreno/api`
9
+ - Frontend creates a `SyncDb` client with `createSyncDb` and starts it after login
10
+ - MongoDB runs as a replica set (change streams power sync and flag pushes)
10
11
 
11
- ## Installation
12
+ ## Backend Setup
12
13
 
13
- Socket.io dependencies are already included in `@terreno/rtk`:
14
+ ### 1. Keep a reference to `RealtimeApp`
14
15
 
15
- ``````bash
16
- bun install @terreno/rtk
17
- # socket.io-client is a peer dependency
18
- bun install socket.io-client
19
- ``````
16
+ `RealtimeApp` attaches Socket.io to the server `TerrenoApp.start()` creates, authenticates each
17
+ socket (JWT and/or Better Auth), and joins it to these rooms:
20
18
 
21
- ## Backend Setup
19
+ | Room | Members |
20
+ |------|---------|
21
+ | `user:<userId>` | Every socket of that user |
22
+ | `authenticated` | Every signed-in socket |
23
+ | `admin` | Sockets of admin users |
22
24
 
23
- ### 1. Add Socket.io to Express Server
25
+ Keep the instance so you can emit through `getIo()`:
24
26
 
25
- ``````typescript
26
- import {createServer} from "node:http";
27
- import {Server} from "socket.io";
28
- import {setupServer} from "@terreno/api";
29
-
30
- const app = setupServer({
31
- userModel: User,
32
- addRoutes: (router) => {
33
- // Your routes
34
- },
35
- });
27
+ ```typescript
28
+ import {RealtimeApp, SyncApp, TerrenoApp} from "@terreno/api";
36
29
 
37
- const httpServer = createServer(app);
38
- const io = new Server(httpServer, {
39
- cors: {
40
- origin: process.env.FRONTEND_URL || "http://localhost:8081",
41
- credentials: true,
42
- },
43
- transports: ["websocket"],
30
+ export const realtimeApp = new RealtimeApp({
31
+ betterAuth: {auth: betterAuthInstance, userModel: User},
44
32
  });
45
33
 
46
- // Authentication middleware
47
- io.use(async (socket, next) => {
48
- const token = socket.handshake.auth.token?.replace("Bearer ", "");
49
-
50
- if (!token) {
51
- return next(new Error("Authentication required"));
52
- }
53
-
54
- try {
55
- // Verify JWT token
56
- const decoded = verifyToken(token);
57
- const user = await User.findById(decoded.sub);
58
-
59
- if (!user) {
60
- return next(new Error("User not found"));
61
- }
62
-
63
- socket.data.user = user;
64
- next();
65
- } catch (error) {
66
- next(new Error("Invalid token"));
67
- }
68
- });
34
+ new TerrenoApp({userModel: User})
35
+ .register(todoRouter)
36
+ .register(new SyncApp())
37
+ .register(realtimeApp)
38
+ .start();
39
+ ```
69
40
 
70
- // Connection handling
71
- io.on("connection", (socket) => {
72
- const user = socket.data.user;
73
- console.log(`User connected: ${user.email} (${socket.id})`);
74
-
75
- // Join user-specific room
76
- socket.join(`user:${user._id}`);
77
-
78
- // Handle custom events
79
- socket.on("subscribe:todos", () => {
80
- socket.join("todos");
81
- console.log(`${user.email} subscribed to todos`);
82
- });
83
-
84
- socket.on("disconnect", (reason) => {
85
- console.log(`User disconnected: ${user.email} (${reason})`);
86
- });
87
- });
88
-
89
- httpServer.listen(4000, () => {
90
- console.log("Server listening on port 4000");
91
- });
41
+ `getIo()` returns `null` until the server starts, so call it when you emit, not at import time.
92
42
 
93
- export {io}; // Export for emitting events from routes
94
- ``````
43
+ ### 2. Emit events
95
44
 
96
- ### 2. Emit Events from API Routes
45
+ Emit app events with names that do not start with `sync:` (the sync protocol owns that prefix):
97
46
 
98
- ``````typescript
99
- import {io} from "./server";
47
+ ```typescript
48
+ import {realtimeApp} from "./realtime";
100
49
 
101
- // In your modelRouter postCreate hook
102
- modelRouter(Todo, {
103
- permissions: {
104
- create: [Permissions.IsAuthenticated],
105
- },
106
- postCreate: (todo, req) => {
107
- // Emit to all clients in the todos room
108
- io.to("todos").emit("todo:created", {
109
- id: todo._id,
110
- title: todo.title,
111
- ownerId: todo.ownerId,
112
- });
113
-
114
- // Emit to specific user
115
- io.to(`user:${todo.ownerId}`).emit("notification", {
116
- type: "todo_created",
117
- message: `Todo "${todo.title}" created`,
50
+ modelRouter("/announcements", Announcement, {
51
+ permissions: {create: [Permissions.IsAdmin] /* ... */},
52
+ postCreate: (announcement) => {
53
+ realtimeApp.getIo()?.to("authenticated").emit("announcementPublished", {
54
+ id: announcement._id,
118
55
  });
119
56
  },
120
57
  });
121
- ``````
122
-
123
- ## Frontend Setup
124
58
 
125
- ### 1. Configure WebSocket URL
59
+ // One user only:
60
+ realtimeApp.getIo()?.to(`user:${userId}`).emit("exportReady", {url});
61
+ ```
126
62
 
127
- Add to your `app.json` or environment:
128
-
129
- ``````json
130
- {
131
- "expo": {
132
- "extra": {
133
- "WEBSOCKET_URL": "wss://api.example.com",
134
- "WEBSOCKETS_DEBUG": false
135
- }
136
- }
137
- }
138
- ``````
63
+ Send ids or small hints, not documents the client may not be allowed to read; let the client
64
+ refetch through the API with its own permissions.
139
65
 
140
- ### 2. Register the token source
66
+ For feature flags, point `FeatureFlagsApp` at the same server and the client refreshes with no
67
+ extra code:
141
68
 
142
- The hook reads its handshake token from the token source the app registers once at startup
143
- with `setTokenSource`. A JWT app registers nothing: the default source reads the JWT in
144
- storage. A Better Auth app registers the adapter it builds for syncdb:
145
-
146
- ``````typescript
147
- // store/tokenSource.ts: import once from the app entry, before the store is created
148
- import {setTokenSource} from "@terreno/rtk";
149
- import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
150
- import {authClient} from "@/lib/betterAuth";
151
-
152
- setTokenSource(betterAuthAdapter(bridgeBetterAuthReactClient(authClient)));
153
- ``````
69
+ ```typescript
70
+ new FeatureFlagsApp({liveUpdates: {socketIoServer: () => realtimeApp.getIo()}});
71
+ ```
154
72
 
155
- With `webAuth: "bearer"`, also pass the adapter `getBearerToken`
156
- ([syncdb adapter](../reference/syncdb.md#createsyncdb-configuration)).
157
-
158
- With a token source, the hook:
73
+ ## Frontend Setup
159
74
 
160
- - reads `getToken()` on every connect and reconnect;
161
- - calls `refresh()` once when the server rejects the token, then reconnects, and stops after a
162
- second rejection (`onConnectError` fires). With no `refresh`, or a failed one, it stops at
163
- once;
164
- - reconnects with the new token when `onAuthChange` fires, and disconnects when there is no
165
- token.
75
+ ### 1. Listen in components with `useSocketEvent`
166
76
 
167
- To use a different source for one socket, pass it as `tokenSource`. Passing the legacy
168
- `getAuthToken` getter instead turns the token source off for that hook. See
169
- [Token source](../reference/legacy/rtk.md#token-source).
77
+ `useSocketEvent(event, handler, options?)` from `@terreno/syncdb/react` binds a listener on the
78
+ `SyncDbProvider` client's socket while the component is mounted:
170
79
 
171
- ### 3. Set Up Connection Hook
80
+ ```typescript
81
+ import {useApiQuery, useSocketEvent} from "@terreno/syncdb/react";
172
82
 
173
- Create a custom hook to manage the connection. A Better Auth app gates `shouldConnect` on
174
- `selectBetterAuthUserId` instead of the JWT user id:
83
+ export const AnnouncementsBadge: React.FC = () => {
84
+ const {data, refetch} = useApiQuery<{data: Announcement[]}>({url: "/announcements"});
175
85
 
176
- ``````typescript
177
- // hooks/useWebSocket.ts
178
- import {useSocketConnection, baseWebsocketsUrl} from "@terreno/rtk";
179
- import {selectBetterAuthUserId} from "@terreno/rtk";
180
- import {useSelector} from "react-redux";
181
- import {useCallback} from "react";
182
- import * as Sentry from "@sentry/react-native";
183
-
184
- export const useWebSocket = () => {
185
- const userId = useSelector(selectBetterAuthUserId); // JWT apps: useSelectCurrentUserId()
186
-
187
- const {socket, isSocketConnected} = useSocketConnection({
188
- baseUrl: baseWebsocketsUrl,
189
- shouldConnect: !!userId,
190
- onConnect: () => {
191
- console.info("[WebSocket] Connected");
192
- },
193
- onDisconnect: () => {
194
- console.warn("[WebSocket] Disconnected");
195
- },
196
- onConnectError: (error) => {
197
- console.error("[WebSocket] Connection error:", error.message);
198
- },
199
- captureEvent: (eventName, data) => {
200
- Sentry.captureMessage(eventName, {
201
- level: "info",
202
- extra: data,
203
- });
204
- },
86
+ useSocketEvent("announcementPublished", () => {
87
+ void refetch();
205
88
  });
206
-
207
- return {socket, isSocketConnected};
89
+
90
+ return <Badge value={String(data?.data.length ?? 0)} />;
208
91
  };
209
- ``````
92
+ ```
93
+
94
+ - The handler can change on every render; the latest one runs and the listener is not rebound.
95
+ - Outside a `SyncDbProvider`, pass the client: `useSocketEvent(name, handler, {client: syncDb})`.
96
+ - `{client: null}` or `{enabled: false}` listens to nothing (for example while signed out).
210
97
 
211
98
  #### Transports
212
99
 
213
- `useSocketConnection` starts with HTTP long-polling and upgrades to WebSocket (`transports: ["polling", "websocket"]`), so a restarting dev server does not fail the first handshake. Polling needs every request of a session to reach the same server process. Hosts without session affinity, such as Vercel Functions, need WebSocket only:
100
+ The syncdb socket starts with HTTP long-polling and upgrades to WebSocket (`socketTransports: ["polling", "websocket"]`), so a restarting dev server does not fail the first handshake. Polling needs every request of a session to reach the same server process. Hosts without session affinity, such as Vercel Functions, need WebSocket only:
214
101
 
215
102
  ``````typescript
216
- useSocketConnection({
103
+ createSyncDb({
217
104
  baseUrl,
218
- shouldConnect: !!userId,
219
- transports: ["websocket"],
105
+ collections: ["todos"],
106
+ socketTransports: ["websocket"],
220
107
  });
221
108
  ``````
222
109
 
223
- `@terreno/syncdb` takes the same list as `createSyncDb({socketTransports: ["websocket"]})`. Cloud Run with `--session-affinity` works with the default. See [Run Terreno WebSockets on Vercel Functions](vercel-function-websockets.md).
110
+ Cloud Run with `--session-affinity` works with the default. See [Run Terreno WebSockets on Vercel Functions](vercel-function-websockets.md).
224
111
 
225
- `baseWebsocketsUrl` replaces `api.` with `ws.` in the API host (`https://api.example.com` becomes `ws.example.com`), for deployments that run sockets on a separate host. If sockets are served by the API host itself, as on Vercel, pass `baseUrl` instead.
112
+ Synced collections already update live through `useQuery` / `useEntity`; use socket events for
113
+ everything else.
226
114
 
227
- ### 4. Subscribe to Events in Components
115
+ ### 2. Listen outside React with `onSocketEvent`
228
116
 
229
- ``````typescript
230
- // screens/TodoList.tsx
231
- import {useWebSocket} from "@/hooks/useWebSocket";
232
- import {useEffect, useCallback} from "react";
233
-
234
- export const TodoListScreen = () => {
235
- const {data: todos, refetch} = useGetTodosQuery();
236
- const {socket} = useWebSocket();
237
-
238
- // Subscribe to todo events
239
- useEffect(() => {
240
- if (!socket) return;
241
-
242
- // Join the todos room
243
- socket.emit("subscribe:todos");
244
-
245
- // Handle todo created event
246
- const handleTodoCreated = (todo) => {
247
- console.info("New todo created:", todo);
248
- refetch(); // Refresh the list
249
- };
250
-
251
- // Handle todo updated event
252
- const handleTodoUpdated = (todo) => {
253
- console.info("Todo updated:", todo);
254
- refetch();
255
- };
256
-
257
- socket.on("todo:created", handleTodoCreated);
258
- socket.on("todo:updated", handleTodoUpdated);
259
-
260
- // Cleanup
261
- return () => {
262
- socket.off("todo:created", handleTodoCreated);
263
- socket.off("todo:updated", handleTodoUpdated);
264
- };
265
- }, [socket, refetch]);
266
-
267
- return (
268
- <Page title="Todos">
269
- {todos?.data.map((todo) => (
270
- <TodoItem key={todo._id} todo={todo} />
271
- ))}
272
- </Page>
273
- );
274
- };
275
- ``````
276
-
277
- ### 5. Show Connection Status (Optional)
117
+ ```typescript
118
+ import {syncDb} from "@/store/syncdb";
278
119
 
279
- ``````typescript
280
- import {useWebSocket} from "@/hooks/useWebSocket";
281
- import {Box, Text, Badge} from "@terreno/ui";
282
-
283
- export const ConnectionStatus = () => {
284
- const {isSocketConnected} = useWebSocket();
285
-
286
- return (
287
- <Box direction="row" gap={2} alignItems="center">
288
- <Badge
289
- variant={isSocketConnected.isConnected ? "success" : "error"}
290
- text={isSocketConnected.isConnected ? "Connected" : "Disconnected"}
291
- />
292
- {!isSocketConnected.isConnected && isSocketConnected.lastDisconnectedAt && (
293
- <Text size="sm" color="secondaryDark">
294
- Reconnecting...
295
- </Text>
296
- )}
297
- </Box>
298
- );
299
- };
300
- ``````
301
-
302
- ## Advanced Patterns
303
-
304
- ### Optimistic Updates with Real-time Sync
305
-
306
- ``````typescript
307
- const [updateTodo] = usePatchTodosMutation();
308
-
309
- const handleToggle = useCallback(async (todo) => {
310
- // Optimistic update
311
- const completed = !todo.completed;
312
-
313
- try {
314
- await updateTodo({
315
- id: todo._id,
316
- body: {completed},
317
- }).unwrap();
318
-
319
- // Server will emit event, triggering refetch for other clients
320
- } catch (error) {
321
- console.error("Failed to update todo:", error);
322
- // Revert optimistic update
323
- }
324
- }, [updateTodo]);
325
- ``````
326
-
327
- ### Room-based Subscriptions
328
-
329
- ``````typescript
330
- // Backend: User joins project-specific room
331
- socket.on("join:project", (projectId) => {
332
- socket.join(`project:${projectId}`);
120
+ const unsubscribe = syncDb.onSocketEvent("exportReady", (payload) => {
121
+ console.info("export ready", payload);
333
122
  });
123
+ // later
124
+ unsubscribe();
125
+ ```
334
126
 
335
- // Emit to project room
336
- io.to(`project:${projectId}`).emit("project:updated", project);
337
-
338
- // Frontend: Subscribe to project updates
339
- useEffect(() => {
340
- if (!socket || !projectId) return;
341
-
342
- socket.emit("join:project", projectId);
343
- socket.on("project:updated", handleProjectUpdate);
344
-
345
- return () => {
346
- socket.off("project:updated", handleProjectUpdate);
347
- };
348
- }, [socket, projectId]);
349
- ``````
127
+ Listeners stay bound across `stop()` / `start()` and reconnects; they fire only while the
128
+ socket is connected. Binding a reserved name (`sync:*`, `connect`, `disconnect`,
129
+ `connect_error`, …) throws.
350
130
 
351
- ### Presence System
131
+ ### 3. Show connection status (optional)
352
132
 
353
- ``````typescript
354
- // Backend: Track active users
355
- const activeUsers = new Map();
356
-
357
- io.on("connection", (socket) => {
358
- const user = socket.data.user;
359
- activeUsers.set(user._id, {socketId: socket.id, lastSeen: new Date()});
360
-
361
- io.emit("presence:update", Array.from(activeUsers.keys()));
362
-
363
- socket.on("disconnect", () => {
364
- activeUsers.delete(user._id);
365
- io.emit("presence:update", Array.from(activeUsers.keys()));
366
- });
367
- });
133
+ `useSyncStatus()` exposes the same socket's state:
368
134
 
369
- // Frontend: Show active users
370
- const [activeUsers, setActiveUsers] = useState<string[]>([]);
135
+ ```typescript
136
+ import {useSyncStatus} from "@terreno/syncdb/react";
137
+ import {Badge} from "@terreno/ui";
371
138
 
372
- useEffect(() => {
373
- if (!socket) return;
374
-
375
- socket.on("presence:update", (userIds) => {
376
- setActiveUsers(userIds);
377
- });
378
-
379
- return () => {
380
- socket.off("presence:update");
381
- };
382
- }, [socket]);
383
- ``````
384
-
385
- ## Debugging
386
-
387
- Enable debug logging in your `app.json`:
388
-
389
- ``````json
390
- {
391
- "expo": {
392
- "extra": {
393
- "WEBSOCKETS_DEBUG": true
394
- }
395
- }
396
- }
397
- ``````
398
-
399
- This logs:
400
- - Connection attempts
401
- - Disconnection events with reasons
402
- - Token refresh attempts
403
- - Reconnection failures
139
+ export const ConnectionStatus: React.FC = () => {
140
+ const {isOnline} = useSyncStatus();
141
+ return <Badge status={isOnline ? "success" : "error"} value={isOnline ? "Online" : "Offline"} />;
142
+ };
143
+ ```
404
144
 
405
145
  ## Troubleshooting
406
146
 
407
- ### "Authentication required" error
147
+ ### Events never arrive
408
148
 
409
- **Cause:** Token not provided or invalid.
149
+ 1. Confirm the client has started: `syncDb.getSyncStatus().isOnline` is `true`.
150
+ 2. Confirm the server emits on `realtimeApp.getIo()`, not a second `new Server(...)`; a second
151
+ server has no syncdb clients connected.
152
+ 3. Check the room. `user:<id>` uses the id the socket authenticated as.
153
+ 4. Check the event name matches exactly on both sides.
410
154
 
411
- **Solution:**
412
- 1. Ensure the token source returns a valid token: `await getTokenSource()?.getToken()` (or
413
- `getAuthToken()` on a JWT app with no source registered)
414
- 2. Check backend token verification logic
415
- 3. Enable `WEBSOCKETS_DEBUG` to see token status
155
+ ### Two socket connections in the network panel
416
156
 
417
- ### Frequent disconnections
157
+ Something still opens its own Socket.io client. Remove it and listen through
158
+ `useSocketEvent` / `onSocketEvent`. The example app's Playwright suite asserts one connection
159
+ (`example-frontend/e2e/socket-flags.spec.ts`).
418
160
 
419
- **Cause:** Token expiration, network issues, or server restarts.
161
+ ### Authentication errors
420
162
 
421
- **Solution:**
422
- - `useSocketConnection` automatically reconnects (5 attempts)
423
- - Tokens are refreshed if expiring within 60 seconds
424
- - Toast notifications inform users after 9+ seconds
425
- - On Vercel Functions, every socket closes at the Function max duration (300s by default); see [reconnects](vercel-function-websockets.md#reconnects)
163
+ The socket sends the token from the `authProvider` passed to `createSyncDb` on every connection
164
+ attempt. An auth rejection pauses sync (`useSyncStatus().paused === "auth"`) until the session
165
+ refreshes. On Vercel Functions, every socket closes at the Function max duration (300s by default); clients reconnect automatically. See [syncdb reference](../reference/syncdb.md) and [Run Terreno WebSockets on Vercel Functions](vercel-function-websockets.md#reconnects).
426
166
 
427
167
  ### "Session ID unknown" or a 400 on `transport=polling`
428
168
 
429
- **Cause:** Polling requests for one session reached different server instances (no session affinity).
430
-
431
- **Solution:** Pass `transports: ["websocket"]` to `useSocketConnection` and `socketTransports: ["websocket"]` to `createSyncDb`, or enable session affinity on the host.
432
-
433
- ### Events not received
434
-
435
- **Cause:** Not subscribed to the correct room, or event name mismatch.
436
-
437
- **Solution:**
438
- 1. Verify room subscription: `socket.emit("subscribe:todos")`
439
- 2. Check event names match between backend and frontend
440
- 3. Use `socket.onAny((event, ...args) => console.log(event, args))` to debug
441
-
442
- ### Token refresh loops
443
-
444
- **Cause:** Backend rejecting refresh token.
445
-
446
- **Solution:**
447
- 1. Check `REFRESH_TOKEN_SECRET` matches between backend and frontend config
448
- 2. Verify refresh token hasn't expired
449
- 3. Check Sentry/logs for token verification errors
450
-
451
- ## Environment Variables
169
+ Polling requests for one session reached different server instances (no session affinity). Pass `socketTransports: ["websocket"]` to `createSyncDb`, or enable session affinity on the host.
452
170
 
453
- | Variable | Required | Description |
454
- |----------|----------|-------------|
455
- | `WEBSOCKET_URL` | Yes | WebSocket server URL (e.g., `wss://api.example.com`) |
456
- | `WEBSOCKETS_DEBUG` | No | Enable debug logging (default: false) |
457
- | `TOKEN_SECRET` | Yes | JWT secret for token verification (backend) |
458
- | `REFRESH_TOKEN_SECRET` | Yes | Refresh token secret (backend) |
459
171
 
460
172
  ## Security Considerations
461
173
 
462
- 1. **Use WSS (TLS):** Always use `wss://` in production
463
- 2. **Validate tokens:** Always verify JWT tokens in Socket.io middleware
464
- 3. **Room authorization:** Check user permissions before joining rooms
465
- 4. **Rate limiting:** Implement rate limits on event emissions
466
- 5. **Input validation:** Validate all event data on the server
174
+ 1. Use `https://` / `wss://` in production.
175
+ 2. Emit per-user data only to `user:<id>` rooms.
176
+ 3. Keep payloads to ids and hints; clients fetch details through permission-checked APIs.
177
+ 4. Validate any data your handlers trust.
467
178
 
468
179
  ## Related Documentation
469
180
 
470
- - [@terreno/syncdb Reference](../reference/syncdb.md) — Local-first data layer (replaces RTK for synced collections)
471
- - [@terreno/rtk Reference (legacy)](../reference/legacy/rtk.md) — RTK Query utilities during the support window
472
- - [Authentication Architecture](../explanation/authentication.md) — JWT system deep-dive
181
+ - [@terreno/syncdb reference](../reference/syncdb.md) — `onSocketEvent`, `useSocketEvent`
182
+ - [Feature flags reference](../reference/feature-flags.md) — live flag updates
183
+ - [Authentication architecture](../explanation/authentication.md)
473
184
  - [Run Terreno WebSockets on Vercel Functions](vercel-function-websockets.md) — Vercel's WebSocket beta: transports, reconnects, cross-instance state
474
- - [Socket.io Documentation](https://socket.io/docs/v4/) — Official Socket.io docs
185
+ - [Socket.io documentation](https://socket.io/docs/v4/)
@@ -103,7 +103,7 @@ All decisions resolved by the maintainer on 2026-08-09:
103
103
  | [orgs-and-teams](orgs-and-teams.md) | Superseded by [org-management-ui](org-management-ui.md) | — |
104
104
  | [org-management-ui](org-management-ui.md) | Draft — awaiting approval | [tasks](../tasks/org-management-ui.md) |
105
105
  | [billing-stripe](billing-stripe.md) | Draft | [tasks](../tasks/billing-stripe.md) |
106
- | [native-module-baseline](native-module-baseline.md) | Draft | [tasks](../tasks/native-module-baseline.md) |
106
+ | [native-module-baseline](native-module-baseline.md) | In progress | [tasks](../tasks/native-module-baseline.md) |
107
107
  | [rbac-permissions](rbac-permissions.md) | Complete (PR #932) | — |
108
108
  | [notification-center](notification-center.md) | Approved | [tasks](../tasks/notification-center.md) |
109
109
  | [framework-audit-log](framework-audit-log.md) | Approved | [tasks](../tasks/framework-audit-log.md) |
@@ -7,6 +7,11 @@
7
7
  **Created:** 2026-09-01
8
8
  **Grow run:** https://cursor.com/agents/bc-47350376-6566-4c5a-aee2-e6a28698de61
9
9
 
10
+ > **Terreno 58 follow-up (done):** the dual-run window closed. `useAdminApi`, admin
11
+ > `injectEndpoints`, and the admin `api` prop were removed (umbrella issue #1252).
12
+ > ObjectId and API-only admin CRUD now use the `AdminProvider` fetch client through
13
+ > `useAdminModelApi`. The plan below is kept as written for history.
14
+
10
15
  ## Goal
11
16
 
12
17
  Move **built-in admin model list/read/create/update/delete** for collections that already use **String `_id`** onto **windowed local-first syncdb**, with **REST remaining the membership source** (search, sort, pagination, RBAC). Remaining admin RPC (config, scripts, roles, comms, AI, consent, documents, version-config, background-tasks) leaves RTK `injectEndpoints` for a **tiny native `fetch` wrapper** with **host-injected auth**.
@@ -1,6 +1,6 @@
1
1
  # Implementation Plan: Native module baseline for the next major release
2
2
 
3
- **Status:** Draft
3
+ **Status:** In progress (Phase 1 and diagnostics story landed on `release-58.0.0`; EAS builds pending)
4
4
  **Roadmap issue:** https://github.com/TerrenoLabs/terreno/issues/1026
5
5
  **Priority:** High
6
6
  **Effort:** Small batch (code) — coordination cost is the release itself
@@ -28,12 +28,36 @@ JS/OTA update against the same binary.
28
28
 
29
29
  | Question | Decision |
30
30
  |----------|----------|
31
- | Manifest (D7, resolved) | `@stripe/stripe-react-native`, `react-native-purchases` + `react-native-purchases-ui`, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, `react-native-otp-verify` |
31
+ | Manifest (D7, resolved) | `@stripe/stripe-react-native`, `react-native-purchases` + `react-native-purchases-ui`, `expo-device`, `expo-crypto`, `expo-local-authentication`, `expo-system-ui`, `react-native-otp-verify` (dropped at implementation; see below) |
32
32
  | Where dependencies live | Root catalog entries; linked in `example-frontend` and `demo` app manifests; `@terreno/ui`/feature packages reference them only when their feature IPs land (this IP is binary readiness, not usage) |
33
- | Stripe Android constraints | Pin Kotlin 2.x / `compileSdkVersion` 36 via `expo-build-properties` per stripe-react-native requirements |
33
+ | Stripe Android constraints | Pin Kotlin 2.x / `compileSdkVersion` via `expo-build-properties` per stripe-react-native requirements. Implemented as Kotlin `2.2.21` (Stripe's default, in Expo's KSP table) and `compileSdkVersion` `37`: React Native 0.88 already compiles against 37, so pinning 36 would downgrade it. Stripe needs at least 36 |
34
34
  | Expo Go story | Document which flows degrade in Expo Go (purchases → RevenueCat Preview API mode; stripe/local-auth → dev build required); CI smoke keeps web unaffected |
35
35
  | Release process | Follows the `release` skill + the upgrade-notes-for-breaking-releases CI gate (PR #987): major tag, upgrade note required |
36
36
 
37
+ ## Implementation notes (Terreno 58)
38
+
39
+ Versions, chosen against Expo SDK 58 (`bundledNativeModules.json` from `expo@58.0.14`):
40
+
41
+ | Package | Pin | Source |
42
+ | --- | --- | --- |
43
+ | `@stripe/stripe-react-native` | `0.81.0` | Expo 58 bundled version |
44
+ | `react-native-purchases`, `react-native-purchases-ui` | `10.12.2` | Newest release past the 7-day publish cooldown; peers `react-native >= 0.73` |
45
+ | `expo-device` | `~58.0.5` | Expo 58 bundled version |
46
+ | `expo-crypto` | `~58.0.2` | Existing catalog pin, unchanged |
47
+ | `expo-local-authentication` | `~58.0.2` | Expo 58 bundled version |
48
+ | `expo-system-ui` | `~58.0.6` | Expo 58 bundled version |
49
+ | `expo-build-properties` | `~58.0.11` | Expo 58 bundled version |
50
+
51
+ **Dropped: `react-native-otp-verify`.** `expo-doctor` reports it as unsupported on the New
52
+ Architecture (React Native Directory metadata), and React Native 0.88 runs only the New
53
+ Architecture. The latest release (1.2.0) is a legacy bridge module with no iOS sources. One-time
54
+ codes use `TextInput` `textContentType="oneTimeCode"` (iOS) and `autoComplete="sms-otp"`
55
+ (Android), which need no native module. Android SMS Retriever auto-read needs a New
56
+ Architecture module in a later binary if a feature requires it.
57
+
58
+ `expo-local-authentication` also gets its config plugin (`faceIDPermission`) so Face ID works
59
+ on iOS without another binary.
60
+
37
61
  ## Architecture
38
62
 
39
63
  Changes are dependency + config only:
@@ -94,8 +118,8 @@ See [docs/tasks/native-module-baseline.md](../tasks/native-module-baseline.md).
94
118
 
95
119
  ## Acceptance Criteria
96
120
 
97
- - [ ] `bun install`, full compile, and `example-frontend` web export succeed with the
98
- seven packages added.
121
+ - [x] `bun install`, full compile, and `example-frontend` web export succeed with the
122
+ seven packages added (`react-native-otp-verify` dropped; see Implementation notes).
99
123
  - [ ] iOS and Android EAS dev builds compile; the diagnostics story shows all seven
100
124
  modules resolved on device.
101
125
  - [ ] Android build carries the Kotlin 2.x / compileSdk 36 pins without breaking