@terreno/agent-plugin 0.0.0-stage → 58.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +86 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +74 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +294 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +108 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +104 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +174 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +165 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +112 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +288 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +89 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +50 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +55 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +87 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +93 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,112 @@
1
+ # modelRouter — Terreno
2
+
3
+ ## Registration signatures
4
+
5
+ ```typescript
6
+ // TerrenoApp (recommended):
7
+ const todoRouter = modelRouter("/todos", Todo, options);
8
+ app.register(todoRouter);
9
+
10
+ // setupServer (legacy):
11
+ router.use("/todos", modelRouter(Todo, options));
12
+ ```
13
+
14
+ ## Full options reference
15
+
16
+ ```typescript
17
+ modelRouter("/todos", Todo, {
18
+ permissions: {
19
+ create: [Permissions.IsAuthenticated],
20
+ list: [Permissions.IsAuthenticated],
21
+ read: [Permissions.IsOwner],
22
+ update: [Permissions.IsOwner],
23
+ delete: [], // disabled
24
+ },
25
+ queryFields: ["completed", "ownerId"],
26
+ queryFilter: OwnerQueryFilter,
27
+ sort: "-created",
28
+ defaultLimit: 100,
29
+ maxLimit: 500,
30
+ populatePaths: [{path: "ownerId", fields: ["name", "email"]}],
31
+ preCreate: (body, req) => ({...body, ownerId: req.user?._id}),
32
+ preUpdate: (body, req) => body,
33
+ postCreate: (doc, req) => {},
34
+ postUpdate: (doc, body, req, prev) => {},
35
+ postDelete: (req, doc) => {},
36
+ responseHandler: (value, method, req, options) => value,
37
+ validation: {
38
+ validateCreate: true,
39
+ validateUpdate: true,
40
+ validateQuery: true,
41
+ excludeFromCreate: ["ownerId"],
42
+ excludeFromUpdate: ["ownerId"],
43
+ },
44
+ sync: {scope: {type: "owner"}},
45
+ collectionActions: {
46
+ bulkComplete: {
47
+ method: "POST",
48
+ permissions: [Permissions.IsAuthenticated],
49
+ body: z.object({ids: z.array(z.string()).min(1)}).strict(),
50
+ response: z.object({matched: z.number(), modified: z.number()}).strict(),
51
+ summary: "Mark multiple todos complete",
52
+ handler: async ({body, user}) => { /* ... */ },
53
+ },
54
+ },
55
+ instanceActions: {
56
+ markComplete: {
57
+ method: "POST",
58
+ permissions: [Permissions.IsOwner],
59
+ summary: "Mark a single todo complete",
60
+ handler: async ({doc}) => { /* ... */ },
61
+ },
62
+ },
63
+ collectionActions: { /* named GET/POST on /todos/{name} */ },
64
+ instanceActions: { /* named GET/POST on /todos/:id/{name} */ },
65
+ // Last resort only — prefer collectionActions / instanceActions.
66
+ endpoints: (router) => {
67
+ // Custom routes registered before CRUD
68
+ },
69
+ });
70
+ ```
71
+
72
+ ## Query features
73
+
74
+ - Pagination: `?limit=20&page=2`
75
+ - Sort: `?sort=-created`
76
+ - Filters: `?completed=true` (field must be in `queryFields`)
77
+ - Complex: `?$and=[{...}]`, `?$or=[{...}]`
78
+
79
+ ## Lifecycle hooks
80
+
81
+ | Hook | When | Use for |
82
+ |------|------|---------|
83
+ | `preCreate` | Before insert | Set `ownerId`, defaults |
84
+ | `preUpdate` | Before patch | Strip immutable fields |
85
+ | `postCreate` | After insert | Side effects, notifications |
86
+ | `postUpdate` | After patch | Audit, cache invalidation |
87
+ | `postDelete` | After delete | Cleanup |
88
+
89
+ ## User casting in callbacks
90
+
91
+ ```typescript
92
+ const user = u as unknown as UserDocument;
93
+ // or in route handlers:
94
+ (req.user as unknown as UserDocument)?._id
95
+ ```
96
+
97
+ Never use `as any as UserDocument`.
98
+
99
+ ## Plugins on schemas
100
+
101
+ | Plugin | Adds |
102
+ |--------|------|
103
+ | `createdUpdatedPlugin` | `created`, `updated` timestamps |
104
+ | `isDeletedPlugin` | Soft delete (`deleted` field) |
105
+ | `findExactlyOne` | Static that throws on 0 or >1 match |
106
+ | `findOneOrNone` | Static that returns null on 0, throws on >1 |
107
+
108
+ ## OpenAPI
109
+
110
+ `setupServer` / `TerrenoApp` auto-generates `/openapi.json`. Field `description` on schema paths flows to the spec via `mongoose-to-swagger`.
111
+
112
+ After any model or router change, regenerate the frontend SDK.
@@ -0,0 +1,116 @@
1
+ # Server Setup — Terreno
2
+
3
+ ## TerrenoApp (recommended)
4
+
5
+ ```typescript
6
+ import {TerrenoApp} from "@terreno/api";
7
+ import {User} from "./models/user";
8
+ import {todoRouter} from "./api/todos";
9
+
10
+ const app = new TerrenoApp({
11
+ userModel: User,
12
+ loggingOptions: {logRequests: true, logSlowRequests: true},
13
+ })
14
+ .register(todoRouter)
15
+ .start();
16
+ ```
17
+
18
+ Methods: `.register()`, `.addMiddleware()`, `.build()`, `.start()`.
19
+
20
+ ## setupServer (legacy)
21
+
22
+ ```typescript
23
+ import {setupServer} from "@terreno/api";
24
+
25
+ setupServer({
26
+ userModel: User,
27
+ addRoutes: (router, options) => {
28
+ router.use("/todos", modelRouter(Todo, {...options, ...}));
29
+ },
30
+ });
31
+ ```
32
+
33
+ ## Required environment variables
34
+
35
+ | Variable | Purpose |
36
+ |----------|---------|
37
+ | `MONGO_URI` | MongoDB connection (replica set for change streams) |
38
+ | `TOKEN_SECRET` | JWT signing secret |
39
+ | `TOKEN_ISSUER` | JWT issuer claim |
40
+ | `REFRESH_TOKEN_SECRET` | Refresh token secret |
41
+ | `SESSION_SECRET` | Express session secret |
42
+ | `PORT` | Server port (default 4000) |
43
+
44
+ Optional: `ENABLE_SWAGGER=true`, `USE_SENTRY_LOGGING=true`, GitHub OAuth vars, Better Auth vars.
45
+
46
+ ## MongoDB for local dev
47
+
48
+ The dev server needs a real replica set (change streams). See `AGENTS.md` for the single-node replica set setup with `mongodb-memory-server` cache binary.
49
+
50
+ ```bash
51
+ MONGO_URI="mongodb://127.0.0.1:27017/terreno-example?replicaSet=rs0" \
52
+ TOKEN_SECRET=dev-token-secret TOKEN_ISSUER=terreno-dev \
53
+ REFRESH_TOKEN_SECRET=dev-refresh-secret SESSION_SECRET=dev-session-secret \
54
+ PORT=4000 bun run backend:dev
55
+ ```
56
+
57
+ Seed users: `cd example-backend && bun run seed` → `test@example.com` / `testpassword123`.
58
+
59
+ ## Health check
60
+
61
+ ```bash
62
+ curl localhost:4000/health
63
+ # {"healthy":true,...}
64
+ ```
65
+
66
+ ## OpenAPI / Swagger
67
+
68
+ - Spec: `http://localhost:4000/openapi.json`
69
+ - Swagger UI: `http://localhost:4000/swagger` (when `ENABLE_SWAGGER=true`)
70
+
71
+ ## Logging
72
+
73
+ ```typescript
74
+ import {createScopedLogger, logger} from "@terreno/api";
75
+
76
+ logger.info("Server started", {port: 4000});
77
+
78
+ const log = createScopedLogger({
79
+ prefix: "[Billing]",
80
+ labels: {invoiceId: id.toString()},
81
+ });
82
+ log.info("Processing payment");
83
+ ```
84
+
85
+ Never use `console.log` for permanent server logs.
86
+
87
+ ## Testing
88
+
89
+ ```typescript
90
+ import {describe, it, expect} from "bun:test";
91
+ import request from "supertest";
92
+ import {getBaseServer, authAsUser, setupDb} from "@terreno/api/tests";
93
+
94
+ describe("todos", () => {
95
+ it("lists todos for authenticated user", async () => {
96
+ const app = await getBaseServer();
97
+ const agent = await authAsUser(app, "notAdmin");
98
+ const res = await agent.get("/todos");
99
+ expect(res.status).toBe(200);
100
+ });
101
+ });
102
+ ```
103
+
104
+ See `backend-test-env` skill when tests mutate `process.env`.
105
+
106
+ ## Plugins
107
+
108
+ Register `TerrenoPlugin` implementations:
109
+
110
+ ```typescript
111
+ import {BetterAuthApp, type BetterAuthConfig} from "@terreno/api";
112
+ import {AdminApp} from "@terreno/admin-backend";
113
+
114
+ app.register(new BetterAuthApp({config: betterAuthConfig, userModel: User}));
115
+ admin.register(app);
116
+ ```
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: terreno-backend-tests
3
+ description: >-
4
+ Write and fix Bun tests for a Terreno backend: modelRouter routes and actions, permissions,
5
+ Mongoose models and hooks, services that call SendGrid / Twilio / Expo push / Cloud Tasks /
6
+ GCS / AI models, env-dependent code, time, and async side effects. Covers supertest +
7
+ @terreno/test helpers, auth tokens, test data, service mocks, and flake-free waiting. Use
8
+ when adding or changing backend behavior or when a backend test fails. If the app has no
9
+ test preload yet, run terreno-test-setup first.
10
+ ---
11
+
12
+ # Writing Terreno backend tests
13
+
14
+ Tests use `bun:test`, run against a real in-memory MongoDB, and call the app through HTTP with
15
+ supertest. Never mock `@terreno/api` internals or your own models — exercise the real stack.
16
+
17
+ ## 1. Pick the seam
18
+
19
+ | Behavior | Test through |
20
+ |----------|--------------|
21
+ | CRUD, actions, permissions, query filters, response shape | HTTP route (`supertest`) — default |
22
+ | Model statics, methods, hooks, validation | the model directly |
23
+ | A service with no route | its exported function |
24
+ | Third-party side effect (email, SMS, task, upload, AI call) | the route or service, asserting on the preload's mock handle |
25
+
26
+ Done when: the test file sits next to the code (`todos.ts` → `todos.test.ts`) and names the
27
+ seam in its `describe`.
28
+
29
+ ## 2. Build the app per file
30
+
31
+ ```typescript
32
+ import {beforeEach, describe, expect, it} from "bun:test";
33
+ import {generateTokens, TerrenoApp} from "@terreno/api";
34
+ import {authWithToken, expectAPIError} from "@terreno/test";
35
+ import supertest from "supertest";
36
+ import {Todo} from "../models/todo";
37
+ import {User} from "../models/user";
38
+ import {todoRouter} from "./todos";
39
+
40
+ const app = new TerrenoApp({skipListen: true, userModel: User as never})
41
+ .register(todoRouter)
42
+ .build();
43
+ ```
44
+
45
+ Register only the routers and plugins under test. Do not call the app's `start()`: it connects
46
+ to the dev database and registers everything.
47
+
48
+ ## 3. Authenticate
49
+
50
+ ```typescript
51
+ const createUser = async (email: string, admin = false) =>
52
+ User.register({admin, email, name: email} as never, "password12345");
53
+
54
+ const agentFor = async (user: unknown) =>
55
+ authWithToken(app, {token: (await generateTokens(user)).token});
56
+ ```
57
+
58
+ - `authWithToken` + `generateTokens` mints the same JWT `/auth/login` issues, without the
59
+ ~200ms password hash. Use it everywhere except tests of the login flow itself.
60
+ - `authAsUser(app, {email, password})` really logs in; keep it for login, lockout, and
61
+ password tests.
62
+ - `supertest(app)` with no token is the anonymous client.
63
+
64
+ Permission failures by layer — assert the exact status:
65
+
66
+ | Case | Status |
67
+ |------|--------|
68
+ | No or invalid token on a protected route | 401 |
69
+ | Method-level permission fails (`list`, `create`, or a disabled `[]` method) | 405 |
70
+ | Object-level permission fails (`read`, `update`, `delete` on a loaded document, e.g. `IsOwner`) | 403 |
71
+ | `list` where `queryFilter` excludes another user's documents | 200, those documents absent |
72
+ | Id that does not exist | 404 |
73
+
74
+ Write one test per role that matters: owner, other user, admin, anonymous.
75
+
76
+ ## 4. Create and clean data
77
+
78
+ - Create exactly what the test needs, in the test or a `beforeEach`, with small factory
79
+ functions (`createUser`, `createTodo({ownerId, ...overrides})`) local to the file or in
80
+ `src/tests/factories.ts`.
81
+ - The default preload does not wipe the database. Start each test clean for the collections
82
+ it reads: `beforeEach(async () => { await Todo.deleteMany({}); await User.deleteMany({}); })`.
83
+ (Apps on `registerBackendPreload({useTransactions: true})` roll back automatically; skip
84
+ the deletes there.)
85
+ - Dates are Luxon: `DateTime.fromISO("2024-06-01T10:00:00Z")`, compared with `.toISO()` or
86
+ `.toMillis()`.
87
+ - Expected values are literals from the example, never recomputed with production code.
88
+
89
+ ## 5. Assert
90
+
91
+ ```typescript
92
+ it("lets an owner list only their todos", async () => {
93
+ const owner = await createUser("owner@example.com");
94
+ const other = await createUser("other@example.com");
95
+ await Todo.create({ownerId: owner._id, title: "Mine"});
96
+ await Todo.create({ownerId: other._id, title: "Theirs"});
97
+
98
+ const res = await (await agentFor(owner)).get("/todos").expect(200);
99
+
100
+ expect(res.body.data.map((todo: {title: string}) => todo.title)).toEqual(["Mine"]);
101
+ expect(res.body.total).toBe(1);
102
+ });
103
+
104
+ it("rejects a todo without a title", async () => {
105
+ const res = await (await agentFor(await createUser("a@example.com"))).post("/todos").send({});
106
+ expectAPIError(res, {status: 400, title: /validation/i});
107
+ });
108
+ ```
109
+
110
+ - List responses are `{data, limit, more, page, total}`; single documents are `{data}`.
111
+ - `expectAPIError(res, {status, title?, detail?, code?, fields?})` matches the JSONAPI error
112
+ body; `fields` partially matches `meta.fields`.
113
+ - On routes that return user or PHI data, pin the response shape so a new model field cannot
114
+ leak: `assertStrictResponseProperties(res.body, ["data._id", "data.name", "limit", "more",
115
+ "page", "total"])`.
116
+ - After a write, re-read through the API (or `Model.findById`) to prove persistence.
117
+
118
+ ## 6. Services and side effects
119
+
120
+ Mocks are registered once in `src/tests/bunSetup.ts` and exported (see `terreno-test-setup`).
121
+ Import the handle and assert what the app sent:
122
+
123
+ ```typescript
124
+ import {cloudTasks, gcs, sendGrid} from "../tests/bunSetup";
125
+
126
+ expect(sendGrid.getSentMessages()).toEqual([
127
+ expect.objectContaining({subject: "Welcome", to: "new@example.com"}),
128
+ ]);
129
+
130
+ await cloudTasks.drainTasks({app, headers: {authorization: "Bearer task-secret"}});
131
+
132
+ gcs.putObject({bucket: "uploads", contents: "hi", name: "a.txt"});
133
+ ```
134
+
135
+ - Cloud Tasks: assert `getEnqueuedTasks()` (decoded bodies), then `drainTasks({app})` to run
136
+ the task handlers through HTTP; it also drains tasks the handlers enqueue.
137
+ - Make a provider fail with the mock's standard API, e.g.
138
+ `sendGrid.send.mockRejectedValueOnce(new Error("503"))`, and assert the app's handling.
139
+ - AI: `new AIService({model: createMockLanguageModel({responses: [{text: "Hi"}, {toolCalls:
140
+ [...]}]})})`; assert prompts via `model.doGenerate.mock.calls` / `doStream.mock.calls`.
141
+ - Never `mock.module` inside a test file: it replaces the module for every later file in the
142
+ run. Use the preload, dependency injection, or `spyOn(object, "method")` (restored
143
+ automatically after each test).
144
+
145
+ ## 7. Env, time, and async
146
+
147
+ - Env: set only the keys the test needs (`process.env.FEATURE_X = "true"`); express "unset" as
148
+ `Reflect.deleteProperty(process.env, "KEY")`. The preload re-applies the canonical env after
149
+ every test. Never snapshot and restore all of `process.env`.
150
+ - Time: `freezeTime("2024-06-01T10:30:00Z")` from `@terreno/test` freezes `Date.now()`,
151
+ `new Date()`, and Luxon for the current test and restores afterwards.
152
+ - Async side effects (post-save hooks, fire-and-forget writes, jobs): wait for the outcome, not
153
+ a duration — `await waitForDocuments(AuditEvent, {action: "create"}, 1)`,
154
+ `await waitUntil(() => sendGrid.send.mock.calls.length > 0, {label: "welcome email"})`, or
155
+ `await waitForCalls(mockFn, 2)`. Never `await new Promise((r) => setTimeout(r, n))`.
156
+
157
+ ## 8. Run
158
+
159
+ ```bash
160
+ bun test src/api/todos.test.ts # one file
161
+ bun test -t "lists only their todos" # by name
162
+ bun test --only-failures # rerun what failed last time
163
+ bun test --coverage # coverage report
164
+ ```
165
+
166
+ A test that passes alone and fails in the full run is leaking state: a module mock in a test
167
+ file, a collection not cleaned, or an env key not reset. Fix the leak; do not reorder files.
168
+
169
+ ## Checklist
170
+
171
+ - [ ] Happy path plus each rejected role/case asserts an exact status
172
+ - [ ] Data created in the test; collections it reads cleaned in `beforeEach`
173
+ - [ ] Third-party calls asserted on the preload mock handles; no `mock.module` in the file
174
+ - [ ] No sleeps; async outcomes awaited with `waitUntil` / `waitForDocuments`
175
+ - [ ] Time-dependent behavior uses `freezeTime`; env changes are targeted
176
+ - [ ] `bun test` passes for the whole backend, not just the file
@@ -0,0 +1,288 @@
1
+ ---
2
+ name: terreno-data-fetching
3
+ description: >-
4
+ Use when implementing or debugging ANY network request, API call, or data
5
+ fetching in a Terreno app. Covers syncdb collection CRUD, generated request
6
+ hooks (store/apiSdk.ts) for non-synced routes, session auth, socket pushes,
7
+ feature flags, admin's host-bound fetch client, and SDK regeneration. Replaces direct fetch, axios, React Query, and SWR in
8
+ application code.
9
+ Lifecycle composition: Grow for data-flow shape, Pick for implementation, Roast
10
+ for independent network/cache behavior proof.
11
+ ---
12
+ # Terreno Data Fetching
13
+
14
+ **You MUST use this skill for ANY networking work in Terreno apps.** Everything goes
15
+ through `@terreno/syncdb`: `useQuery` / `useMutate` for synced collection CRUD, and
16
+ generated hooks on the `@terreno/syncdb/api` request client for every other route.
17
+ Never call `axios`, raw `fetch`, React Query, or SWR from application screens.
18
+ `@terreno/rtk` and Redux were removed in Terreno 58; upgrade old code with
19
+ `docs/how-to/migrate-rtk-to-syncdb.md`.
20
+
21
+ **Related skills:** `generate-sdk` (regenerate hooks after backend changes), `terreno-backend-api` (create backend routes), `building-terreno-apps` (screen integration).
22
+
23
+ ## Documentation
24
+
25
+ 1. Read `docs/reference/syncdb.md`, `docs/how-to/migrate-rtk-to-syncdb.md`, `docs/how-to/use-syncdb-correctly.md`, and auth/realtime explanation pages before changing fetch, cache, or session behavior.
26
+ 2. Implement against that design.
27
+ 3. Update those pages in the same slice with `update-docs`.
28
+ 4. Ship without matching docs is a failed slice.
29
+
30
+ ## References
31
+
32
+ ```
33
+ references/
34
+ auth-and-tokens.md SessionProvider/useSession, login/logout, token refresh, JWT
35
+ realtime.md Socket events, feature flags, sync status
36
+ sdk-customization.md Generated apiSdk.ts, ad-hoc useApiQuery/useApiMutation, cache tags
37
+ ```
38
+
39
+ ## When to Use
40
+
41
+ - Implementing API requests in screens or hooks
42
+ - Setting up authentication (login, logout, token refresh)
43
+ - Debugging network failures or stale data
44
+ - Connecting realtime updates (sockets, feature flags)
45
+ - Configuring API URLs and environment variables
46
+ - After any backend route or model change (regenerate SDK)
47
+
48
+ ## Core Rules
49
+
50
+ 1. **Use syncdb hooks** (`useQuery`, `useEntity`, `useWindowQuery`, `useMutate`) for synced collection CRUD.
51
+ 2. **Use generated hooks** from `store/apiSdk.ts` (`terreno-syncdb-codegen --api-out`) for every
52
+ non-synced route: `/auth/me`, modelRouter actions, AI, consents, announcements.
53
+ 3. **Never edit `store/apiSdk.ts` or `store/syncDbSdk.ts`** — regenerate with `generate-sdk`.
54
+ 4. **Ad-hoc routes not in OpenAPI** use `useApiQuery` / `useLazyApiQuery` / `useApiMutation`
55
+ from `@terreno/syncdb/react`. Prefer documenting the route in OpenAPI and regenerating.
56
+ 5. **Never use `axios` or raw `fetch`** in app code. Exceptions: SSE streams (`/gpt/prompt`)
57
+ authenticated with `authProvider.getToken()`, and framework-owned admin requests
58
+ (`@terreno/admin-frontend` binds `adminRequest` from `AdminProvider` `credentials` /
59
+ `getAuthHeaders` / `apiOrigin`).
60
+ 6. **Admin uses its own fetch client.** String-`_id` models with `adminBroadcast` use
61
+ windowed syncdb; ObjectId/API-only CRUD uses `useAdminModelApi`; admin RPC uses
62
+ `useAdminRpc`. Admin components take no `api` prop (removed in Terreno 58).
63
+
64
+ ## Quick Start
65
+
66
+ ### Synced collection read
67
+
68
+ ```tsx
69
+ import {useQuery} from "@terreno/syncdb/react";
70
+
71
+ const TodosScreen: React.FC = () => {
72
+ const todos = useQuery<Todo>("todos", {
73
+ filter: (todo) => !todo.completed,
74
+ });
75
+ };
76
+ ```
77
+
78
+ ### Synced collection write
79
+
80
+ ```tsx
81
+ import {generateMutationId} from "@terreno/syncdb";
82
+ import {useMutate} from "@terreno/syncdb/react";
83
+
84
+ const {create, update} = useMutate("todos");
85
+
86
+ const handleCreate = useCallback((): void => {
87
+ create({data: {_id: generateMutationId(), title: "New todo", completed: false}});
88
+ }, [create]);
89
+
90
+ const handleToggle = useCallback((id: string, completed: boolean): void => {
91
+ update({id, data: {completed: !completed}});
92
+ }, [update]);
93
+ ```
94
+
95
+ ### Non-synced route
96
+
97
+ ```tsx
98
+ import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
99
+
100
+ const {data: me, isLoading} = useGetAuthMeQuery(undefined, {skip: !userId});
101
+ const [updateMe, {isLoading: isSaving}] = usePatchAuthMeMutation();
102
+ await updateMe({name}).unwrap();
103
+ ```
104
+
105
+ ### Auth
106
+
107
+ ```tsx
108
+ import {useSession} from "@terreno/syncdb/auth";
109
+
110
+ const {userId, isAuthenticated, isLoading, logout} = useSession();
111
+ ```
112
+
113
+ See `./references/auth-and-tokens.md` for the full auth flow.
114
+
115
+ ## Client Setup
116
+
117
+ One `authProvider` feeds the sync client and the request client, so both send the same
118
+ token and refresh on a 401 the same way. Reference: `example-frontend/store/syncdb.ts`,
119
+ `store/apiClient.ts`, `app/_layout.tsx`.
120
+
121
+ ```tsx
122
+ // store/syncdb.ts
123
+ export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient));
124
+ export const syncDb = createSyncDb({authProvider, baseUrl, collections: [...SYNC_COLLECTIONS], name: "myapp", wipeOnSignOut: true});
125
+
126
+ // store/apiClient.ts
127
+ export const apiClient = createApiClient({authProvider, baseUrl, credentials: "include", headers: appHeaders});
128
+
129
+ // app/_layout.tsx — no Redux store
130
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
131
+ <ApiClientProvider client={apiClient}>
132
+ <SyncDbProvider client={syncDb}>{children}</SyncDbProvider>
133
+ </ApiClientProvider>
134
+ </SessionProvider>
135
+ ```
136
+
137
+ ## Environment Variables
138
+
139
+ | Variable | Purpose |
140
+ |----------|---------|
141
+ | `EXPO_PUBLIC_API_URL` | Backend URL for web dev (e.g. `http://localhost:4000`) |
142
+ | `EXPO_PUBLIC_DEV_API_PORT` | Dev API port when using Expo host URI (default `4000`) |
143
+ | `expo.extra.BASE_URL` | Production/staging API URL in `app.json` |
144
+
145
+ Base URL resolution priority (`baseUrl` from `@terreno/syncdb/config`):
146
+
147
+ 1. `expo.extra.BASE_URL`
148
+ 2. `EXPO_PUBLIC_API_URL`
149
+ 3. Expo dev server host + dev API port
150
+ 4. `http://localhost:4000`
151
+
152
+ Never put server secrets in `EXPO_PUBLIC_*` variables.
153
+
154
+ ## Error Handling
155
+
156
+ Request hooks expose `ApiError` (`@terreno/syncdb/api`) on the result:
157
+
158
+ ```tsx
159
+ const {error, isError} = useGetAuthMeQuery();
160
+
161
+ if (isError) {
162
+ // error.status, error.data.title, error.data.detail; status 0 = network error or timeout
163
+ console.error("Query failed", error);
164
+ }
165
+ ```
166
+
167
+ For mutations, use `.unwrap()`:
168
+
169
+ ```tsx
170
+ try {
171
+ await updateMe({name}).unwrap();
172
+ } catch (error) {
173
+ if (isApiError(error)) {
174
+ console.error("Mutation failed", error.data.title);
175
+ }
176
+ }
177
+ ```
178
+
179
+ Report every request failure once with `createApiClient({onError})` (skip Sentry when
180
+ `error.data.disableExternalErrorTracking` is `true`). Synced writes never throw: rejected
181
+ writes surface in `useConflicts()` and `useSyncStatus().failedCount`.
182
+
183
+ ## SDK Regeneration
184
+
185
+ After a backend API change, run the `generate-sdk` skill.
186
+
187
+ ```bash
188
+ # Backend running on :4000, then:
189
+ cd example-frontend && bun run sdk # store/apiSdk.ts (non-synced routes)
190
+ cd example-frontend && bun run sync-sdk # store/syncDbSdk.ts (synced collections)
191
+ ```
192
+
193
+ Triggers: custom routes, modelRouter actions, non-synced `modelRouter` routes, and OpenAPI
194
+ edits (`sdk`); synced schema or `sync` config changes (`sync-sdk`).
195
+
196
+ ## Decision Tree
197
+
198
+ ```
199
+ Need data in a Terreno app?
200
+ |-- Synced collection CRUD?
201
+ | \-- @terreno/syncdb hooks
202
+ |
203
+ |-- Non-synced API route?
204
+ | \-- useGet*/usePost* from store/apiSdk.ts
205
+ |
206
+ |-- Built-in admin?
207
+ | |-- String id + adminBroadcast -> windowed syncdb
208
+ | |-- Framework RPC -> AdminProvider adminRequest (useAdminRpc)
209
+ | \-- ObjectId / API-only CRUD -> useAdminModelApi
210
+ |
211
+ |-- Login/logout?
212
+ | \-- references/auth-and-tokens.md
213
+ |
214
+ |-- Server push / feature flags?
215
+ | \-- references/realtime.md (useSocketEvent, @terreno/syncdb/flags)
216
+ |
217
+ |-- Custom endpoint not in SDK?
218
+ | |-- Backend exists? -> generate-sdk first
219
+ | \-- Route outside OpenAPI? -> references/sdk-customization.md (useApiQuery)
220
+ |
221
+ |-- Backend doesn't exist?
222
+ | \-- terreno-backend-api, then generate-sdk
223
+ |
224
+ \-- Using axios/direct fetch/React Query?
225
+ \-- STOP — choose syncdb or a generated hook
226
+ ```
227
+
228
+ ## Common Mistakes
229
+
230
+ **Wrong: axios**
231
+
232
+ ```tsx
233
+ const result = await axios.get("/todos");
234
+ ```
235
+
236
+ **Right: syncdb for a synced collection**
237
+
238
+ ```tsx
239
+ const todos = useQuery<Todo>("todos");
240
+ ```
241
+
242
+ **Wrong: manual token in fetch**
243
+
244
+ ```tsx
245
+ fetch(url, {headers: {Authorization: `Bearer ${token}`}});
246
+ ```
247
+
248
+ **Right: a generated hook handles auth for a non-synced route**
249
+
250
+ ```tsx
251
+ const {data: profile} = useGetAuthMeQuery();
252
+ ```
253
+
254
+ **Wrong: editing store/apiSdk.ts**
255
+
256
+ ```tsx
257
+ // Adding a new endpoint by hand
258
+ ```
259
+
260
+ **Right: regenerate**
261
+
262
+ ```bash
263
+ cd example-frontend && bun run sdk
264
+ ```
265
+
266
+ **Wrong: refusing a synced write with 403 from a router hook**
267
+
268
+ ```typescript
269
+ throw new APIError({status: 403, title: "Not a member"}); // in preCreate — halts the client outbox
270
+ ```
271
+
272
+ **Right: 422, so syncdb nacks it as `validation`**
273
+
274
+ ```typescript
275
+ throw new APIError({status: 422, title: "Not a member"});
276
+ ```
277
+
278
+ **Wrong: rendering the empty state from `useQuery` before the stream bootstraps.** An empty array before bootstrap looks the same as an empty collection. Gate on the stream's `bootstrapped` cell. See `docs/how-to/use-syncdb-correctly.md` for this and the other pitfalls (cold-start cost per stream, dropped ack warnings, phantom creates, unresolved conflicts).
279
+
280
+ ## Example Invocations
281
+
282
+ - "How do I fetch todos?" → `useQuery("todos")` from `@terreno/syncdb/react`
283
+ - "How do I handle login?" → `betterAuthClient.signIn.email` then `useSession().refresh()`
284
+ - "Should I use React Query?" → No — syncdb for collections, generated `store/apiSdk.ts` hooks for non-synced routes
285
+ - "API calls return 401" → Check the shared `authProvider`; see auth-and-tokens.md
286
+ - "Old code imports `@terreno/rtk`" → `docs/how-to/migrate-rtk-to-syncdb.md`
287
+ - "Hooks are missing after backend change" → Run `generate-sdk` skill
288
+ - "How do I add a custom endpoint?" → Backend route + OpenAPI, then regenerate the SDK
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Terreno Data Fetching"
3
+ short_description: "syncdb collection hooks, generated request hooks, session auth, and socket events for Terreno apps — never axios or raw fetch"
4
+ default_prompt: "Use $terreno-data-fetching to implement API calls with @terreno/syncdb collection hooks, generated store/apiSdk.ts hooks, and session auth in a Terreno app."