@terreno/mcp 57.19.2 → 57.20.0

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 (197) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +32 -269
  3. package/dist/bootstrap.d.ts +12 -0
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +77 -122
  6. package/dist/bootstrap.js.map +1 -1
  7. package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
  8. package/dist/docs/resources/overview.md +4 -2
  9. package/dist/docs/resources/patterns.md +237 -176
  10. package/dist/docs/resources/ui.md +48 -17
  11. package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  12. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  13. package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  14. package/dist/docs/upgrades/57.20.0.md +238 -0
  15. package/dist/docs/versioned/next/approval-rubric.md +30 -12
  16. package/dist/docs/versioned/next/explanation/README.md +5 -0
  17. package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
  18. package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
  19. package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  20. package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  21. package/dist/docs/versioned/next/explanation/authentication.md +45 -1
  22. package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
  23. package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
  24. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
  25. package/dist/docs/versioned/next/explanation/organizations.md +37 -0
  26. package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
  27. package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
  28. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  29. package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
  30. package/dist/docs/versioned/next/how-to/README.md +7 -2
  31. package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
  32. package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  33. package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  34. package/dist/docs/versioned/next/how-to/circleci.md +15 -5
  35. package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  36. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  37. package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  38. package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  39. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  40. package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
  41. package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  42. package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  43. package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  44. package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  45. package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  46. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  47. package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  48. package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  49. package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  50. package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  51. package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  52. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  53. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  54. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  55. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  56. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  57. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  58. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  59. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  60. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  61. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  62. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  63. package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  64. package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  65. package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  66. package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  67. package/dist/docs/versioned/next/reference/README.md +1 -0
  68. package/dist/docs/versioned/next/reference/account-security.md +700 -0
  69. package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
  70. package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
  71. package/dist/docs/versioned/next/reference/api.md +461 -35
  72. package/dist/docs/versioned/next/reference/cli.md +1 -1
  73. package/dist/docs/versioned/next/reference/comms.md +8 -0
  74. package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
  75. package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
  76. package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
  77. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  78. package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
  79. package/dist/docs/versioned/next/reference/syncdb.md +8 -1
  80. package/dist/docs/versioned/next/reference/ui.md +1271 -27
  81. package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  82. package/dist/docs/versioned/next/tutorials/README.md +4 -0
  83. package/dist/httpApp.d.ts +14 -0
  84. package/dist/httpApp.d.ts.map +1 -0
  85. package/dist/httpApp.js +99 -0
  86. package/dist/httpApp.js.map +1 -0
  87. package/dist/index.js +8 -90
  88. package/dist/index.js.map +1 -1
  89. package/dist/local/localTools.d.ts.map +1 -1
  90. package/dist/local/localTools.js +44 -14
  91. package/dist/local/localTools.js.map +1 -1
  92. package/dist/local/metro/metroDevSession.d.ts +18 -0
  93. package/dist/local/metro/metroDevSession.d.ts.map +1 -1
  94. package/dist/local/metro/metroDevSession.js +42 -3
  95. package/dist/local/metro/metroDevSession.js.map +1 -1
  96. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  97. package/dist/local/tools/databaseQuery.js +3 -13
  98. package/dist/local/tools/databaseQuery.js.map +1 -1
  99. package/dist/local/tools/databaseSchema.d.ts.map +1 -1
  100. package/dist/local/tools/databaseSchema.js +3 -13
  101. package/dist/local/tools/databaseSchema.js.map +1 -1
  102. package/dist/local/tools/localMongo.d.ts +10 -0
  103. package/dist/local/tools/localMongo.d.ts.map +1 -0
  104. package/dist/local/tools/localMongo.js +30 -0
  105. package/dist/local/tools/localMongo.js.map +1 -0
  106. package/dist/local/tools/runtime.d.ts +18 -2
  107. package/dist/local/tools/runtime.d.ts.map +1 -1
  108. package/dist/local/tools/runtime.js +75 -20
  109. package/dist/local/tools/runtime.js.map +1 -1
  110. package/dist/local/tools/syncDb.d.ts +11 -0
  111. package/dist/local/tools/syncDb.d.ts.map +1 -1
  112. package/dist/local/tools/syncDb.js +15 -3
  113. package/dist/local/tools/syncDb.js.map +1 -1
  114. package/dist/rateLimit.d.ts +35 -0
  115. package/dist/rateLimit.d.ts.map +1 -0
  116. package/dist/rateLimit.js +112 -0
  117. package/dist/rateLimit.js.map +1 -0
  118. package/dist/tools.d.ts.map +1 -1
  119. package/dist/tools.js +42 -60
  120. package/dist/tools.js.map +1 -1
  121. package/package.json +4 -3
  122. package/src/docs/guidelines/ui/guidelines/core.md +1 -1
  123. package/src/docs/resources/overview.md +4 -2
  124. package/src/docs/resources/patterns.md +237 -176
  125. package/src/docs/resources/ui.md +48 -17
  126. package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
  127. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
  128. package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
  129. package/src/docs/upgrades/57.20.0.md +238 -0
  130. package/src/docs/versioned/next/approval-rubric.md +30 -12
  131. package/src/docs/versioned/next/explanation/README.md +5 -0
  132. package/src/docs/versioned/next/explanation/account-linking.md +169 -0
  133. package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
  134. package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
  135. package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
  136. package/src/docs/versioned/next/explanation/authentication.md +45 -1
  137. package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
  138. package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
  139. package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
  140. package/src/docs/versioned/next/explanation/organizations.md +37 -0
  141. package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
  142. package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
  143. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
  144. package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
  145. package/src/docs/versioned/next/how-to/README.md +7 -2
  146. package/src/docs/versioned/next/how-to/audit-log.md +22 -3
  147. package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
  148. package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
  149. package/src/docs/versioned/next/how-to/circleci.md +15 -5
  150. package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
  151. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
  152. package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
  153. package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
  154. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
  155. package/src/docs/versioned/next/how-to/password-reset.md +1 -1
  156. package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
  157. package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
  158. package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
  159. package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
  160. package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
  161. package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
  162. package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
  163. package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
  164. package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
  165. package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
  166. package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
  167. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
  168. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
  169. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
  170. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
  171. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
  172. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
  173. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
  174. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
  175. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
  176. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
  177. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
  178. package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
  179. package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
  180. package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
  181. package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
  182. package/src/docs/versioned/next/reference/README.md +1 -0
  183. package/src/docs/versioned/next/reference/account-security.md +700 -0
  184. package/src/docs/versioned/next/reference/admin-backend.md +3 -1
  185. package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
  186. package/src/docs/versioned/next/reference/api.md +461 -35
  187. package/src/docs/versioned/next/reference/cli.md +1 -1
  188. package/src/docs/versioned/next/reference/comms.md +8 -0
  189. package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
  190. package/src/docs/versioned/next/reference/environment-variables.md +9 -3
  191. package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
  192. package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
  193. package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
  194. package/src/docs/versioned/next/reference/syncdb.md +8 -1
  195. package/src/docs/versioned/next/reference/ui.md +1271 -27
  196. package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
  197. package/src/docs/versioned/next/tutorials/README.md +4 -0
@@ -0,0 +1,169 @@
1
+ # Account linking
2
+
3
+ One person can sign in to a Terreno app in several ways: email and password, Google, GitHub,
4
+ Apple, or a passkey. Account linking decides what happens when a new sign-in method arrives with
5
+ an email that already belongs to an account. Done carelessly, it is an account takeover. This page
6
+ explains the three modes, why Terreno asks for proof of the email before it links anything, and
7
+ how apps that started on JWT auth move their users over.
8
+
9
+ Setup: [Auth features](../how-to/configure-better-auth.md#auth-features). Reference:
10
+ [`GET {basePath}/terreno/methods`](../reference/api.md#better-auth-authentication).
11
+
12
+ ## Two layers of accounts
13
+
14
+ A Terreno app with Better Auth has two records per person:
15
+
16
+ - **The Better Auth user** (`user` collection) owns sign-in. Each sign-in method is an `account`
17
+ row under it: `credential` for email and password, `google`, `github`, and so on. Better Auth
18
+ keeps emails unique, so there is at most one Better Auth user per email.
19
+ - **The app `User`** (your Mongoose model) owns everything else: permissions, `admin`,
20
+ `disabled`, org memberships, and your data. It points at its Better Auth user through
21
+ `betterAuthId`.
22
+
23
+ Linking happens at both layers. Better Auth decides whether a new provider `account` joins an
24
+ existing Better Auth user. Terreno decides whether a Better Auth user may claim an existing app
25
+ `User` (`syncBetterAuthUser`).
26
+
27
+ ## The three modes
28
+
29
+ Set `accountLinking: {mode}` on `BetterAuthConfig`. The mode controls the Better Auth layer.
30
+
31
+ | Mode | A same-email sign-in with a new method | `POST {basePath}/link-social` |
32
+ | --- | --- | --- |
33
+ | `verified-email` (default) | Joins the account when both emails are verified | Links |
34
+ | `explicit` | Refused with `account_not_linked`; nothing is created | Links, from a signed-in session |
35
+ | `never` | Refused with `account_not_linked`; nothing is created | Refused with 403 `LINKING_DISABLED` |
36
+
37
+ - **`verified-email`** is the least friction: someone who signed up with email and password and
38
+ later taps "Continue with Google" on the same address lands in the same account.
39
+ - **`explicit`** makes the user prove they own the existing account first. They sign in the old
40
+ way, open the Security screen, and link the provider there. A login screen should turn
41
+ `account_not_linked` into "sign in with your original method, then link this one".
42
+ - **`never`** keeps every sign-in method separate. Because Better Auth keeps emails unique, a
43
+ second method on the same email cannot sign in at all. Pick it when a shared inbox must not
44
+ join two people's records.
45
+
46
+ `/terreno/methods` reports the mode so clients can word their guidance. Unlinking the only
47
+ remaining sign-in method is refused in every mode (400 `FAILED_TO_UNLINK_LAST_ACCOUNT`), so a user
48
+ cannot lock themselves out. rtk's `TerrenoAccountSecurityScreen` checks the same rule before it
49
+ asks, and a social-only user can add a password there (`POST {basePath}/terreno/set-password`) to
50
+ gain a second method before unlinking a provider.
51
+
52
+ ### What the refusals look like
53
+
54
+ | Path | `account_not_linked` | Unverified email matches an app `User` |
55
+ | --- | --- | --- |
56
+ | OAuth redirect (`/callback/{provider}`) | Redirect with `?error=account_not_linked` | Redirect with `?error=VERIFY_EMAIL_TO_USE_ACCOUNT` |
57
+ | ID-token sign-in (`/sign-in/social` with `idToken`) | 401, `message: "account not linked"` | 409, `code: "VERIFY_EMAIL_TO_USE_ACCOUNT"` |
58
+ | Email sign-up or sign-in | n/a | 409, `code: "VERIFY_EMAIL_TO_USE_ACCOUNT"`, `message: "Verify your email to use this account"` |
59
+
60
+ ## Why both emails must be verified
61
+
62
+ Linking by email trusts that whoever controls the email controls the account. That trust has to
63
+ hold on both sides:
64
+
65
+ - **The new method's email.** A provider that reports an unverified email has not checked that
66
+ the person owns it. Linking on it lets anyone who types your address into that provider into
67
+ your account. An unverified provider email never links, in any mode. `trustedProviders` turns
68
+ this check off for the providers it lists, so list only providers that prove email ownership.
69
+ - **The existing account's email.** If the existing account was itself created without proving
70
+ the email, an attacker can **pre-register**: sign up with the victim's address and a password
71
+ the attacker knows, wait for the victim to sign in with Google, and share the account.
72
+ Terreno keeps Better Auth's `requireLocalEmailVerified: true` in every mode, so an unverified
73
+ local account never absorbs a new method.
74
+
75
+ The same rule applies one layer down. A Better Auth user claims an existing app `User` only when
76
+ the provider verified its email, or when the user proves they own that `User` another way (see
77
+ the migration below). Terreno checks this **before the Better Auth user is created**. An
78
+ unverified sign-up for someone else's address gets 409 "Verify your email to use this account"
79
+ and leaves nothing behind: no Better Auth user, no `account` row, no verification email. That
80
+ matters because a Better Auth user left behind would be a pre-registration one layer down: once
81
+ anyone verified that email (the victim clicking the mail the attacker's sign-up triggered, or an
82
+ admin), the attacker's password would open the victim's `User`. Terreno never inserts a second
83
+ `User` for the same email either.
84
+
85
+ Two more rules hold whichever way the claim is proven. A `User` that already carries another
86
+ Better Auth account's `betterAuthId` is never relinked by email (409 `ACCOUNT_ALREADY_EXISTS`).
87
+ And when a verified email links a `User` whose own `emailVerified` is not `true`, the `User`'s
88
+ legacy password is removed and its `tokenEpoch` bumped: whoever set that password never proved
89
+ they own the mailbox, so it and the JWT refresh tokens issued from it stop working. A link proven
90
+ by the legacy password itself keeps that password. Operator seeds pass `trustEmail` to
91
+ `syncBetterAuthUser`, which skips these checks for accounts the seed created itself.
92
+
93
+ Emails are compared ignoring case, so a legacy `User` stored as `Jane@Example.com` is protected
94
+ from a sign-up as `jane@example.com`, and claiming it never creates a second `User`.
95
+
96
+ **Trade-off: the 409 reveals that an account exists.** A sign-up that gets 409
97
+ `VERIFY_EMAIL_TO_USE_ACCOUNT` tells the caller that an app `User` already has that email. Terreno
98
+ accepts this account-existence signal: refusing silently would either leave the
99
+ pre-registration hole open or leave legitimate users stuck with no explanation, and Better Auth's
100
+ own sign-up already answers `USER_ALREADY_EXISTS` for emails it knows. Apps that must hide
101
+ membership (for example, where an account implies care) should rate-limit sign-up (the `auth`
102
+ bucket) and word the client message neutrally.
103
+
104
+ The guard needs a user model on the Better Auth instance. `BetterAuthApp` uses its own
105
+ `userModel`, or adopts `TerrenoApp`'s when it was given none, before it creates Better Auth. If you
106
+ call `createBetterAuth` yourself, pass `userModel` to it whenever a session middleware links app
107
+ `User`s.
108
+
109
+ Terreno then syncs the app `User` as each Better Auth session is created, while the request that
110
+ proves who the user is (its password, its provider) is still in hand. That is also when
111
+ `oauthProvider` is recorded. The session check repeats the claim rule as a second guard, and a
112
+ password-proven link that fails to save fails the sign-in (500 `APP_USER_LINK_FAILED`) instead of
113
+ leaving a session that every request would refuse.
114
+
115
+ ## Apple private-relay emails
116
+
117
+ Sign in with Apple lets users hide their address. Apple then reports a relay address such as
118
+ `abc123@privaterelay.appleid.com`, marked verified, that forwards to the real inbox. It never
119
+ matches the email the person used elsewhere, so Apple sign-in creates a separate account in every
120
+ mode. Users who want one account sign in their usual way and link Apple from the Security screen
121
+ (`/link-social`). That link works only when Apple shares the real address ("Share My Email"):
122
+ Better Auth refuses a link whose email differs from the account's unless `allowDifferentEmails`
123
+ is on. Do not merge relay accounts by name: a name proves nothing.
124
+
125
+ ## Several accounts on one device
126
+
127
+ Linking answers "is this the same person?". Some people really have several accounts, for example
128
+ a parent with their own login and a child's. They should not merge them. `multiSession` lets one
129
+ device hold several signed-in accounts and switch between them without signing out, and
130
+ `signUp: {mode: "closed"}` stops new accounts altogether. Together with the linking mode, these
131
+ are the three knobs an app has over "multiple accounts".
132
+
133
+ ## Migrating users from JWT auth
134
+
135
+ Apps that started on Terreno's JWT and passport auth already have app `User`s with
136
+ passport-local-mongoose password hashes. When such a user first signs in through Better Auth with
137
+ the same email, Terreno links the new Better Auth user to their existing `User` if either:
138
+
139
+ - the sign-in comes from a provider (Google, GitHub, Apple, ...) that reports the email as
140
+ verified, or
141
+ - the password they sign up with verifies against the `User`'s existing passport hash. The
142
+ plaintext is checked only during that request and never stored. The check does not count as a
143
+ failed JWT login, so with passport's `limitAttempts` a stranger signing up with the victim's
144
+ email cannot lock the victim out.
145
+
146
+ Otherwise the sign-up answers 409 "Verify your email to use this account" and nothing is created.
147
+ Verifying the email afterwards cannot link anything, because there is no Better Auth user to
148
+ verify. Before this change, any unverified sign-up claimed the `User` with the same email, which
149
+ let anyone take over a legacy account by signing up with its address.
150
+
151
+ Users who signed in with GitHub through JWT have no password hash. They get in one of three ways:
152
+
153
+ - a provider sign-in whose email the provider verified;
154
+ - a password reset sent to their email through the JWT forgot-password flow
155
+ (`POST /auth/forgotPassword`), then a Better Auth sign-up with the new password;
156
+ - an admin-issued account recovery link (see [account security](../reference/api.md#better-auth-authentication)
157
+ once recovery is enabled) that sets their password and creates the Better Auth user.
158
+
159
+ To migrate smoothly:
160
+
161
+ 1. Keep the JWT auth routes mounted while users move over, so forgot-password still works.
162
+ 2. Tell users to sign up with the email and password they already use; those link at once.
163
+ 3. Point users without a password at a provider sign-in or at JWT forgot-password.
164
+
165
+ ## Learn more
166
+
167
+ - [Authentication architecture](authentication.md)
168
+ - [Configure Better Auth](../how-to/configure-better-auth.md)
169
+ - [Better Auth: users and accounts](https://www.better-auth.com/docs/concepts/users-accounts#account-linking)
@@ -55,7 +55,7 @@ row is `AuditEvent` when `AuditApp` is registered. `isAuditLogModel` also matche
55
55
  legacy `AdminAuditLog`, `audit-log`, and `audit-events` names. Jobs is a custom
56
56
  screen (`name: "jobs"`); the shell lifts it the same way.
57
57
 
58
- Below 768px the rail becomes a hamburger drawer. The main column is a body-style
58
+ Below the desktop floor (1024pt) the rail becomes a hamburger drawer. The main column is a body-style
59
59
  canvas (`neutral-050`, or `neutral-900` in dark mode so `surface.base` cards stand out).
60
60
  The colorful teal rail has no dark palette, so dark mode always uses the clinical rail.
61
61
  Host apps must give React Navigation a theme that follows the Terreno color scheme
@@ -0,0 +1,173 @@
1
+ # Agent guidelines
2
+
3
+ An agent writes Terreno-shaped code only when Terreno's conventions are in its context.
4
+ Those conventions reach it through four channels. Each one has a single source of truth,
5
+ and every other copy is generated from it.
6
+
7
+ | Channel | Source of truth | How the agent gets it | Status |
8
+ | --- | --- | --- | --- |
9
+ | Hosted MCP docs | `docs/`, `website/versioned_docs/`, `mcp-server/src/docs/resources/` | `terreno_search_docs`, `terreno_get_component_docs`, `terreno://docs/*` resources | Shipped |
10
+ | Per-package guidelines | `<package>/.ai/guidelines/core.md` | Merged into the files `terreno_bootstrap_ai_rules` returns | Shipped |
11
+ | App rule files | The app's `.rulesync/rules/` | Generated per editor by `rulesync generate` | Shipped |
12
+ | Agent skills | `plugins/terreno-planning/skills/`, `plugins/terreno-scan/skills/`, `.rulesync/skills/` | `npx skills add TerrenoLabs/terreno` or a host plugin | Shipped |
13
+
14
+ The runtime side of the loop, meaning what the agent can see in a running app, is covered
15
+ in [AI development loop](ai-development-loop.md). This page covers only conventions.
16
+
17
+ ## Hosted docs and search
18
+
19
+ The hosted server (`terreno-mcp`) bundles the docs at build time. `sync-versioned-docs`
20
+ copies `docs/` as the `next` version and each `website/versioned_docs/version-*` tree as a
21
+ released version. `sync-ui-docs` copies the component type data.
22
+
23
+ `terreno_search_docs` runs BM25 over that corpus and the resource pages. It takes several
24
+ queries in one call, an optional package filter, and an optional lockstep `version`. With a
25
+ version, it answers from the nearest retained snapshot, so an app on an older release gets
26
+ that release's guidance.
27
+
28
+ Resources are the fixed entry points: `terreno://docs/overview`, `api`, `ui`, `syncdb`,
29
+ `rtk`, `patterns`, plus one page per `@terreno/ui` component. A client can read them
30
+ without a tool call.
31
+
32
+ Search is pull-based. The agent sees nothing until it asks. That is why the search tool's
33
+ own description tells the agent to call it before guessing at Terreno APIs.
34
+
35
+ ## Per-package `.ai/` guidelines
36
+
37
+ Each core package keeps a short `.ai/guidelines/core.md` next to its code, so a change to
38
+ the package and a change to its guidance land in the same PR. Two packages also carry skill
39
+ copies under `.ai/skills/`.
40
+
41
+ | Package | `.ai/` contents | In the npm tarball |
42
+ | --- | --- | --- |
43
+ | `@terreno/api` | `guidelines/core.md`, `skills/mongoose-schema-safety/` | Yes (no `files` field) |
44
+ | `@terreno/ui` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
45
+ | `@terreno/rtk` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
46
+ | `@terreno/syncdb` | `guidelines/core.md`, `skills/debug-syncdb-with-mcp/` | No (`files` is `dist` and `src` only) |
47
+ | `@terreno/admin-backend` | `guidelines/core.md` | Yes (no `files` field) |
48
+ | `@terreno/admin-frontend` | `guidelines/core.md` | Yes (`.ai/**/*` in `files`) |
49
+
50
+ The path that matters today runs through the MCP server, not `node_modules`.
51
+ `mcp-server/scripts/sync-package-guidelines.ts` copies every `.ai/` tree into
52
+ `mcp-server/src/docs/guidelines/<package>/`. `@terreno/mcp` publishes that directory, and
53
+ `terreno_bootstrap_ai_rules` reads it.
54
+
55
+ Nothing in Terreno reads `node_modules/@terreno/*/.ai/` in a consumer project yet; the
56
+ tarball copies are available to any tool that looks for vendor guidelines there.
57
+
58
+ Planned (from the per-package guidelines phase of the MCP parity plan, not built):
59
+
60
+ - Versioned guidelines (`.ai/guidelines/<major>/core.md`) picked by the app's installed
61
+ version. Guidelines are a single flat `core.md` per package today.
62
+ - Shipping `@terreno/syncdb`'s `.ai/` in its tarball. Its `files` field leaves it out.
63
+
64
+ ## `terreno_bootstrap_ai_rules`
65
+
66
+ This hosted tool turns the package guidelines into rule files for one app. It does not
67
+ write files. It returns their contents and instructions, and the agent writes them.
68
+
69
+ | File | Contents |
70
+ | --- | --- |
71
+ | `.rulesync/rules/00-root.md` | The one root rule, for Cursor, Claude Code, Copilot, and `AGENTS.md`: app description, code style, and the merged `core.md` of `api`, `ui`, `rtk`, `syncdb` |
72
+ | `backend/AGENTS.md`, `backend/CLAUDE.md` | Backend conventions, plus the `admin-backend` guidelines when requested |
73
+ | `frontend/AGENTS.md`, `frontend/CLAUDE.md` | Frontend conventions, the MCP servers to use, plus the `admin-frontend` guidelines when requested |
74
+ | `rulesync.jsonc` | rulesync config: `rules` feature, targets `cursor`, `claudecode`, `copilot`, `agentsmd`, `delete: true` |
75
+
76
+ `packages` narrows the merge. Pass the app's `@terreno/*` dependencies so a project without
77
+ the admin panel gets no admin guidance. Unknown ids are dropped, and an empty result falls
78
+ back to all six packages.
79
+
80
+ The root rules go through rulesync. The `backend/` and `frontend/` files do not: they are
81
+ direct output, written as-is.
82
+
83
+ `bunx create-terreno-app` writes its own `.cursorrules` and `CLAUDE.md`. The
84
+ `terreno_bootstrap` prompt tells the agent to run `create-terreno-app` first and this tool
85
+ second.
86
+
87
+ The returned instructions run `bunx rulesync@9 generate`, the major this repository pins.
88
+ A test in `mcp-server` runs `rulesync generate` on the returned files. From `00-root.md`,
89
+ rulesync writes `.cursor/rules/00-root.mdc`, `CLAUDE.md`, `.github/copilot-instructions.md`,
90
+ and `AGENTS.md`. Windsurf, Codex, and other agents read
91
+ `AGENTS.md`; rulesync 9 has no `windsurf` target. The generated `CLAUDE.md` replaces the
92
+ one `create-terreno-app` wrote, and `.cursor/rules/00-root.mdc` supersedes its
93
+ `.cursorrules`, so delete that file. The root rule contains everything both files said,
94
+ including the "Adding Features" steps.
95
+
96
+ Earlier versions of the tool also returned `.rulesync/rules/01-claudecode-root.md`, a
97
+ `localRoot` rule that rulesync 9 rejects without a Claude Code `root` rule. Delete that file
98
+ when you re-bootstrap. The root rule now covers Claude Code.
99
+
100
+ ## The `.rulesync/` pattern
101
+
102
+ This repository and the apps the tool bootstraps follow the same rule. Agent instructions
103
+ are written once under `.rulesync/`, and every editor-specific file is generated from them.
104
+
105
+ In this repository:
106
+
107
+ | Source | Generated targets |
108
+ | --- | --- |
109
+ | `.rulesync/rules/**` | `CLAUDE.md`, `.claude/rules/`, `AGENTS.md`, `.cursor/rules/`, `.github/copilot-instructions.md`, `.github/instructions/`, `.devin/rules/` |
110
+ | `.rulesync/skills/`, `.rulesync/subagents/`, `.rulesync/hooks.json` | Per-host skill, subagent, and hook directories (`.claude/`, `.cursor/`, `.github/`, `.devin/`, `.agents/`) |
111
+ | `plugins/terreno-planning/skills/`, `plugins/terreno-scan/skills/`, plus `.rulesync/skills/` | `skills/` (installable set), `plugins/terreno-claude/` and `plugins/terreno-scan-claude/` (Claude Code plugins) |
112
+
113
+ `rulesync.jsonc` maps each target to its features. `bun run rules` regenerates the rulesync
114
+ targets. `bun run skills:sync` regenerates `skills/` and the Claude plugin. The
115
+ `rulesync-check` workflow regenerates and fails on any diff, and `bun run skills:check` does
116
+ the same for skills.
117
+
118
+ The config sets `delete: true`. rulesync removes stale generated files before it writes, so
119
+ a generated file holds only what its source says. A hand edit to a generated file is lost
120
+ on the next run.
121
+
122
+ ## Extending the rules in your app
123
+
124
+ Regeneration replaces two kinds of file. rulesync replaces every generated editor file. A
125
+ re-run of `terreno_bootstrap_ai_rules` returns fresh copies of everything in its table,
126
+ including `00-root.md`, `rulesync.jsonc`, and the `backend/`/`frontend/` agent files. Anything you add to those files is lost.
127
+
128
+ `rulesync generate` runs with `delete: true`. It overwrites the root `CLAUDE.md`,
129
+ `AGENTS.md`, and `.github/copilot-instructions.md`. It deletes every file under
130
+ `.claude/rules/`, `.cursor/rules/`, `.github/instructions/`, and `.agents/memories/` that
131
+ it did not generate. Before the first `rulesync generate`, and before every later one,
132
+ move any edits to those root files and any hand-written files in those directories into
133
+ `.rulesync/rules/10-project.md` or another `.rulesync/rules/*.md` source. Otherwise they
134
+ are lost.
135
+
136
+ Keep your conventions in rulesync sources the tool never emits:
137
+
138
+ 1. Leave `00-root.md` untouched. Treat it as Terreno-owned.
139
+ 2. Add one or more files such as `.rulesync/rules/10-project.md`. Use a number above `00`
140
+ and a name the tool does not use.
141
+ 3. Scope with `globs` instead of editing `backend/AGENTS.md` or `frontend/AGENTS.md`. A rule
142
+ with `globs: ["backend/**"]` applies to backend files in every editor.
143
+ 4. Run `bunx rulesync@9 generate` and commit the sources and the output together.
144
+
145
+ ```markdown
146
+ ---
147
+ targets: ["*"]
148
+ description: "Acme backend conventions"
149
+ globs: ["backend/**"]
150
+ ---
151
+
152
+ # Acme backend
153
+
154
+ - Store money as integer cents in fields named `*Cents`.
155
+ - Every tenant-scoped model has an `orgId` index.
156
+ ```
157
+
158
+ From one source, rulesync 9 writes `.cursor/rules/10-project.mdc` (`globs`),
159
+ `.claude/rules/10-project.md` (`paths`), `.github/instructions/10-project.instructions.md`
160
+ (`applyTo`), and `.agents/memories/10-project.md` (linked from `AGENTS.md`). A re-bootstrap
161
+ followed by `rulesync generate` leaves these files in place and
162
+ replaces only the Terreno-owned ones.
163
+
164
+ If you changed `rulesync.jsonc`, for example to add a target, reapply that change after a
165
+ re-bootstrap. The tool returns the default config.
166
+
167
+ ## Related
168
+
169
+ - [AI development loop](ai-development-loop.md): what the agent sees at runtime
170
+ - [Set up Terreno MCP](../how-to/set-up-terreno-mcp.md): connect both servers to your editor
171
+ - [MCP server reference](../reference/mcp-server.md#terreno_bootstrap_ai_rules): tool parameters
172
+ - [Install agent skills](../how-to/install-agent-skills.md): the skills channel
173
+ - [Planning plugins](planning-plugins.md): how the lifecycle skills are packaged
@@ -0,0 +1,196 @@
1
+ # AI development loop
2
+
3
+ A coding agent working on a Terreno app runs the same five-stage loop on every change:
4
+ it asks how something should be done, generates the code, runs the app, observes what the
5
+ app did, and fixes what it finds. Terreno's two MCP servers supply a tool for each stage.
6
+
7
+ - **`terreno-mcp`** is the hosted server. It holds the docs, the code generators, and the
8
+ convention validators. It knows Terreno but not your app.
9
+ - **`terreno-mcp-local`** runs in your project over stdio. It reads your app's logs,
10
+ client state, and database, and drives the running app. It knows your app but not the
11
+ docs.
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ ASK["1. Ask<br/>terreno_search_docs<br/>terreno_get_component_docs<br/>application_info"]
16
+ GEN["2. Generate<br/>terreno_generate_*<br/>terreno_validate_model_schema"]
17
+ RUN["3. Run<br/>backend dev server<br/>Metro (Expo)"]
18
+ OBS["4. Observe<br/>read_logs (backend / browser / metro / app)<br/>last_error<br/>get_client_state"]
19
+ FIX["5. Fix<br/>edit, then navigate<br/>browser snapshot + screenshot"]
20
+ ASK --> GEN --> RUN --> OBS --> FIX
21
+ FIX -->|"iterate"| ASK
22
+ ```
23
+
24
+ This page covers what the agent can see and do at each stage, and where that stops. How
25
+ Terreno's conventions reach the agent in the first place is in
26
+ [Agent guidelines](agent-guidelines.md).
27
+
28
+ ## The five stages
29
+
30
+ ### 1. Ask
31
+
32
+ The agent looks up the current way to do something before it writes code.
33
+
34
+ | Tool | Server | Contributes |
35
+ | --- | --- | --- |
36
+ | `application_info` | local | The app's `@terreno/*`, Expo, React Native, and Mongoose versions, so later answers match the installed release |
37
+ | `terreno_search_docs` | hosted | BM25 search over the bundled docs, several queries per call, filtered by package and lockstep `version` |
38
+ | `terreno_get_component_docs` | hosted | The full props table for one `@terreno/ui` component |
39
+ | `terreno_get_upgrade_guide`, `terreno_search_update_notes`, `terreno_get_update_note`, `terreno_ask_update_help` | hosted | Upgrade notes for a version range, and questions against them |
40
+ | `terreno://docs/*` resources | hosted | Fixed entry pages a client can read without a tool call |
41
+
42
+ Version matters here. An agent that passes the app's version to `terreno_search_docs` gets
43
+ the guidance for the release the app is on, not for the newest one.
44
+
45
+ ### 2. Generate
46
+
47
+ The agent produces code in Terreno's shape instead of inventing it.
48
+
49
+ | Tool | Server | Contributes |
50
+ | --- | --- | --- |
51
+ | `terreno_generate_model`, `terreno_generate_route`, `terreno_generate_screen`, `terreno_generate_form_fields` | hosted | A Mongoose model with the standard plugins and types, a `modelRouter` with permissions, an Expo screen, form fields |
52
+ | `terreno_validate_model_schema` | hosted | Checks a schema against the conventions, such as a `description` on every field |
53
+ | `terreno_validate_ui_blocks` | hosted | Checks an agent reply document against the blocks catalog |
54
+ | `terreno_install_admin` | hosted | Admin panel integration snippets |
55
+ | `terreno_bootstrap_ai_rules` | hosted | Rule files for the app's editors |
56
+
57
+ The generators return file contents and instructions. They do not write files, so the agent
58
+ stays in control of where code lands. A new app is scaffolded by `create-terreno-app`, not by
59
+ an MCP tool.
60
+
61
+ ### 3. Run
62
+
63
+ The app runs the way a developer runs it: the backend dev server, and Expo's Metro dev
64
+ server for the web, simulator, or device build. There is no special agent runtime. The
65
+ local server attaches to what is already running.
66
+
67
+ Two pieces of development-only instrumentation make the running app observable:
68
+
69
+ - The backend's logger writes JSONL to `.terreno/logs/app.log`. By default this is on
70
+ whenever `NODE_ENV` is not `production`.
71
+ - On web, `installTerrenoDevConsoleLogger` (from `@terreno/syncdb`) forwards `console.error`,
72
+ `console.warn`, and uncaught errors and rejections to the backend's `POST /__terreno/browser-logs` route, which writes
73
+ `.terreno/logs/browser.log`. The route exists only in development and is never registered
74
+ in production. See [Development browser-log ingestion](../reference/api.md#development-browser-log-ingestion).
75
+
76
+ ### 4. Observe
77
+
78
+ The agent reads what the app did.
79
+
80
+ | Tool | Contributes |
81
+ | --- | --- |
82
+ | `read_logs` | One merged, time-ordered tail from four sources: `backend` (server JSONL), `browser` (web errors and warnings JSONL), `metro` (bundler events), and `app` (the Hermes console over the Chrome DevTools Protocol). Its `connection` field says which live sources are attached. |
83
+ | `last_error` | The newest error-level entry across the same sources, as the raw line. It includes a stack when the entry carries one. |
84
+ | `get_client_state` | The client data layer in the running app. It reports which `layer` it found (`rtk`, `syncdb`, both, or `none`) and returns the RTK Query cache and auth state, the SyncDB state, or both. `get_rtk_state` is a deprecated alias. |
85
+ | `get_syncdb_state`, `syncdb_snapshot` | SyncDB entities, outbox, conflicts, cursors, and debug events, plus snapshots to compare before and after a change |
86
+ | `database_schema`, `database_query` | The app's MongoDB collections and documents, through a read-only allowlist |
87
+
88
+ All of these are reads. None of them needs an opt-in.
89
+
90
+ ### 5. Fix
91
+
92
+ The agent edits code in its own editor, then reproduces the path that failed and checks the
93
+ result.
94
+
95
+ | Tool | Contributes |
96
+ | --- | --- |
97
+ | `navigate` | Moves a native app to an expo-router path over the same CDP connection the logs use |
98
+ | `browser` | Drives the web app in a headless WebView: open, click, type, scroll, then `snapshot` the rendered page and save a `screenshot` as proof |
99
+ | `evaluate` | Runs a JavaScript expression in the app's Hermes runtime |
100
+ | `syncdb_action` | Changes a live SyncDB client: mutate, flush, resync, resolve conflicts, go offline |
101
+
102
+ `evaluate`, `navigate`, `syncdb_action`, and the `browser` tool's `evaluate` action run code
103
+ or change state inside the app, so they refuse unless the local server was started with
104
+ `TERRENO_MCP_EVAL=1`. Generated editor configuration never sets it. The other `browser`
105
+ actions do not need it.
106
+
107
+ After the fix, the loop goes back to Observe. A change is done when the logs and state show
108
+ the new behavior, not when the edit compiles.
109
+
110
+ ## Tool layer versus process layer
111
+
112
+ The loop is half of how Terreno works with agents. This page covers the tool layer. The
113
+ `/terreno-*` lifecycle skills are the process layer, described in
114
+ [Planning plugins](planning-plugins.md).
115
+
116
+ | Layer | Answers | Delivered by |
117
+ | --- | --- | --- |
118
+ | **Tool** | What can the agent see and do? | MCP servers: docs search, codegen, merged logs, client state, navigation |
119
+ | **Process** | In what order should the agent work, and who checks it? | `/terreno-*`: question-first planning, TDD, fresh-context independent review, evidence gates, reactive review loop |
120
+
121
+ The two compose. The lifecycle's build and prove stages use the loop's tools to gather their
122
+ evidence, and the tools are most useful when the lifecycle decides when to call them.
123
+
124
+ ## What makes this different
125
+
126
+ The agent observes what the app actually did, not a description of it.
127
+
128
+ Without these tools, an agent learns about a failure from a person's summary, a pasted stack
129
+ trace, or its own guess about what the code should do. With them, it reads the 500 from the
130
+ server log, the uncaught error from the browser console, and the cache entry the screen
131
+ rendered from. It reads them from the same run it just changed.
132
+
133
+ The rest is table stakes. Docs search, generators, and rule files exist for other frameworks
134
+ too, and Terreno does not claim otherwise.
135
+
136
+ ## What the agent cannot see
137
+
138
+ The loop ends at the edge of the local development session, and some sources have gaps
139
+ inside it.
140
+
141
+ - **Production.** The backend log file is off in production by default, the browser-log
142
+ route is never registered there, and no tool reads a hosted log service. Production errors
143
+ reach the agent only if a person brings them in.
144
+ - **Native crash reports.** A native crash outside the dev loop, such as an iOS crash report
145
+ or an Android tombstone, never reaches the JavaScript console, so no log source carries it.
146
+ - **Devices not attached to a dev server.** The `app` source, `navigate`, and `evaluate` reach
147
+ a runtime only through Metro's inspector. A phone running a release build, or one not
148
+ connected to Metro, is invisible.
149
+ - **The `app` source on web.** The web build has no Hermes target, so `app` returns nothing
150
+ there. On web, errors, warnings, and uncaught exceptions arrive through the `browser` source
151
+ instead; `console.log` and `console.info` reach no source. `app` needs a native
152
+ development build or Expo Go attached to Metro.
153
+ - **Metro events under Expo CLI.** The `metro` source reads Metro's `/events` socket. In Expo
154
+ CLI 57 that socket accepts connections but never sends events, so `metro` stays empty even
155
+ when its status reads connected.
156
+ - **The Hermes debugger connection.** The local server holds one CDP session to the app. On a
157
+ default `expo start` with current React Native, that session and React Native DevTools
158
+ coexist. On a target that allows a single debugger, or when Expo runs headless, opening
159
+ DevTools disconnects the local server. The next call that reads the app reconnects and
160
+ disconnects DevTools in turn, and console entries logged in between are lost. The
161
+ `connection` field reports the close reason.
162
+ - **Missing stacks.** `last_error` returns the line as it was logged. If the code logged a
163
+ message without a stack, there is no stack to return.
164
+
165
+ ## How this composes
166
+
167
+ Terreno does not build its own simulator or browser automation. It composes with three tools
168
+ that already do it well, and keeps to the part they lack: reading the app's logs, client
169
+ state, and data.
170
+
171
+ | Tool | Use it for | Lives |
172
+ | --- | --- | --- |
173
+ | `expo-mcp` | Interacting with the iOS simulator or Android emulator: screenshots and taps on the native app | Agent session |
174
+ | Playwright MCP | Interacting with the web app in a full browser while working | Agent session |
175
+ | Maestro | Flows worth keeping, such as login, signup, and creating a todo, committed and run in CI | Repository |
176
+
177
+ The division of labor:
178
+
179
+ - **Interaction is the composed servers' job.** `expo-mcp` and Playwright MCP move the app
180
+ and show what is on screen. Terreno's `browser` tool covers the same ground on web when a
181
+ lightweight headless session is enough.
182
+ - **Observation is Terreno's job.** After a tap or a click, `read_logs`, `last_error`, and
183
+ `get_client_state` show what the app did behind the screen.
184
+ - **Durability is Maestro's job.** An agent session ends. A flow that proved a fix and should
185
+ keep proving it becomes a Maestro flow, so CI repeats the check on every change.
186
+
187
+ `create-terreno-app` writes `expo` and `playwright` entries next to the two Terreno servers
188
+ in the app's `.cursor/mcp.json` and `.mcp.json`, so a new app starts with all four.
189
+
190
+ ## Related
191
+
192
+ - [Set up Terreno MCP](../how-to/set-up-terreno-mcp.md): connect the servers to your editor
193
+ - [Debug a Terreno app with MCP](../how-to/debug-with-mcp.md): the loop on a real failure
194
+ - [MCP server reference](../reference/mcp-server.md): every tool, prompt, resource, and variable
195
+ - [Agent guidelines](agent-guidelines.md): how Terreno's conventions reach the agent
196
+ - [Planning plugins](planning-plugins.md): the process layer
@@ -37,11 +37,55 @@ Modern session-based authentication with built-in social OAuth support.
37
37
  3. Backend redirects to OAuth provider or validates credentials
38
38
  4. Better Auth creates session in MongoDB
39
39
  5. Frontend receives a session cookie (web, default), a bearer session token (native), or, on web with `webAuth: "bearer"`, a bearer token from the `set-auth-token` header. Bearer on web is the option when the web app and the API are on different sites, where browsers block the third-party session cookie. See [Frontend and API on different sites](../how-to/configure-better-auth.md#frontend-and-api-on-different-sites)
40
- 6. On the first authenticated request or socket handshake, Terreno looks up the app `User` by `betterAuthId` and creates it if missing (`syncBetterAuthUser`). An email alone does not prove someone owns an existing account, so an existing app `User` with the same email is linked only after Better Auth verifies that email, and never when the row already belongs to another Better Auth account. Linking removes a legacy password the row never proved with a verified email. `RealtimeApp` uses the registered `BetterAuthApp` instance for socket sessions without extra wiring
40
+ 6. As the session is created, Terreno looks up the app `User` by `betterAuthId` and creates it if missing (`syncBetterAuthUser`), recording the OAuth provider in `oauthProvider`. An email alone does not prove someone owns an existing account, so a Better Auth user claims an existing `User` with the same email only when Better Auth verified that email or its sign-up password matches that `User`'s passport hash; otherwise the sign-up answers 409 "Verify your email to use this account" before any Better Auth user is created. A `User` that already belongs to another Better Auth account is never relinked (409 `ACCOUNT_ALREADY_EXISTS`). Linking through a verified email removes a legacy password the row never proved with a verified email, and bumps `tokenEpoch`. See [Account linking](account-linking.md). The first authenticated request or socket handshake retries the sync if it did not run. `RealtimeApp` uses the registered `BetterAuthApp` instance for socket sessions without extra wiring
41
41
  7. `req.user` is that app document for subsequent `modelRouter` permissions. A session whose app `User` has `disabled: true` no longer authenticates: `req.user` stays unset, so protected routes answer 401 and disabling an account ends its live sessions. Better Auth's own routes, such as sign-out, still work
42
42
 
43
43
  Email/password create omits `oauthProvider`. Apps that never use social login can keep `strict: "throw"` without declaring that field. OAuth create sets `oauthProvider` and the User schema must declare it.
44
44
 
45
+ #### Challenges without cookies
46
+
47
+ Bearer mode sends no cookies, but Better Auth keeps three short-lived values in signed cookies:
48
+ the two-factor challenge set when a sign-in needs a second factor, the trust-device token set
49
+ when the user ticks "trust this device", and the passkey challenge set by the WebAuthn options
50
+ endpoints. A cross-site bearer client never sends them back, so without help it could not finish
51
+ a 2FA or passkey sign-in.
52
+
53
+ When `twoFactor` or `passkey` is configured, Terreno adds a plugin that carries those cookies in
54
+ the `X-Terreno-Auth-Challenge` header instead:
55
+
56
+ 1. A bearer client sends the header on every `{basePath}/two-factor/*`, `/passkey/*`, and
57
+ `/sign-in/*` call. It sends `-` when it holds nothing, which opts it in.
58
+ 2. For a request that sent the header, the response copies each challenge cookie it sets into
59
+ the header as `name=value` pairs (URL search params), still signed with the auth secret. Only
60
+ values that response sets are copied, never cookies the request already had. An expired
61
+ cookie comes back with an empty value. A `rememberMe: false` sign-in's `dont_remember` cookie
62
+ rides along with the two-factor challenge, so the verified session stays short. The header is
63
+ added to `Access-Control-Expose-Headers`, the same way `bearer()` exposes `set-auth-token`.
64
+ A sign-in answered with a two-factor challenge loses its `set-auth-token`, which named the
65
+ session two-factor had just deleted.
66
+ 3. On those paths the server turns the header back into cookies before Better Auth reads them.
67
+ A real cookie always wins, and only the challenge cookies (plus `dont_remember`) are accepted. The header can
68
+ never set a session cookie, and a tampered value fails Better Auth's signature check (the 2FA
69
+ verify answers 401, a passkey verify 400 `CHALLENGE_NOT_FOUND`, and a tampered trust value
70
+ just asks for the second factor).
71
+ 4. rtk's bearer mode stores the header under `${storagePrefix}_auth_challenge` in the same
72
+ `bearerStorage` as the bearer token. It never stores a token from a response that asks for a
73
+ second factor. It drops the 2FA challenge and `dont_remember` after a successful verify, and
74
+ everything but the trust-device value on sign-out. The trust-device value stays until the server
75
+ expires it or `trustDeviceDays` runs out, so the device stays trusted across sign-outs.
76
+
77
+ Clients that do not send the header (cookie mode, native) never receive it, so their challenge
78
+ cookies stay httpOnly. That holds only for requests the page's own code does not send with the
79
+ header: a script on the page that adds it to a sign-in or verify call can read the challenge
80
+ values that call's response sets.
81
+
82
+ Exposure: in bearer mode the trust-device token lives in web storage beside the bearer token.
83
+ Any XSS on the frontend's origin can read both, and a stolen trust value skips the second factor
84
+ for that account until it expires (it is still useless without the password). Apps that handle
85
+ PHI should pass `bearerStorage: window.sessionStorage`, which ends trust with the tab, and serve
86
+ a strict Content-Security-Policy. See
87
+ [Frontend and API on different sites](../how-to/configure-better-auth.md#frontend-and-api-on-different-sites).
88
+
45
89
  **Key properties:**
46
90
  - Session-based (cookies / bearer session) vs. stateless JWT
47
91
  - A session cookie is sent with requests other sites start, so a cookie-authenticated write counts only when it comes from a trusted origin. Bearer tokens are never sent automatically, so they need no such check