@syncular/server 0.1.3 → 0.2.1

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 (758) hide show
  1. package/README.md +773 -13
  2. package/dist/admin.d.ts +136 -0
  3. package/dist/admin.js +168 -0
  4. package/dist/blob-handlers.d.ts +69 -0
  5. package/dist/blob-handlers.js +245 -0
  6. package/dist/blob-store.d.ts +111 -0
  7. package/dist/blob-store.js +0 -0
  8. package/dist/content-encoding.d.ts +27 -0
  9. package/dist/content-encoding.js +69 -0
  10. package/dist/context.d.ts +151 -0
  11. package/dist/context.js +21 -0
  12. package/dist/crdt-merger.d.ts +28 -0
  13. package/dist/crdt-merger.js +11 -0
  14. package/dist/d1-storage.d.ts +50 -0
  15. package/dist/d1-storage.js +0 -0
  16. package/dist/errors.d.ts +35 -9
  17. package/dist/errors.js +220 -11
  18. package/dist/events-ring.d.ts +55 -0
  19. package/dist/events-ring.js +96 -0
  20. package/dist/events.d.ts +231 -0
  21. package/dist/events.js +22 -0
  22. package/dist/frame-bytes.d.ts +20 -0
  23. package/dist/frame-bytes.js +75 -0
  24. package/dist/handler.d.ts +9 -0
  25. package/dist/handler.js +490 -0
  26. package/dist/index.d.ts +42 -33
  27. package/dist/index.js +45 -27
  28. package/dist/lease-store.d.ts +50 -0
  29. package/dist/lease-store.js +0 -0
  30. package/dist/pg-executor-pglite.d.ts +27 -0
  31. package/dist/pg-executor-pglite.js +33 -0
  32. package/dist/pg-executor.d.ts +67 -0
  33. package/dist/pg-executor.js +56 -0
  34. package/dist/postgres-fanout.d.ts +83 -0
  35. package/dist/postgres-fanout.js +71 -0
  36. package/dist/postgres-storage.d.ts +69 -0
  37. package/dist/postgres-storage.js +766 -0
  38. package/dist/prune.d.ts +24 -39
  39. package/dist/prune.js +44 -146
  40. package/dist/pull.d.ts +42 -67
  41. package/dist/pull.js +387 -1187
  42. package/dist/push.d.ts +31 -66
  43. package/dist/push.js +406 -655
  44. package/dist/realtime.d.ts +197 -0
  45. package/dist/realtime.js +848 -0
  46. package/dist/relational-rows.d.ts +229 -0
  47. package/dist/relational-rows.js +514 -0
  48. package/dist/s3-blob-store.d.ts +142 -0
  49. package/dist/s3-blob-store.js +458 -0
  50. package/dist/s3-segment-store.d.ts +95 -0
  51. package/dist/s3-segment-store.js +380 -0
  52. package/dist/schema.d.ts +82 -286
  53. package/dist/schema.js +133 -9
  54. package/dist/scopes.d.ts +48 -0
  55. package/dist/scopes.js +98 -0
  56. package/dist/segment-download.d.ts +14 -0
  57. package/dist/segment-download.js +125 -0
  58. package/dist/segment-store.d.ts +81 -0
  59. package/dist/segment-store.js +63 -0
  60. package/dist/signed-url.d.ts +139 -0
  61. package/dist/signed-url.js +143 -0
  62. package/dist/sigv4.d.ts +97 -0
  63. package/dist/sigv4.js +161 -0
  64. package/dist/sqlite-blob-store.d.ts +22 -0
  65. package/dist/sqlite-blob-store.js +86 -0
  66. package/dist/sqlite-dialect.d.ts +101 -0
  67. package/dist/sqlite-dialect.js +250 -0
  68. package/dist/sqlite-image.d.ts +27 -0
  69. package/dist/sqlite-image.js +83 -0
  70. package/dist/sqlite-lease-store.d.ts +21 -0
  71. package/dist/sqlite-lease-store.js +94 -0
  72. package/dist/sqlite-segment-store.d.ts +25 -0
  73. package/dist/sqlite-segment-store.js +115 -0
  74. package/dist/sqlite-storage.d.ts +47 -0
  75. package/dist/sqlite-storage.js +477 -0
  76. package/dist/storage.d.ts +252 -0
  77. package/dist/storage.js +1 -0
  78. package/dist/validate.d.ts +83 -0
  79. package/dist/validate.js +44 -0
  80. package/package.json +17 -325
  81. package/src/admin.ts +312 -0
  82. package/src/blob-handlers.ts +340 -0
  83. package/src/blob-store.ts +0 -0
  84. package/src/content-encoding.ts +94 -0
  85. package/src/context.ts +176 -0
  86. package/src/crdt-merger.ts +33 -0
  87. package/src/d1-storage.ts +0 -0
  88. package/src/errors.ts +241 -19
  89. package/src/events-ring.ts +116 -0
  90. package/src/events.ts +291 -0
  91. package/src/frame-bytes.ts +94 -0
  92. package/src/handler.ts +678 -0
  93. package/src/index.ts +44 -27
  94. package/src/lease-store.ts +0 -0
  95. package/src/pg-executor-pglite.ts +63 -0
  96. package/src/pg-executor.ts +87 -0
  97. package/src/postgres-fanout.ts +143 -0
  98. package/src/postgres-storage.ts +1182 -0
  99. package/src/prune.ts +63 -289
  100. package/src/pull.ts +505 -1805
  101. package/src/push.ts +623 -1006
  102. package/src/realtime.ts +1082 -0
  103. package/src/relational-rows.ts +633 -0
  104. package/src/s3-blob-store.ts +616 -0
  105. package/src/s3-segment-store.ts +519 -0
  106. package/src/schema.ts +242 -281
  107. package/src/scopes.ts +127 -0
  108. package/src/segment-download.ts +169 -0
  109. package/src/segment-store.ts +154 -0
  110. package/src/signed-url.ts +308 -0
  111. package/src/sigv4.ts +267 -0
  112. package/src/sqlite-blob-store.ts +125 -0
  113. package/src/sqlite-dialect.ts +337 -0
  114. package/src/sqlite-image.ts +124 -0
  115. package/src/sqlite-lease-store.ts +144 -0
  116. package/src/sqlite-segment-store.ts +204 -0
  117. package/src/sqlite-storage.ts +792 -0
  118. package/src/storage.ts +316 -0
  119. package/src/validate.ts +123 -0
  120. package/dist/auth-leases.d.ts +0 -85
  121. package/dist/auth-leases.d.ts.map +0 -1
  122. package/dist/auth-leases.js +0 -368
  123. package/dist/auth-leases.js.map +0 -1
  124. package/dist/better-sqlite3.d.ts +0 -22
  125. package/dist/better-sqlite3.d.ts.map +0 -1
  126. package/dist/better-sqlite3.js +0 -16
  127. package/dist/better-sqlite3.js.map +0 -1
  128. package/dist/blobs/access.d.ts +0 -35
  129. package/dist/blobs/access.d.ts.map +0 -1
  130. package/dist/blobs/access.js +0 -149
  131. package/dist/blobs/access.js.map +0 -1
  132. package/dist/blobs/adapters/database.d.ts +0 -100
  133. package/dist/blobs/adapters/database.d.ts.map +0 -1
  134. package/dist/blobs/adapters/database.js +0 -247
  135. package/dist/blobs/adapters/database.js.map +0 -1
  136. package/dist/blobs/index.d.ts +0 -9
  137. package/dist/blobs/index.d.ts.map +0 -1
  138. package/dist/blobs/index.js +0 -9
  139. package/dist/blobs/index.js.map +0 -1
  140. package/dist/blobs/manager.d.ts +0 -220
  141. package/dist/blobs/manager.d.ts.map +0 -1
  142. package/dist/blobs/manager.js +0 -625
  143. package/dist/blobs/manager.js.map +0 -1
  144. package/dist/blobs/migrate.d.ts +0 -27
  145. package/dist/blobs/migrate.d.ts.map +0 -1
  146. package/dist/blobs/migrate.js +0 -127
  147. package/dist/blobs/migrate.js.map +0 -1
  148. package/dist/blobs/types.d.ts +0 -58
  149. package/dist/blobs/types.d.ts.map +0 -1
  150. package/dist/blobs/types.js +0 -5
  151. package/dist/blobs/types.js.map +0 -1
  152. package/dist/bun-sqlite.d.ts +0 -19
  153. package/dist/bun-sqlite.d.ts.map +0 -1
  154. package/dist/bun-sqlite.js +0 -19
  155. package/dist/bun-sqlite.js.map +0 -1
  156. package/dist/clients.d.ts +0 -15
  157. package/dist/clients.d.ts.map +0 -1
  158. package/dist/clients.js +0 -7
  159. package/dist/clients.js.map +0 -1
  160. package/dist/cloudflare/durable-object.d.ts +0 -93
  161. package/dist/cloudflare/durable-object.d.ts.map +0 -1
  162. package/dist/cloudflare/durable-object.js +0 -210
  163. package/dist/cloudflare/durable-object.js.map +0 -1
  164. package/dist/cloudflare/index.d.ts +0 -22
  165. package/dist/cloudflare/index.d.ts.map +0 -1
  166. package/dist/cloudflare/index.js +0 -22
  167. package/dist/cloudflare/index.js.map +0 -1
  168. package/dist/cloudflare/r2.d.ts +0 -180
  169. package/dist/cloudflare/r2.d.ts.map +0 -1
  170. package/dist/cloudflare/r2.js +0 -258
  171. package/dist/cloudflare/r2.js.map +0 -1
  172. package/dist/cloudflare/scope-cache.d.ts +0 -54
  173. package/dist/cloudflare/scope-cache.d.ts.map +0 -1
  174. package/dist/cloudflare/scope-cache.js +0 -223
  175. package/dist/cloudflare/scope-cache.js.map +0 -1
  176. package/dist/cloudflare/sentry.d.ts +0 -47
  177. package/dist/cloudflare/sentry.d.ts.map +0 -1
  178. package/dist/cloudflare/sentry.js +0 -163
  179. package/dist/cloudflare/sentry.js.map +0 -1
  180. package/dist/cloudflare/worker.d.ts +0 -46
  181. package/dist/cloudflare/worker.d.ts.map +0 -1
  182. package/dist/cloudflare/worker.js +0 -63
  183. package/dist/cloudflare/worker.js.map +0 -1
  184. package/dist/commit-integrity.d.ts +0 -38
  185. package/dist/commit-integrity.d.ts.map +0 -1
  186. package/dist/commit-integrity.js +0 -260
  187. package/dist/commit-integrity.js.map +0 -1
  188. package/dist/compaction.d.ts +0 -27
  189. package/dist/compaction.d.ts.map +0 -1
  190. package/dist/compaction.js +0 -49
  191. package/dist/compaction.js.map +0 -1
  192. package/dist/crdt-yjs/index.d.ts +0 -99
  193. package/dist/crdt-yjs/index.d.ts.map +0 -1
  194. package/dist/crdt-yjs/index.js +0 -629
  195. package/dist/crdt-yjs/index.js.map +0 -1
  196. package/dist/d1.d.ts +0 -13
  197. package/dist/d1.d.ts.map +0 -1
  198. package/dist/d1.js +0 -14
  199. package/dist/d1.js.map +0 -1
  200. package/dist/dialect/base.d.ts +0 -93
  201. package/dist/dialect/base.d.ts.map +0 -1
  202. package/dist/dialect/base.js +0 -181
  203. package/dist/dialect/base.js.map +0 -1
  204. package/dist/dialect/helpers.d.ts +0 -16
  205. package/dist/dialect/helpers.d.ts.map +0 -1
  206. package/dist/dialect/helpers.js +0 -83
  207. package/dist/dialect/helpers.js.map +0 -1
  208. package/dist/dialect/index.d.ts +0 -7
  209. package/dist/dialect/index.d.ts.map +0 -1
  210. package/dist/dialect/index.js +0 -7
  211. package/dist/dialect/index.js.map +0 -1
  212. package/dist/dialect/types.d.ts +0 -187
  213. package/dist/dialect/types.d.ts.map +0 -1
  214. package/dist/dialect/types.js +0 -8
  215. package/dist/dialect/types.js.map +0 -1
  216. package/dist/encrypted-crdt.d.ts +0 -67
  217. package/dist/encrypted-crdt.d.ts.map +0 -1
  218. package/dist/encrypted-crdt.js +0 -426
  219. package/dist/encrypted-crdt.js.map +0 -1
  220. package/dist/errors.d.ts.map +0 -1
  221. package/dist/errors.js.map +0 -1
  222. package/dist/filesystem/index.d.ts +0 -44
  223. package/dist/filesystem/index.d.ts.map +0 -1
  224. package/dist/filesystem/index.js +0 -164
  225. package/dist/filesystem/index.js.map +0 -1
  226. package/dist/handlers/collection.d.ts +0 -20
  227. package/dist/handlers/collection.d.ts.map +0 -1
  228. package/dist/handlers/collection.js +0 -42
  229. package/dist/handlers/collection.js.map +0 -1
  230. package/dist/handlers/create-handler.d.ts +0 -156
  231. package/dist/handlers/create-handler.d.ts.map +0 -1
  232. package/dist/handlers/create-handler.js +0 -626
  233. package/dist/handlers/create-handler.js.map +0 -1
  234. package/dist/handlers/index.d.ts +0 -4
  235. package/dist/handlers/index.d.ts.map +0 -1
  236. package/dist/handlers/index.js +0 -4
  237. package/dist/handlers/index.js.map +0 -1
  238. package/dist/handlers/types.d.ts +0 -295
  239. package/dist/handlers/types.d.ts.map +0 -1
  240. package/dist/handlers/types.js +0 -2
  241. package/dist/handlers/types.js.map +0 -1
  242. package/dist/helpers/conflict.d.ts +0 -52
  243. package/dist/helpers/conflict.d.ts.map +0 -1
  244. package/dist/helpers/conflict.js +0 -49
  245. package/dist/helpers/conflict.js.map +0 -1
  246. package/dist/helpers/emitted-change.d.ts +0 -56
  247. package/dist/helpers/emitted-change.d.ts.map +0 -1
  248. package/dist/helpers/emitted-change.js +0 -46
  249. package/dist/helpers/emitted-change.js.map +0 -1
  250. package/dist/helpers/index.d.ts +0 -12
  251. package/dist/helpers/index.d.ts.map +0 -1
  252. package/dist/helpers/index.js +0 -12
  253. package/dist/helpers/index.js.map +0 -1
  254. package/dist/helpers/paginate.d.ts +0 -49
  255. package/dist/helpers/paginate.d.ts.map +0 -1
  256. package/dist/helpers/paginate.js +0 -54
  257. package/dist/helpers/paginate.js.map +0 -1
  258. package/dist/helpers/scope-authorization.d.ts +0 -7
  259. package/dist/helpers/scope-authorization.d.ts.map +0 -1
  260. package/dist/helpers/scope-authorization.js +0 -19
  261. package/dist/helpers/scope-authorization.js.map +0 -1
  262. package/dist/helpers/scope-commit-index.d.ts +0 -12
  263. package/dist/helpers/scope-commit-index.d.ts.map +0 -1
  264. package/dist/helpers/scope-commit-index.js +0 -35
  265. package/dist/helpers/scope-commit-index.js.map +0 -1
  266. package/dist/helpers/scope-strings.d.ts +0 -74
  267. package/dist/helpers/scope-strings.d.ts.map +0 -1
  268. package/dist/helpers/scope-strings.js +0 -82
  269. package/dist/helpers/scope-strings.js.map +0 -1
  270. package/dist/hono/api-key-auth.d.ts +0 -49
  271. package/dist/hono/api-key-auth.d.ts.map +0 -1
  272. package/dist/hono/api-key-auth.js +0 -108
  273. package/dist/hono/api-key-auth.js.map +0 -1
  274. package/dist/hono/audit-redaction.d.ts +0 -20
  275. package/dist/hono/audit-redaction.d.ts.map +0 -1
  276. package/dist/hono/audit-redaction.js +0 -85
  277. package/dist/hono/audit-redaction.js.map +0 -1
  278. package/dist/hono/blobs.d.ts +0 -75
  279. package/dist/hono/blobs.d.ts.map +0 -1
  280. package/dist/hono/blobs.js +0 -586
  281. package/dist/hono/blobs.js.map +0 -1
  282. package/dist/hono/console/gateway.d.ts +0 -42
  283. package/dist/hono/console/gateway.d.ts.map +0 -1
  284. package/dist/hono/console/gateway.js +0 -2158
  285. package/dist/hono/console/gateway.js.map +0 -1
  286. package/dist/hono/console/live-auth.d.ts +0 -7
  287. package/dist/hono/console/live-auth.d.ts.map +0 -1
  288. package/dist/hono/console/live-auth.js +0 -39
  289. package/dist/hono/console/live-auth.js.map +0 -1
  290. package/dist/hono/console/route-descriptor.d.ts +0 -6
  291. package/dist/hono/console/route-descriptor.d.ts.map +0 -1
  292. package/dist/hono/console/route-descriptor.js +0 -16
  293. package/dist/hono/console/route-descriptor.js.map +0 -1
  294. package/dist/hono/console/routes/api-keys.d.ts +0 -8
  295. package/dist/hono/console/routes/api-keys.d.ts.map +0 -1
  296. package/dist/hono/console/routes/api-keys.js +0 -575
  297. package/dist/hono/console/routes/api-keys.js.map +0 -1
  298. package/dist/hono/console/routes/clients.d.ts +0 -8
  299. package/dist/hono/console/routes/clients.d.ts.map +0 -1
  300. package/dist/hono/console/routes/clients.js +0 -322
  301. package/dist/hono/console/routes/clients.js.map +0 -1
  302. package/dist/hono/console/routes/commits.d.ts +0 -8
  303. package/dist/hono/console/routes/commits.d.ts.map +0 -1
  304. package/dist/hono/console/routes/commits.js +0 -882
  305. package/dist/hono/console/routes/commits.js.map +0 -1
  306. package/dist/hono/console/routes/context.d.ts +0 -188
  307. package/dist/hono/console/routes/context.d.ts.map +0 -1
  308. package/dist/hono/console/routes/context.js +0 -633
  309. package/dist/hono/console/routes/context.js.map +0 -1
  310. package/dist/hono/console/routes/events.d.ts +0 -8
  311. package/dist/hono/console/routes/events.d.ts.map +0 -1
  312. package/dist/hono/console/routes/events.js +0 -501
  313. package/dist/hono/console/routes/events.js.map +0 -1
  314. package/dist/hono/console/routes/maintenance.d.ts +0 -8
  315. package/dist/hono/console/routes/maintenance.d.ts.map +0 -1
  316. package/dist/hono/console/routes/maintenance.js +0 -341
  317. package/dist/hono/console/routes/maintenance.js.map +0 -1
  318. package/dist/hono/console/routes/shared.d.ts +0 -174
  319. package/dist/hono/console/routes/shared.d.ts.map +0 -1
  320. package/dist/hono/console/routes/shared.js +0 -583
  321. package/dist/hono/console/routes/shared.js.map +0 -1
  322. package/dist/hono/console/routes/stats.d.ts +0 -8
  323. package/dist/hono/console/routes/stats.d.ts.map +0 -1
  324. package/dist/hono/console/routes/stats.js +0 -295
  325. package/dist/hono/console/routes/stats.js.map +0 -1
  326. package/dist/hono/console/routes/storage.d.ts +0 -8
  327. package/dist/hono/console/routes/storage.d.ts.map +0 -1
  328. package/dist/hono/console/routes/storage.js +0 -121
  329. package/dist/hono/console/routes/storage.js.map +0 -1
  330. package/dist/hono/console/routes.d.ts +0 -36
  331. package/dist/hono/console/routes.d.ts.map +0 -1
  332. package/dist/hono/console/routes.js +0 -112
  333. package/dist/hono/console/routes.js.map +0 -1
  334. package/dist/hono/console/schema-errors.d.ts +0 -2
  335. package/dist/hono/console/schema-errors.d.ts.map +0 -1
  336. package/dist/hono/console/schema-errors.js +0 -17
  337. package/dist/hono/console/schema-errors.js.map +0 -1
  338. package/dist/hono/console/schemas.d.ts +0 -1515
  339. package/dist/hono/console/schemas.d.ts.map +0 -1
  340. package/dist/hono/console/schemas.js +0 -661
  341. package/dist/hono/console/schemas.js.map +0 -1
  342. package/dist/hono/console/types.d.ts +0 -213
  343. package/dist/hono/console/types.d.ts.map +0 -1
  344. package/dist/hono/console/types.js +0 -2
  345. package/dist/hono/console/types.js.map +0 -1
  346. package/dist/hono/console/ui.d.ts +0 -38
  347. package/dist/hono/console/ui.d.ts.map +0 -1
  348. package/dist/hono/console/ui.js +0 -43
  349. package/dist/hono/console/ui.js.map +0 -1
  350. package/dist/hono/create-server.d.ts +0 -71
  351. package/dist/hono/create-server.d.ts.map +0 -1
  352. package/dist/hono/create-server.js +0 -121
  353. package/dist/hono/create-server.js.map +0 -1
  354. package/dist/hono/errors.d.ts +0 -13
  355. package/dist/hono/errors.d.ts.map +0 -1
  356. package/dist/hono/errors.js +0 -21
  357. package/dist/hono/errors.js.map +0 -1
  358. package/dist/hono/index.d.ts +0 -20
  359. package/dist/hono/index.d.ts.map +0 -1
  360. package/dist/hono/index.js +0 -31
  361. package/dist/hono/index.js.map +0 -1
  362. package/dist/hono/openapi.d.ts +0 -72
  363. package/dist/hono/openapi.d.ts.map +0 -1
  364. package/dist/hono/openapi.js +0 -99
  365. package/dist/hono/openapi.js.map +0 -1
  366. package/dist/hono/proxy/connection-manager.d.ts +0 -78
  367. package/dist/hono/proxy/connection-manager.d.ts.map +0 -1
  368. package/dist/hono/proxy/connection-manager.js +0 -251
  369. package/dist/hono/proxy/connection-manager.js.map +0 -1
  370. package/dist/hono/proxy/index.d.ts +0 -8
  371. package/dist/hono/proxy/index.d.ts.map +0 -1
  372. package/dist/hono/proxy/index.js +0 -8
  373. package/dist/hono/proxy/index.js.map +0 -1
  374. package/dist/hono/proxy/routes.d.ts +0 -86
  375. package/dist/hono/proxy/routes.d.ts.map +0 -1
  376. package/dist/hono/proxy/routes.js +0 -183
  377. package/dist/hono/proxy/routes.js.map +0 -1
  378. package/dist/hono/rate-limit.d.ts +0 -101
  379. package/dist/hono/rate-limit.d.ts.map +0 -1
  380. package/dist/hono/rate-limit.js +0 -184
  381. package/dist/hono/rate-limit.js.map +0 -1
  382. package/dist/hono/realtime-sync-packs.d.ts +0 -43
  383. package/dist/hono/realtime-sync-packs.d.ts.map +0 -1
  384. package/dist/hono/realtime-sync-packs.js +0 -219
  385. package/dist/hono/realtime-sync-packs.js.map +0 -1
  386. package/dist/hono/routes/audit.d.ts +0 -12
  387. package/dist/hono/routes/audit.d.ts.map +0 -1
  388. package/dist/hono/routes/audit.js +0 -385
  389. package/dist/hono/routes/audit.js.map +0 -1
  390. package/dist/hono/routes/auth-leases.d.ts +0 -8
  391. package/dist/hono/routes/auth-leases.d.ts.map +0 -1
  392. package/dist/hono/routes/auth-leases.js +0 -88
  393. package/dist/hono/routes/auth-leases.js.map +0 -1
  394. package/dist/hono/routes/combined.d.ts +0 -8
  395. package/dist/hono/routes/combined.d.ts.map +0 -1
  396. package/dist/hono/routes/combined.js +0 -392
  397. package/dist/hono/routes/combined.js.map +0 -1
  398. package/dist/hono/routes/context.d.ts +0 -314
  399. package/dist/hono/routes/context.d.ts.map +0 -1
  400. package/dist/hono/routes/context.js +0 -1147
  401. package/dist/hono/routes/context.js.map +0 -1
  402. package/dist/hono/routes/health.d.ts +0 -8
  403. package/dist/hono/routes/health.d.ts.map +0 -1
  404. package/dist/hono/routes/health.js +0 -16
  405. package/dist/hono/routes/health.js.map +0 -1
  406. package/dist/hono/routes/realtime.d.ts +0 -9
  407. package/dist/hono/routes/realtime.d.ts.map +0 -1
  408. package/dist/hono/routes/realtime.js +0 -626
  409. package/dist/hono/routes/realtime.js.map +0 -1
  410. package/dist/hono/routes/shared.d.ts +0 -689
  411. package/dist/hono/routes/shared.d.ts.map +0 -1
  412. package/dist/hono/routes/shared.js +0 -972
  413. package/dist/hono/routes/shared.js.map +0 -1
  414. package/dist/hono/routes/snapshots.d.ts +0 -10
  415. package/dist/hono/routes/snapshots.d.ts.map +0 -1
  416. package/dist/hono/routes/snapshots.js +0 -281
  417. package/dist/hono/routes/snapshots.js.map +0 -1
  418. package/dist/hono/routes.d.ts +0 -19
  419. package/dist/hono/routes.d.ts.map +0 -1
  420. package/dist/hono/routes.js +0 -37
  421. package/dist/hono/routes.js.map +0 -1
  422. package/dist/hono/validation.d.ts +0 -5
  423. package/dist/hono/validation.d.ts.map +0 -1
  424. package/dist/hono/validation.js +0 -47
  425. package/dist/hono/validation.js.map +0 -1
  426. package/dist/hono/websocket-origin.d.ts +0 -9
  427. package/dist/hono/websocket-origin.d.ts.map +0 -1
  428. package/dist/hono/websocket-origin.js +0 -96
  429. package/dist/hono/websocket-origin.js.map +0 -1
  430. package/dist/hono/ws.d.ts +0 -341
  431. package/dist/hono/ws.d.ts.map +0 -1
  432. package/dist/hono/ws.js +0 -711
  433. package/dist/hono/ws.js.map +0 -1
  434. package/dist/index.d.ts.map +0 -1
  435. package/dist/index.js.map +0 -1
  436. package/dist/libsql.d.ts +0 -29
  437. package/dist/libsql.d.ts.map +0 -1
  438. package/dist/libsql.js +0 -25
  439. package/dist/libsql.js.map +0 -1
  440. package/dist/migrate.d.ts +0 -14
  441. package/dist/migrate.d.ts.map +0 -1
  442. package/dist/migrate.js +0 -13
  443. package/dist/migrate.js.map +0 -1
  444. package/dist/neon.d.ts +0 -21
  445. package/dist/neon.d.ts.map +0 -1
  446. package/dist/neon.js +0 -22
  447. package/dist/neon.js.map +0 -1
  448. package/dist/notify.d.ts +0 -77
  449. package/dist/notify.d.ts.map +0 -1
  450. package/dist/notify.js +0 -233
  451. package/dist/notify.js.map +0 -1
  452. package/dist/pglite.d.ts +0 -37
  453. package/dist/pglite.d.ts.map +0 -1
  454. package/dist/pglite.js +0 -37
  455. package/dist/pglite.js.map +0 -1
  456. package/dist/plugins/index.d.ts +0 -2
  457. package/dist/plugins/index.d.ts.map +0 -1
  458. package/dist/plugins/index.js +0 -2
  459. package/dist/plugins/index.js.map +0 -1
  460. package/dist/plugins/types.d.ts +0 -73
  461. package/dist/plugins/types.d.ts.map +0 -1
  462. package/dist/plugins/types.js +0 -30
  463. package/dist/plugins/types.js.map +0 -1
  464. package/dist/postgres/index.d.ts +0 -68
  465. package/dist/postgres/index.d.ts.map +0 -1
  466. package/dist/postgres/index.js +0 -924
  467. package/dist/postgres/index.js.map +0 -1
  468. package/dist/proxy/collection.d.ts +0 -7
  469. package/dist/proxy/collection.d.ts.map +0 -1
  470. package/dist/proxy/collection.js +0 -6
  471. package/dist/proxy/collection.js.map +0 -1
  472. package/dist/proxy/handler.d.ts +0 -42
  473. package/dist/proxy/handler.d.ts.map +0 -1
  474. package/dist/proxy/handler.js +0 -102
  475. package/dist/proxy/handler.js.map +0 -1
  476. package/dist/proxy/index.d.ts +0 -9
  477. package/dist/proxy/index.d.ts.map +0 -1
  478. package/dist/proxy/index.js +0 -14
  479. package/dist/proxy/index.js.map +0 -1
  480. package/dist/proxy/mutation-detector.d.ts +0 -35
  481. package/dist/proxy/mutation-detector.d.ts.map +0 -1
  482. package/dist/proxy/mutation-detector.js +0 -246
  483. package/dist/proxy/mutation-detector.js.map +0 -1
  484. package/dist/proxy/oplog.d.ts +0 -30
  485. package/dist/proxy/oplog.d.ts.map +0 -1
  486. package/dist/proxy/oplog.js +0 -137
  487. package/dist/proxy/oplog.js.map +0 -1
  488. package/dist/proxy/types.d.ts +0 -44
  489. package/dist/proxy/types.d.ts.map +0 -1
  490. package/dist/proxy/types.js +0 -7
  491. package/dist/proxy/types.js.map +0 -1
  492. package/dist/prune.d.ts.map +0 -1
  493. package/dist/prune.js.map +0 -1
  494. package/dist/pull.d.ts.map +0 -1
  495. package/dist/pull.js.map +0 -1
  496. package/dist/push.d.ts.map +0 -1
  497. package/dist/push.js.map +0 -1
  498. package/dist/realtime/in-memory.d.ts +0 -13
  499. package/dist/realtime/in-memory.d.ts.map +0 -1
  500. package/dist/realtime/in-memory.js +0 -28
  501. package/dist/realtime/in-memory.js.map +0 -1
  502. package/dist/realtime/index.d.ts +0 -4
  503. package/dist/realtime/index.d.ts.map +0 -1
  504. package/dist/realtime/index.js +0 -3
  505. package/dist/realtime/index.js.map +0 -1
  506. package/dist/realtime/types.d.ts +0 -62
  507. package/dist/realtime/types.d.ts.map +0 -1
  508. package/dist/realtime/types.js +0 -19
  509. package/dist/realtime/types.js.map +0 -1
  510. package/dist/relay/client-role/forward-engine.d.ts +0 -63
  511. package/dist/relay/client-role/forward-engine.d.ts.map +0 -1
  512. package/dist/relay/client-role/forward-engine.js +0 -267
  513. package/dist/relay/client-role/forward-engine.js.map +0 -1
  514. package/dist/relay/client-role/index.d.ts +0 -9
  515. package/dist/relay/client-role/index.d.ts.map +0 -1
  516. package/dist/relay/client-role/index.js +0 -9
  517. package/dist/relay/client-role/index.js.map +0 -1
  518. package/dist/relay/client-role/pull-engine.d.ts +0 -72
  519. package/dist/relay/client-role/pull-engine.d.ts.map +0 -1
  520. package/dist/relay/client-role/pull-engine.js +0 -249
  521. package/dist/relay/client-role/pull-engine.js.map +0 -1
  522. package/dist/relay/client-role/sequence-mapper.d.ts +0 -65
  523. package/dist/relay/client-role/sequence-mapper.d.ts.map +0 -1
  524. package/dist/relay/client-role/sequence-mapper.js +0 -161
  525. package/dist/relay/client-role/sequence-mapper.js.map +0 -1
  526. package/dist/relay/evaluation/relay-paths.d.ts +0 -41
  527. package/dist/relay/evaluation/relay-paths.d.ts.map +0 -1
  528. package/dist/relay/evaluation/relay-paths.js +0 -504
  529. package/dist/relay/evaluation/relay-paths.js.map +0 -1
  530. package/dist/relay/evaluation/rust-boundary.d.ts +0 -47
  531. package/dist/relay/evaluation/rust-boundary.d.ts.map +0 -1
  532. package/dist/relay/evaluation/rust-boundary.js +0 -220
  533. package/dist/relay/evaluation/rust-boundary.js.map +0 -1
  534. package/dist/relay/index.d.ts +0 -37
  535. package/dist/relay/index.d.ts.map +0 -1
  536. package/dist/relay/index.js +0 -44
  537. package/dist/relay/index.js.map +0 -1
  538. package/dist/relay/migrate.d.ts +0 -18
  539. package/dist/relay/migrate.d.ts.map +0 -1
  540. package/dist/relay/migrate.js +0 -99
  541. package/dist/relay/migrate.js.map +0 -1
  542. package/dist/relay/mode-manager.d.ts +0 -60
  543. package/dist/relay/mode-manager.d.ts.map +0 -1
  544. package/dist/relay/mode-manager.js +0 -114
  545. package/dist/relay/mode-manager.js.map +0 -1
  546. package/dist/relay/realtime.d.ts +0 -90
  547. package/dist/relay/realtime.d.ts.map +0 -1
  548. package/dist/relay/realtime.js +0 -147
  549. package/dist/relay/realtime.js.map +0 -1
  550. package/dist/relay/relay.d.ts +0 -190
  551. package/dist/relay/relay.d.ts.map +0 -1
  552. package/dist/relay/relay.js +0 -320
  553. package/dist/relay/relay.js.map +0 -1
  554. package/dist/relay/schema.d.ts +0 -158
  555. package/dist/relay/schema.d.ts.map +0 -1
  556. package/dist/relay/schema.js +0 -7
  557. package/dist/relay/schema.js.map +0 -1
  558. package/dist/relay/server-role/index.d.ts +0 -56
  559. package/dist/relay/server-role/index.d.ts.map +0 -1
  560. package/dist/relay/server-role/index.js +0 -193
  561. package/dist/relay/server-role/index.js.map +0 -1
  562. package/dist/relay/server-role/pull.d.ts +0 -27
  563. package/dist/relay/server-role/pull.d.ts.map +0 -1
  564. package/dist/relay/server-role/pull.js +0 -24
  565. package/dist/relay/server-role/pull.js.map +0 -1
  566. package/dist/relay/server-role/push.d.ts +0 -29
  567. package/dist/relay/server-role/push.d.ts.map +0 -1
  568. package/dist/relay/server-role/push.js +0 -122
  569. package/dist/relay/server-role/push.js.map +0 -1
  570. package/dist/s3/index.d.ts +0 -83
  571. package/dist/s3/index.d.ts.map +0 -1
  572. package/dist/s3/index.js +0 -223
  573. package/dist/s3/index.js.map +0 -1
  574. package/dist/schema.d.ts.map +0 -1
  575. package/dist/schema.js.map +0 -1
  576. package/dist/service-worker/index.d.ts +0 -114
  577. package/dist/service-worker/index.d.ts.map +0 -1
  578. package/dist/service-worker/index.js +0 -487
  579. package/dist/service-worker/index.js.map +0 -1
  580. package/dist/snapshot-artifacts/sqlite-bun.d.ts +0 -15
  581. package/dist/snapshot-artifacts/sqlite-bun.d.ts.map +0 -1
  582. package/dist/snapshot-artifacts/sqlite-bun.js +0 -128
  583. package/dist/snapshot-artifacts/sqlite-bun.js.map +0 -1
  584. package/dist/snapshot-artifacts.d.ts +0 -206
  585. package/dist/snapshot-artifacts.d.ts.map +0 -1
  586. package/dist/snapshot-artifacts.js +0 -545
  587. package/dist/snapshot-artifacts.js.map +0 -1
  588. package/dist/snapshot-chunks/db-metadata.d.ts +0 -56
  589. package/dist/snapshot-chunks/db-metadata.d.ts.map +0 -1
  590. package/dist/snapshot-chunks/db-metadata.js +0 -360
  591. package/dist/snapshot-chunks/db-metadata.js.map +0 -1
  592. package/dist/snapshot-chunks/index.d.ts +0 -8
  593. package/dist/snapshot-chunks/index.d.ts.map +0 -1
  594. package/dist/snapshot-chunks/index.js +0 -8
  595. package/dist/snapshot-chunks/index.js.map +0 -1
  596. package/dist/snapshot-chunks/types.d.ts +0 -80
  597. package/dist/snapshot-chunks/types.d.ts.map +0 -1
  598. package/dist/snapshot-chunks/types.js +0 -8
  599. package/dist/snapshot-chunks/types.js.map +0 -1
  600. package/dist/snapshot-chunks.d.ts +0 -90
  601. package/dist/snapshot-chunks.d.ts.map +0 -1
  602. package/dist/snapshot-chunks.js +0 -304
  603. package/dist/snapshot-chunks.js.map +0 -1
  604. package/dist/sqlite/index.d.ts +0 -53
  605. package/dist/sqlite/index.d.ts.map +0 -1
  606. package/dist/sqlite/index.js +0 -795
  607. package/dist/sqlite/index.js.map +0 -1
  608. package/dist/sqlite3.d.ts +0 -22
  609. package/dist/sqlite3.d.ts.map +0 -1
  610. package/dist/sqlite3.js +0 -99
  611. package/dist/sqlite3.js.map +0 -1
  612. package/dist/stats.d.ts +0 -28
  613. package/dist/stats.d.ts.map +0 -1
  614. package/dist/stats.js +0 -93
  615. package/dist/stats.js.map +0 -1
  616. package/dist/subscriptions/cache.d.ts +0 -58
  617. package/dist/subscriptions/cache.d.ts.map +0 -1
  618. package/dist/subscriptions/cache.js +0 -250
  619. package/dist/subscriptions/cache.js.map +0 -1
  620. package/dist/subscriptions/index.d.ts +0 -3
  621. package/dist/subscriptions/index.d.ts.map +0 -1
  622. package/dist/subscriptions/index.js +0 -3
  623. package/dist/subscriptions/index.js.map +0 -1
  624. package/dist/subscriptions/resolve.d.ts +0 -40
  625. package/dist/subscriptions/resolve.d.ts.map +0 -1
  626. package/dist/subscriptions/resolve.js +0 -275
  627. package/dist/subscriptions/resolve.js.map +0 -1
  628. package/dist/sync.d.ts +0 -24
  629. package/dist/sync.d.ts.map +0 -1
  630. package/dist/sync.js +0 -27
  631. package/dist/sync.js.map +0 -1
  632. package/src/auth-leases.ts +0 -649
  633. package/src/better-sqlite3.ts +0 -35
  634. package/src/blobs/access.ts +0 -244
  635. package/src/blobs/adapters/database.ts +0 -397
  636. package/src/blobs/index.ts +0 -9
  637. package/src/blobs/manager.ts +0 -901
  638. package/src/blobs/migrate.ts +0 -158
  639. package/src/blobs/types.ts +0 -74
  640. package/src/bun-sqlite-ambient.d.ts +0 -19
  641. package/src/bun-sqlite.ts +0 -27
  642. package/src/clients.ts +0 -22
  643. package/src/cloudflare/durable-object.ts +0 -289
  644. package/src/cloudflare/index.ts +0 -22
  645. package/src/cloudflare/r2.ts +0 -526
  646. package/src/cloudflare/scope-cache.ts +0 -341
  647. package/src/cloudflare/sentry.ts +0 -230
  648. package/src/cloudflare/worker.ts +0 -77
  649. package/src/commit-integrity.ts +0 -371
  650. package/src/compaction.ts +0 -77
  651. package/src/crdt-yjs/index.ts +0 -931
  652. package/src/d1.ts +0 -16
  653. package/src/dialect/base.ts +0 -360
  654. package/src/dialect/helpers.ts +0 -92
  655. package/src/dialect/index.ts +0 -7
  656. package/src/dialect/types.ts +0 -247
  657. package/src/encrypted-crdt.ts +0 -786
  658. package/src/filesystem/index.ts +0 -262
  659. package/src/handlers/collection.ts +0 -121
  660. package/src/handlers/create-handler.ts +0 -1134
  661. package/src/handlers/index.ts +0 -3
  662. package/src/handlers/types.ts +0 -403
  663. package/src/helpers/conflict.ts +0 -64
  664. package/src/helpers/emitted-change.ts +0 -69
  665. package/src/helpers/index.ts +0 -12
  666. package/src/helpers/paginate.ts +0 -82
  667. package/src/helpers/scope-authorization.ts +0 -27
  668. package/src/helpers/scope-commit-index.ts +0 -52
  669. package/src/helpers/scope-strings.ts +0 -101
  670. package/src/hono/api-key-auth.ts +0 -177
  671. package/src/hono/audit-redaction.ts +0 -135
  672. package/src/hono/blobs.ts +0 -851
  673. package/src/hono/console/gateway.ts +0 -3046
  674. package/src/hono/console/live-auth.ts +0 -46
  675. package/src/hono/console/route-descriptor.ts +0 -22
  676. package/src/hono/console/routes/api-keys.ts +0 -721
  677. package/src/hono/console/routes/clients.ts +0 -447
  678. package/src/hono/console/routes/commits.ts +0 -1137
  679. package/src/hono/console/routes/context.ts +0 -956
  680. package/src/hono/console/routes/events.ts +0 -669
  681. package/src/hono/console/routes/maintenance.ts +0 -461
  682. package/src/hono/console/routes/shared.ts +0 -816
  683. package/src/hono/console/routes/stats.ts +0 -392
  684. package/src/hono/console/routes/storage.ts +0 -159
  685. package/src/hono/console/routes.ts +0 -146
  686. package/src/hono/console/schema-errors.ts +0 -23
  687. package/src/hono/console/schemas.ts +0 -914
  688. package/src/hono/console/types.ts +0 -223
  689. package/src/hono/console/ui.ts +0 -100
  690. package/src/hono/create-server.ts +0 -230
  691. package/src/hono/errors.ts +0 -50
  692. package/src/hono/index.ts +0 -54
  693. package/src/hono/openapi.ts +0 -139
  694. package/src/hono/proxy/connection-manager.ts +0 -340
  695. package/src/hono/proxy/index.ts +0 -8
  696. package/src/hono/proxy/routes.ts +0 -272
  697. package/src/hono/rate-limit.ts +0 -319
  698. package/src/hono/realtime-sync-packs.ts +0 -354
  699. package/src/hono/routes/audit.ts +0 -499
  700. package/src/hono/routes/auth-leases.ts +0 -119
  701. package/src/hono/routes/combined.ts +0 -583
  702. package/src/hono/routes/context.ts +0 -1629
  703. package/src/hono/routes/health.ts +0 -26
  704. package/src/hono/routes/realtime.ts +0 -808
  705. package/src/hono/routes/shared.ts +0 -1626
  706. package/src/hono/routes/snapshots.ts +0 -345
  707. package/src/hono/routes.ts +0 -68
  708. package/src/hono/validation.ts +0 -81
  709. package/src/hono/websocket-origin.ts +0 -131
  710. package/src/hono/ws.ts +0 -1134
  711. package/src/libsql.ts +0 -51
  712. package/src/migrate.ts +0 -20
  713. package/src/neon.ts +0 -28
  714. package/src/notify.ts +0 -341
  715. package/src/pglite.ts +0 -68
  716. package/src/plugins/index.ts +0 -1
  717. package/src/plugins/types.ts +0 -144
  718. package/src/postgres/index.ts +0 -1291
  719. package/src/proxy/collection.ts +0 -17
  720. package/src/proxy/handler.ts +0 -159
  721. package/src/proxy/index.ts +0 -21
  722. package/src/proxy/mutation-detector.ts +0 -281
  723. package/src/proxy/oplog.ts +0 -181
  724. package/src/proxy/types.ts +0 -46
  725. package/src/realtime/in-memory.ts +0 -33
  726. package/src/realtime/index.ts +0 -7
  727. package/src/realtime/types.ts +0 -90
  728. package/src/relay/bun-types.d.ts +0 -50
  729. package/src/relay/client-role/forward-engine.ts +0 -355
  730. package/src/relay/client-role/index.ts +0 -9
  731. package/src/relay/client-role/pull-engine.ts +0 -329
  732. package/src/relay/client-role/sequence-mapper.ts +0 -201
  733. package/src/relay/evaluation/relay-paths.ts +0 -699
  734. package/src/relay/evaluation/rust-boundary.ts +0 -464
  735. package/src/relay/index.ts +0 -50
  736. package/src/relay/migrate.ts +0 -113
  737. package/src/relay/mode-manager.ts +0 -142
  738. package/src/relay/realtime.ts +0 -207
  739. package/src/relay/relay.ts +0 -431
  740. package/src/relay/schema.ts +0 -171
  741. package/src/relay/server-role/index.ts +0 -338
  742. package/src/relay/server-role/pull.ts +0 -43
  743. package/src/relay/server-role/push.ts +0 -164
  744. package/src/s3/index.ts +0 -346
  745. package/src/service-worker/index.ts +0 -773
  746. package/src/snapshot-artifacts/sqlite-bun.ts +0 -168
  747. package/src/snapshot-artifacts.ts +0 -896
  748. package/src/snapshot-chunks/db-metadata.ts +0 -537
  749. package/src/snapshot-chunks/index.ts +0 -8
  750. package/src/snapshot-chunks/types.ts +0 -105
  751. package/src/snapshot-chunks.ts +0 -453
  752. package/src/sqlite/index.ts +0 -1064
  753. package/src/sqlite3.ts +0 -137
  754. package/src/stats.ts +0 -180
  755. package/src/subscriptions/cache.ts +0 -376
  756. package/src/subscriptions/index.ts +0 -2
  757. package/src/subscriptions/resolve.ts +0 -357
  758. package/src/sync.ts +0 -111
package/README.md CHANGED
@@ -1,24 +1,784 @@
1
1
  # @syncular/server
2
2
 
3
- Framework-agnostic server sync engine for Syncular: commit log, push/pull, scopes/subscriptions, snapshots, blobs, pruning/compaction, and proxy support.
3
+ Framework-free embeddable SSP2 protocol library. Core surface:
4
+ `handleSyncRequest(bytes, ctx) → bytes` over host-provided storage /
5
+ scope-resolution / segment-store interfaces, plus the transport-agnostic
6
+ realtime hub (§8), the direct segment download handler (§5.5), commit-log
7
+ pruning (§4.6), and signed-URL token issuance (§5.4). `SPEC.md` is
8
+ normative for everything on the wire; this README covers the **host
9
+ surface** — in particular the ops seam and the pruning runbook.
4
10
 
5
- Typically used with `@syncular/server/hono` for HTTP routes, WebSocket realtime, and the admin console API.
11
+ ## Deployment matrix (runtime adapters, TODO §4.2)
6
12
 
7
- ## Install
13
+ The server core is **runtime-neutral TypeScript** — `handleSyncRequest` and
14
+ the realtime session speak only Web `Request`/`Response`/`fetch`/Web-Crypto,
15
+ no Bun- or Node-only builtin (enforced by a static import-graph scan,
16
+ `test/runtime-neutrality.test.ts`). Adapters wire that core to a runtime.
17
+ The supported set, and what deliberately does **not** get an adapter:
8
18
 
9
- ```bash
10
- npm install @syncular/server
19
+ | Runtime | Adapter | Transport | Storage | Status |
20
+ | --- | --- | --- | --- | --- |
21
+ | **Bun / Node** | `@syncular/server-hono` | HTTP (`POST /sync`, segments, blobs) **+ WS realtime** (§8, host-driven upgrade) | any: `SqliteServerStorage`, `PostgresServerStorage`, memory | **Supported now** — the reference deployment; runs the full conformance catalog on both bindings. |
22
+ | **Cloudflare Workers** | `@syncular/server-workers` | HTTP binding via Hono (Workers-native) **+ WS realtime** (§8, Durable Object host with hibernation) | `D1ServerStorage` (D1); R2-as-S3 for segments/blobs (§5.4 delegated presign) | **Supported now** — this rung. Realtime rides a **Durable Object** (`SyncularRealtimeDO`), opt-in; HTTP-only is also fully conformant (below). |
23
+ | Raw Deno / edge-misc | — | — | — | **Not adapted** (policy below). |
24
+
25
+ **The policy for "not adapted".** Untested ≠ unsupported forever. The core
26
+ is runtime-neutral TS, so Deno/edge would very likely run it — but an adapter
27
+ is only *supported* where the conformance catalog can run against it. We ship
28
+ adapters for the runtimes where we run conformance (Bun/Node fully; Workers
29
+ HTTP via the fetch-handler round-trip tests), and we do not claim runtimes we
30
+ do not test. Deno is a plausible future adapter the day someone runs the
31
+ catalog on it; until then it is neutral-core-friendly, not supported.
32
+
33
+ **Workers realtime — the Durable Object.** SPEC §1.1's two bindings are two
34
+ framings of one handler; an **HTTP-only deployment is fully conformant**
35
+ (clients that cannot open the socket sync over `POST /sync`, identical
36
+ semantics — a smaller complete deployment, not a degraded one), so realtime on
37
+ Workers is opt-in. When enabled it rides a **Durable Object**
38
+ (`SyncularRealtimeDO`): one DO per partition hosting the `RealtimeHub` (the DO
39
+ id derived from the partition, so a partition's sockets and its commit fan-out
40
+ are co-located and single-threaded — also the natural per-partition write
41
+ serialization point the D1 storage wants); WebSocket **hibernation** so idle
42
+ sockets don't bill wall time (the existing `RealtimeSession` is the
43
+ per-connection state machine, driven from the hibernation callbacks and
44
+ rehydrated from a minimal socket attachment + the D1 client record on wake);
45
+ storage via the same **D1** binding so realtime rounds and `POST /sync` rounds
46
+ share one commit log and one segment store; commit fan-out (§8.2) runs in-DO
47
+ (no LISTEN/NOTIFY needed — writes and sockets are co-located), and an HTTP push
48
+ landing in a plain isolate wakes the partition's DO (the in-platform
49
+ LISTEN/NOTIFY analogue). Full shape, wiring, hibernation semantics, and the
50
+ manual real-workerd smoke recipe in `@syncular/server-workers/README.md`.
51
+
52
+ **Relay does not return (decision).** v1 shipped a *relay* — a bridge that let
53
+ a self-hosted server forward realtime to a managed realtime service, because
54
+ v1's realtime was a separate socketed subsystem the self-hosted core couldn't
55
+ serve on its own. v2 has no such gap: realtime is the **second binding of the
56
+ same handler** (§8.7, Direction decision 1 — the WS-native loop), so any host
57
+ that runs the core serves realtime directly; multi-instance fanout is covered
58
+ by **LISTEN/NOTIFY** on Postgres (below), and the Workers case is covered by
59
+ the **DO design** (writes and sockets co-located per partition). Every job the
60
+ relay did is now done by a binding of the core or by in-database fanout —
61
+ reintroducing a relay would add a hop, a second protocol surface, and a
62
+ managed dependency for zero capability the core lacks. So it is retired, not
63
+ ported.
64
+
65
+ ## Structured events (the ops seam)
66
+
67
+ One optional interface, `SyncularServerEvents`, carries every
68
+ operator-relevant signal as a typed, JSON-able, stable-shaped event:
69
+
70
+ ```ts
71
+ import { consoleJsonEvents, type SyncServerConfig } from '@syncular/server';
72
+
73
+ const config: SyncServerConfig = {
74
+ schema, storage, segments, resolveScopes,
75
+ events: consoleJsonEvents(), // one JSON line per event on stdout
76
+ };
77
+ ```
78
+
79
+ There is no logger dependency and no formatting — emission only. The same
80
+ shapes feed one-line JSON logs, metrics counters, and error trackers; a
81
+ Sentry adapter is a ~20-line `emit` implementation over this seam. The
82
+ events sink rides on `SyncServerConfig`, so the Hono adapter (and any
83
+ other adapter that spreads the config into the request context) passes it
84
+ through with no extra wiring. The realtime hub and `pruneCommitLog` take
85
+ the same sink via their own config/options (they run outside the request
86
+ context). The demo server wires it behind `SYNCULAR_DEMO_EVENTS=1`.
87
+
88
+ ### Guarantees
89
+
90
+ - **Never throws through.** Emission is fire-and-forget: a throwing
91
+ `emit` is swallowed at the seam and cannot affect request processing,
92
+ realtime delivery, or pruning. (Tested.)
93
+ - **Zero cost when off.** With no sink configured, no event object is
94
+ ever built — every call site checks the sink before constructing the
95
+ event. The benches run with events unset.
96
+ - **Stable, JSON-able shapes.** Flat objects, no `undefined` values, no
97
+ classes; `JSON.stringify` round-trips every event. Shapes and `type`
98
+ strings are append-only surface.
99
+ - **Virtual-clock clean.** All timestamps and durations come from the ctx
100
+ clock (`clock` on the config / hub; `nowMs` for prune), so conformance
101
+ and tests under a virtual clock stay deterministic. Wall clock is never
102
+ read behind the host's back.
103
+
104
+ ### Event catalog
105
+
106
+ | Event | When | Key fields |
107
+ | --- | --- | --- |
108
+ | `request.handled` | Once per `POST /sync`, after the response bytes are fully produced (or the request was rejected up front) | `kind` (`sync`), `partition`, `actorId`, `durationMs`, `bytesIn`, `bytesOut`, `outcome` (`ok` \| `schema_floor` \| `rejected` \| `error`), `errorCode?`, `pushCommits`, `pulled`, `subscriptions` |
109
+ | `push.applied` | A `PUSH_COMMIT` applied, or replayed from the idempotency cache (§2.3) | `clientId`, `clientCommitId`, `operations`, `commitSeq?`, `replay` |
110
+ | `push.rejected` | A commit rejected (§6.3) | `clientId`, `clientCommitId`, `operations`, `code` (§10.2), `opIndex` |
111
+ | `push.conflicted` | A commit terminated by a version conflict (§6.2) | `clientId`, `clientCommitId`, `operations`, `opIndex` |
112
+ | `pull.served` | Once per served pull half, after all sections streamed | `clientId`, `subscriptions[]`: `{id, table, status, mode` (`bootstrap` \| `incremental` \| `none`)`, fromCursor, nextCursor, commits, changes, segments[]}`; each segment: `{mediaType` (`rows` \| `sqlite`)`, delivery` (`inline` \| `ref`)`, origin` (`built` \| `reused`)`, bytes, rows}` |
113
+ | `segment.downloaded` | Every direct segment download (§5.5), success or failure | `segmentId`, `outcome` (`ok` \| `error`), `errorCode?`, `mediaType?`, `bytes?`, `durationMs` |
114
+ | `blob.swept` | Every `sweepOrphanBlobs` pass (§5.9.2 orphan GC) | `partition`, `swept` (deleted count), `referenced` (keep-set size), `graceMs` |
115
+ | `realtime.opened` | A socket registered with the hub and got `hello` (§8.1) | `sessionId`, `clientId`, `registrations`, `cursor`, `latestSeq` |
116
+ | `realtime.closed` | A session left the hub (once per session) | `sessionId`, `durationMs` |
117
+ | `realtime.delta` | A delta message pushed over the socket (§8.2) | `sessionId`, `commitSeq`, `bytes`, `changes` |
118
+ | `realtime.wake` | A `sync` wake-up sent (§8.3) | `sessionId`, `reason` (`catchup-required` \| `delta-too-large` \| `reset-required`) |
119
+ | `prune.completed` | Every `pruneCommitLog` pass, moved or not | `partition`, `previousHorizonSeq`, `horizonSeq`, `advanced`, `removedCommits` |
120
+ | `scopes.resolve_failed` | The host `resolveScopes` callback threw — the §3.2/§3.4 fail-loud path | `phase` (`request` \| `realtime` \| `segment-download`), `message` |
121
+
122
+ All events also carry `type`, `atMs`, and (where a request identity
123
+ exists) `partition` / `actorId`.
124
+
125
+ ## Admin / console surface (`SyncularAdmin`)
126
+
127
+ The operator-facing read surface over the server core. It is a module in
128
+ this package — **not** a separate UI package — and adds **zero** wire
129
+ protocol: SPEC.md says nothing about it, because authorization for these
130
+ reads is entirely the host's. It is the v2 answer to v1's full React
131
+ console app: the same 80% operator value (who's connected, what's flowing,
132
+ horizon health, the event tail) as a handful of read-only, partition-scoped,
133
+ JSON-able queries.
134
+
135
+ ### The event ring (the "event stream")
136
+
137
+ `RingBufferEvents` is a `SyncularServerEvents` sink that retains the last N
138
+ events in memory (bounded — oldest dropped when full) with a
139
+ `query({type?, sinceMs?, limit})`. It is the event stream without any
140
+ infrastructure dependency. Compose it with any other sink so the console
141
+ tail and your logs/metrics see the same emissions:
142
+
143
+ ```ts
144
+ import {
145
+ RingBufferEvents, composeEvents, consoleJsonEvents, SyncularAdmin,
146
+ } from '@syncular/server';
147
+
148
+ const ring = new RingBufferEvents({ capacity: 1000 });
149
+ const config: SyncServerConfig = {
150
+ schema, storage, segments, resolveScopes,
151
+ events: composeEvents(ring, consoleJsonEvents()), // both see every event
152
+ };
153
+ const admin = SyncularAdmin.fromConfig(config, { ring });
154
+ ```
155
+
156
+ ### Query surface
157
+
158
+ Every method is read-only and partition-scoped:
159
+
160
+ | Method | Returns |
161
+ | --- | --- |
162
+ | `listClients(partition)` | Known clients: `clientId`, `actorId`, `cursor`, `updatedAtMs`, `subscriptions[]`, and an `active` flag (cursor touched within the §4.6 active window). |
163
+ | `listCommits(partition, {afterSeq?, limit?, table?})` | Commit-log **metadata** (never payloads), newest first: `commitSeq`, `clientId`, `clientCommitId`, `actorId`, `createdAtMs`, `changeCount`, `tables[]`. |
164
+ | `inspectRow(partition, table, rowId)` | `{exists, serverVersion?, scopes?}` — current row version + stored scopes, payload **not** decoded. |
165
+ | `scopeActivity(partition, {variable, value}, {limit?})` | Recent commits touching one scope key, via the §3.1 change-scope index (never a log scan). |
166
+ | `horizonStatus(partition)` | `{maxCommitSeq, horizonSeq, retainedCommits, activeCursorFloor, recommendedHorizonSeq, recommendation}` — the horizon a prune pass would reach now (§4.6) + a coarse `up-to-date` / `prune-recommended`. |
167
+ | `segmentStats()` / `blobStats(partition)` / `stats(partition)` | Counts/bytes where the stores expose them (segments split rows/sqlite). `undefined` when a store omits `stats()`. |
168
+ | `events({type?, sinceMs?, limit?})` | The ring tail, newest first. Empty when no ring is wired (`hasEventStream` reports which). |
169
+
170
+ The query surface leans on **additive, optional** storage/store methods
171
+ (`ServerStorage.listClientRecords` / `listCommitMetadata` / `scopeActivity` /
172
+ `getRowScopes`; `SegmentStore.stats`; `BlobStore.stats`) — the established
173
+ optional-method pattern. `SqliteServerStorage`, `PostgresServerStorage`,
174
+ `D1ServerStorage`, and the memory/sqlite stores implement them; the shared
175
+ `ServerStorage` contract suite exercises them on all backends. A backend that
176
+ omits one makes the corresponding admin read fail loud (it never returns a
177
+ silently-empty console). The `S3SegmentStore` **does** report `stats()` — from
178
+ a LIST-free pointer-object accumulator (see "S3 stats" below) — but its
179
+ counters are marked `approximate: true`, an additive field the admin surface
180
+ carries through so the console can label them honestly. The exact in-process
181
+ stores (memory/sqlite) omit the marker.
182
+
183
+ ### HTTP routes + the single console page
184
+
185
+ `@syncular/server-hono` exports `createSyncularAdminRoutes(admin, opts)`,
186
+ a mountable Hono sub-app. **The auth seam is required**: the factory throws
187
+ if you omit the `authorize` guard — there is no default-open admin. Every
188
+ endpoint (including the page) runs the guard first; a falsy result is a 401.
189
+
190
+ ```ts
191
+ import { createSyncularAdminRoutes } from '@syncular/server-hono';
192
+
193
+ const routes = createSyncularAdminRoutes(admin, {
194
+ defaultPartition: 'main',
195
+ authorize: ({ request }) => isOperator(request), // YOUR check — mandatory
196
+ });
197
+ app.route('/admin', routes);
198
+ ```
199
+
200
+ | Route | Mirrors |
201
+ | --- | --- |
202
+ | `GET /` | The console page (see below). |
203
+ | `GET /clients` | `listClients` |
204
+ | `GET /commits?afterSeq&limit&table` | `listCommits` |
205
+ | `GET /rows/:table/:rowId` | `inspectRow` |
206
+ | `GET /scope-activity?variable&value&limit` | `scopeActivity` |
207
+ | `GET /horizon` | `horizonStatus` |
208
+ | `GET /stats` | `stats` |
209
+ | `GET /events?type&sinceMs&limit` | `events` (ring tail) |
210
+
211
+ `?partition=` selects the partition (falls back to `defaultPartition`).
212
+
213
+ `GET /` (or `/admin`) serves a **single static HTML page** — zero
214
+ framework, no build step, no React. It fetches the sibling JSON endpoints
215
+ (relative to its own mount path, so it works under any prefix and the same
216
+ guard covers its XHRs), renders tables for horizon, store stats, clients,
217
+ recent commits, and the event tail, with an auto-refresh toggle (2 s poll).
218
+ This is the ~300-line answer to v1's console app: 5% of the code, the 80%
219
+ operator value.
220
+
221
+ **No SSE (yet).** `GET /events` is a polled ring query; the page's
222
+ auto-refresh polls it. Server-Sent-Events streaming was deliberately
223
+ skipped for this rung — the ring is pull-only, so SSE would need a
224
+ push-notification path from the sink into open connections (extra
225
+ machinery for marginal benefit at admin cadence). Polling is the right
226
+ rung; SSE is a noted follow-up.
227
+
228
+ The demo server (`apps/demo`) mounts the admin behind a dev guard:
229
+ `SYNCULAR_DEMO_ADMIN=1` enables `/admin` (optionally token-gated with
230
+ `SYNCULAR_DEMO_ADMIN_TOKEN`), so the console is inspectable live.
231
+
232
+ > Docs-site coverage of the console is a follow-up: the docs app is owned
233
+ > by a concurrent workstream this round (the schema-bump page), so this
234
+ > README is the console's documentation home for now.
235
+
236
+ ## Segment storage on S3 / R2 (`S3SegmentStore`)
237
+
238
+ Three `SegmentStore` backends ship in-tree and pass one shared contract
239
+ suite (`test/segment-store-contract.ts`): `MemorySegmentStore` (tests,
240
+ single process), `SqliteSegmentStore` (single node), and
241
+ `S3SegmentStore` — the production backend for any S3-compatible object
242
+ store (AWS S3, Cloudflare R2, MinIO). It is dependency-free: SigV4 is
243
+ hand-rolled over `fetch` (`sigv4.ts`, pinned by the published AWS test
244
+ vectors).
245
+
246
+ ```ts
247
+ import { S3SegmentStore, s3PresignedUrls } from '@syncular/server';
248
+
249
+ const segments = new S3SegmentStore({
250
+ endpoint: 'https://s3.eu-central-1.amazonaws.com', // origin only, no bucket
251
+ region: 'eu-central-1',
252
+ bucket: 'my-app-segments',
253
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
254
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
255
+ keyPrefix: 'syncular/', // optional namespace inside the bucket
256
+ ttlMs: 24 * 60 * 60 * 1000, // §5.1 default
257
+ });
258
+
259
+ const config: SyncServerConfig = {
260
+ schema, storage, resolveScopes,
261
+ segments,
262
+ signedUrls: s3PresignedUrls(segments, { ttlSeconds: 900 }), // §5.4 delegated presign
263
+ };
264
+ ```
265
+
266
+ **Cloudflare R2 specifics.** The endpoint is your account's S3 API host
267
+ and the region is always `auto`:
268
+
269
+ ```ts
270
+ const segments = new S3SegmentStore({
271
+ endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
272
+ region: 'auto',
273
+ bucket: 'my-app-segments',
274
+ accessKeyId: R2_ACCESS_KEY_ID, // R2 API token pair
275
+ secretAccessKey: R2_SECRET_ACCESS_KEY,
276
+ });
277
+ ```
278
+
279
+ MinIO works the same way (`endpoint: 'http://127.0.0.1:9000'`, any
280
+ region string). Requests are path-style (`{endpoint}/{bucket}/{key}`),
281
+ which all three providers accept.
282
+
283
+ **Key layout.** Deterministic, so every lookup is a GET/HEAD — never a
284
+ LIST:
285
+
286
+ - `{keyPrefix}seg/sha256/{hex}` — the segment bytes, verbatim (the
287
+ object body is exactly the content-addressed bytes, so presigned GETs
288
+ serve them directly and the client's §5.1 hash check passes). The
289
+ record metadata rides in object user metadata
290
+ (`x-amz-meta-syncular-record`, base64url JSON), so `get` is one GET.
291
+ - `{keyPrefix}find/{sha256(reuse key)}.json` — the §5.3 whole-table
292
+ reuse pointer, written only for `rowCursor: null` segments; `find` is
293
+ one GET plus a HEAD to confirm the segment object still exists.
294
+
295
+ **TTL and lifecycle.** Expiry is store-side and authoritative:
296
+ `expiresAtMs` (`put` time + `ttlMs`, default 24 h) is recorded with the
297
+ record; `get` returns expired records so the §5.5 endpoint can answer
298
+ the precise, retryable `sync.segment_expired`, and `find` filters them
299
+ itself. Bucket lifecycle expiration is *garbage collection only* — set
300
+ it comfortably **above** `ttlMs` (e.g. 2 days for the 24 h default) and
301
+ never below it. After lifecycle deletes an object, clients see
302
+ `sync.not_found` instead of `sync.segment_expired`; both recover by
303
+ re-pulling, but the former loses the "just re-pull, this is normal"
304
+ signal, so keep the GC margin generous.
305
+
306
+ ### S3 stats — a LIST-free, approximate accumulator
307
+
308
+ `S3SegmentStore.stats()` reports store-wide `{count, bytes, rowsSegments,
309
+ sqliteSegments}` for the admin console **without a bucket LIST** — a LIST
310
+ would defeat the store's whole every-lookup-is-a-GET/HEAD design and cost
311
+ real money at scale. Instead the store keeps a tiny counter object under a
312
+ fixed key (`{keyPrefix}stats/segments.json`) and folds each new segment into
313
+ it read-modify-write on `put`. A `HEAD` before the segment PUT detects an
314
+ idempotent re-put (same content address ⇒ same key), so each distinct
315
+ segment is counted once.
316
+
317
+ The accumulator write is guarded by an **ETag compare-and-swap** — `If-Match`
318
+ against the ETag we read (or `If-None-Match: *` to create) — and the store
319
+ retries on a `412 PreconditionFailed`, so two writers folding concurrently do
320
+ not silently clobber each other's increment. AWS S3 and Cloudflare R2 both
321
+ honor these conditional headers.
322
+
323
+ Even so, the counters are **approximate**, and `stats()` marks them
324
+ `approximate: true` (an additive field the admin surface carries through, so
325
+ `admin.segmentStats()` / `admin.stats()` expose it and the console labels the
326
+ numbers honestly). They can drift: a crash between the segment PUT and the
327
+ accumulator CAS, lifecycle GC deleting objects the accumulator still counts,
328
+ or enough concurrent writers to exhaust the CAS retry budget. They are a
329
+ health gauge, not an invoice — reconcile against a periodic inventory report
330
+ if you need an exact number. The exact in-process stores (memory / sqlite)
331
+ count on demand and omit the marker.
332
+
333
+ The blob store's `stats()` uses the same accumulator + `approximate: true`
334
+ marker (see "Blob bytes on S3 / R2" below).
335
+
336
+ ## Blob bytes on S3 / R2 (`S3BlobStore`)
337
+
338
+ File-attachment bytes (§5.9) get the same object-store backend as segments.
339
+ `S3BlobStore` is the blob twin of `S3SegmentStore` — same hand-rolled SigV4,
340
+ same content-addressed key layout, same LIST-free-on-the-hot-path stats
341
+ accumulator — for AWS S3, Cloudflare R2, or MinIO. It closes the
342
+ "attachments are SQLite-only" gap: a Workers/edge or horizontally-scaled
343
+ deployment can now store blobs durably in an object store instead of the
344
+ database.
345
+
346
+ ```ts
347
+ import {
348
+ S3BlobStore,
349
+ s3PresignedBlobUploads,
350
+ s3PresignedBlobUrls,
351
+ } from '@syncular/server';
352
+
353
+ const blobs = new S3BlobStore({
354
+ endpoint: 'https://s3.us-east-1.amazonaws.com',
355
+ region: 'us-east-1',
356
+ bucket: 'my-attachments',
357
+ accessKeyId: AWS_ACCESS_KEY_ID,
358
+ secretAccessKey: AWS_SECRET_ACCESS_KEY,
359
+ keyPrefix: 'syncular/', // optional namespace inside the bucket
360
+ });
361
+
362
+ const config: SyncServerConfig = {
363
+ // …schema, storage, segments, resolveScopes…
364
+ blobs,
365
+ // Presigned DOWNLOAD (always-issue): serve blob downloads as provider
366
+ // presigned GET URLs (§5.9.5). The client fetches bytes straight from the
367
+ // object store — the sync server exits the download egress path.
368
+ blobSignedUrls: s3PresignedBlobUrls(blobs, { ttlSeconds: 900 }),
369
+ // Presigned UPLOAD (direct-to-storage): mint single presigned PUT URLs so
370
+ // clients upload straight to the object store, bypassing the server upload
371
+ // bandwidth path (§5.9.3). Optional — absent ⇒ clients stream through the
372
+ // direct `PUT /blobs/{blobId}` endpoint (a capability, never a fallback).
373
+ blobUploadUrls: s3PresignedBlobUploads(blobs, { ttlSeconds: 900 }),
374
+ };
11
375
  ```
12
376
 
13
- ## Documentation
377
+ ### Presigned blobs, end to end (§5.9.3 / §5.9.5)
14
378
 
15
- - Server setup: https://syncular.dev/docs/build/server-setup
16
- - How sync works: https://syncular.dev/docs/introduction/architecture
17
- - Server overview: https://syncular.dev/docs/server/overview
379
+ Two independent presign switches let the sync server step out of the blob
380
+ byte path in both directions:
18
381
 
19
- ## Links
382
+ **Download — `blobSignedUrls` (always-issue).** After the §5.9.5 row-derived
383
+ authorization check passes, `GET /blobs/{blobId}` returns
384
+ `{ url, urlExpiresAtMs }` and **no bytes**; the client fetches the URL directly
385
+ (no host auth — the URL is the entire grant) and re-verifies the content
386
+ address. **Always-issue, not accept-bit negotiation** (the pinned decision,
387
+ §5.9.5): unlike segments — where a descriptor rides the pull stream to a client
388
+ that may be unable to fetch a bare URL, so issuance is gated on accept bit 3 —
389
+ a blob download is a plain request/response, so the response simply carries the
390
+ URL and a client that cannot consume it re-requests. Always-issue is *harmless*
391
+ (the authorized endpoint is the same route; nothing is stuck on a stream) and
392
+ *simpler* (no accept-bit plumbing on a non-pull path). Recovery mirrors §5.4:
393
+ a failed or expired URL fetch **re-requests** the endpoint (which re-authorizes
394
+ and mints a fresh URL) — never a fall-through.
20
395
 
21
- - GitHub: https://github.com/syncular/syncular
22
- - Issues: https://github.com/syncular/syncular/issues
396
+ **Upload `blobUploadUrls` (the grant flow).** `POST
397
+ /blobs/{blobId}/upload-grant` (host-authenticated, body
398
+ `{ byteLength, mediaType? }`) mints a single presigned PUT; the client PUTs
399
+ bytes straight to the object store. **Upload authz is host-authentication only**
400
+ (any authenticated actor may obtain a grant within the size cap) — uploading
401
+ bytes is not a scope-bearing act, because the content address discloses nothing
402
+ and integrity is enforced at *reference time* (the §5.9.6 push existence check
403
+ + every download's content-address verify), never by a store-side hash
404
+ recompute. The **size cap is enforced up front** against the declared
405
+ `byteLength`, before any URL is minted (the object-store hop cannot re-check the
406
+ streamed byte count). An already-present blob returns `{ present: true }` (skip
407
+ the PUT, idempotent §5.9.3). A **single PUT only** — never a multipart or chunk
408
+ protocol; resumable upload, when it lands, is provider multipart behind this
409
+ same grant. Absent config ⇒ the client streams through the direct
410
+ host-authenticated `PUT /blobs/{blobId}` endpoint — a capability choice, not a
411
+ fallback (that endpoint was always the other path).
23
412
 
24
- > Status: Alpha. APIs and storage layouts may change between releases.
413
+ **Cloudflare R2.** Identical to the segment store point `endpoint` at
414
+ `https://<account-id>.r2.cloudflarestorage.com`, `region: 'auto'`, and use an
415
+ R2 API-token access-key pair. R2 honors the SigV4 header/query auth and the
416
+ `ListObjectsV2` the sweep uses.
417
+
418
+ **Key layout.** `{keyPrefix}blob/{partition}/sha256/{hex}` — content-addressed
419
+ and **partition-scoped** (the same bytes uploaded under two partitions are two
420
+ objects; a partition cannot read another's attachment by guessing a content
421
+ address). The object body is the blob bytes **verbatim**, so a presigned GET
422
+ serves exactly the content-addressed bytes and the client's §5.9.1 hash check
423
+ passes. `byteLength` + optional `mediaType` + `createdAtMs` ride along as
424
+ object user metadata (`x-amz-meta-syncular-blob` = base64url(JSON)), so `get`
425
+ is a single GET.
426
+
427
+ ### Durability, not TTL — the difference from segments
428
+
429
+ This is the honest interface difference from `S3SegmentStore`. **Segments are
430
+ TTL cache entries; blobs are durable.** A blob referenced by a live row must
431
+ stay downloadable **indefinitely** (§5.9.5 B3). So `S3BlobStore` writes **no
432
+ `expiresAtMs`, has no `ttlMs` config, and maps to no S3 lifecycle-expiration
433
+ rule.** Reclamation is **reference-driven, not time-driven**: the only thing
434
+ that deletes a blob is the orphan sweep, and it deletes only blobs *no live
435
+ row references*.
436
+
437
+ > **Do NOT put an S3/R2 lifecycle-expiration rule on the `blob/` prefix.** It
438
+ > would delete still-referenced attachments out from under live rows. This is
439
+ > the exact opposite of the segment guidance (where lifecycle expiration above
440
+ > `ttlMs` is *encouraged* as GC). Blobs are cleaned by the sweep below.
441
+
442
+ ### Orphan sweep (the GC runbook)
443
+
444
+ Nothing reclaims blobs automatically — the host schedules the sweep, the blob
445
+ analogue of `pruneCommitLog`. `sweepOrphanBlobs(storage, blobStore, partition,
446
+ { graceMs })` reads the live keep-set from the §5.9.4 reference index
447
+ (`storage.listReferencedBlobIds`) and deletes every blob that is **both**
448
+ unreferenced **and** older than the grace period. It emits one `blob.swept`
449
+ ops event (`{ swept, referenced, graceMs }`) and returns the deleted ids.
450
+
451
+ ```ts
452
+ import { sweepOrphanBlobs } from '@syncular/server';
453
+
454
+ // A periodic per-partition GC job (hourly to daily is sensible).
455
+ const { swept } = await sweepOrphanBlobs(storage, blobs, partition, {
456
+ graceMs: 24 * 60 * 60 * 1000, // default; see the race note below
457
+ events,
458
+ });
459
+ ```
460
+
461
+ **The grace period is not optional cleverness — it is the correctness
462
+ mechanism.** Uploads are *content-addressed and land before the referencing
463
+ push* (§5.9.2 upload-before-reference): a client `PUT`s the bytes, then pushes
464
+ the row. Between those two steps the blob is legitimately **unreferenced**. If
465
+ the sweep ran with no grace it would delete that fresh upload before its push
466
+ arrived. So the grace period must comfortably exceed any sane upload→push
467
+ latency — the **default is 24 h**, deliberately far above any push window.
468
+ Lower it only if you fully understand your clients' outbox latency; there is
469
+ no upside to a tight grace and a real data-loss risk. The sweep compares
470
+ against the blob's **upload time** (`createdAtMs` from object metadata), and an
471
+ idempotent re-upload does **not** reset that clock.
472
+
473
+ `sweepOrphanBlobs` **requires** `storage.listReferencedBlobIds` (the §5.9.4
474
+ reference index — SQLite, D1, and Postgres all implement it). Against a
475
+ storage without it, the helper throws rather than sweep with an empty keep-set
476
+ (which would delete everything). The `S3BlobStore` sweep is the store's **only
477
+ LISTing operation** — it pages `ListObjectsV2` over the partition's `blob/`
478
+ prefix, an admin/GC path off the hot path; every point lookup is still a
479
+ GET/HEAD.
480
+
481
+ ### Blob stats — the same approximate accumulator
482
+
483
+ `S3BlobStore.stats()` reports store-wide `{count, bytes}` from a fixed counter
484
+ object (`{keyPrefix}stats/blobs.json`) folded read-modify-write on `put` under
485
+ the same ETag compare-and-swap as segments, and marks the result
486
+ `approximate: true` (the marker already present on `BlobStoreStats`, carried
487
+ through `admin.blobStats()`). A `HEAD` before the PUT detects an idempotent
488
+ re-upload so distinct blobs count once; the sweep decrements on delete. Same
489
+ caveats as segment stats — a health gauge, not an invoice. The in-process
490
+ stores (memory / sqlite) count on demand and omit the marker.
491
+
492
+ ### Presigned blob downloads (§5.9.5)
493
+
494
+ Setting `blobSignedUrls: s3PresignedBlobUrls(blobs)` makes the server issue a
495
+ provider-presigned GET URL for a blob download — **but only after the
496
+ row-derived authorization check passes** (`handleBlobDownload` resolves the
497
+ actor's scopes and tests the referencing rows first; a `blobId` is never a
498
+ bearer capability minted from the id alone). The signed object key embeds the
499
+ `blobId`, TTL SHOULD be ≤ 15 min (default 900 s), and the URL is a short-lived
500
+ grant to exactly those immutable bytes. The issued `url`/`urlExpiresAtMs` ride
501
+ additively on `BlobDownloadResult` alongside the bytes; the mounted §5.9.5
502
+ direct-download endpoint stays the default serving path. Client consumption of
503
+ the presigned URL (following it instead of streaming bytes through the sync
504
+ server) is a later rung — the server-side issuance ships now.
505
+
506
+ ### Native HMAC vs delegated presign (§5.4)
507
+
508
+ `SyncServerConfig.signedUrls` accepts either scheme; the pull emits
509
+ `SEGMENT_REF.url`/`urlExpiresAtMs` identically for both (issuance always
510
+ happens inside the pull, immediately after scope resolution), and
511
+ clients cannot tell them apart.
512
+
513
+ - **Native HMAC (`SignedUrlConfig`)** — you serve the segment bytes
514
+ yourself (or from something that delegates auth to you, e.g. a CDN
515
+ worker calling `verifySegmentToken` at the edge). The `st` token binds
516
+ segment + scope digest + partition audience. Choose this when segments
517
+ live in `SqliteSegmentStore` or when you want claim-level binding at
518
+ your own edge.
519
+ - **Delegated presign (`DelegatedPresignConfig`, via
520
+ `s3PresignedUrls(store)`)** — the object store enforces the grant; the
521
+ sync server never proxies segment bytes (zero egress through it — the
522
+ bootstrap-storm answer). The §5.4 equivalence rule holds by
523
+ construction: the signed object key embeds exactly one `segmentId`,
524
+ and the expiry obeys the same ≤ 15 min TTL guidance (default 900 s for
525
+ both schemes).
526
+
527
+ Either way, keep the §5.5 direct-download endpoint mounted: it is the
528
+ mandatory fallback for expired/failed URLs and for clients that never
529
+ advertised accept bit 3.
530
+
531
+ ### CDN in front
532
+
533
+ Segment URLs are safe to cache *by content*: the object key is the
534
+ content address (`seg/sha256/{hex}`), the bytes are immutable for a
535
+ given key, and the client verifies the hash after download (§5.1) — so
536
+ a CDN can cache segment objects keyed on the path alone and can never
537
+ serve wrong bytes, only stale-but-correct ones. Two rules:
538
+
539
+ - **Strip the query from the cache key, never from the auth check.**
540
+ Presigned query parameters (or the native `st` token) differ per
541
+ client; the path is the content address. Configure the CDN to cache on
542
+ the path while still forwarding the query for origin authorization
543
+ (or validate at the edge: `verifySegmentToken` for native tokens).
544
+ Never cache the *authorization decision*.
545
+ - **Align the CDN TTL with the store TTL.** Cache lifetime at or below
546
+ `ttlMs` keeps the CDN from serving objects the store already declared
547
+ expired (harmless — the client would still verify and apply — but it
548
+ masks the §5.1 cache-entry semantics and can hide lifecycle GC).
549
+ Content-addressing makes over-caching safe, not useful.
550
+
551
+ The §5.5 endpoint responses stay `Cache-Control: private, max-age=0`
552
+ — only segment-object URLs are CDN-cacheable, never the re-authorized
553
+ download path.
554
+
555
+ ## Horizon & pruning: operational guidance
556
+
557
+ The commit log grows forever unless you prune it. `pruneCommitLog`
558
+ (SPEC §4.6) advances the per-partition `horizonSeq` and deletes commits
559
+ at or below it. Nothing prunes automatically — the host schedules it.
560
+
561
+ **When to run.** A periodic job per partition — hourly to daily is the
562
+ sensible range; there is no benefit below the granularity of your
563
+ `activeWindowMs`. Prune is cheap when there is nothing to do (one cursor
564
+ scan + two point reads), so err on the side of running it often rather
565
+ than letting a backlog build. Pass `events` to get `prune.completed`
566
+ per pass.
567
+
568
+ **The retention floors (§4.6, encoded in `RetentionPolicy`).** The
569
+ horizon never advances past `min(cursor)` of *active* clients — clients
570
+ whose cursor record was touched within `activeWindowMs` (default 14
571
+ days). Two escape hatches keep laggards from pinning the log forever:
572
+ commits older than `ageForceMs` (default 30 days) may be pruned
573
+ regardless, and at least the newest `minRetainedCommits` (default 1000)
574
+ commits are always kept. Defaults are the v1 production values; raise
575
+ them freely, lower them with care.
576
+
577
+ **What `sync.cursor_expired` means operationally.** A client whose
578
+ cursor fell behind the horizon gets `SUB_START.status = reset` and
579
+ re-bootstraps from scratch (§4.7). That is correct behavior, not an
580
+ error — but its *rate* is your pruning health signal. A steady trickle
581
+ means devices returning from >30-day absences (expected). A spike means
582
+ you pruned faster than your fleet syncs: `ageForceMs` or
583
+ `activeWindowMs` is too tight for real usage, and you are paying for it
584
+ in bootstrap load (full re-scans + segment builds), not just in resets.
585
+ Observe it via `pull.served` subscriptions with `status: "reset"`.
586
+
587
+ **Segment TTL interplay.** Segments are cache entries, not durable state
588
+ (§5.1; default TTL 24 h). A bootstrap that resumes past segment expiry
589
+ answers `sync.segment_expired` and the client re-pulls for fresh
590
+ descriptors — again correct, again a cost signal. Keep the segment TTL
591
+ comfortably longer than the slowest plausible bootstrap (a multi-page
592
+ bootstrap must finish while its segments live), and note that pruning
593
+ and segment expiry compound: a reset storm triggers a bootstrap storm,
594
+ which the §5.3 image-reuse rule absorbs only while images stay
595
+ unexpired. If you see `origin: "built"` dominating `"reused"` for the
596
+ same table+scope during a storm, your TTL is shorter than the storm.
597
+
598
+ **What to alert on.**
599
+
600
+ - `push.rejected` rate, by `code` — a rising `sync.forbidden` share
601
+ usually means an authorization regression, not misbehaving clients.
602
+ (`push.conflicted` is normal offline-first traffic; alert only on
603
+ gross shifts.)
604
+ - `scopes.resolve_failed` — any nonzero rate. This is the fail-loud
605
+ path: every occurrence revokes subscriptions or rejects writes for a
606
+ real request, and it is almost always a host bug or a dead dependency
607
+ of the resolver.
608
+ - `request.handled` with `outcome: "error"` and `errorCode: "internal"`
609
+ — storage failures surfacing mid-stream.
610
+ - Reset rate (`pull.served` → `status: "reset"`) — see above; alert on
611
+ spikes relative to fleet size.
612
+ - Prune backlog: `prune.completed` with `advanced: false` for many
613
+ consecutive passes *while the log grows* means one laggard cursor
614
+ inside the active window is pinning retention — inspect
615
+ `listClientCursors` for the offender; the §4.6 floors bound the damage
616
+ to `ageForceMs`.
617
+ - `realtime.wake` with `reason: "delta-too-large"` — sustained
618
+ occurrences mean commits routinely exceed `maxDeltaBytes` and clients
619
+ are falling back to HTTP pulls; raise the limit or shrink commits.
620
+
621
+ ## Postgres storage (the production database path)
622
+
623
+ `SqliteServerStorage` (bun:sqlite) is the dev-speed default. For
624
+ production, `PostgresServerStorage` implements the same `ServerStorage`
625
+ contract against Postgres, with the inverted scope index carried through
626
+ as **covering indexes** so scope fanout is an index range scan, never a
627
+ scan-before-LIMIT (REVISE B2 — this was v1's production wound). The
628
+ schema (`POSTGRES_DDL`) and its index design live in
629
+ `src/postgres-storage.ts`; `storage.migrate()` applies it idempotently
630
+ (every DDL is `CREATE … IF NOT EXISTS`, run statement-by-statement, so
631
+ `migrate()` is safe to call on every boot).
632
+
633
+ **Blobs (§5.9.4) on Postgres.** `PostgresServerStorage` implements the
634
+ optional blob-reference index — `setBlobRefs` (in the commit transaction)
635
+ plus `listRowsReferencingBlob` / `listReferencedBlobIds` — at full parity
636
+ with the SQLite and D1 storages. The `sync_blob_refs` table keys
637
+ `(partition, tbl, row_id, blob_id)` and carries a secondary
638
+ `(partition, blob_id)` index that drives the §5.9.5 download-authorization
639
+ candidate set as an index range (asserted in `postgres-explain.test.ts`, same
640
+ no-`Seq Scan` doctrine as the scope indexes). So a Bun/Node **or** Workers
641
+ deployment on Postgres supports file attachments end-to-end: push writes the
642
+ row's references atomically with the commit, and the blob-download handler
643
+ authorizes via the reference index. The shared `ServerStorage` contract runs
644
+ its blob section on pglite alongside sqlite and D1.
645
+
646
+ ### The `PgExecutor` seam (zero runtime deps)
647
+
648
+ The server library never imports a Postgres driver. `PostgresServerStorage`
649
+ is written against the minimal `PgExecutor` interface (`query(text, params)`
650
+ plus a `transaction(fn)` scope) — you wire your driver of choice:
651
+
652
+ **Bun.sql** (built into bun):
653
+
654
+ ```ts
655
+ import {
656
+ PostgresServerStorage,
657
+ type PgExecutor,
658
+ type PgQueryable,
659
+ } from '@syncular/server';
660
+
661
+ function bunSqlExecutor(sql: import('bun').SQL): PgExecutor {
662
+ const over = (h: any): PgQueryable => ({
663
+ async query(text, params) {
664
+ const rows = await h.unsafe(text, params ? [...params] : []);
665
+ return { rows, rowCount: rows.length };
666
+ },
667
+ });
668
+ return {
669
+ query: over(sql).query,
670
+ transaction: (fn) => sql.begin((tx: any) => fn(over(tx))),
671
+ close: () => sql.end(),
672
+ };
673
+ }
674
+
675
+ const storage = new PostgresServerStorage(
676
+ bunSqlExecutor(new Bun.SQL(process.env.DATABASE_URL!)),
677
+ );
678
+ await storage.migrate();
679
+ ```
680
+
681
+ **node-postgres** (`pg`) — adapt a `Pool`:
682
+
683
+ ```ts
684
+ import { Pool, type PoolClient } from 'pg';
685
+ import { PostgresServerStorage, type PgExecutor } from '@syncular/server';
686
+
687
+ function pgPoolExecutor(pool: Pool): PgExecutor {
688
+ const over = (c: Pool | PoolClient) => ({
689
+ query: (text: string, params?: readonly unknown[]) =>
690
+ c.query(text, params ? [...params] : []),
691
+ });
692
+ return {
693
+ query: over(pool).query,
694
+ async transaction(fn) {
695
+ const client = await pool.connect();
696
+ try {
697
+ await client.query('BEGIN');
698
+ const result = await fn(over(client));
699
+ await client.query('COMMIT');
700
+ return result;
701
+ } catch (error) {
702
+ await client.query('ROLLBACK');
703
+ throw error;
704
+ } finally {
705
+ client.release();
706
+ }
707
+ },
708
+ close: () => pool.end(),
709
+ };
710
+ }
711
+ ```
712
+
713
+ **Type-parser note.** `commit_seq`/`server_version` are `int8`. Drivers
714
+ decode `int8` differently (node-postgres → `string`, Bun.sql → `bigint`,
715
+ pglite → `number`); the storage layer coerces every sequence read through
716
+ `Number(...)`, so no driver-specific type-parser config is required.
717
+ `bytea` must decode to `Uint8Array`/`Buffer` (all three do).
718
+
719
+ **Tests** wire `@electric-sql/pglite` (embedded WASM Postgres, a
720
+ devDependency — hermetic, no docker) via `pgliteExecutor` from
721
+ `@syncular/server/pglite`. Both backends run the shared
722
+ `ServerStorage` contract (`test/storage-contract.ts`), and
723
+ `test/postgres-explain.test.ts` asserts via `EXPLAIN` that the fanout
724
+ candidate scans are index-driven so the scan-before-LIMIT regression
725
+ cannot silently return.
726
+
727
+ ### commitSeq allocation under concurrency
728
+
729
+ Per-partition `commitSeq` is dense and gap-free (§2.1). `appendCommit`
730
+ allocates it with `UPDATE sync_partitions SET max_commit_seq =
731
+ max_commit_seq + 1 … RETURNING`, which takes a row-level write lock on the
732
+ partition row for the transaction's duration — concurrent pushes to the
733
+ same partition serialize on that row; cross-partition pushes never
734
+ contend. A Postgres `SEQUENCE` is deliberately **not** used: it would leave
735
+ gaps on rollback, which the §4.5 pull-window arithmetic does not tolerate.
736
+
737
+ ### Multi-instance fanout (LISTEN/NOTIFY)
738
+
739
+ Behind a load balancer, a commit applied on instance A fans out to A's
740
+ local realtime sessions in-memory, but a client whose socket lives on
741
+ instance B never sees it. `PostgresFanout` bridges the gap: after a commit
742
+ lands, the originating instance `NOTIFY`s `syncular_commit` with a
743
+ `<partition>:<commitSeq>` payload; every instance runs a `listen()` loop
744
+ that, on a notification, calls `hub.wake(partition, 'catchup-required')` —
745
+ remote sessions then pull the delta from the shared Postgres storage they
746
+ already read from (§8.3). NOTIFY payloads are capped (~8 KB) and are not an
747
+ ordered delta channel, so we wake rather than re-broadcast bytes; only
748
+ cross-instance delivery pays the re-pull. Single-instance deployments
749
+ install no fanout at all.
750
+
751
+ ```ts
752
+ import { PostgresFanout, type PgNotificationConnection } from '@syncular/server';
753
+
754
+ // node-postgres: a dedicated Client for LISTEN + the pool for NOTIFY.
755
+ const conn: PgNotificationConnection = {
756
+ async listen(channel, handler) {
757
+ const client = await pool.connect(); // long-lived, NOT released
758
+ client.on('notification', (m) => m.payload && handler(m.payload));
759
+ await client.query(`LISTEN ${channel}`);
760
+ },
761
+ notify: (channel, payload) =>
762
+ pool.query('SELECT pg_notify($1, $2)', [channel, payload]).then(() => {}),
763
+ };
764
+ const fanout = new PostgresFanout(conn);
765
+ await fanout.install(hub); // start the LISTEN loop
766
+ // after a push commit lands:
767
+ await fanout.notifyCommit(partition, commitSeq);
768
+ ```
769
+
770
+ pglite is single-connection and cannot exercise cross-connection NOTIFY,
771
+ so the fanout integration test is env-gated on `SYNCULAR_PG_URL` (it wires
772
+ Bun.sql as a worked example) and skips cleanly; the payload encode/parse
773
+ and wake wiring are unit-tested hermetically.
774
+
775
+ ### Bench lane
776
+
777
+ `bench` has an env-gated Postgres lane measuring 100k bootstrap +
778
+ propagation on the production path. It runs only with `SYNCULAR_PG_URL`
779
+ set and is **never** part of `bench:ci` budgets (those stay on the
780
+ deterministic in-process sqlite loopback):
781
+
782
+ ```sh
783
+ SYNCULAR_PG_URL=postgres://user:pass@localhost:5432/db bun run bench
784
+ ```