@terreno/mcp 0.31.0 → 56.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 (116) hide show
  1. package/dist/bootstrap.d.ts.map +1 -1
  2. package/dist/bootstrap.js +8 -9
  3. package/dist/bootstrap.js.map +1 -1
  4. package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
  5. package/dist/docs/upgrades/56.0.0-beta.2.md +59 -0
  6. package/dist/docs/versioned/next/README.md +2 -0
  7. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
  8. package/dist/docs/versioned/next/how-to/README.md +2 -0
  9. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
  10. package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
  11. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
  12. package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
  13. package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
  14. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
  15. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
  16. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  17. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  18. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  19. package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
  20. package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
  21. package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
  22. package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
  23. package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
  24. package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
  25. package/dist/docs/versioned/next/reference/README.md +2 -0
  26. package/dist/docs/versioned/next/reference/comms.md +132 -0
  27. package/dist/docs/versioned/next/reference/environment-variables.md +9 -0
  28. package/dist/docs/versioned/next/reference/syncdb.md +456 -0
  29. package/dist/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
  30. package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
  31. package/dist/docs/versioned/next/tasks/comms-abstraction.md +29 -16
  32. package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
  33. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
  34. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
  35. package/dist/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  36. package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  37. package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
  38. package/dist/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
  39. package/dist/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
  40. package/package.json +3 -3
  41. package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
  42. package/src/docs/upgrades/56.0.0-beta.2.md +59 -0
  43. package/src/docs/versioned/next/README.md +2 -0
  44. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +21 -0
  45. package/src/docs/versioned/next/how-to/README.md +2 -0
  46. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
  47. package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +28 -50
  48. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -2
  49. package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +98 -15
  50. package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +63 -13
  51. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +65 -12
  52. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +9 -3
  53. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  54. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  55. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  56. package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
  57. package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
  58. package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
  59. package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
  60. package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
  61. package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
  62. package/src/docs/versioned/next/reference/README.md +2 -0
  63. package/src/docs/versioned/next/reference/comms.md +132 -0
  64. package/src/docs/versioned/next/reference/environment-variables.md +9 -0
  65. package/src/docs/versioned/next/reference/syncdb.md +456 -0
  66. package/src/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
  67. package/src/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
  68. package/src/docs/versioned/next/tasks/comms-abstraction.md +29 -16
  69. package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +17 -7
  70. package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +17 -7
  71. package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +3 -3
  72. package/src/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  73. package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  74. package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
  75. package/src/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
  76. package/src/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
  77. package/dist/__tests__/bootstrap.test.d.ts +0 -2
  78. package/dist/__tests__/bootstrap.test.d.ts.map +0 -1
  79. package/dist/__tests__/bootstrap.test.js +0 -299
  80. package/dist/__tests__/bootstrap.test.js.map +0 -1
  81. package/dist/__tests__/databaseQuery.test.d.ts +0 -2
  82. package/dist/__tests__/databaseQuery.test.d.ts.map +0 -1
  83. package/dist/__tests__/databaseQuery.test.js +0 -83
  84. package/dist/__tests__/databaseQuery.test.js.map +0 -1
  85. package/dist/__tests__/docsRoot.test.d.ts +0 -2
  86. package/dist/__tests__/docsRoot.test.d.ts.map +0 -1
  87. package/dist/__tests__/docsRoot.test.js +0 -59
  88. package/dist/__tests__/docsRoot.test.js.map +0 -1
  89. package/dist/__tests__/inferPackages.test.d.ts +0 -2
  90. package/dist/__tests__/inferPackages.test.d.ts.map +0 -1
  91. package/dist/__tests__/inferPackages.test.js +0 -35
  92. package/dist/__tests__/inferPackages.test.js.map +0 -1
  93. package/dist/__tests__/preload.d.ts +0 -2
  94. package/dist/__tests__/preload.d.ts.map +0 -1
  95. package/dist/__tests__/preload.js +0 -12
  96. package/dist/__tests__/preload.js.map +0 -1
  97. package/dist/__tests__/prompts.test.d.ts +0 -2
  98. package/dist/__tests__/prompts.test.d.ts.map +0 -1
  99. package/dist/__tests__/prompts.test.js +0 -295
  100. package/dist/__tests__/prompts.test.js.map +0 -1
  101. package/dist/__tests__/resources.test.d.ts +0 -2
  102. package/dist/__tests__/resources.test.d.ts.map +0 -1
  103. package/dist/__tests__/resources.test.js +0 -352
  104. package/dist/__tests__/resources.test.js.map +0 -1
  105. package/dist/__tests__/search.test.d.ts +0 -2
  106. package/dist/__tests__/search.test.d.ts.map +0 -1
  107. package/dist/__tests__/search.test.js +0 -163
  108. package/dist/__tests__/search.test.js.map +0 -1
  109. package/dist/__tests__/tools.test.d.ts +0 -2
  110. package/dist/__tests__/tools.test.d.ts.map +0 -1
  111. package/dist/__tests__/tools.test.js +0 -540
  112. package/dist/__tests__/tools.test.js.map +0 -1
  113. package/dist/__tests__/upgradeGuide.test.d.ts +0 -2
  114. package/dist/__tests__/upgradeGuide.test.d.ts.map +0 -1
  115. package/dist/__tests__/upgradeGuide.test.js +0 -22
  116. package/dist/__tests__/upgradeGuide.test.js.map +0 -1
@@ -0,0 +1,303 @@
1
+ # @terreno/syncdb API inventory (temporary)
2
+
3
+ Working document for the RTK → syncdb migration. Lists every public export from the three package entry points, with source locations verified against `syncdb/src/`.
4
+
5
+ ## M5 — Does `bun run sdk` still work?
6
+
7
+ **Yes.** The example app still generates RTK Query hooks from the backend OpenAPI spec:
8
+
9
+ - `example-frontend/package.json:73` — `"sdk": "bun scripts/generate-sdk.ts && bun biome check --write scripts/generate-sdk.ts"`
10
+ - `example-frontend/scripts/generate-sdk.ts:22` — runs `@rtk-query/codegen-openapi` against `openapi-config.ts`
11
+ - `example-frontend/openapi-config.ts:11` — output is `./store/openApiSdk.ts`
12
+
13
+ **What it generates now (post-migration):** typed RTK Query hooks for **non-synced** routes — Better Auth, `GET /auth/me`, admin, AI explorer, version config, feature-flag configuration, and any custom REST endpoints. Synced collections (e.g. todos) are **not** read or written through generated hooks; they use `@terreno/syncdb` hooks instead. `example-frontend/store/sdk.ts` still wraps `openApiSdk.ts` with tag enhancements and realtime wiring for endpoints that remain on RTK (todos realtime config is legacy/unused by the syncdb screen).
14
+
15
+ ## M7 — Local-first only vs opt-in?
16
+
17
+ **Local-first only — there is no server-first mode and no opt-in flag.**
18
+
19
+ The on-device TinyBase store is always the UI source of truth; reads come from local state, writes apply optimistically, and the server reconciles asynchronously.
20
+
21
+ - `syncdb/src/index.ts:4-7` — package doc: "reads come from the local store, writes apply optimistically… the server is asynchronous reconciliation"
22
+ - `syncdb/README.md:3-4` — "TinyBase MergeableStore … is the UI's source of truth"
23
+ - `syncdb/src/client.ts:60-151` (`SyncDbConfig`) — no `mode`, `serverFirst`, or similar option
24
+
25
+ ---
26
+
27
+ ## `@terreno/syncdb` (`syncdb/src/index.ts`)
28
+
29
+ ### Client factory and constants
30
+
31
+ | Symbol | Kind | Description | Source |
32
+ |--------|------|-------------|--------|
33
+ | `createSyncDb` | `function (config: SyncDbConfig): SyncDb` | Assemble the local-first client | `client.ts:281` |
34
+ | `DEFAULT_RECONCILE_INTERVAL_MS` | `const` (5 min) | Default periodic reconcile interval | `client.ts:38` |
35
+ | `DEFAULT_SEQ_JUMP_RECONCILE_MIN_INTERVAL_MS` | `const` (30s) | Seq-jump reconcile rate limit | `client.ts:41` |
36
+ | `DEFAULT_TOMBSTONE_RETENTION_MS` | `const` (90 days) | Client tombstone compaction window | `client.ts:44` |
37
+ | `DEFAULT_START_AUTH_RETRY_ATTEMPTS` | `const` (3) | `start()` auth-resolution retries | `client.ts:55` |
38
+ | `DEFAULT_START_AUTH_RETRY_DELAY_MS` | `const` (250) | Delay between `start()` auth retries | `client.ts:56` |
39
+
40
+ ### `SyncDbConfig` fields
41
+
42
+ | Field | Type | Default | Description | Source |
43
+ |-------|------|---------|-------------|--------|
44
+ | `name` | `string` | — (required) | Persisted database name | `client.ts:62` |
45
+ | `collections` | `string[]` | — (required) | Collections to sync | `client.ts:64` |
46
+ | `authProvider` | `AuthProvider` | — (required) | Token/user/auth-change surface | `client.ts:65` |
47
+ | `baseUrl` | `string?` | — | Server origin; required unless `transport` + `httpChannel` injected | `client.ts:67` |
48
+ | `transport` | `SyncTransport?` | socket transport from `baseUrl` | Transport override (tests) | `client.ts:69` |
49
+ | `httpChannel` | `HttpChannel?` | from `baseUrl` | HTTP channel override | `client.ts:71` |
50
+ | `persisterFactory` | `PersisterFactory?` | platform default | Persister override | `client.ts:73` |
51
+ | `keyProvider` | `KeyProvider?` | server-derived via `/sync/key` | Web encryption key provider | `client.ts:75` |
52
+ | `idbGetImpl` | test hook | — | IndexedDB read override (tests) | `client.ts:82` |
53
+ | `idbSetImpl` | test hook | — | IndexedDB write override (tests) | `client.ts:83` |
54
+ | `reconcileIntervalMs` | `number?` | `DEFAULT_RECONCILE_INTERVAL_MS` (5 min); `0` disables | Periodic reconcile timer | `client.ts:85` |
55
+ | `seqJumpReconcileMinIntervalMs` | `number?` | `DEFAULT_SEQ_JUMP_RECONCILE_MIN_INTERVAL_MS` | Per-stream seq-jump rate limit | `client.ts:87` |
56
+ | `now` | `() => number?` | `Date.now` | Injectable clock | `client.ts:89` |
57
+ | `random` | `() => number?` | `Math.random` | Injectable RNG for backoff jitter | `client.ts:91` |
58
+ | `debug` | `boolean \| SyncDebugLogOptions?` | `false` (off) | In-memory debug event log | `client.ts:98` |
59
+ | `onAuthRequired` | `() => void?` | — | Fires once per auth-pause episode | `client.ts:104` |
60
+ | `wipeOnSignOut` | `boolean?` | `false` | `signOut()` also wipes local data | `client.ts:111` |
61
+ | `batchSize` | `number?` | `50` (server caps at 100) | Max mutations per batched drain | `client.ts:113` |
62
+ | `haltQueueOnConflict` | `boolean?` | `false` | Conflict halts entire drain vs per-entity | `client.ts:119` |
63
+ | `onDecryptFailure` | `() => void?` | wipe + re-bootstrap (with `console.warn`) | Override decrypt-failure recovery | `client.ts:129` |
64
+ | `tombstoneRetentionMs` | `number?` | `DEFAULT_TOMBSTONE_RETENTION_MS`; `0` disables | Client tombstone compaction after reconcile | `client.ts:138` |
65
+ | `startAuthRetryAttempts` | `number?` | `DEFAULT_START_AUTH_RETRY_ATTEMPTS`; `1` disables | `start()` user-id resolution attempts | `client.ts:148` |
66
+ | `startAuthRetryDelayMs` | `number?` | `DEFAULT_START_AUTH_RETRY_DELAY_MS` | Delay between those attempts | `client.ts:150` |
67
+
68
+ ### `SyncDb` methods and properties
69
+
70
+ | Member | Signature / type | Description | Source |
71
+ |--------|------------------|-------------|--------|
72
+ | `store` | `readonly SyncStore` | Local-first entity store | `client.ts:193` |
73
+ | `outbox` | `readonly Outbox` | Durable mutation outbox | `client.ts:195` |
74
+ | `debug` | `readonly SyncDebugLog \| undefined` | Debug log when `debug` enabled | `client.ts:270` |
75
+ | `start` | `() => Promise<void>` | Resolve user, persist, connect, bootstrap | `client.ts:202` |
76
+ | `stop` | `() => Promise<void>` | Disconnect, stop persistence, clear timers | `client.ts:204` |
77
+ | `goOffline` | `() => void` | Simulated outage (local writes continue) | `client.ts:212` |
78
+ | `goOnline` | `() => Promise<void>` | End simulated outage | `client.ts:218` |
79
+ | `mutate` | `(args: MutateArgs) => {mutationId, id}` | Optimistic write + enqueue + replay | `client.ts:224` |
80
+ | `reconcile` | `() => Promise<void>` | HTTP snapshot catch-up all streams | `client.ts:226` |
81
+ | `forceResync` | `() => Promise<ForceResyncResult>` | Purge streams and re-bootstrap from 0 | `client.ts:233` |
82
+ | `replayOutbox` | `() => Promise<void>` | Drain queued mutations now | `client.ts:235` |
83
+ | `resolveConflict` | `({mutationId, strategy}) => void` | Apply `useServer` or `keepMine` | `client.ts:237` |
84
+ | `retryFailed` | `({entityId}) => void` | Re-enable blocked entity queue | `client.ts:244` |
85
+ | `signOut` | `() => Promise<void>` | Explicit teardown; optional wipe | `client.ts:254` |
86
+ | `getSyncStatus` | `() => SyncStatus` | Aggregate sync state snapshot | `client.ts:256` |
87
+ | `onStatusChange` | `(callback) => () => void` | Subscribe to connectivity/syncing changes | `client.ts:264` |
88
+
89
+ ### Supporting client types
90
+
91
+ | Symbol | Kind | Source |
92
+ |--------|------|--------|
93
+ | `MutateArgs` | `interface` `{collection, operation, id?, data?}` | `client.ts:182` |
94
+ | `ForceResyncSkipReason` | `type` union | `client.ts:162` |
95
+ | `ForceResyncResult` | `interface` | `client.ts:170` |
96
+
97
+ ### Protocol, status, and conflict types (`types.ts`)
98
+
99
+ | Symbol | Kind | Source |
100
+ |--------|------|--------|
101
+ | `SyncMutationOperation` | `type` `"create" \| "update" \| "delete"` | `types.ts:9` |
102
+ | `SyncDelta` | `interface` | `types.ts:12` |
103
+ | `SyncMutateRequest` | `interface` | `types.ts:35` |
104
+ | `SyncAck` | `interface` | `types.ts:49` |
105
+ | `SyncNackCode` | `type` | `types.ts:63` |
106
+ | `SyncNack` | `interface` | `types.ts:66` |
107
+ | `SyncMutateBatchRequest` | `interface` | `types.ts:94` |
108
+ | `SyncMutateBatchResult` | `type` | `types.ts:107` |
109
+ | `SyncMutateBatchResponse` | `interface` | `types.ts:116` |
110
+ | `SyncSnapshotEntity` | `interface` | `types.ts:121` |
111
+ | `SyncEntitiesResponse` | `interface` | `types.ts:129` |
112
+ | `SyncSnapshotResponse` | `interface` | `types.ts:134` |
113
+ | `SyncStreamInfo` | `interface` | `types.ts:156` |
114
+ | `OutboxStatus` | `type` | `types.ts:164` |
115
+ | `OutboxMutation` | `interface` | `types.ts:167` |
116
+ | `SyncConflict` | `interface` | `types.ts:186` |
117
+ | `ConflictResolutionStrategy` | `type` `"useServer" \| "keepMine"` | `types.ts:206` |
118
+ | `SyncCollectionStatus` | `interface` | `types.ts:213` |
119
+ | `SyncStatus` | `interface` | `types.ts:220` |
120
+ | `AuthProvider` | `interface` | `types.ts:269` |
121
+
122
+ ### Store / outbox / wipe
123
+
124
+ | Symbol | Kind | Source |
125
+ |--------|------|--------|
126
+ | `listConflicts` | `function` | `mutations/conflicts.ts:71` |
127
+ | `generateMutationId` | `function` | `mutations/outbox.ts` (re-export `index.ts:75`) |
128
+ | `Outbox` | `type` | `mutations/outbox.ts` |
129
+ | `SyncStore` | `type` | `storage/store.ts` |
130
+ | `OUTBOX_TABLE` | `const` | `storage/types.ts` |
131
+ | `SyncEntity` | `type` | `storage/types.ts` |
132
+ | `wipeLocalData` | `async function` | `storage/wipe.ts:19` |
133
+
134
+ ### Auth adapter
135
+
136
+ | Symbol | Kind | Source |
137
+ |--------|------|--------|
138
+ | `betterAuthAdapter` | `function` | `auth/betterAuthAdapter.ts:39` |
139
+ | `DEFAULT_AUTH_POLL_INTERVAL_MS` | `const` (5000) | `auth/betterAuthAdapter.ts:5` |
140
+ | `BetterAuthAdapterOptions` | `interface` | `auth/betterAuthAdapter.ts:7` |
141
+ | `BetterAuthClientLike` | `type` | `auth/types.ts` |
142
+ | `BetterAuthGetSessionResult` | `type` | `auth/types.ts` |
143
+ | `BetterAuthSessionAtomLike` | `type` | `auth/types.ts` |
144
+ | `BetterAuthSessionDataLike` | `type` | `auth/types.ts` |
145
+ | `BetterAuthSessionLike` | `type` | `auth/types.ts` |
146
+ | `BetterAuthUserLike` | `type` | `auth/types.ts` |
147
+
148
+ ### Encryption and key providers
149
+
150
+ | Symbol | Kind | Source |
151
+ |--------|------|--------|
152
+ | `AES_GCM_ENVELOPE_VERSION` | `const` | `crypto/aesGcmCodec.ts` |
153
+ | `createAesGcmCodec` | `function` | `crypto/aesGcmCodec.ts` |
154
+ | `PayloadIntegrityError` | `class` | `crypto/aesGcmCodec.ts` |
155
+ | `UnknownEnvelopeVersionError` | `class` | `crypto/aesGcmCodec.ts` |
156
+ | `identityCodec` | `const` | `crypto/identityCodec.ts` |
157
+ | `createKeyProviderCodec` | `function` | `crypto/keyProviders.ts` |
158
+ | `createLocalKeyProvider` | `function` | `crypto/keyProviders.ts` |
159
+ | `createServerKeyProvider` | `function` | `crypto/keyProviders.ts` |
160
+ | `DEFAULT_KEY_CACHE_DB_NAME` | `const` | `crypto/keyProviders.ts` |
161
+ | `KeyProvider` | `interface` | `crypto/types.ts` |
162
+ | `PayloadCodec` | `interface` | `crypto/types.ts` |
163
+
164
+ ### Persisters
165
+
166
+ | Symbol | Kind | Source |
167
+ |--------|------|--------|
168
+ | `createDefaultPersisterFactory` | `function` | `persisters/defaultPersisterFactory.ts` |
169
+ | `createEncryptedIndexedDbPersister` | `function` | `persisters/encryptedIndexedDbPersister.ts` |
170
+ | `clearMemoryPersisterData` | `function` | `persisters/memoryPersister.ts` |
171
+ | `createMemoryPersister` | `function` | `persisters/memoryPersister.ts` |
172
+ | `memoryPersisterFactory` | `function` | `persisters/memoryPersister.ts` |
173
+ | `DefaultPersisterFactoryConfig` | `type` | `persisters/types.ts` |
174
+ | `PersisterFactory` | `type` | `persisters/types.ts` |
175
+
176
+ ### Transports and HTTP channel
177
+
178
+ | Symbol | Kind | Source |
179
+ |--------|------|--------|
180
+ | `AuthRequiredError` | `class` | `sync/httpChannel.ts` |
181
+ | `createHttpChannel` | `function` | `sync/httpChannel.ts` |
182
+ | `FetchLike` | `type` | `sync/httpChannel.ts` |
183
+ | `FetchSnapshotPageArgs` | `type` | `sync/httpChannel.ts` |
184
+ | `HttpChannel` | `interface` | `sync/httpChannel.ts` |
185
+ | `HttpChannelConfig` | `interface` | `sync/httpChannel.ts` |
186
+ | `createSocketTransport` | `function` | `sync/socketTransport.ts` |
187
+ | `SocketTransportConfig` | `interface` | `sync/socketTransport.ts` |
188
+ | `DEFAULT_BATCH_SIZE` | `const` | `sync/transport.ts` |
189
+ | `DEFAULT_MUTATION_TIMEOUT_MS` | `const` | `sync/transport.ts` |
190
+ | `SendMutationBatchResult` | `type` | `sync/transport.ts` |
191
+ | `SendMutationResult` | `type` | `sync/transport.ts` |
192
+ | `SyncTransport` | `interface` | `sync/transport.ts` |
193
+ | `TransportStatus` | `interface` | `sync/transport.ts` |
194
+
195
+ ### Debug log types
196
+
197
+ | Symbol | Kind | Source |
198
+ |--------|------|--------|
199
+ | `SyncDebugDirection` | `type` | `debug/debugLog.ts:43` |
200
+ | `SyncDebugEvent` | `interface` | `debug/debugLog.ts:46` |
201
+ | `SyncDebugEventType` | `type` | `debug/debugLog.ts:27` |
202
+ | `SyncDebugLog` | `interface` | `debug/debugLog.ts` |
203
+ | `SyncDebugLogOptions` | `interface` | `debug/debugLog.ts` |
204
+ | `SyncDebugRecordInput` | `interface` | `debug/debugLog.ts` |
205
+ | `SyncDebugSnapshot` | `interface` | `debug/debugLog.ts` |
206
+ | `SyncDebugStats` | `interface` | `debug/debugLog.ts` |
207
+
208
+ **Not exported (internal):** `resolveConflict` (mutation writer), `writeConflict`, cursor mutators, IndexedDB helpers, debug broadcast bridge — use `client.resolveConflict` / `useConflicts().resolve` instead.
209
+
210
+ ---
211
+
212
+ ## `@terreno/syncdb/react` (`syncdb/src/react/index.ts` → hooks + provider)
213
+
214
+ | Symbol | Kind | Description | Source |
215
+ |--------|------|-------------|--------|
216
+ | `SyncDbProvider` | `React.FC<SyncDbProviderProps>` | Provides client to descendants | `provider.tsx:13` |
217
+ | `SyncDbProviderProps` | `interface` `{client, children}` | Provider props | `provider.tsx:7` |
218
+ | `useSyncDbClient` | `() => SyncDb` | Access client from context | `provider.tsx:18` |
219
+ | `useEntity` | `<T>(collection, id) => UseEntityResult<T>` | Subscribe to one entity | `hooks.ts:117` |
220
+ | `UseEntityResult` | `interface` `{data, deleted, seq, isPending}` | `useEntity` return shape | `hooks.ts:105` |
221
+ | `useQuery` | `<T>(collection, options?) => T[]` | Subscribe to collection (decoded data) | `hooks.ts:170` |
222
+ | `UseQueryOptions` | `interface` `{filter?, sort?, includeDeleted?}` | Query options | `hooks.ts:149` |
223
+ | `useEntityIds` | `<T>(collection, options?) => string[]` | Stable id list for virtualization | `hooks.ts:221` |
224
+ | `useMutate` | `(collection) => UseMutateResult` | `{create, update, remove}` | `hooks.ts:266` |
225
+ | `UseMutateResult` | `interface` | Mutation helpers | `hooks.ts:253` |
226
+ | `useSyncStatus` | `() => SyncStatus` | Reactive aggregate status | `hooks.ts:296` |
227
+ | `useConflicts` | `() => UseConflictsResult` | `{conflicts, resolve}` | `hooks.ts:328` |
228
+ | `UseConflictsResult` | `interface` | Conflict hook return | `hooks.ts:320` |
229
+ | `useSyncDebugLog` | `() => UseSyncDebugLogResult` | Debug log subscription | `hooks.ts:379` |
230
+ | `UseSyncDebugLogResult` | `interface` `{enabled, events, stats, log, clear}` | Debug hook return | `hooks.ts:355` |
231
+
232
+ ---
233
+
234
+ ## `@terreno/syncdb/testing` (`syncdb/src/testing/index.ts`)
235
+
236
+ | Symbol | Kind | Description | Source |
237
+ |--------|------|-------------|--------|
238
+ | `createFakeTransport` | `() => FakeTransport` | In-memory transport double | `testing/fakeTransport.ts:74` |
239
+ | `FakeTransport` | `interface` extends `SyncTransport` | Test controls + recording | `testing/fakeTransport.ts:32` |
240
+ | `FakeMutationResponder` | `type` | Single-mutation responder | `testing/fakeTransport.ts:22` |
241
+ | `FakeBatchResponder` | `type` | Batch responder | `testing/fakeTransport.ts:27` |
242
+
243
+ ---
244
+
245
+ ## Reference migration (`example-frontend`)
246
+
247
+ Before paths are from `origin/master`; after paths are on the current branch.
248
+
249
+ ### (a) Store setup
250
+
251
+ | | Path | What changed |
252
+ |---|------|--------------|
253
+ | **Before** | `example-frontend/store/index.ts` (master) | `generateAuthSlice(terrenoApi)`, `createOfflineMiddleware`, Redux persist, RTK Query reducer |
254
+ | **After** | `example-frontend/store/index.ts` | `generateBetterAuthSlice`, RTK only for non-synced API + app state |
255
+ | **After** | `example-frontend/store/syncdb.ts` | `createSyncDb({authProvider: betterAuthAdapter(...), collections: ["todos"], ...})` |
256
+
257
+ ### (b) Provider wiring
258
+
259
+ | | Path | What changed |
260
+ |---|------|--------------|
261
+ | **Before** | `example-frontend/app/_layout.tsx` (master) | Redux `Provider` only; todos screen uses RTK hooks directly |
262
+ | **After** | `example-frontend/app/(tabs)/index.tsx` | `SyncDbProvider client={syncDb}` wraps `SyncTodosScreen` |
263
+ | **After** | `example-frontend/app/_layout.tsx` | `syncDb.start()` / `stop()` on login; `SyncDbProvider` + `SyncHealthToast` at root when `userId` set |
264
+
265
+ ### (c) Reads
266
+
267
+ | | Path | What changed |
268
+ |---|------|--------------|
269
+ | **Before** | `example-frontend/app/(tabs)/index.tsx` (master) | `useGetTodosQuery({}, {skip: !userId})`, `isLoading`, `refetch` |
270
+ | **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useEntityIds<SyncTodo>("todos", {filter, sort})` + per-row `useEntity("todos", id)` |
271
+
272
+ ### (d) Writes
273
+
274
+ | | Path | What changed |
275
+ |---|------|--------------|
276
+ | **Before** | `example-frontend/app/(tabs)/index.tsx` (master) | `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`, `.unwrap()`, per-item `isUpdating` spinners |
277
+ | **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useMutate("todos")` → `create({data})`, `update({id, data})`, `remove({id})`; gated on `useSyncDbReady()` |
278
+
279
+ ### (e) Conflict UI
280
+
281
+ | | Path | What changed |
282
+ |---|------|--------------|
283
+ | **Before** | master todos screen | `useServerStatus` undismissed conflicts (RTK offline slice) |
284
+ | **After** | `example-frontend/components/SyncConflictsController.tsx` | App-wide conflict sheet controller |
285
+ | **After** | `example-frontend/app/_layout.tsx` | `ConflictSheet` via `SyncHealthToast` `renderConflictsModal` |
286
+ | **After** | `example-frontend/components/SyncTodosScreen.tsx` | `SyncStatusBanner` `onOpenConflicts` → `openConflicts("todos")` |
287
+
288
+ ### (f) Sync-status UI
289
+
290
+ | | Path | What changed |
291
+ |---|------|--------------|
292
+ | **Before** | master todos screen | `useServerStatus`, `OfflineBanner`, request `isLoading` / `isFetching` / `RefreshControl` |
293
+ | **After** | `example-frontend/components/SyncTodosScreen.tsx` | `useSyncStatus()` + `SyncStatusBanner` |
294
+ | **After** | `example-frontend/components/SyncHealthToast.tsx` | Persistent toasts for conflicts, failures, backlog |
295
+ | **After** | `example-frontend/components/syncHealthSignals.ts` | Toast signal computation |
296
+
297
+ ### Not covered by the reference migration
298
+
299
+ - **Auth migration details** — Better Auth session atom bridge in `store/syncdb.ts`; login still uses `@terreno/rtk` Better Auth slice (documented in migration guide §7).
300
+ - **Feature flags** — OpenFeature bridge in `_layout.tsx` (`OpenFeatureBridge` + `useTerrenoFeatureFlags`); no per-screen flag gating todos (syncdb-only).
301
+ - **Other screens** — Profile, admin, AI explorer, consent flow still use generated RTK hooks (`store/sdk.ts`).
302
+ - **SDK realtime config for todos** — `TODO_REALTIME_ENDPOINTS` in `sdk.ts` remains but is unused by `SyncTodosScreen`.
303
+ - **Debug tooling** — `example-frontend/app/syncdb-debug.tsx` (`useSyncDebugLog`).
@@ -0,0 +1,252 @@
1
+ # Implementation Plan: @terreno/syncdb-codegen
2
+
3
+ **Status:** Draft
4
+ **Branch:** cursor/ip-syncdb-codegen-41d6
5
+ **Owner:**
6
+ **Created:** 2026-07-08
7
+
8
+ ## Goal
9
+
10
+ Ship `@terreno/syncdb-codegen`: a small CLI (runnable as an npm bin under bun and buildable as a standalone compiled bun binary) that reads a backend OpenAPI spec and generates a static, typed set of syncdb hooks — the local-first analogue of the existing `@rtk-query/codegen-openapi` flow (`bun run sdk` → `store/openApiSdk.ts`).
11
+
12
+ Today syncdb consumers hand-write both the entity types and the collection strings:
13
+
14
+ ```typescript
15
+ // example-frontend/components/SyncTodosScreen.tsx — all manual today
16
+ interface SyncTodo { _id: string; title?: string; completed?: boolean; created?: string; }
17
+ const todos = useQuery<SyncTodo>("todos", {sort: sortByCreatedDesc});
18
+ const {update, remove} = useMutate("todos"); // update/remove payloads are Record<string, unknown>
19
+ ```
20
+
21
+ After this IP, a generated `store/syncDbSdk.ts` provides one hook per operation with the exact names RTK codegen would produce for the same routes — `useGetTodosQuery` (list query), `useGetTodosByIdQuery` (read), `usePostTodosMutation` (create), `usePatchTodosByIdMutation` (update/patch), `useDeleteTodosByIdMutation` (delete) — with payload types derived from the same OpenAPI schemas RTK codegen uses (`Todo`, `CreateTodoBody`, `UpdateTodoBody`), plus a `SYNC_COLLECTIONS` constant that feeds `createSyncDb`. Migrating a screen from RTK to syncdb becomes (mostly) an import-path change. Custom hooks are supported the same way RTK supports custom endpoints: the generated file is never edited, and consumers extend it in a sibling file using the same public factory the generated code uses.
22
+
23
+ ## Non-Goals
24
+
25
+ - Covering the whole syncdb surface. Only the five CRUD operations (list query, read, create, update/patch, delete) get generated per-collection hooks. `useSyncStatus`, `useConflicts`, `useSyncDbClient`, and `SyncDbProvider` are global (not per-collection) and stay as direct imports from `@terreno/syncdb/react`.
26
+ - Full RTK return-shape emulation. Hook **names and call sites** match RTK; return shapes are syncdb-native where the semantics differ (no `isLoading`/`error` async state on mutations — writes are synchronous local applies plus a durable outbox; sync state lives in `useSyncStatus`). The mapping is documented in the generated file header and the migration guide.
27
+ - Runtime schema validation of mutation payloads (types are compile-time only, matching RTK codegen).
28
+ - Publishing per-platform compiled binaries to npm or GitHub releases (the compiled binary is a local/CI build target for now; `@terreno/syncdb` itself is not in `publish-on-tag.yml` yet either).
29
+ - Watch mode, incremental generation, or multi-output-file support.
30
+ - Generating Redux integration or migrating non-synced endpoints — per `docs/how-to/migrate-rtk-to-syncdb.md`, custom routes stay on RTK.
31
+
32
+ ## Decisions
33
+
34
+ Options were considered where the design was genuinely open; each row records the recommendation this plan is built on. Rejected alternatives are listed so review can overrule them cheaply.
35
+
36
+ | Question | Decision |
37
+ |----------|----------|
38
+ | **How does codegen know which collections are synced?** OpenAPI does not mark sync today; the server-side `syncRegistry` (`api/src/sync/registry.ts`) is in-memory only. Options: (a) emit an `x-terreno-sync` vendor extension on the list path from `modelRouter` when `options.sync` is set; (b) require an explicit `--collections` CLI arg; (c) add a `GET /sync/config` manifest endpoint. | **(a) with (b) as override.** The extension is a ~10-line change in `@terreno/api`'s list OpenAPI middleware, keeps the spec the single input (same as RTK codegen), and can't drift from the backend. `--collections` remains as an explicit filter/override for apps that sync a subset. (c) adds server surface for no extra information and is rejected. |
39
+ | **How is "retries vs not" expressed?** RTK disables retries per-endpoint via `extraOptions: {maxRetries: 0}` (see `rtk/src/authSlice.ts`). syncdb retries live in the replay engine: `MAX_ERROR_NACK_ATTEMPTS = 5` with exponential backoff in `syncdb/src/sync/replayCoordinator.ts`, not configurable per mutation. Options: (a) plumb an optional per-mutation `maxAttempts` through `client.mutate` → outbox row → replay coordinator, and expose `retries` in the codegen config; (b) codegen-only option that is silently ignored until the engine supports it. | **(a).** Small, contained change: `MutateArgs.maxAttempts?`, one optional outbox cell, one `??` in the replay coordinator's error-nack branch. `retries: false` maps to `maxAttempts: 1` (fail fast, like RTK's `maxRetries: 0`), a number maps directly, and omitted keeps the engine default of 5. (b) would generate config that lies. Note syncdb mutations are idempotent via `mutationId`, so unlike RTK the motivation is fail-fast UX (surface a terminal failure immediately), not duplicate-write safety. |
40
+ | **Config surface: flags only, or a config file?** The binary should "take a few args", but per-collection overrides (retries, exclusions) don't map well to flags. | **Flags for the common path, optional JSON config for overrides.** `--schema`, `--out`, `--collections` cover the RTK-equivalent happy path. `--config ./syncdb-codegen.json` adds `{overrides: {<collection>: {retries?: boolean \| number}}, exportName?, sdkImportPath?}`. JSON (not TS) because a compiled bun binary cannot reliably import arbitrary consumer TypeScript at runtime; RTK's TS `ConfigFile` needs the `tsx`/`TS_NODE_PROJECT` dance we currently work around in `example-frontend/scripts/generate-sdk.ts`. |
41
+ | **How are TS types emitted from OpenAPI schemas?** Options: (a) a small hand-rolled emitter for the subset `mongoose-to-swagger` actually produces (objects, primitives, enums as string unions, arrays, `$ref`); (b) depend on `openapi-typescript` or oazapfts. | **(a).** The @terreno/api spec is a narrow, known dialect; a ~150-line emitter with snapshot tests keeps the binary dependency-free and small. If the emitter grows past that dialect, revisit (b). |
42
+ | **Where does the shared hook factory live?** Generated code and custom hooks must be built the same way (the RTK parallel: generated `injectEndpoints` and app `sdk.ts` both build on `emptySplitApi`). Options: (a) new `createCollectionHooks` export in `@terreno/syncdb/react`; (b) inline the factory into every generated file; (c) a runtime export of `@terreno/syncdb-codegen`. | **(a).** One implementation, versioned with the hooks it wraps, and consumers already depend on `@terreno/syncdb`. (b) duplicates logic into generated output; (c) would make the codegen package a runtime dependency of apps, which it should never be. |
43
+ | **Hook granularity and naming.** Options: (a) one grouped `useTodosMutate()` returning `{create, update, remove}`; (b) one hook per operation with RTK codegen's exact naming scheme (`use{Method}{Path}Query/Mutation`); (c) per-operation hooks with friendlier names (`useCreateTodo`). | **(b) — per-operation, RTK names.** Generated per collection: `useGetTodosQuery`, `useGetTodosByIdQuery`, `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`. Name derivation is the same deterministic pascal-case of the route path RTK uses (no singularization heuristics). This makes RTK→syncdb migration an import swap. Two consequences are accepted: (1) name **collisions with `openApiSdk.ts`** are intentional — during migration both files export e.g. `useGetTodosQuery`, and the import path picks the data source; imports must never be mixed via a barrel that re-exports both. (2) "update" and "patch" are the **same hook**: modelRouter only exposes PATCH (merge semantics) and syncdb's update is a merge, so `usePatchTodosByIdMutation` is the update hook — there is no separate PUT/replace hook to generate. |
44
+ | **Mutation hook return shape.** RTK mutation hooks return `[trigger, {isLoading, error, ...}]` and triggers return promises with `.unwrap()`. syncdb writes are synchronous local applies; there is no per-call async state. | **One-element tuple `[trigger]`.** Keeps the RTK call site (`const [createTodo] = usePostTodosMutation()`) working unchanged; the trigger returns `{mutationId, id}` synchronously instead of a promise, so `.unwrap()`/`isLoading` usages are deleted during migration (the compiler flags them). Pending/conflict state is per-entity (`isPending` on reads) and global (`useSyncStatus`), not per-call. |
45
+ | **Distribution.** "Compiled bun binary" vs npm reality. | **Both, npm-bin first.** The package ships `"bin": {"terreno-syncdb-codegen": "./dist/cli.js"}` with a `#!/usr/bin/env bun` shebang (the `@terreno/mcp` pattern), plus a `build:binary` script running `bun build --compile src/cli.ts --outfile dist/terreno-syncdb-codegen` (the `example-backend/Dockerfile` pattern) for standalone use. Publishing platform binaries is future work. |
46
+
47
+ ## Architecture
48
+
49
+ ```
50
+ example-backend example-frontend
51
+ modelRouter("/todos", Todo, {sync: {...}}) bun run sync-sdk
52
+ └─ list OpenAPI middleware emits └─ terreno-syncdb-codegen
53
+ x-terreno-sync on GET /todos --schema http://localhost:4000/openapi.json
54
+ │ --out ./store/syncDbSdk.ts
55
+ ▼ │
56
+ /openapi.json ──── fetch/read ────────────────────────┘
57
+ │ emits
58
+ ▼
59
+ store/syncDbSdk.ts (generated, never edited)
60
+ ├─ interface Todo / CreateTodoBody / UpdateTodoBody
61
+ ├─ export const SYNC_COLLECTIONS = ["todos"] as const
62
+ └─ export const {useGetTodosQuery, useGetTodosByIdQuery,
63
+ usePostTodosMutation, usePatchTodosByIdMutation,
64
+ useDeleteTodosByIdMutation} =
65
+ createCollectionHooks<...>({collection: "todos"})
66
+ │ extended by (custom hooks)
67
+ ▼
68
+ store/syncSdk.ts (hand-written, optional)
69
+ └─ createCollectionHooks<LocalNote>({collection: "notes"})
70
+ ```
71
+
72
+ Three packages change:
73
+
74
+ 1. **`@terreno/syncdb`** (runtime): `createCollectionHooks` factory in `src/react/`, plus per-mutation `maxAttempts` plumbing through `client.mutate` → outbox → replay coordinator.
75
+ 2. **`@terreno/api`**: `listOpenApiMiddleware` (or the shared path-building step in `api/src/openApi.ts`) adds `"x-terreno-sync": {collection, scope}` to the list operation when `options.sync` is present.
76
+ 3. **`@terreno/syncdb-codegen`** (new): CLI that parses the spec, discovers synced collections, and emits the SDK file.
77
+
78
+ ### Runtime factory (`@terreno/syncdb/react`)
79
+
80
+ ```typescript
81
+ export interface CollectionHooksConfig {
82
+ collection: string;
83
+ /** false → 1 attempt (fail fast); number → max replay attempts; omitted → engine default (5). */
84
+ retries?: boolean | number;
85
+ }
86
+
87
+ export type MutationTrigger<TArgs> = (args: TArgs) => {mutationId: string; id: string};
88
+
89
+ export interface CollectionHooks<TData, TCreate, TUpdate> {
90
+ /** List query (RTK: useGet{Path}Query). */
91
+ useListQuery: (options?: UseQueryOptions<TData>) => {data: TData[]};
92
+ /** Single-entity read (RTK: useGet{Path}ByIdQuery). */
93
+ useReadQuery: (id: string) => UseEntityResult<TData>;
94
+ /** Create (RTK: usePost{Path}Mutation). */
95
+ useCreateMutation: () => [MutationTrigger<{data: TCreate}>];
96
+ /** Update via merge — patch semantics (RTK: usePatch{Path}ByIdMutation). */
97
+ useUpdateMutation: () => [MutationTrigger<{id: string; data: TUpdate}>];
98
+ /** Soft delete (RTK: useDelete{Path}ByIdMutation). */
99
+ useDeleteMutation: () => [MutationTrigger<{id: string}>];
100
+ }
101
+
102
+ export const createCollectionHooks = <
103
+ TData = Record<string, unknown>,
104
+ TCreate = Record<string, unknown>,
105
+ TUpdate = Partial<TCreate>,
106
+ >(config: CollectionHooksConfig): CollectionHooks<TData, TCreate, TUpdate> => { ... };
107
+ ```
108
+
109
+ The factory returns **operation-named keys** (`useListQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`); the generated file renames them to the RTK-style collection-specific names via destructuring (`useListQuery: useGetTodosQuery`, ...). Custom hooks can keep the generic names or rename the same way. Each hook delegates to the existing `useQuery`/`useEntity`/`useMutate` internals (no listener logic duplicated) and threads `retries` into `client.mutate` as `maxAttempts`. This is the `emptySplitApi` of the syncdb world: generated code calls it, and custom hooks call it directly — that is the whole "custom hooks the same way RTK does" story.
110
+
111
+ ### Generated file shape
112
+
113
+ ```typescript
114
+ // store/syncDbSdk.ts — GENERATED by @terreno/syncdb-codegen. Do not edit.
115
+ import {createCollectionHooks} from "@terreno/syncdb/react";
116
+
117
+ export interface Todo { _id: string; title: string; completed: boolean; ownerId: string; created: string; updated: string; }
118
+ export interface CreateTodoBody { title: string; completed?: boolean; }
119
+ export type UpdateTodoBody = Partial<CreateTodoBody>;
120
+
121
+ /** Collections with sync enabled on the backend; pass to createSyncDb. */
122
+ export const SYNC_COLLECTIONS = ["todos"] as const;
123
+
124
+ export const {
125
+ useListQuery: useGetTodosQuery,
126
+ useReadQuery: useGetTodosByIdQuery,
127
+ useCreateMutation: usePostTodosMutation,
128
+ useUpdateMutation: usePatchTodosByIdMutation,
129
+ useDeleteMutation: useDeleteTodosByIdMutation,
130
+ } = createCollectionHooks<Todo, CreateTodoBody, UpdateTodoBody>({collection: "todos"});
131
+ ```
132
+
133
+ Call-site comparison for migration:
134
+
135
+ ```typescript
136
+ // RTK // syncdb (generated)
137
+ const {data} = useGetTodosQuery({}); const {data} = useGetTodosQuery();
138
+ const {data} = useGetTodosByIdQuery({id}); const {data, isPending} = useGetTodosByIdQuery(id);
139
+ const [createTodo] = usePostTodosMutation(); const [createTodo] = usePostTodosMutation();
140
+ await createTodo({body}).unwrap(); createTodo({data}); // sync, returns {mutationId, id}
141
+ const [patchTodo] = usePatchTodosByIdMutation(); const [patchTodo] = usePatchTodosByIdMutation();
142
+ const [deleteTodo] = useDeleteTodosByIdMutation(); const [deleteTodo] = useDeleteTodosByIdMutation();
143
+ ```
144
+
145
+ `SYNC_COLLECTIONS` replaces the hand-maintained list in `example-frontend/store/syncdb.ts`, closing the "client string must match backend route" gap.
146
+
147
+ ### CLI
148
+
149
+ ```
150
+ terreno-syncdb-codegen --schema <url|path> --out <file> [--collections a,b] [--config <json>] [--no-format]
151
+ ```
152
+
153
+ - `--schema` (required): OpenAPI URL or local JSON file (same source as `openapi-config.ts`'s `schemaFile`).
154
+ - `--out` (required): output `.ts` path.
155
+ - `--collections` (optional): comma-separated allowlist; also the fallback when the spec has no `x-terreno-sync` extensions (older backends). Errors clearly if neither extensions nor the flag identify any collection.
156
+ - `--config` (optional): JSON file with `{overrides: {todos: {retries: false}}}` per-collection settings.
157
+ - `--no-format` (optional): skip the biome/prettier formatting pass (codegen formats output via `Bun.spawn`-ed `bunx biome check --write` when available, mirroring `generate-sdk.ts`; falls back to raw emit).
158
+
159
+ ## Models
160
+
161
+ No Mongoose model changes. One local-storage schema change in syncdb: the `_outbox` reserved table gains an optional `maxAttempts` numeric cell (`syncdb/src/storage/schema.ts`, `syncdb/src/mutations/outbox.ts`). Missing cell means engine default, so existing persisted stores need no migration.
162
+
163
+ ## APIs
164
+
165
+ No new HTTP endpoints. One OpenAPI spec addition in `@terreno/api`: list operations of sync-enabled model routers gain
166
+
167
+ ```json
168
+ "x-terreno-sync": {"collection": "todos", "scope": "owner"}
169
+ ```
170
+
171
+ Vendor extensions are legal OpenAPI and ignored by the RTK codegen, Swagger UI, and the AJV validator, so this is backward-compatible.
172
+
173
+ ## Notifications
174
+
175
+ None needed.
176
+
177
+ ## UI
178
+
179
+ No new screens. `example-frontend/components/SyncTodosScreen.tsx` swaps its hand-rolled `SyncTodo` interface and string-based hooks for the generated `useGetTodosQuery`/`usePostTodosMutation`/`usePatchTodosByIdMutation`/`useDeleteTodosByIdMutation`, and `store/syncdb.ts` imports `SYNC_COLLECTIONS` from the generated file. Behavior is unchanged; the existing syncdb Playwright e2e suite is the regression net.
180
+
181
+ ## Phases
182
+
183
+ Single PR is plausible but the work splits cleanly into four phases if review prefers smaller units:
184
+
185
+ 1. **syncdb runtime** — `createCollectionHooks` + `maxAttempts` plumbing (mutate → outbox → replay coordinator), with unit tests. No consumer-visible breaking changes.
186
+ 2. **api spec extension** — `x-terreno-sync` on list operations, with an OpenAPI generation test asserting the extension appears for `example-backend`-style sync configs and is absent otherwise.
187
+ 3. **codegen package** — new `syncdb-codegen/` workspace package: arg parsing, spec loading, collection discovery, type emitter, file emitter, snapshot tests against a fixture spec captured from `example-backend`, `build:binary` script.
188
+ 4. **integration + docs** — `example-frontend` `sync-sdk` script and screen migration; README updates (`syncdb/README.md`, new `syncdb-codegen/README.md`, `docs/how-to/migrate-rtk-to-syncdb.md` note); root `package.json` convenience scripts.
189
+
190
+ ## Feature Flags & Migrations
191
+
192
+ - No new feature flags. The existing `use-syncdb` flag continues to gate the syncdb path in `example-frontend`.
193
+ - No data migrations. The new `_outbox.maxAttempts` cell is optional and defaulted.
194
+ - Publishing: not wired into `publish-on-tag.yml` in this IP (neither is `@terreno/syncdb` yet); tracked in Future Work so both are added together.
195
+
196
+ ## Activity Log & User Updates
197
+
198
+ None — developer tooling only.
199
+
200
+ ## Not Included / Future Work
201
+
202
+ - Publishing `@terreno/syncdb` and `@terreno/syncdb-codegen` to npm (add `publish-syncdb` + `publish-syncdb-codegen` jobs to `publish-on-tag.yml`).
203
+ - Per-platform prebuilt binaries as release artifacts.
204
+ - Generated wrappers for `useSyncStatus`/`useConflicts` (no per-collection typing to add).
205
+ - Server-driven filters/sorts in `useQuery` options (syncdb filters run client-side in JS; nothing for codegen to type beyond `TData`).
206
+ - Zod/AJV runtime validation of mutation payloads against the OpenAPI schema.
207
+ - Custom hook naming via config (`overrides.todos.hookPrefix`), for apps that need to avoid the intentional name overlap with RTK-generated hooks.
208
+ - RTK-compatible async mutation state (`isLoading`, promise-returning triggers with `.unwrap()`), should a drop-in compatibility mode ever be wanted.
209
+ - A `--watch` mode.
210
+
211
+ ## Files to Create / Modify
212
+
213
+ **Create**
214
+ - `syncdb-codegen/package.json` — name `@terreno/syncdb-codegen`, `bin`, `build:binary`, catalog deps (dev-only: typescript, biome).
215
+ - `syncdb-codegen/tsconfig.json`, `syncdb-codegen/biome.jsonc` — copied from `syncdb/` siblings.
216
+ - `syncdb-codegen/src/cli.ts` — `#!/usr/bin/env bun` entry; arg parsing (`Bun.argv` + `util.parseArgs`), exit codes, error messages.
217
+ - `syncdb-codegen/src/loadSpec.ts` — fetch/read + JSON parse of the OpenAPI document.
218
+ - `syncdb-codegen/src/discoverCollections.ts` — walk `paths`, collect `x-terreno-sync` operations, resolve entity/create/update schema refs from the list/create/patch operations; apply `--collections` filter/fallback.
219
+ - `syncdb-codegen/src/emitTypes.ts` — OpenAPI schema subset → TS interfaces.
220
+ - `syncdb-codegen/src/emitSdk.ts` — assemble the output file (header, types, `SYNC_COLLECTIONS`, `createCollectionHooks` calls with overrides applied).
221
+ - `syncdb-codegen/src/*.test.ts` — unit + snapshot tests against `syncdb-codegen/src/fixtures/openapi.example.json`.
222
+ - `syncdb-codegen/README.md` — usage, args, config format, custom-hooks pattern.
223
+ - `example-frontend/syncdb-codegen.json` — per-collection overrides example (todos with default retries).
224
+
225
+ **Modify**
226
+ - `syncdb/src/react/collectionHooks.ts` (new file in existing package) + `syncdb/src/react/index.ts` — `createCollectionHooks` export.
227
+ - `syncdb/src/client.ts` — `MutateArgs.maxAttempts?`, pass-through to outbox.
228
+ - `syncdb/src/mutations/outbox.ts`, `syncdb/src/storage/schema.ts` — optional `maxAttempts` cell.
229
+ - `syncdb/src/sync/replayCoordinator.ts` — `row.maxAttempts ?? MAX_ERROR_NACK_ATTEMPTS` in the error-nack branch.
230
+ - `api/src/openApi.ts` (`listOpenApiMiddleware`) — emit `x-terreno-sync` when `options.sync` is set.
231
+ - `example-frontend/package.json` — `"sync-sdk": "terreno-syncdb-codegen --schema ... --out ./store/syncDbSdk.ts --config ./syncdb-codegen.json"` script; add `@terreno/syncdb-codegen` as a workspace dev dependency.
232
+ - `example-frontend/store/syncDbSdk.ts` — generated output (checked in, like `openApiSdk.ts`).
233
+ - `example-frontend/store/syncdb.ts` — import `SYNC_COLLECTIONS` from the generated file.
234
+ - `example-frontend/components/SyncTodosScreen.tsx` — use generated hooks; delete the local `SyncTodo` interface.
235
+ - Root `package.json` — workspace entry + `syncdb-codegen:compile` / `syncdb-codegen:test` scripts.
236
+ - `syncdb/README.md`, `docs/how-to/migrate-rtk-to-syncdb.md` — codegen sections.
237
+
238
+ ## Task List
239
+
240
+ See [docs/tasks/syncdb-codegen.md](../tasks/syncdb-codegen.md).
241
+
242
+ ## Acceptance Criteria
243
+
244
+ - [ ] `createCollectionHooks<Todo>({collection: "todos"})` returns the five per-operation hooks (`useListQuery`, `useReadQuery`, `useCreateMutation`, `useUpdateMutation`, `useDeleteMutation`) whose behavior matches direct `useQuery`/`useEntity`/`useMutate` calls; mutation hooks return `[trigger]` tuples so RTK-style destructuring compiles (unit tests in `syncdb/src/react/`).
245
+ - [ ] `createCollectionHooks({collection: "todos", retries: false})` produces mutations whose outbox rows fail terminally after a single `error` nack; `retries: 3` fails after three attempts; omitted keeps the current 5-attempt backoff (replay coordinator tests).
246
+ - [ ] `GET /openapi.json` from a backend with `sync: {scope: {type: "owner"}}` on `/todos` includes `x-terreno-sync: {collection: "todos", scope: "owner"}` on the list operation; non-synced routes have no extension (api test).
247
+ - [ ] `terreno-syncdb-codegen --schema <fixture> --out out.ts` emits a file that type-checks under the example-frontend tsconfig and contains `SYNC_COLLECTIONS`, typed interfaces, and per synced collection the five RTK-named hooks (`useGetTodosQuery`, `useGetTodosByIdQuery`, `usePostTodosMutation`, `usePatchTodosByIdMutation`, `useDeleteTodosByIdMutation`) — names byte-identical to what `@rtk-query/codegen-openapi` derives for the same routes (snapshot test).
248
+ - [ ] `--collections todos` filters output to that collection; a spec without extensions and no `--collections` flag exits non-zero with an actionable message.
249
+ - [ ] A config file with `{overrides: {todos: {retries: false}}}` produces `createCollectionHooks<...>({collection: "todos", retries: false})` in the output.
250
+ - [ ] `bun run build:binary` in `syncdb-codegen/` produces a standalone executable that generates identical output to the bin entry.
251
+ - [ ] `bun run sync-sdk` in `example-frontend` (backend running) regenerates `store/syncDbSdk.ts` with no diff when the backend is unchanged; SyncTodosScreen works against the generated hooks (existing syncdb e2e suite passes).
252
+ - [ ] `bun run lint`, `bun run syncdb:test`, `bun run api:test`, and the new `syncdb-codegen:test` pass.