@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.
- package/CHANGELOG.md +393 -0
- package/README.md +32 -269
- package/dist/bootstrap.d.ts +12 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +77 -122
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/ui/guidelines/core.md +1 -1
- package/dist/docs/resources/overview.md +4 -2
- package/dist/docs/resources/patterns.md +237 -176
- package/dist/docs/resources/ui.md +48 -17
- package/dist/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/dist/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/dist/docs/upgrades/57.20.0.md +238 -0
- package/dist/docs/versioned/next/approval-rubric.md +30 -12
- package/dist/docs/versioned/next/explanation/README.md +5 -0
- package/dist/docs/versioned/next/explanation/account-linking.md +169 -0
- package/dist/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/dist/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/dist/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/dist/docs/versioned/next/explanation/authentication.md +45 -1
- package/dist/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/dist/docs/versioned/next/explanation/organizations.md +37 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/dist/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/dist/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/dist/docs/versioned/next/how-to/README.md +7 -2
- package/dist/docs/versioned/next/how-to/audit-log.md +22 -3
- package/dist/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/dist/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/dist/docs/versioned/next/how-to/circleci.md +15 -5
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/dist/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/dist/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/dist/docs/versioned/next/how-to/password-reset.md +1 -1
- package/dist/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/dist/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/dist/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/dist/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/dist/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/dist/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/dist/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/dist/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/dist/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/dist/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/dist/docs/versioned/next/reference/README.md +1 -0
- package/dist/docs/versioned/next/reference/account-security.md +700 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +3 -1
- package/dist/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/dist/docs/versioned/next/reference/api.md +461 -35
- package/dist/docs/versioned/next/reference/cli.md +1 -1
- package/dist/docs/versioned/next/reference/comms.md +8 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -3
- package/dist/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/dist/docs/versioned/next/reference/syncdb.md +8 -1
- package/dist/docs/versioned/next/reference/ui.md +1271 -27
- package/dist/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/dist/docs/versioned/next/tutorials/README.md +4 -0
- package/dist/httpApp.d.ts +14 -0
- package/dist/httpApp.d.ts.map +1 -0
- package/dist/httpApp.js +99 -0
- package/dist/httpApp.js.map +1 -0
- package/dist/index.js +8 -90
- package/dist/index.js.map +1 -1
- package/dist/local/localTools.d.ts.map +1 -1
- package/dist/local/localTools.js +44 -14
- package/dist/local/localTools.js.map +1 -1
- package/dist/local/metro/metroDevSession.d.ts +18 -0
- package/dist/local/metro/metroDevSession.d.ts.map +1 -1
- package/dist/local/metro/metroDevSession.js +42 -3
- package/dist/local/metro/metroDevSession.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +3 -13
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/local/tools/databaseSchema.d.ts.map +1 -1
- package/dist/local/tools/databaseSchema.js +3 -13
- package/dist/local/tools/databaseSchema.js.map +1 -1
- package/dist/local/tools/localMongo.d.ts +10 -0
- package/dist/local/tools/localMongo.d.ts.map +1 -0
- package/dist/local/tools/localMongo.js +30 -0
- package/dist/local/tools/localMongo.js.map +1 -0
- package/dist/local/tools/runtime.d.ts +18 -2
- package/dist/local/tools/runtime.d.ts.map +1 -1
- package/dist/local/tools/runtime.js +75 -20
- package/dist/local/tools/runtime.js.map +1 -1
- package/dist/local/tools/syncDb.d.ts +11 -0
- package/dist/local/tools/syncDb.d.ts.map +1 -1
- package/dist/local/tools/syncDb.js +15 -3
- package/dist/local/tools/syncDb.js.map +1 -1
- package/dist/rateLimit.d.ts +35 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +112 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +42 -60
- package/dist/tools.js.map +1 -1
- package/package.json +4 -3
- package/src/docs/guidelines/ui/guidelines/core.md +1 -1
- package/src/docs/resources/overview.md +4 -2
- package/src/docs/resources/patterns.md +237 -176
- package/src/docs/resources/ui.md +48 -17
- package/src/docs/templates/bootstrap/frontend/app/(tabs)/profile.template.tsx +0 -26
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +3 -2
- package/src/docs/templates/bootstrap/frontend/app/login.template.tsx +1 -1
- package/src/docs/upgrades/57.20.0.md +238 -0
- package/src/docs/versioned/next/approval-rubric.md +30 -12
- package/src/docs/versioned/next/explanation/README.md +5 -0
- package/src/docs/versioned/next/explanation/account-linking.md +169 -0
- package/src/docs/versioned/next/explanation/admin-interface.md +1 -1
- package/src/docs/versioned/next/explanation/agent-guidelines.md +173 -0
- package/src/docs/versioned/next/explanation/ai-development-loop.md +196 -0
- package/src/docs/versioned/next/explanation/authentication.md +45 -1
- package/src/docs/versioned/next/explanation/consent-forms.md +105 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +1 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +1 -1
- package/src/docs/versioned/next/explanation/organizations.md +37 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +4 -0
- package/src/docs/versioned/next/explanation/responsive-design.md +143 -0
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +11 -0
- package/src/docs/versioned/next/explanation/schema-metadata.md +5 -0
- package/src/docs/versioned/next/how-to/README.md +7 -2
- package/src/docs/versioned/next/how-to/audit-log.md +22 -3
- package/src/docs/versioned/next/how-to/automate-terreno-upgrades.md +169 -0
- package/src/docs/versioned/next/how-to/build-list-detail-screens.md +160 -0
- package/src/docs/versioned/next/how-to/circleci.md +15 -5
- package/src/docs/versioned/next/how-to/configure-better-auth.md +673 -53
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +8 -5
- package/src/docs/versioned/next/how-to/debug-with-mcp.md +393 -23
- package/src/docs/versioned/next/how-to/enable-mcp-oauth.md +54 -3
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +9 -0
- package/src/docs/versioned/next/how-to/password-reset.md +1 -1
- package/src/docs/versioned/next/how-to/recover-locked-out-user.md +112 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +37 -8
- package/src/docs/versioned/next/how-to/set-up-social-login.md +424 -0
- package/src/docs/versioned/next/how-to/set-up-terreno-mcp.md +241 -36
- package/src/docs/versioned/next/how-to/sign-consent-on-behalf.md +139 -0
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +4 -0
- package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +47 -32
- package/src/docs/versioned/next/implementationPlans/auth-experience.md +952 -0
- package/src/docs/versioned/next/implementationPlans/consent-forms.md +8 -0
- package/src/docs/versioned/next/implementationPlans/list-detail-layout.md +297 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/HANDOFF.md +211 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/AnotherWay.html +121 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/BackupCode.html +72 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Cancelled.html +87 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ContinueEmail.html +64 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/FirstVisit.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/Main.html +88 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/ManageAccounts.html +77 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/NotLinked.html +92 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/SigningIn.html +60 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactor.html +76 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/TwoFactorError.html +81 -0
- package/src/docs/versioned/next/implementationPlans/login-redesign/boards/WrongPassword.html +69 -0
- package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -1
- package/src/docs/versioned/next/plans/2026-10-23-provisional-consent.md +297 -0
- package/src/docs/versioned/next/plans/2026-10-29-fix-mcp-oauth-native-and-machine.md +68 -0
- package/src/docs/versioned/next/reference/README.md +1 -0
- package/src/docs/versioned/next/reference/account-security.md +700 -0
- package/src/docs/versioned/next/reference/admin-backend.md +3 -1
- package/src/docs/versioned/next/reference/admin-frontend.md +225 -1
- package/src/docs/versioned/next/reference/api.md +461 -35
- package/src/docs/versioned/next/reference/cli.md +1 -1
- package/src/docs/versioned/next/reference/comms.md +8 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +3 -1
- package/src/docs/versioned/next/reference/environment-variables.md +9 -3
- package/src/docs/versioned/next/reference/legacy/rtk.md +19 -3
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +6 -0
- package/src/docs/versioned/next/reference/mcp-server.md +1158 -441
- package/src/docs/versioned/next/reference/syncdb.md +8 -1
- package/src/docs/versioned/next/reference/ui.md +1271 -27
- package/src/docs/versioned/next/tasks/ai-dev-loop-boost.md +28 -16
- package/src/docs/versioned/next/tutorials/README.md +4 -0
|
@@ -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.
|
|
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
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Consent forms
|
|
2
|
+
|
|
3
|
+
`ConsentApp` gives an app versioned consent forms (terms, privacy, HIPAA, research, and
|
|
4
|
+
custom agreements), a record of every response, and a gate that keeps a user out of the app
|
|
5
|
+
until they have answered every form that applies to them.
|
|
6
|
+
|
|
7
|
+
## Forms and versions
|
|
8
|
+
|
|
9
|
+
A `ConsentForm` has a `slug`, a `version`, localized markdown `content`, and options such as
|
|
10
|
+
required checkboxes, a drawn signature, and whether the user may decline. Publishing
|
|
11
|
+
(`POST /consent-forms/:id/publish`) copies the form to `version + 1`, activates the copy, and
|
|
12
|
+
deactivates every other version of the same slug. Only active forms are shown to users. Publish a new version rather than editing
|
|
13
|
+
a form users have already answered, so each response still points at the text the user saw.
|
|
14
|
+
|
|
15
|
+
An app can narrow which active forms apply to a user with `resolveConsentForms(user, forms)`,
|
|
16
|
+
for example to show a research consent only to enrolled members. Every consent endpoint that
|
|
17
|
+
asks "which forms apply to this user" runs the same resolver.
|
|
18
|
+
|
|
19
|
+
## Responses
|
|
20
|
+
|
|
21
|
+
A `ConsentResponse` records one answer to one form version: `agreed`, `agreedAt`, the locale
|
|
22
|
+
shown, checkbox values, an optional signature, and `formVersionSnapshot`. With
|
|
23
|
+
`auditTrail: true`, it also keeps the user's IP address and user agent and a snapshot of the
|
|
24
|
+
exact content they saw. Responses are append-only. A user who answers again gets a new
|
|
25
|
+
record, so the history of what they agreed to, and when, is never rewritten.
|
|
26
|
+
|
|
27
|
+
## The gate
|
|
28
|
+
|
|
29
|
+
`GET /consents/pending` returns the forms that apply to the user and that the user has not
|
|
30
|
+
answered at the form's current version. The client (`ConsentNavigator` in `@terreno/ui`)
|
|
31
|
+
shows them one at a time until the list is empty. A decline also clears the form from the
|
|
32
|
+
list: the user has answered it.
|
|
33
|
+
|
|
34
|
+
When a form is republished, every user's earlier answer is for an older version, so the new
|
|
35
|
+
version reappears in their gate.
|
|
36
|
+
|
|
37
|
+
## Provisional signatures
|
|
38
|
+
|
|
39
|
+
Some consent is given before the user ever opens the app. A guardian may agree on the phone
|
|
40
|
+
at intake, or staff may record verbal consent during a visit. For compliance the form must
|
|
41
|
+
count as signed from that moment, but the user should still read the form and sign it
|
|
42
|
+
themselves.
|
|
43
|
+
|
|
44
|
+
A provisional signature is a `ConsentResponse` with `provisional: true`. Staff create it with
|
|
45
|
+
`POST /consent-responses/provisional`, naming the target user, the form, and a required note
|
|
46
|
+
that explains how consent was obtained. The response records:
|
|
47
|
+
|
|
48
|
+
- `agreed: true` and `agreedAt` (server time)
|
|
49
|
+
- `provisionalSignedBy`: the staff member who recorded it
|
|
50
|
+
- `provisionalNote`: the note (trimmed, at most 2000 characters)
|
|
51
|
+
- `formVersionSnapshot`, plus `contentSnapshot`, `provisionalSignerIp`, and
|
|
52
|
+
`provisionalSignerUserAgent` when `auditTrail` is on
|
|
53
|
+
|
|
54
|
+
`ipAddress`, `userAgent`, `signature`, and `signedAt` stay empty. Those fields describe the
|
|
55
|
+
user's own device and signature, and the user did not sign.
|
|
56
|
+
|
|
57
|
+
### Why a provisional signature does not clear the gate
|
|
58
|
+
|
|
59
|
+
Only the user's own responses remove a form from `/consents/pending`. A provisional signature
|
|
60
|
+
is a record that someone else vouched for the user's consent. It is not the user's consent in
|
|
61
|
+
their own words. Keeping the form in the gate means the user still meets it, reads it, and
|
|
62
|
+
agrees or declines. The gate tells them who signed on their behalf and when, but not the
|
|
63
|
+
signer's note: that stays in the admin audit history. The user's own answer is stored as a separate response, so both records
|
|
64
|
+
stay for audit.
|
|
65
|
+
|
|
66
|
+
A separate `ProvisionalConsent` model was rejected. A field on `ConsentResponse` keeps one
|
|
67
|
+
audit trail per user and reuses the existing admin screens, populate paths, and PDF code.
|
|
68
|
+
|
|
69
|
+
### Voiding a provisional signature
|
|
70
|
+
|
|
71
|
+
A provisional signature recorded in error (the wrong member, the wrong form) is voided with
|
|
72
|
+
`POST /consent-responses/:id/void` and a required reason. Voiding sets `voidedAt`,
|
|
73
|
+
`voidedBy`, and `voidReason`; it never deletes the record, so the audit history shows both
|
|
74
|
+
the signature and its void. A voided signature no longer counts as consent, and the user's
|
|
75
|
+
gate no longer mentions it. Only provisional signatures can be voided: the user's own
|
|
76
|
+
answers are theirs to change by answering again.
|
|
77
|
+
|
|
78
|
+
### What counts as signed
|
|
79
|
+
|
|
80
|
+
`getConsentStatuses` (and `GET /consent-responses/status`) is the "considered signed"
|
|
81
|
+
contract. For each form that applies to the user, only non-voided responses to the current
|
|
82
|
+
version count. The user's own latest answer always wins: agreeing makes the form `signed`, declining
|
|
83
|
+
makes it `declined`. Without one, a provisional signature makes it `provisional`, which
|
|
84
|
+
counts as consented. Otherwise it is `pending`. App code that needs to know whether a user
|
|
85
|
+
may proceed (for example before scheduling a visit) checks `isConsented` on these rows
|
|
86
|
+
instead of reading responses directly.
|
|
87
|
+
|
|
88
|
+
### Who may sign for another user
|
|
89
|
+
|
|
90
|
+
Pass `accessControl` to `ConsentApp` to use RBAC. Any authenticated user whose roles grant
|
|
91
|
+
`consentResponse:signOnBehalf` may then sign for others. The built-in `admin` role includes
|
|
92
|
+
that action, and existing unsealed admin roles gain it the next time roles are seeded. The
|
|
93
|
+
merge runs on every seed, so removing `signOnBehalf` from the `admin` role does not stick.
|
|
94
|
+
To keep admins from signing, give them a custom role instead of `admin`.
|
|
95
|
+
Without `accessControl`, only users with `admin: true` may sign, and `ConsentApp` logs a
|
|
96
|
+
warning at register.
|
|
97
|
+
|
|
98
|
+
There is no relationship check: a permitted signer may sign for any user. A signer cannot
|
|
99
|
+
sign for themselves, and the form must be active and must apply to the target user under
|
|
100
|
+
`resolveConsentForms`. The same permission covers reading another user's consent statuses.
|
|
101
|
+
When `AuditApp` is registered, every provisional signature, void, and status read writes an
|
|
102
|
+
`AuditEvent` naming the actor and the target user. Audit events gain a `read` operation and
|
|
103
|
+
a `viewed` verb for these reads.
|
|
104
|
+
|
|
105
|
+
Reference: [ConsentApp](../reference/api.md#consent-forms-consentapp).
|
|
@@ -27,6 +27,7 @@ that file (and its imports) to a working example. Gaps stay explicit.
|
|
|
27
27
|
| File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts`, `file-uploads` flag in `example-backend/src/api/fileUploads.ts` | yes — `example-frontend/app/(tabs)/documents.tsx` (Files section, every signed-in user, 1 upload / IP / min), `example-frontend/app/(tabs)/ai.tsx`, `example-frontend/app/gcs-settings.tsx` | The `file-uploads` feature flag hides upload controls and rejects uploads when off. A missing flag leaves uploads enabled. Deployed bucket is terraform-managed (`google_storage_bucket.example_documents`). |
|
|
28
28
|
| Better Auth | yes — `example-backend/src/utils/betterAuthConfig.ts`, `BetterAuthApp` in `example-backend/src/server.ts` | yes — `example-frontend/app/login.tsx`, `example-frontend/lib/betterAuth.ts` | Default `AUTH_PROVIDER` is `better-auth`. |
|
|
29
29
|
| syncdb local-first | yes — `SyncApp` in `example-backend/src/server.ts` | yes — `example-frontend/store/syncdb.ts`, `example-frontend/app/(tabs)/index.tsx` | PR #869 closed without merge; `@terreno/syncdb` shipped on the launch line and is the example todos path. |
|
|
30
|
+
| List/detail layout | n/a — reuses the synced todos | yes — `example-frontend/app/todo-browser/` (`ListDetailLayout` group, opened from the todos screen's Browse button), list in `example-frontend/components/TodoBrowserList.tsx` | Phones push each todo; at 768px and wider the list stays on the left. e2e: `example-frontend/e2e/todo-browser.spec.ts` (phone, tablet, deep link, resize, keyboard). The demo inbox at `demo/app/demo/list-detail/` shows the same layout with static data. Steps: [build-list-detail-screens.md](../how-to/build-list-detail-screens.md). |
|
|
30
31
|
| SyncDB diagnostics | n/a | yes — `debug` registration in `example-frontend/store/syncdb.ts`, debugger UI at `example-frontend/app/syncdb-debug.tsx` | `terreno-mcp-local` can inspect, snapshot/compare/merge, mutate, flush, reconcile/resync, and exercise offline/conflict paths against the running development client. |
|
|
31
32
|
| RBAC | yes — `example-backend/src/rbacRoles.ts` (including optional `aiObservabilityViewer` / `aiObservabilityOperator` roles), `access.ts`, `accessControl: access` on `ObservabilityApp` / `AdminApp`, `access: {resource: "todo"}` on the todo router | yes — `example-frontend/app/admin/roles.tsx` | Shipped. Plan: [rbac-permissions.md](../implementationPlans/rbac-permissions.md). |
|
|
32
33
|
| Organizations | yes — `createAccess({organizations: true})`, `organizationSettingsSchema`, `orgScopedPlugin` on projects | yes — admin org directory/settings at `example-frontend/app/admin/orgs/` | Settings live on `Organization.settings`; example registers a nested timezone schema. |
|
|
@@ -35,7 +35,7 @@ Terreno 57.
|
|
|
35
35
|
## What you start doing
|
|
36
36
|
|
|
37
37
|
1. **Conflict resolution UI** — When the server rejects a mutation (409 / `sync:nack`), the user must choose a strategy (`useServer` or `keepMine`). Use `ConflictSheet` from `@terreno/ui` or build your own with `useConflicts`.
|
|
38
|
-
2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`).
|
|
38
|
+
2. **Sync-status UX** — Surface offline state, queued mutations, and drain progress with `useSyncStatus` (and optionally `SyncStatusBanner`, which waits `offlineGraceMs` — 3s by default — before showing "Offline" so startup and brief socket drops do not flash it).
|
|
39
39
|
3. **Encryption at rest (web)** — Web persistence uses AES-GCM via the default `createServerKeyProvider`; understand key fetch and `onDecryptFailure` for support flows.
|
|
40
40
|
4. **Key lifecycle on logout** — Call `syncDb.stop()` and consider `wipeLocalData` when the authenticated user changes so one account does not read another's local store.
|
|
41
41
|
5. **Backend sync registration** — Every synced collection needs `syncPlugin`, `isDeletedPlugin`, and a `sync` block on `modelRouter`, plus `SyncApp` and `RealtimeApp` on the server. Optional `sync.adminBroadcast: true` also emits `sync:delta` to `{collection}|admin`. Admin clients subscribe with `mode: "window"` (`createSyncDb({windowCollections})`) so they do not snapshot or reconcile that collection; they call `hydrateWindow` (REST membership ids + `GET /sync/entities`). Pass the same `accessControl` into `SyncApp` that `AdminApp` uses so `{collection}|admin` requires `admin:access`. `AdminApp.register` also installs that model's admin list/read/`queryFilter` on the admin window so hydrate and live `|admin` deltas match `/admin` REST. Without RBAC, `user.admin` is enough for the shell gate. Live deltas on that stream apply only to ids already in the window; Refresh is a REST re-query plus `hydrateWindow`.
|
|
@@ -47,6 +47,13 @@ Tenant isolation uses multiple checks rather than one convention:
|
|
|
47
47
|
admin models whose schema contains `organizationId`. Models without that path
|
|
48
48
|
remain platform-scoped.
|
|
49
49
|
|
|
50
|
+
Adding, changing, or removing a membership through `POST` / `PATCH` / `DELETE
|
|
51
|
+
/orgs/:id/members` clears that user's cached permissions, so a new org-admin can open the
|
|
52
|
+
admin page (and a demoted or removed one loses it) on their next request instead of after
|
|
53
|
+
the permission cache's 30-second TTL. Disabling (`PATCH /orgs/:id` with `disabled: true`) or
|
|
54
|
+
deleting (`DELETE /orgs/:id`) an organization suspends all of its memberships and clears the
|
|
55
|
+
cached permissions of every member the same way.
|
|
56
|
+
|
|
50
57
|
## Disabled organizations
|
|
51
58
|
|
|
52
59
|
When `Organization.disabled` is `true`, tenant context and membership mutations
|
|
@@ -64,6 +71,35 @@ stop, but operators can still manage the organization record itself:
|
|
|
64
71
|
Disabling or deleting an organization suspends its memberships. Re-enabling does
|
|
65
72
|
not automatically reactivate them.
|
|
66
73
|
|
|
74
|
+
## Member recovery
|
|
75
|
+
|
|
76
|
+
When the app registers `AccountSecurityApp`, an org-admin can help a locked-out member without
|
|
77
|
+
platform staff. `OrgsApp` mounts the same recovery handlers the platform uses at
|
|
78
|
+
`/orgs/:orgId/members/:userId/security/*`, behind an org authorizer.
|
|
79
|
+
|
|
80
|
+
- **Reach.** An org-admin may act on any active member of their org, even one who also belongs
|
|
81
|
+
to other orgs (D2). A suspended member, or a user outside the org, answers `404`, so the routes
|
|
82
|
+
do not reveal who exists elsewhere.
|
|
83
|
+
- **Guards.** Platform staff (`superadmin`, `operator`) and `admin: true` users are never reachable
|
|
84
|
+
through an org (D3). Neither is anyone who holds a permission the org-admin lacks, nor the
|
|
85
|
+
org-admin themselves (D9). Disabled orgs answer `403`.
|
|
86
|
+
- **Disable means suspend.** One org must not lock a user out of every other org, so the org
|
|
87
|
+
path has no account disable. It offers `suspend-membership` instead, which suspends the
|
|
88
|
+
membership in that org only (Q8). The user still signs in and keeps their other orgs.
|
|
89
|
+
`User.disabled` and `issue-sign-in-link` stay platform-only.
|
|
90
|
+
- **Attach-then-act.** An org-admin can attach any existing user by email
|
|
91
|
+
(`POST /orgs/:id/members`). Every org-admin action is audited with the org's id and emails the
|
|
92
|
+
user, so they learn about it ([notifications](../reference/account-security.md#notifications);
|
|
93
|
+
the org is named only with `notifications.includeOrgName`). Apps that want more can set
|
|
94
|
+
`orgRecovery.minMembershipDays` to refuse members who joined too recently.
|
|
95
|
+
- **Platform staff** holding `user:manageSecurity` pass the org checks. The target must still be
|
|
96
|
+
an active member of the org in the path. Their reach check is the platform one: both sides are
|
|
97
|
+
resolved outside the organization, so an org-only grant does not put the member out of reach.
|
|
98
|
+
|
|
99
|
+
See [Recover a locked-out user](../how-to/recover-locked-out-user.md) for who can do what, and
|
|
100
|
+
the [account security reference](../reference/account-security.md#org-admin-routes) for every
|
|
101
|
+
response.
|
|
102
|
+
|
|
67
103
|
## Why organizations are optional
|
|
68
104
|
|
|
69
105
|
Automatically registering routes, roles, or Mongoose models would change
|
|
@@ -83,6 +119,7 @@ multi-tenant-ready apps.
|
|
|
83
119
|
## Related
|
|
84
120
|
|
|
85
121
|
- [Add organizations](../how-to/add-organizations.md)
|
|
122
|
+
- [Recover a locked-out user](../how-to/recover-locked-out-user.md)
|
|
86
123
|
- [API reference](../reference/api.md#organizations)
|
|
87
124
|
- [Admin backend reference](../reference/admin-backend.md#organization-scoping)
|
|
88
125
|
- [Admin frontend reference](../reference/admin-frontend.md#orgdirectoryscreen)
|
|
@@ -112,3 +112,7 @@ Install both from the marketplaces described in
|
|
|
112
112
|
[install agent skills](../how-to/install-agent-skills.md). Claude Code resolves a plugin
|
|
113
113
|
skill's command from its frontmatter `name`, which is why the two `*-claude/` trees are
|
|
114
114
|
generated with shortened names by `bun run skills:sync`.
|
|
115
|
+
|
|
116
|
+
## Related
|
|
117
|
+
|
|
118
|
+
- [AI development loop](ai-development-loop.md): the tool layer these stages use to see the running app
|