@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
@@ -7,11 +7,13 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
7
7
  - [@terreno/api](api.md) — modelRouter, auth, permissions, setupServer
8
8
  - [@terreno/ui](ui.md) — Components, theming, layout
9
9
  - [@terreno/rtk](rtk.md) — Auth slice, SDK codegen, token handling
10
+ - [@terreno/syncdb](syncdb.md) — Local-first data layer (reads, writes, offline sync)
10
11
  - [@terreno/ai](ai.md) — AI service, GPT routes, Langfuse integration
11
12
  - [@terreno/admin-backend](admin-backend.md) — Auto-generated admin CRUD endpoints
12
13
  - [@terreno/admin-frontend](admin-frontend.md) — Admin panel UI components
13
14
  - [@terreno/admin-spa](admin-spa.md) — Standalone admin SPA + Express serve plugin
14
15
  - [@terreno/api-health](api-health.md) — Health check TerrenoPlugin
16
+ - [@terreno/comms](comms.md) — Pluggable mail, SMS, push, and verification providers
15
17
  - [@terreno/feature-flags](feature-flags.md) — Feature flags and A/B testing plugin
16
18
  - [@terreno/mcp](mcp-server.md) — AI coding assistant integration (MCP)
17
19
  - [@terreno/test](test.md) — Bun test helpers and in-memory MongoDB
@@ -0,0 +1,132 @@
1
+ # @terreno/comms
2
+
3
+ Provider-agnostic transactional communications for Terreno backends. The core package defines
4
+ mail, SMS, push, and verification contracts without installing SendGrid, Twilio, or Expo SDKs.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ bun add @terreno/comms
10
+ ```
11
+
12
+ Peer dependency: `mongoose ^8.0.0 || ^9.0.0`.
13
+
14
+ ## Register the plugin
15
+
16
+ ```typescript
17
+ import {
18
+ CommsApp,
19
+ ConsoleMailProvider,
20
+ ConsolePushProvider,
21
+ ConsoleSmsProvider,
22
+ ConsoleVerificationProvider,
23
+ } from "@terreno/comms";
24
+ import {TerrenoApp} from "@terreno/api";
25
+
26
+ new TerrenoApp({userModel: User})
27
+ .register(
28
+ new CommsApp({
29
+ defaultFrom: "notifications@example.com",
30
+ mail: new ConsoleMailProvider(),
31
+ push: new ConsolePushProvider(),
32
+ sms: new ConsoleSmsProvider(),
33
+ verification: new ConsoleVerificationProvider(),
34
+ })
35
+ )
36
+ .start();
37
+ ```
38
+
39
+ Console providers log only non-sensitive metadata such as message lengths and recipient counts.
40
+ They never log message content, recipient identifiers, push tokens, or verification codes.
41
+
42
+ ## Send messages
43
+
44
+ Use the registered service from routes, jobs, or other plugins:
45
+
46
+ ```typescript
47
+ import {getCommsService} from "@terreno/comms";
48
+
49
+ await getCommsService().sendMail({
50
+ subject: "Welcome",
51
+ text: "Thanks for joining.",
52
+ to: "person@example.com",
53
+ });
54
+
55
+ await getCommsService().sendSms({
56
+ body: "Your appointment is tomorrow.",
57
+ to: "+15555550100",
58
+ });
59
+ ```
60
+
61
+ `sendPushToUser()` resolves active device tokens and deactivates tokens only when a provider marks
62
+ a failure as permanent. `startVerification()` and `checkVerification()` delegate to the configured
63
+ verification provider. `startVerification()` accepts `channel: "sms"` or `channel: "email"`;
64
+ `checkVerification()` verifies the code against the same phone number or email destination and may
65
+ return an `error` reason when `valid` is false. Start attempts store the verification channel in
66
+ delivery-log metadata while recipient values remain redacted.
67
+
68
+ ## Provider contracts
69
+
70
+ | Interface | Method |
71
+ |---|---|
72
+ | `MailProvider` | `sendMail(message)` |
73
+ | `SmsProvider` | `sendSms(message)` |
74
+ | `PushProvider` | `sendPush(message)` — one `SendResult` per token |
75
+ | `VerificationProvider` | `startVerification(options)`, `checkVerification(options)` |
76
+
77
+ Concrete SendGrid, Twilio, and Expo providers are separate adapter packages/subpath exports. Core
78
+ `@terreno/comms` has no concrete provider SDK dependencies.
79
+
80
+ ## Configuration
81
+
82
+ ```typescript
83
+ interface CommsAppOptions {
84
+ basePath?: string; // default: "/comms"
85
+ defaultFrom?: string;
86
+ logMessages?: boolean; // default: true
87
+ mail?: MailProvider;
88
+ onDeliveryEvent?: (event: DeliveryEvent) => Promise<void>;
89
+ push?: PushProvider;
90
+ redactRecipients?: boolean; // default: true
91
+ sms?: SmsProvider;
92
+ verification?: VerificationProvider;
93
+ }
94
+ ```
95
+
96
+ When a channel is unconfigured:
97
+
98
+ - non-production environments use the matching console provider and emit a warning;
99
+ - production throws a `501` `APIError` titled `Comms channel not configured`.
100
+
101
+ Delivery attempts are stored in `CommsMessage`. Recipient values are stored as `[redacted]` unless
102
+ `redactRecipients` is explicitly `false`.
103
+
104
+ ## Routes
105
+
106
+ The default `basePath` is `/comms`.
107
+
108
+ | Method | Path | Access | Purpose |
109
+ |---|---|---|---|
110
+ | `POST` | `/comms/pushTokens` | Authenticated | Register or refresh a device token |
111
+ | `GET` | `/comms/pushTokens` | Authenticated owner | List the current user's tokens |
112
+ | `GET` | `/comms/pushTokens/:id` | Owner | Read one token |
113
+ | `DELETE` | `/comms/pushTokens/:id` | Owner | Deactivate a token |
114
+ | `GET` | `/comms/messages` | Admin | Filtered, paginated delivery log |
115
+
116
+ An active token cannot be claimed by another user. After its owner deactivates it, another
117
+ authenticated user on the same device may register it.
118
+
119
+ ## Templates
120
+
121
+ `renderTemplate()` replaces top-level `{{variable}}` placeholders in `subject`, `text`, and `html`.
122
+ Missing or inherited properties render as empty strings.
123
+
124
+ ```typescript
125
+ const message = renderTemplate({
126
+ data: {name: "Ada"},
127
+ template: {
128
+ subject: "Welcome, {{name}}",
129
+ text: "Hello {{name}}",
130
+ },
131
+ });
132
+ ```
@@ -128,6 +128,15 @@ Resolution order for API base URL (`rtk/src/constants.ts`):
128
128
  |----------|---------|----------|---------|--------|-------|
129
129
  | `FEATURE_FLAGS_DEBUG` | `@terreno/feature-flags` | ❌ | — | No | server |
130
130
 
131
+ ## Communications
132
+
133
+ | Variable | Read by | Required | Default | Secret | Scope |
134
+ |----------|---------|----------|---------|--------|-------|
135
+ | `COMMS_ENABLED` | example-backend | ❌ | `true` | No | server |
136
+
137
+ Set `COMMS_ENABLED=false` to omit the example backend's communications plugin and routes.
138
+ Provider credentials are documented by their separate adapter packages.
139
+
131
140
  ## Observability
132
141
 
133
142
  | Variable | Read by | Required | Default | Secret | Scope |
@@ -0,0 +1,456 @@
1
+ # @terreno/syncdb
2
+
3
+ Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up. Supersedes `@terreno/rtk` for data-synchronization concerns.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Architecture](#architecture)
9
+ - [Backend setup](#backend-setup)
10
+ - [createSyncDb configuration](#createsyncdb-configuration)
11
+ - [SyncDb client methods](#syncdb-client-methods)
12
+ - [React hooks](#react-hooks)
13
+ - [Conflict API](#conflict-api)
14
+ - [Sync status API](#sync-status-api)
15
+ - [Stream scoping](#stream-scoping)
16
+ - [Sync protocol](#sync-protocol)
17
+ - [Encryption at rest](#encryption-at-rest)
18
+ - [Testing](#testing)
19
+ - [Environment variables](#environment-variables)
20
+ - [Conventions](#conventions)
21
+
22
+ ## Key exports
23
+
24
+ - `createSyncDb`, `SyncDb`, `SyncDbConfig`, `MutateArgs`
25
+ - `betterAuthAdapter`, `AuthProvider`
26
+ - `listConflicts`, `wipeLocalData`, `generateMutationId`
27
+ - React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`
28
+ - Testing (`@terreno/syncdb/testing`): `createFakeTransport`
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ bun install @terreno/syncdb
34
+ ```
35
+
36
+ **Peer dependencies:** `react` (optional — only for `@terreno/syncdb/react`), `luxon`, `tinybase`.
37
+
38
+ **Native persistence:** install `expo-sqlite` in the **app** (not only in a library):
39
+
40
+ ```bash
41
+ bunx expo install expo-sqlite
42
+ ```
43
+
44
+ Expo autolinking walks the app's own dependencies; a nested copy leaves the native module out of the build. Rebuild the native project after adding it.
45
+
46
+ **Why `@terreno/syncdb/react` is a separate subpath:** `react` is an optional peer dependency. Keeping React bindings off the main entry lets non-React consumers (Node tests, scripts) import `@terreno/syncdb` without loading React.
47
+
48
+ ## Architecture
49
+
50
+ ```
51
+ FRONTEND (@terreno/syncdb) BACKEND (@terreno/api)
52
+ ┌───────────────────────────────────┐ ┌────────────────────────────────────┐
53
+ │ React hooks (useQuery, useEntity, │ │ modelRouter(path, Model, {sync}) │
54
+ │ useMutate, useSyncStatus, ...) │ │ └─ sync registry + validation │
55
+ │ │ ▲ │ │ │
56
+ │ ▼ │ │ │ syncPlugin (schema) │
57
+ │ TinyBase MergeableStore │ │ └─ stamps _syncSeq per write │
58
+ │ {collection} tables + _outbox │ │ │
59
+ │ + _cursors + _conflicts │ │ SyncApp (HTTP) RealtimeApp │
60
+ │ │ ▲ │ │ /sync/snapshot (Socket.io + │
61
+ │ ▼ │ │ │ /sync/mutate change streams)│
62
+ │ Persister (AES-GCM IndexedDB on │ │ /sync/key │ │
63
+ │ web, expo-sqlite on native) │ └─────────┬───────────────┼──────────┘
64
+ └──────┬─────────────────▲──────────┘ │ │
65
+ │ │ │ │
66
+ │ sync:mutate ─────────────────────────────►│ │
67
+ │ sync:ack / sync:nack ◄────────────────────┘ │
68
+ │ sync:delta ◄──────────────────────────────────────────────┘
69
+ │ GET /sync/snapshot (bootstrap + catch-up, HTTP)
70
+ └── POST /sync/mutate (fallback while the socket is down)
71
+ ```
72
+
73
+ Every mutation executes the same `@terreno/api` modelRouter write path as REST — identical permissions, hooks, and validation.
74
+
75
+ ## Backend setup
76
+
77
+ Register sync on the backend before the frontend client can sync a collection.
78
+
79
+ ### Required schema plugins
80
+
81
+ ```typescript
82
+ import {isDeletedPlugin, syncPlugin} from "@terreno/api";
83
+
84
+ todoSchema.plugin(isDeletedPlugin); // soft delete — required for tombstone catch-up
85
+ todoSchema.plugin(syncPlugin); // stamps per-stream _syncSeq on every write
86
+ ```
87
+
88
+ Apply plugins **before** the model compiles. Registration validates their presence.
89
+
90
+ ### modelRouter sync config
91
+
92
+ Use the three-argument `modelRouter` form:
93
+
94
+ ```typescript
95
+ import {modelRouter, OwnerQueryFilter, Permissions} from "@terreno/api";
96
+
97
+ const todoRouter = modelRouter("/todos", Todo, {
98
+ permissions: {
99
+ create: [Permissions.IsAuthenticated],
100
+ delete: [Permissions.IsOwner],
101
+ list: [Permissions.IsAuthenticated],
102
+ read: [Permissions.IsOwner],
103
+ update: [Permissions.IsOwner],
104
+ },
105
+ preCreate: (body, req) => ({...body, ownerId: (req.user as unknown as UserDocument)?._id}),
106
+ queryFilter: OwnerQueryFilter,
107
+ sync: {
108
+ scope: {type: "owner"}, // stream = todos|owner:{ownerId}
109
+ },
110
+ });
111
+ ```
112
+
113
+ ### SyncApp and RealtimeApp
114
+
115
+ ```typescript
116
+ import {RealtimeApp, SyncApp, TerrenoApp} from "@terreno/api";
117
+
118
+ new TerrenoApp({userModel: User})
119
+ .register(todoRouter)
120
+ .register(new SyncApp()) // GET /sync/snapshot, POST /sync/mutate, GET /sync/key, ...
121
+ .register(new RealtimeApp()) // Socket.io; sync:subscribe, sync:delta, sync:mutate
122
+ .start();
123
+ ```
124
+
125
+ - **`SyncApp`** — HTTP sync routes (`/sync/snapshot`, `/sync/mutate`, `/sync/mutate/batch`, `/sync/key`, `/sync/streams`, `/sync/entities`).
126
+ - **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams).
127
+ - **`ensureSyncIndexes`** — `TerrenoApp.start()` awaits index builds for snapshot queries and sync bookkeeping (`SyncMutation.mutationId` unique, `SyncCounter.stream` unique, etc.). Hosts that build Express without `TerrenoApp.start()` should await `ensureSyncIndexes()` themselves.
128
+
129
+ Socket auth accepts legacy JWTs by default. For Better Auth sessions:
130
+
131
+ ```typescript
132
+ new RealtimeApp({betterAuth: {auth, userModel: User}})
133
+ ```
134
+
135
+ ## createSyncDb configuration
136
+
137
+ ```typescript
138
+ import {betterAuthAdapter, createSyncDb} from "@terreno/syncdb";
139
+
140
+ export const syncDb = createSyncDb({
141
+ name: "myapp",
142
+ collections: ["todos"],
143
+ authProvider: betterAuthAdapter(authClient),
144
+ baseUrl: "http://localhost:4000",
145
+ });
146
+ ```
147
+
148
+ | Option | Type | Default | Description |
149
+ |--------|------|---------|-------------|
150
+ | `name` | `string` | — (required) | Persisted database name |
151
+ | `collections` | `string[]` | — (required) | Collection names to sync (local tables + subscriptions) |
152
+ | `authProvider` | `AuthProvider` | — (required) | `{getToken, getUserId, onAuthChange, refresh?}` |
153
+ | `baseUrl` | `string` | — | Server origin; required unless both `transport` and `httpChannel` are injected |
154
+ | `transport` | `SyncTransport` | socket transport from `baseUrl` | Override for tests or custom wiring |
155
+ | `httpChannel` | `HttpChannel` | built from `baseUrl` | HTTP fallback channel override |
156
+ | `persisterFactory` | `PersisterFactory` | platform default (IndexedDB web, expo-sqlite native) | Storage backend override |
157
+ | `keyProvider` | `KeyProvider` | server-derived via `GET /sync/key` | Web encryption key provider |
158
+ | `idbGetImpl` / `idbSetImpl` | test hooks | — | Simulate IndexedDB failures (tests only) |
159
+ | `reconcileIntervalMs` | `number` | `300000` (5 min); `0` disables | Periodic reconcile + outbox replay timer |
160
+ | `seqJumpReconcileMinIntervalMs` | `number` | `30000` (30s) | Per-stream rate limit for seq-jump-triggered reconciles |
161
+ | `now` | `() => number` | `Date.now` | Injectable clock (tests) |
162
+ | `random` | `() => number` | `Math.random` | Injectable RNG for backoff jitter (tests) |
163
+ | `debug` | `boolean \| SyncDebugLogOptions` | `false` | In-memory debug event log |
164
+ | `onAuthRequired` | `() => void` | — | Fires once per auth-pause episode (prompt re-login) |
165
+ | `wipeOnSignOut` | `boolean` | `false` | `signOut()` also wipes local data |
166
+ | `batchSize` | `number` | `50` (server caps at 100) | Max mutations per batched drain send |
167
+ | `haltQueueOnConflict` | `boolean` | `false` | `true` = conflict halts entire drain; `false` = per-entity blocking |
168
+ | `onDecryptFailure` | `() => void` | wipe + re-bootstrap (with `console.warn`) | Override web decrypt-failure recovery |
169
+ | `tombstoneRetentionMs` | `number` | `7776000000` (90 days); `0` disables | Client tombstone compaction after successful reconcile |
170
+ | `startAuthRetryAttempts` | `number` | `3`; `1` disables | `start()` attempts to resolve `getUserId()` |
171
+ | `startAuthRetryDelayMs` | `number` | `250` | Delay between those attempts |
172
+
173
+ `start()` needs an authenticated user. It retries `getUserId()` briefly (transient session races after login) before rejecting.
174
+
175
+ ## SyncDb client methods
176
+
177
+ | Method | Description |
178
+ |--------|-------------|
179
+ | `start()` | Resolve user, run wipe-on-user-change check, start persistence, connect transport, subscribe collections, start reconcile timer. Resolves even when offline. Idempotent while already started. |
180
+ | `stop()` | Disconnect, stop persistence, clear timers and listeners. |
181
+ | `goOffline()` | Simulated outage: disconnect transport, pause replay/reconcile/timer; local mutations keep queueing. |
182
+ | `goOnline()` | End simulated outage; reconnect triggers reconcile + outbox replay. |
183
+ | `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
184
+ | `reconcile()` | HTTP snapshot catch-up for every known stream; runs tombstone compaction on success. |
185
+ | `forceResync()` | Purge every known stream locally and re-bootstrap from cursor 0 (outbox/conflicts untouched). Returns `{ok, reason?, streams, purged, repaired}`. |
186
+ | `replayOutbox()` | Drain queued mutations for the current user now. |
187
+ | `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
188
+ | `retryFailed({entityId})` | Re-enable an entity's queued successors after a terminal validation failure. |
189
+ | `signOut()` | Explicit teardown like `stop()`; wipes local data only when `wipeOnSignOut: true`. |
190
+ | `getSyncStatus()` | Snapshot of aggregate sync state (see [Sync status API](#sync-status-api)). |
191
+ | `onStatusChange(callback)` | Subscribe to connectivity/syncing changes; returns unsubscribe. |
192
+ | `store` | Read surface over the local MergeableStore (`SyncStore`). |
193
+ | `outbox` | Durable mutation outbox (`Outbox`). |
194
+ | `debug` | `SyncDebugLog` when `debug` is enabled; otherwise `undefined`. |
195
+
196
+ ## React hooks
197
+
198
+ Import from `@terreno/syncdb/react`:
199
+
200
+ ```typescript
201
+ import {
202
+ SyncDbProvider,
203
+ useConflicts,
204
+ useEntity,
205
+ useEntityIds,
206
+ useMutate,
207
+ useQuery,
208
+ useSyncDbClient,
209
+ useSyncDebugLog,
210
+ useSyncStatus,
211
+ } from "@terreno/syncdb/react";
212
+ ```
213
+
214
+ Wrap the app (or a subtree) in `SyncDbProvider`:
215
+
216
+ ```tsx
217
+ <SyncDbProvider client={syncDb}>
218
+ <TodoList />
219
+ </SyncDbProvider>
220
+ ```
221
+
222
+ ### `useEntity(collection, id)`
223
+
224
+ ```typescript
225
+ const {data, deleted, seq, isPending} = useEntity<Todo>("todos", id);
226
+ ```
227
+
228
+ Subscribe to a single entity. Re-renders when that row changes. `isPending` is true while an outbox mutation protects optimistic state.
229
+
230
+ ### `useQuery(collection, options?)`
231
+
232
+ ```typescript
233
+ const todos = useQuery<Todo>("todos", {
234
+ filter: (t) => !t.completed,
235
+ sort: (a, b) => b.created.localeCompare(a.created),
236
+ includeDeleted: false,
237
+ });
238
+ ```
239
+
240
+ Returns decoded entity data arrays. Tombstones excluded unless `includeDeleted: true`. Filter and sort run in JS — memoize callbacks when collections are large.
241
+
242
+ ### `useEntityIds(collection, options?)`
243
+
244
+ ```typescript
245
+ const ids = useEntityIds<Todo>("todos", {filter: (t) => !t.completed, sort: byCreatedDesc});
246
+ return ids.map((id) => <TodoRow key={id} id={id} />);
247
+ ```
248
+
249
+ Same options as `useQuery`, but returns only ordered ids with **referential stability** — the array identity changes only when membership or order changes, not on field updates. Pair with per-row `useEntity` for large lists.
250
+
251
+ ### `useMutate(collection)`
252
+
253
+ ```typescript
254
+ const {create, update, remove} = useMutate("todos");
255
+
256
+ const {id, mutationId} = create({data: {title: "Milk", completed: false}});
257
+ update({id, data: {completed: true}});
258
+ remove({id});
259
+ ```
260
+
261
+ Each call applies locally, enqueues in the outbox, and kicks off replay.
262
+
263
+ ### `useSyncStatus()`
264
+
265
+ ```typescript
266
+ const status = useSyncStatus();
267
+ // status.isOnline, status.isSyncing, status.queuedCount, status.conflictCount, ...
268
+ ```
269
+
270
+ Reactive aggregate status; re-renders on outbox, conflict, cursor, connectivity, and drain-progress changes.
271
+
272
+ ### `useConflicts()`
273
+
274
+ ```typescript
275
+ const {conflicts, resolve} = useConflicts();
276
+ resolve({mutationId, strategy: "useServer"});
277
+ resolve({mutationId, strategy: "keepMine"});
278
+ ```
279
+
280
+ ### `useSyncDebugLog()`
281
+
282
+ ```typescript
283
+ const {enabled, events, stats, log, clear} = useSyncDebugLog();
284
+ ```
285
+
286
+ Available when the client was created with `debug: true`. `log?.snapshot()` returns a JSON-serializable object suitable for debug UIs or tooling.
287
+
288
+ ### `useSyncDbClient()`
289
+
290
+ Returns the `SyncDb` instance from context (escape hatch for imperative calls like `forceResync()`).
291
+
292
+ ## Conflict API
293
+
294
+ A **conflict** occurs when the client's `baseVersion` (last seen `_syncSeq`) does not match the server's current seq. The server responds with a `conflict` nack (HTTP **409** on `POST /sync/mutate`, or `sync:nack` with `code: "conflict"` on the socket) carrying the canonical server document.
295
+
296
+ The client records conflicts in the local `_conflicts` table. Read them with:
297
+
298
+ ```typescript
299
+ import {listConflicts} from "@terreno/syncdb";
300
+
301
+ const conflicts = listConflicts({store: client.store});
302
+ ```
303
+
304
+ In React, prefer `useConflicts()`.
305
+
306
+ ### Resolution strategies
307
+
308
+ | Strategy | Behavior |
309
+ |----------|----------|
310
+ | `"useServer"` | Overwrite local entity with canonical server data/seq; clear pending state; discard conflicted outbox row. Writes a **tombstone** when the server side is deleted. |
311
+ | `"keepMine"` | Re-enqueue the mutation under a **fresh** `mutationId` with `baseVersion` set to the server's seq; local optimistic data is kept. |
312
+
313
+ Resolve via `client.resolveConflict({mutationId, strategy})` or `useConflicts().resolve(...)`.
314
+
315
+ ### Conflict handling modes
316
+
317
+ - **Default (`haltQueueOnConflict: false`)** — per-entity blocking. Other entities keep draining.
318
+ - **`haltQueueOnConflict: true`** — any conflict halts the entire drain until resolved.
319
+
320
+ `getSyncStatus().blockedEntities` counts entities blocked by conflicts or terminal validation failures.
321
+
322
+ ## Sync status API
323
+
324
+ `getSyncStatus()` / `useSyncStatus()` return `SyncStatus`:
325
+
326
+ | Field | Type | Description |
327
+ |-------|------|-------------|
328
+ | `isOnline` | `boolean` | Transport connected |
329
+ | `isSyncing` | `boolean` | Reconcile or replay in flight |
330
+ | `queuedCount` | `number` | Outbox rows awaiting send |
331
+ | `conflictCount` | `number` | Unresolved conflicts |
332
+ | `failedCount` | `number` | Terminal `failed` outbox rows |
333
+ | `paused` | `"auth"?` | Replay paused for auth failure |
334
+ | `blockedEntities` | `number` | Entities blocked (conflict or validation) |
335
+ | `draining` | `boolean` | Outbox drain actively running |
336
+ | `sentThisDrain` | `number` | Mutations sent in current/most recent drain |
337
+ | `totalThisDrain` | `number` | Queue length when drain began |
338
+ | `streams` | `Record<string, number>` | Per-stream cursors (stream → highest applied seq) |
339
+ | `collections` | `Record<string, SyncCollectionStatus>` | Per-collection queued/conflict/failed counts |
340
+ | `persistence` | `"durable" \| "memory" \| "error"` | Local persistence health (web) |
341
+
342
+ `SyncCollectionStatus`: `{queuedCount, conflictCount, failedCount}`.
343
+
344
+ ## Stream scoping
345
+
346
+ Streams are the unit of ordered delivery, keyed `{collection}|{scope}`:
347
+
348
+ ```typescript
349
+ sync: {scope: {type: "owner"}} // todos|owner:{ownerId}
350
+ sync: {scope: {type: "owner", field: "userId"}} // todos|owner:{userId}
351
+ sync: {scope: {type: "tenant", field: "organizationId"}} // todos|tenant:{orgId}
352
+ sync: {scope: {type: "broadcast"}} // todos|all
353
+ sync: {
354
+ scope: (doc) => String(doc.workspaceId),
355
+ snapshotFilter: (user) => ({workspaceId: {$in: [...]}}), // required for custom
356
+ }
357
+ ```
358
+
359
+ - **Owner** streams use the authenticated socket's user id (client cannot pick another user's stream).
360
+ - **Tenant/custom** scopes resolve memberships via `SyncApp` `getUserScopes`.
361
+ - **`snapshotFilter`** restricts `GET /sync/snapshot` server-side. Auto-derived for owner/tenant; required for custom resolver scopes.
362
+
363
+ ## Sync protocol
364
+
365
+ ### HTTP routes (`SyncApp`, all authenticated)
366
+
367
+ | Endpoint | Purpose |
368
+ |----------|---------|
369
+ | `GET /sync/snapshot?collection=&stream=&cursor=&limit=` | Bootstrap + catch-up per stream |
370
+ | `GET /sync/streams` | Current stream membership for the user |
371
+ | `GET /sync/entities` | Point lookup for entity repair |
372
+ | `POST /sync/mutate` | Single mutation (HTTP fallback) |
373
+ | `POST /sync/mutate/batch` | Batched mutations (max 100, strict order, stop at first non-ack) |
374
+ | `GET /sync/key` | Per-user encryption key material (web) |
375
+
376
+ Conflict responses on mutate: **409** with `{nack}` body (`code: "conflict"`).
377
+
378
+ ### Socket events (`RealtimeApp`)
379
+
380
+ | Event | Direction | Payload |
381
+ |-------|-----------|---------|
382
+ | `sync:subscribe` / `sync:unsubscribe` | client → server | `{collections: string[]}` |
383
+ | `sync:subscribed` | server → client | `{collection, streams}` |
384
+ | `sync:error` | server → client | `{collection, message}` |
385
+ | `sync:delta` | server → client | `{collection, id, method, data?, seq, stream, deleted?, frontierSeq?}` |
386
+ | `sync:mutate` | client → server | `{mutationId, collection, operation, id?, data?, baseVersion?}` |
387
+ | `sync:ack` | server → client | `{mutationId, id, seq}` |
388
+ | `sync:nack` | server → client | `{mutationId, code, serverDoc?, serverSeq?, serverDeleted?, message?, retryAfterMs?}` |
389
+ | `sync:mutateBatch` | client → server | `{mutations: SyncMutateRequest[], batchId?}` |
390
+ | `sync:auth-expired` | server → client | `{reason: "expired" \| "disabled"}` — session re-validation sweep; client enters auth-pause |
391
+
392
+ Limits: 50 collection subscriptions per socket; 100 mutations/second per socket (each batch mutation counts); batches capped at 100.
393
+
394
+ ## Encryption at rest
395
+
396
+ **Web:** AES-256-GCM encrypted IndexedDB by default via pluggable `KeyProvider`:
397
+
398
+ - **`createServerKeyProvider`** (default) — derives key from `GET /sync/key` material (HKDF); server can rotate/revoke.
399
+ - **`createLocalKeyProvider`** — device-local random key; no server copy.
400
+
401
+ ```typescript
402
+ import {createLocalKeyProvider, createSyncDb} from "@terreno/syncdb";
403
+
404
+ const syncDb = createSyncDb({
405
+ name: "myapp",
406
+ collections,
407
+ authProvider,
408
+ baseUrl,
409
+ keyProvider: createLocalKeyProvider(),
410
+ });
411
+ ```
412
+
413
+ Undecryptable persisted data triggers wipe + re-bootstrap by default (`onDecryptFailure` to override). Storage read errors surface `persistence: "error"` without overwriting the blob.
414
+
415
+ **Native:** plaintext SQLite in the OS sandbox (by design).
416
+
417
+ ## Testing
418
+
419
+ ```typescript
420
+ import {createFakeTransport} from "@terreno/syncdb/testing";
421
+ import {createSyncDb} from "@terreno/syncdb";
422
+
423
+ const transport = createFakeTransport();
424
+ const client = createSyncDb({
425
+ name: "test",
426
+ collections: ["todos"],
427
+ authProvider: fakeAuth,
428
+ transport,
429
+ httpChannel: fakeHttp,
430
+ });
431
+
432
+ transport.respondWithAck();
433
+ transport.deliverDelta({...});
434
+ ```
435
+
436
+ `createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders.
437
+
438
+ ## Environment variables
439
+
440
+ Syncdb reads no environment variables directly. Host apps typically set:
441
+
442
+ | Variable | Used by | Description |
443
+ |----------|---------|-------------|
444
+ | `EXPO_PUBLIC_API_URL` | `@terreno/rtk` `baseUrl` | Backend origin passed to `createSyncDb({baseUrl})` |
445
+
446
+ Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for `RealtimeApp` change streams.
447
+
448
+ ## Conventions
449
+
450
+ - **Local-first only** — there is no server-first mode. The local store is always the read source.
451
+ - **Synced models need `String` `_id`** — offline creates mint client ids (UUIDs); default ObjectId `_id` cast-fails.
452
+ - **Soft delete only** on synced models — `isDeletedPlugin` required; hard deletes break tombstone catch-up.
453
+ - **No `bulkWrite` / `updateMany` / `deleteMany`** on synced models — use per-document loops.
454
+ - **Do not write reserved tables** (`_outbox`, `_cursors`, `_conflicts`) directly — use `client.mutate`, hooks, and `resolveConflict`.
455
+ - **User switch wipes local data** — confirmed different-user login clears the previous user's store; bare logout/401 does not (INV-2).
456
+ - **Keep `@terreno/rtk` for non-synced routes** — generated OpenAPI hooks remain the right tool for custom REST endpoints, admin, and auth configuration. See [How to migrate from RTK to syncdb](../how-to/migrate-rtk-to-syncdb.md).