@starci/hfs 4.7.32 → 4.7.33

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 (473) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -164,4 +164,4 @@ liteAdapter:
164
164
  proof: >-
165
165
  `starci app check --wiring` proves the composing lite app's module graph with doubles (R272); declaring event-bus requires
166
166
  its scenario-titled integration specs on `starci supabase start` (R284), including redelivery through the reactor.
167
- see: [knowledge/patterns/be/supabase-queues.yaml]
167
+ see: [knowledge/patterns/be/event-bus.yaml]
@@ -11,7 +11,8 @@ purpose: |-
11
11
  client to a topic of that hub, scoped to the client's principal. SSE is the default door for one-way server-to-client push
12
12
  (job progress, a status that changes, streamed tokens), in lite and in full (owner decision 2026-10-24): it is plain HTTP
13
13
  behind the ingress, reconnects by itself and resumes from `Last-Event-ID`. The socket.io gateway stays for a channel that also
14
- needs client-to-server messages, and the GraphQL subscription stays full-only.
14
+ needs client-to-server messages, and the GraphQL subscription is the restricted door for a typed schema payload on a client that already uses
15
+ GraphQL (BE-REALTIME-6); all three run in both editions.
15
16
  appliesTo:
16
17
  - backend
17
18
  family: be
@@ -24,15 +25,27 @@ provenance:
24
25
  - knowledge/patterns/be/transport.yaml
25
26
  - knowledge/patterns/be/api-auth.yaml
26
27
  - owner decision 2026-10-02 (feature kinds: webhooks, realtime, reactors, saga, jobs, cli; no kind imports another)
27
- - owner decision 2026-10-24 (the lite edition may serve SSE, and SSE is the default door for one-way push; ruling:R212/case-5)
28
+ - owner decision 2026-10-24 (the lite edition may serve SSE, and SSE is the default door for one-way push; ruling:R212/case-5, amended by owner:lite-graphql-subscription-door and ruling:R212/case-7 for the GraphQL subscription door)
28
29
  limitations: |-
29
30
  Static checks prove the door's role, that the hub is the only thing injected and that a subscribed topic is built from the
30
31
  principal. They do not prove the topic naming is collision free across channels, that an initial snapshot is consistent with
31
32
  the stream, or the behaviour of the socket under reconnects; the three `realtime/*` scenarios prove delivery, isolation and
32
33
  cleanup on the real stack. Nothing proves the ingress settings of an SSE channel (no compression, no buffering, HTTP/2, an
33
- idle timeout above the heartbeat): they are reviewed against the stack files and a real stream. The hub is single-replica:
34
- its subscriptions and its replay buffer live in one process, so a second replica of the api does not see the pushes of the
35
- first.
34
+ idle timeout above the heartbeat): they are reviewed against the stack files and a real stream. The memory hub is
35
+ single-process: its subscriptions and its replay buffer live in one process, so it serves tests and one replica only
36
+ (STACKS_REALTIME_SCALED refuses it with more); a shared hub (BE-REALTIME-7) carries pushes and replay across replicas, and
37
+ its socket.io adapters have no connection state recovery, so a socket client resyncs by query. A replay returns at most 64 frames
38
+ per topic; Postgres sweeps frames older than 15 minutes, Redis expires a topic stream after 15 minutes without a retained publish,
39
+ and memory keeps its bounded buffer without a TTL. A client whose
40
+ `Last-Event-ID` is older or unknown receives the newest retained frames with no signal
41
+ of the gap, so a screen refetches its snapshot after any reconnect. A frame with no string `id` is live-only in the memory
42
+ and Redis hubs; Postgres retains it and reloads it after a LISTEN loss by its sequence position, but no client can name it as
43
+ `Last-Event-ID`. Only the Postgres hub publishes inside the publisher's transaction, the other hubs push at once. Each open
44
+ subscription on a shared hub keeps its own last position, initially where its topic stood at join. After a lost LISTEN or subscriber connection
45
+ the hub reloads each topic after the minimum last position of its open subscriptions; each subscription drops positions it has
46
+ already taken, so a new subscription cannot skip frames owed to an older one and replay, reload and held notifications do not
47
+ duplicate a retained frame. A failed reload keeps live delivery paused and retries until the current loss generation reloads.
48
+ graphql-ws has no replay: a screen on a GraphQL subscription refetches its snapshot on reconnect.
36
49
  rules:
37
50
  - id: BE-REALTIME-1
38
51
  title: A channel is a kind folder with one door, its module and its own spec
@@ -47,8 +60,8 @@ rules:
47
60
  `<channel>.gateway.spec.ts`, or `transport/graphql/<channel>.subscription.ts` (one `@Resolver` class whose operations are `@Subscription`) with
48
61
  `<channel>.subscription.spec.ts`, plus optional `dto/` (`<name>.{input,type,args}.ts`). A door carries no query, mutation, field
49
62
  resolver or plain REST route (`@Get`, `@Post` ...; the `@Sse` stream is the event-stream evidence of BE_REST_DOOR_EVIDENCE only in the
50
- sse slot). The kind imports only `modules/*`, never another kind, and only an api-kind app composes its module. SSE and the
51
- socket.io gateway run in lite and in full; the GraphQL subscription is full only.
63
+ sse slot). The kind imports only `modules/*`, never another kind, and only an api-kind app composes its module. All three doors
64
+ run in lite and in full; BE-REALTIME-6 chooses between them.
52
65
  rationale: |-
53
66
  One file per door keeps a protocol's machinery in one place, and keeping queries and mutations out guarantees that what a
54
67
  client can do through a realtime door is listen.
@@ -106,13 +119,13 @@ rules:
106
119
  requirement: |-
107
120
  A realtime door is thin (R203 `BE_FEATURE_THIN`): it maps the subscription or the message and makes one delegating call
108
121
  to the hub. The only value it injects is the `RealtimeHub` of `platform/realtime` (`subscribe(topic, options?)` returns an
109
- async iterable of frames, `publish(topic, frame)` pushes one; a topic is a typed `RealtimeTopic<T>` built by the owning domain
122
+ async iterable of frames, `publish(topic, frame)` pushes one and settles once the hub accepted it; a topic is a typed `RealtimeTopic<T>` built by the owning domain
110
123
  contract, never a string written in a door). An SSE controller calls `hub.stream(topic, { after })` instead, which returns the
111
124
  `Observable<MessageEvent>` Nest needs (the hub maps its own subscription with `eventStream` of `platform/realtime`): the
112
125
  opening frame, one event per frame with its `id`, and the comment heartbeat. No `EntityManager`, `CommandBus`, `QueryBus`, event bus, queue or
113
126
  service of `modules/domain` is injected: a domain service may write, so it is not provably read-only. The data a client
114
127
  needs is pushed by a reactor (or by the domain service that commits the change, in an app with no event bus); the socket
115
- library and the pub/sub engine (`socket.io`, `graphql-subscriptions`, `graphql-ws`) are imported only by `platform/realtime`,
128
+ library and the pub/sub engine (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`, `graphql-subscriptions`, `graphql-ws`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`,
116
129
  and so is the heartbeat timer (`setInterval` is owned by `platform/realtime`, `infraOwners`). Because the door reads only the
117
130
  hub, a snapshot cannot be sent on connect from the door: the client reads the current state through a query operation first
118
131
  (or again on every open), and the stream carries the changes.
@@ -138,7 +151,7 @@ rules:
138
151
  listen to another's channel. The reactor that pushes uses the same topic builder from the owning domain contract, never a
139
152
  hand-written string. The default-deny guards apply to the door like to any other; an SSE stream is never `@Public`. The
140
153
  browser `EventSource` cannot send an `Authorization` header, so the front end reads the stream with `fetch` and the bearer
141
- access token (modules/api/sse read by a use<Channel>Stream hook); a token in the URL is refused because it ends up in logs. The `Last-Event-ID` header is client
154
+ access token (an operation of modules/api/sse read by a useSubscribe<Channel>Swr hook); a token in the URL is refused because it ends up in logs. The `Last-Event-ID` header is client
142
155
  input like any other: it is passed as `{ after }` to `hub.subscribe` and only picks where the replay resumes inside the
143
156
  principal's own topic, it never names a topic.
144
157
  rationale: |-
@@ -160,6 +173,7 @@ rules:
160
173
  hfsRules: [R155]
161
174
  requirement: |-
162
175
  Declaring the pattern `realtime` requires e2e or integration tests titled `realtime/<scenario>: ...` for each scenario
176
+ `push-crosses-replicas` and `resume-after-reconnect` (two api replicas, BE-REALTIME-7),
163
177
  `subscriber-receives-push` (a connected client receives the frame a reactor pushes after the domain event),
164
178
  `foreign-topic-is-isolated` (another principal subscribed to the same channel receives nothing) and
165
179
  `disconnect-releases-subscription` (after the client disconnects the hub holds no subscription for it). The client is a real
@@ -191,8 +205,8 @@ rules:
191
205
  alone (`infraOwners` `setInterval`). The hub retains the last frames of a topic that carry a string `id` in a bounded
192
206
  buffer (a fixed number of frames per topic and of topics, the oldest dropped first), so a client that reconnects with
193
207
  `Last-Event-ID` receives what it missed; an id the buffer no longer holds replays the whole buffer, and the client refetches
194
- its snapshot through the query operation on every open. The hub is single-replica: the api that composes an SSE channel
195
- runs one replica (STACKS_REALTIME_SCALED in lite); a shared hub across replicas is a later decision. At the ingress, the
208
+ its snapshot through the query operation on every open. The memory hub keeps that buffer in one process; a shared hub keeps it in
209
+ the `realtime_events` table or a Redis Stream, so a reconnect that lands on any replica resumes (BE-REALTIME-7). At the ingress, the
196
210
  stream route is never compressed or buffered (Nest sends `X-Accel-Buffering: no` and `Cache-Control: no-cache, no-transform`;
197
211
  Traefik's compress middleware excludes `text/event-stream`, a Caddy route sets no `encode` and `flush_interval -1`), the edge
198
212
  speaks HTTP/2 to the browser so the six-connection limit of HTTP/1.1 per origin does not starve tabs, and an Express
@@ -212,9 +226,230 @@ rules:
212
226
  - BE_INFRA_OWNER
213
227
  manual:
214
228
  - Confirm the ingress route of the api sets no compression and no response buffering for text/event-stream, and that its idle timeout is above the heartbeat.
215
- - Confirm the lite stack pins the api to one replica (STACKS_REALTIME_SCALED refuses a scaled api that composes realtime).
229
+ - Confirm an api that runs more than one replica selects a shared hub (STACKS_REALTIME_SCALED refuses the memory hub there).
216
230
  - Confirm the replay bounds fit the channel (frames per topic, number of topics).
217
231
  relatedExamples: [realtime]
232
+ - id: BE-REALTIME-6
233
+ title: The door follows what is pushed and which way, not the API default
234
+ kind: mandatory
235
+ hfsRules: [R184, R212, R90, R185, R186]
236
+ requirement: |-
237
+ The API's default door is GraphQL (BE-TRANSPORT), but realtime is a separate decision: what is pushed and in which direction
238
+ picks the door, in both editions.
239
+
240
+ Choosing the door (both editions):
241
+ - One-way server-to-client push (a status that changes, job progress, streamed AI tokens): SSE, the default. It is plain HTTP that
242
+ passes every proxy, ingress and HTTP/2, the browser reconnects by itself and resumes from `Last-Event-ID` (the hub replays),
243
+ and it is the cheapest per connection.
244
+ - A push of a schema type the screen already reads through GraphQL, when the typed payload matters (the whole `Order` with its
245
+ generated type): the GraphQL subscription, restricted. It shares the schema, the codegen and the guards of the queries. Its
246
+ limits: graphql-ws has no replay, so on every reconnect the screen refetches its snapshot with a query; it costs more per
247
+ connection; it needs the WebSocket upgrade at the ingress. Use it only when the client already uses GraphQL AND needs the typed
248
+ payload; otherwise SSE.
249
+ - Two-way messages (chat, presence, collaborative input): the WebSocket door (socket.io gateway), the only door where the client
250
+ sends messages. It needs the WebSocket upgrade, plus sticky sessions when the long-polling fallback is on.
251
+
252
+ Every door, in both editions: it reads and pushes only (no write, query, mutation or REST route); its topic is built from
253
+ `@CurrentPrincipal()` and it is never `@Public`; a token never travels in the URL; only `platform/realtime` imports the engine
254
+ (`graphql-ws`, `graphql-subscriptions`, `socket.io`); three `realtime/*` scenarios prove delivery, isolation and cleanup. On the
255
+ front end the reader of a stream, a subscription or a socket is owned by the transport client layer, exposed as an operation and
256
+ consumed only through an SWR subscription hook that revalidates its snapshot query when the stream opens, with no fetch in an effect
257
+ (knowledge/patterns/fe/realtime.yaml).
258
+
259
+ Hub (both editions): the hub default is Redis when Redis is declared (lite or full), Postgres LISTEN/NOTIFY when lite declares no
260
+ Redis, and memory for tests or a single replica only; the memory hub is non-scale. Scale is the hub adapter behind the one
261
+ `RealtimeHub` port, never a different door. The Redis and Postgres hubs, the refusal of a stack with more than one api replica on
262
+ the memory hub (STACKS_REALTIME_SCALED) and the two-replica scenarios are the realtime scale rule BE-REALTIME-7.
263
+
264
+ Lite (ruling:R212/case-10, owner:lite-vector-is-pgvector):
265
+ ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on Postgres/Supabase plus
266
+ Redis when a declared capability needs it. Vector search uses Supabase pgvector. The three doors above are served from that api (the GraphQL subscription door is new
267
+ in lite, ruling:R212/case-7). A lite api with more than one replica uses the Redis hub when Redis is
268
+ declared, else the Postgres hub. The example is the SSE `booking-status` channel.
269
+
270
+ Full: the complete infrastructure, several services with Redis (ioredis, BullMQ), and the api may run several replicas on the Redis hub (BE-REALTIME-7). The three doors and the choice above are the same as lite; no door is full-only. The example is the
271
+ GraphQL subscription `order-status` channel, where the typed `Order` payload matters; a screen that reads it refetches the order on
272
+ reconnect.
273
+ rationale: |-
274
+ A reader who knows the API is GraphQL assumes a GraphQL subscription everywhere; the push is cheaper, easier to proxy and
275
+ resumable over SSE, so the heavier door is chosen only for what only it gives.
276
+ cases:
277
+ - id: case-1
278
+ when: A booking that gets confirmed after a job, shown on a screen that already reads it through a query
279
+ write: 'SSE: transport/sse/booking-status.controller.ts; the front end reads the stream through a useSubscribe<Channel>Swr hook and the query operation carries the snapshot.'
280
+ - id: case-2
281
+ when: An order whose whole typed payload the screen renders, on a client that already uses GraphQL
282
+ write: 'GraphQL subscription: transport/graphql/order-status.subscription.ts; on every reconnect the screen refetches the order with its query because graphql-ws has no replay.'
283
+ verification:
284
+ automated:
285
+ - BE_REALTIME_SHAPE
286
+ - HFS_EDITION_INVALID
287
+ - HFS_EDITION_FORBIDDEN_PRESENT
288
+ - HFS_STARCIWORK_LITE_FORBIDDEN
289
+ - HFS_BACKING_UNDECLARED
290
+ - HFS_BACKING_UNUSED
291
+ - BE_INFRA_OWNER
292
+ - BE_REALTIME_WRITES
293
+ - BE_REALTIME_TOPIC_SCOPE
294
+ manual:
295
+ - Confirm a GraphQL subscription channel serves a client that already uses GraphQL and needs the typed payload, and that its screen refetches the snapshot on reconnect.
296
+ - "Confirm that a stack with more than one api replica uses a shared hub (BE-REALTIME-7)."
297
+ relatedExamples: [realtime]
298
+ - id: BE-REALTIME-7
299
+ title: A realtime channel scales horizontally on one hub port with a shared hub
300
+ kind: mandatory
301
+ hfsRules: [R90, R266, R155]
302
+ requirement: |-
303
+ Both editions run N api replicas. `platform/realtime` keeps ONE port, `RealtimeHub` (`publish(topic, frame, within?): Promise<void>`
304
+ called by a reactor, or by the domain service that owns the state change and passes its own transaction as `within`,
305
+ `subscribe(topic, { after })` for GraphQL and socket doors, `stream(topic, { after })` for the SSE door), so a door never
306
+ changes between a single replica and N replicas: only the hub binding does. Three adapters implement the port:
307
+ `MemoryRealtimeHub` (non-scale: one process, tests and a single-replica dev run, replay from an in-process ring buffer),
308
+ `PostgresRealtimeHub` (stateless: `publish` takes a per-topic advisory lock, writes a `realtime_events` row and NOTIFYs, in the
309
+ publisher's transaction when it passes one, so the push goes out at commit and a rolled-back change pushes nothing; every
310
+ replica LISTENs and fans out to its local subscribers; replay reads the table after the `Last-Event-ID`; when the LISTEN
311
+ connection is lost the hub holds notifications and reloads each followed topic after the minimum last position of its open
312
+ subscriptions. Each subscription starts at the topic's join position and advances as it takes entries, dropping any position
313
+ already taken, so an older subscription receives its outage frames once even when a new subscription joins the same topic.
314
+ A failed live row load takes the same loss path; a failed reload keeps the pause and retries through the injected Timer after
315
+ RECONNECT_DELAY_MS. Only a successful reload of the current loss generation releases and drains held notifications; a second
316
+ loss during a reload leaves the pause to the newer reload. The table is bounded by a fenced TTL sweep job that runs batch after batch) and
317
+ `RedisRealtimeHub` (`publish` runs one atomic script: XADD to a Redis Stream per topic with `MAXLEN ~`, PEXPIRE and PUBLISH;
318
+ replay reads the stream with XRANGE and resumes after the newest entry carrying the id; when the subscriber connection comes
319
+ back the hub reloads each followed topic after the minimum last stream id of its open subscriptions, with the same per-subscription
320
+ duplicate suppression and paused retry policy as Postgres; ioredis, every key and channel carries the
321
+ app prefix `REALTIME_REDIS_PREFIX`). The app selects the hub with `REALTIME_HUB=memory|postgres|redis`, parsed once by
322
+ `parseRealtimeConfig` and validated at boot; unset, the parser defaults to `redis` when `REALTIME_REDIS_URL` is declared (lite
323
+ and full), `postgres` when lite declares no Redis, and never `memory`, so a scaled deploy cannot fall back to the in-process
324
+ hub unseen. All three
325
+ adapters are legal in both editions; select `redis` when Redis is declared (lite with Redis, and full), `postgres` when it is
326
+ not, and `memory` only for tests or one replica. A Postgres hub LISTENs on `REALTIME_DB_URL` when declared, else on the
327
+ primary URL, and proves the connection at boot by hearing a probe the hub sends from another session, so a transaction pooler
328
+ fails the boot instead of delivering nothing. The Redis hub reads its address from the secret `REALTIME_REDIS_URL`
329
+ (password, ACL user and `rediss://` included). The stack check `STACKS_REALTIME_SCALED` refuses an api that composes
330
+ a realtime feature, runs more than one replica and does not select `postgres` or `redis`. The socket.io adapters live in
331
+ `platform/realtime` too and `main.ts` installs the one the hub selects through `realtimeIoAdapter(app)`: the default
332
+ `IoAdapter` on the memory hub, `PostgresIoAdapter` (`@socket.io/postgres-adapter`, payloads of 8 KB or more through the
333
+ `socket_io_attachments` table) or `RedisIoAdapter` (`createShardedAdapter`, Redis 7 or later). Both force
334
+ `transports: ['websocket']`, so on either shared adapter a socket is one connection to one replica, long-polling is off and
335
+ no sticky session exists. The memory hub's default IoAdapter also accepts polling; the canonical client uses websocket only in every mode.
336
+ Neither adapter has connection state recovery, and graphql-ws has no replay, so a socket or GraphQL client refetches its
337
+ snapshot with a query on every reconnect. The engines (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`,
338
+ `graphql-ws`, `graphql-subscriptions`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`
339
+ (`BE_INFRA_OWNER`). The tables are migrations of the app (`realtime_events` and, for the Postgres socket adapter,
340
+ `socket_io_attachments`: a Supabase migration in lite, a TypeORM migration of `platform/realtime` in full). Declaring realtime
341
+ adds two scenarios, proven with two api replicas: `realtime/push-crosses-replicas` (published through replica A, received on
342
+ replica B) and `realtime/resume-after-reconnect` (`Last-Event-ID` sent to the other replica receives the missed frames).
343
+ rationale: |-
344
+ A hub in the process memory loses every push that was published on another replica and every replay buffer a reconnect lands
345
+ away from. Shared state in Postgres or Redis, a websocket-only transport and a client that resyncs by query make any replica
346
+ able to serve any request or reconnect, with no sticky session and no state a replica needs from another.
347
+ cases:
348
+ - id: case-1
349
+ when: SSE, non-scale (tests, one replica)
350
+ write: |-
351
+ The door is the same in every mode. Only the hub binding differs.
352
+ // features/realtime/booking-status/transport/sse/booking-status.controller.ts
353
+ @Controller('booking-status')
354
+ export class BookingStatusController {
355
+ constructor(@InjectRealtimeHub() private readonly hub: RealtimeHub) {}
356
+ @Sse('stream')
357
+ stream(@CurrentPrincipal() principal: Principal, @Headers('last-event-id') lastEventId?: string): Observable<MessageEvent> {
358
+ return this.hub.stream(bookingStatusTopic(principal.id), { after: lastEventId })
359
+ }
360
+ }
361
+ // app root: REALTIME_HUB=memory -> RealtimeModule.register({ isGlobal: true, hub: 'memory' }); replay comes from the
362
+ // in-process buffer and a reconnect must land on the same process.
363
+ - id: case-2
364
+ when: SSE, scale on Postgres (lite without Redis; full may too)
365
+ write: |-
366
+ The same controller. The app root binds the Postgres hub and the stack runs N replicas with no sticky routing:
367
+ // be/apps/api/src/compose/platform.ts
368
+ RealtimeModule.register({ isGlobal: true, ...withConnection(options.realtime, PRIMARY_ENTITY_MANAGER, options.database.url) })
369
+ // stack.yaml: environment REALTIME_HUB: postgres, deploy.replicas: 2
370
+ publish writes the realtime_events row and NOTIFYs; every replica LISTENs and writes the frame to its local streams; a
371
+ reconnect to ANY replica sends Last-Event-ID and the hub reads the missed rows from the table. A domain service that owns the
372
+ change publishes inside its own transaction (`hub.publish(topic, frame, tx)`), so the confirmation and its push commit together. At the ingress the stream is
373
+ not buffered or compressed and the idle timeout is above the heartbeat. Migration: supabase/migrations/<ts>_realtime-events.sql
374
+ (lite) or platform/realtime/persistence/migrations/<epochMs>-create-realtime-events.ts (full).
375
+ - id: case-3
376
+ when: SSE, scale on Redis (Redis declared, lite or full)
377
+ write: |-
378
+ The same controller. REALTIME_HUB=redis binds RedisRealtimeHub: publish runs one atomic script that XADDs `<prefix>:<topic>`
379
+ with MAXLEN ~ 64, expires the key and PUBLISHes the frame on the `<prefix>` channel; replay is XRANGE of the stream, resuming after the newest entry whose id equals the Last-Event-ID; no table and no sweep work (its
380
+ retention tick is a no-op).
381
+ RealtimeModule.register({ isGlobal: true, ...withConnection(options.realtime, ORDER_ENTITY_MANAGER, options.database.url) })
382
+ // REALTIME_HUB=redis, the secret REALTIME_REDIS_URL, REALTIME_REDIS_DB (and REALTIME_REDIS_PREFIX when the server is shared);
383
+ // Redis 7 or later for the sharded socket.io adapter.
384
+ - id: case-4
385
+ when: GraphQL subscription, non-scale
386
+ write: |-
387
+ // features/realtime/order-status/transport/graphql/order-status.subscription.ts (same in every mode)
388
+ @Subscription(() => OrderStatusChangedType, { name: 'orderStatusChanged', resolve: (frame: OrderStatusFrame) => frame })
389
+ @Roles('member')
390
+ orderStatusChanged(@CurrentPrincipal() principal: Principal, @Args('input') input: OrderStatusInput) {
391
+ return this.hub.subscribe(orderStatusTopic(principal.id, input.orderId))
392
+ }
393
+ // REALTIME_HUB=memory replaces the graphql-subscriptions PubSub, which is not for production.
394
+ - id: case-5
395
+ when: GraphQL subscription, scale (Redis or Postgres hub)
396
+ write: |-
397
+ The same resolver. graphql-ws rides ONE WebSocket, so no stickiness is needed; the hub fans the frame out through Redis
398
+ (the full-app order-status example, REALTIME_HUB=redis) or Postgres LISTEN/NOTIFY. graphql-ws has no replay: the screen
399
+ refetches its snapshot query on every reconnect, in every mode.
400
+ - id: case-6
401
+ when: WebSocket (socket.io), non-scale
402
+ write: |-
403
+ The gateway is identical in every mode. `main.ts` calls `app.useWebSocketAdapter(realtimeIoAdapter(app))`; on the memory hub
404
+ that is the default Nest IoAdapter, in process.
405
+ - id: case-7
406
+ when: WebSocket, scale on Postgres
407
+ write: |-
408
+ // platform/realtime/postgres-io.adapter.ts (installed by realtimeIoAdapter on REALTIME_HUB=postgres)
409
+ export class PostgresIoAdapter extends IoAdapter {
410
+ constructor(app: INestApplicationContext, private readonly pool: Pool, private readonly build: typeof createAdapter, logger: Logger) {
411
+ super(app)
412
+ pool.on('error', (cause) => logger.warn(RealtimeLogEvent.PoolFailed, { cause: cause.message })) // an idle client lost in a failover must not crash the replica
413
+ }
414
+ override createIOServer(port: number, options?: Partial<ServerOptions>): Server {
415
+ const server: Server = super.createIOServer(port, { ...options, transports: ['websocket'] }) // no polling, so no sticky session
416
+ server.adapter(this.build(this.pool, { tableName: SOCKET_IO_ATTACHMENTS_TABLE, errorHandler: this.report }))
417
+ return server
418
+ }
419
+ }
420
+ The pool is built by RealtimeModule from the session-mode database URL (LISTEN does not work through a transaction pooler).
421
+ - id: case-8
422
+ when: WebSocket, scale on Redis
423
+ write: |-
424
+ // platform/realtime/redis-io.adapter.ts (installed on REALTIME_HUB=redis)
425
+ export class RedisIoAdapter extends IoAdapter {
426
+ constructor(app: INestApplicationContext, private readonly publisher: Redis, private readonly subscriber: Redis, private readonly build: typeof createShardedAdapter, logger: Logger, private readonly channelPrefix: string) { super(app) }
427
+ override createIOServer(port: number, options?: Partial<ServerOptions>): Server {
428
+ const server: Server = super.createIOServer(port, { ...options, transports: ['websocket'] })
429
+ server.adapter(this.build(this.publisher, this.subscriber, { channelPrefix: this.channelPrefix })) // two connections, errors logged, channels prefixed
430
+ return server
431
+ }
432
+ }
433
+ verification:
434
+ automated:
435
+ - BE_INFRA_OWNER
436
+ - STACKS_SECRET_NOT_EXTERNAL
437
+ - STACKS_SECRET_UNVERSIONED
438
+ - STACKS_SECRET_ENV_LITERAL
439
+ - STACKS_PORT_PUBLISHED
440
+ - STACKS_SECOND_INGRESS
441
+ - STACKS_DNS_UNDECLARED
442
+ - STACKS_HOST_UNROUTED
443
+ - STACKS_TLS_UNDECLARED
444
+ - STACKS_PLACEMENT_MISSING
445
+ - STACKS_HEALTHCHECK_MISSING
446
+ - STACKS_UPDATE_POLICY_MISSING
447
+ - STACKS_REALTIME_SCALED
448
+ - BE_PATTERN_SPEC_MISSING
449
+ manual:
450
+ - Confirm the ingress holds no sticky-session declaration for the api, and that polling is off in the socket.io client configuration.
451
+ - Confirm a Postgres hub connects through a session-mode URL and that the realtime tables are migrations of the app.
452
+ relatedExamples: [realtime]
218
453
  files:
219
454
  - path: src/features/realtime/<channel>/index.ts
220
455
  slot: be.feature.realtime
@@ -305,16 +540,78 @@ files:
305
540
  template: realtime/channel.controller.spec.ts.tpl
306
541
  role: Unit spec of the SSE controller beside it.
307
542
  optional: true
308
- - path: src/modules/platform/realtime/realtime.hub.ts
543
+ - path: src/modules/platform/realtime/realtime.port.ts
544
+ slot: be.platform
545
+ role: RealtimeHub, the one port of the realtime kind (publish, subscribe, stream) and the typed topic; its three adapters are the files below.
546
+ optional: true
547
+ - path: src/modules/platform/realtime/memory-realtime-hub.service.ts
548
+ slot: be.platform
549
+ role: "MemoryRealtimeHub, the non-scale adapter: one process, tests and a single-replica dev run, replay from an in-process ring buffer."
550
+ spec: src/modules/platform/realtime/memory-realtime-hub.service.spec.ts
551
+ optional: true
552
+ - path: src/modules/platform/realtime/postgres-realtime-hub.service.ts
553
+ slot: be.platform
554
+ role: "PostgresRealtimeHub: publish locks the topic, writes a realtime_events row and NOTIFYs (in the publisher's transaction when given), every replica LISTENs and fans out, replay reads the table, and a lost LISTEN connection is reloaded per topic (both editions)."
555
+ spec: src/modules/platform/realtime/postgres-realtime-hub.service.spec.ts
556
+ optional: true
557
+ - path: src/modules/platform/realtime/redis-realtime-hub.service.ts
558
+ slot: be.platform
559
+ role: "RedisRealtimeHub: a Redis Stream per topic for replay and Pub/Sub for the live push, published by one atomic script over ioredis, reloaded per topic after a subscriber reconnect (when Redis is declared)."
560
+ spec: src/modules/platform/realtime/redis-realtime-hub.service.spec.ts
561
+ optional: true
562
+ - path: src/modules/platform/realtime/realtime-subscriptions.policy.ts
563
+ slot: be.platform
564
+ role: "The subscriptions of one replica, shared by the shared hubs: join, replay then live without a duplicate, release on stop."
565
+ spec: src/modules/platform/realtime/realtime-subscriptions.policy.spec.ts
566
+ optional: true
567
+ - path: src/modules/platform/realtime/postgres-listener.client.ts
568
+ slot: be.platform
569
+ role: "The LISTEN connection of the Postgres hub: proves itself with a probe at boot, reconnects, closes the dead client and tells the hub it was lost and when it resumed."
570
+ spec: src/modules/platform/realtime/postgres-listener.client.spec.ts
571
+ optional: true
572
+ - path: src/modules/platform/realtime/realtime-retention.service.ts
309
573
  slot: be.platform
310
- role: RealtimeHub, the one owner of the socket and subscription engine (publish and subscribe, with the bounded replay buffer an SSE reconnect resumes from).
311
- spec: src/modules/platform/realtime/realtime.service.spec.ts
574
+ role: The bounded, batched delete of the replay table that the fenced sweep job runs, asking the fence before every batch.
575
+ spec: src/modules/platform/realtime/realtime-retention.service.spec.ts
576
+ optional: true
577
+ - path: src/modules/platform/realtime/no-realtime-retention.service.ts
578
+ slot: be.platform
579
+ role: "The retention of a hub with no replay table (memory, Redis): a scheduled sweep tick is acknowledged and deletes nothing."
580
+ spec: src/modules/platform/realtime/no-realtime-retention.service.spec.ts
581
+ optional: true
582
+ - path: src/modules/platform/realtime/redis-stream-id.policy.ts
583
+ slot: be.platform
584
+ role: Orders two Redis stream ids, so the Redis hub reloads only what a topic has not delivered.
585
+ spec: src/modules/platform/realtime/redis-stream-id.policy.spec.ts
586
+ optional: true
587
+ - path: src/modules/platform/realtime/postgres-io.adapter.ts
588
+ slot: be.platform
589
+ role: "PostgresIoAdapter: socket.io over @socket.io/postgres-adapter, websocket only, so no sticky session."
590
+ spec: src/modules/platform/realtime/postgres-io.adapter.spec.ts
591
+ optional: true
592
+ - path: src/modules/platform/realtime/redis-io.adapter.ts
593
+ slot: be.platform
594
+ role: "RedisIoAdapter: socket.io over the sharded @socket.io/redis-adapter (ioredis), websocket only."
595
+ spec: src/modules/platform/realtime/redis-io.adapter.spec.ts
596
+ optional: true
597
+ - path: src/modules/platform/realtime/realtime-io.adapter.ts
598
+ slot: be.platform
599
+ role: "realtimeIoAdapter(app), the one call main.ts makes to install the socket.io adapter the selected hub provides."
600
+ spec: src/modules/platform/realtime/realtime-io.adapter.spec.ts
312
601
  optional: true
313
602
  - path: src/modules/platform/realtime/event-stream.mapper.ts
314
603
  slot: be.platform
315
604
  role: eventStream, the one place that turns hub frames into server-sent events (opening frame, ids, comment heartbeat) and owns the heartbeat timer.
316
605
  spec: src/modules/platform/realtime/event-stream.mapper.spec.ts
317
606
  optional: true
607
+ - path: src/modules/platform/realtime/persistence/migrations/<epochMs13>-create-realtime-events.ts
608
+ slot: be.persistence
609
+ role: Full edition migration of the realtime_events replay table and the socket_io_attachments table of the Postgres adapter.
610
+ optional: true
611
+ - path: supabase/migrations/<timestamp>_realtime-events.sql
612
+ slot: repo.side-root-forbidden
613
+ role: Lite edition migration (created with supabase migration new) of the same two tables, forced row level security for app_be, and the pg_cron tick of the retention queue.
614
+ optional: true
318
615
  - path: src/features/reactors/<name>/transport/message/<event>.consumer.ts
319
616
  slot: be.transport.message
320
617
  role: The reactor consumer that receives the domain event and pushes the frame through the hub.
@@ -325,24 +622,26 @@ files:
325
622
  spec: realtime/subscriber-receives-push, realtime/foreign-topic-is-isolated, realtime/disconnect-releases-subscription
326
623
  rules: [BE_PATTERN_SPEC_MISSING]
327
624
  optional: true
328
- # Edition lite (R212, owner ruling 2026-10-14; the SSE door by owner decision 2026-10-24, ruling:R212/case-5): the same ports, door and rules; one instance behind them.
625
+ # Edition lite (R212, owner ruling 2026-10-14; the SSE door by owner decision 2026-10-24, ruling:R212/case-5; the GraphQL subscription door, ruling:R212/case-7 and owner:lite-graphql-subscription-door): the same ports, doors and rules; it scales horizontally and stateless.
329
626
  liteAdapter:
330
627
  summary: >-
331
- Under lite the realtime pattern is the full one with its lite doors: the SSE controller of be.feature.realtime.sse
332
- (transport/sse/<channel>.controller.ts, the default for one-way push) or the socket.io gateway of be.feature.realtime.websocket
333
- (transport/websocket/<channel>.gateway.ts) runs inside the be api process, one instance (the memory hub, its bounded replay
334
- buffer and the socket.io default in-memory adapter, no Redis adapter). A publisher (a reactor, or the domain service that
628
+ Under lite the realtime pattern is the full one with its three doors: the SSE controller of be.feature.realtime.sse
629
+ (transport/sse/<channel>.controller.ts, the default for one-way push), the socket.io gateway of be.feature.realtime.websocket
630
+ (transport/websocket/<channel>.gateway.ts) or the GraphQL subscription of be.feature.realtime.graphql (restricted, BE-REALTIME-6) runs inside the be api process. One replica or a test runs the memory hub (its bounded replay buffer and the socket.io
631
+ default adapter); replicas > 1 run the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub
632
+ (LISTEN/NOTIFY and the bounded realtime_events table) and @socket.io/postgres-adapter, websocket only, with no sticky sessions (BE-REALTIME-7). A publisher (a reactor, or the domain service that
335
633
  commits the change in an app with no event bus) publishes to the RealtimeHub of platform/realtime; the SSE door streams the
336
- principal-scoped topic as text/event-stream and the gateway emits to the principal-scoped room of the namespace. The front end
337
- listens through a use<Channel>Stream hook over modules/api/sse (a fetch reader with the bearer token) or hooks/socketio (knowledge/patterns/fe/realtime.yaml).
338
- graphql-ws subscriptions are not used in lite.
634
+ principal-scoped topic as text/event-stream, the gateway emits to the principal-scoped room of the namespace and the subscription resolver
635
+ returns the hub stream of the principal-scoped topic. The front end listens through a useSubscribe<Channel>Swr hook over modules/api/sse
636
+ (a reader of the transport client layer with the bearer token) or hooks/socketio (knowledge/patterns/fe/realtime.yaml). graphql-ws
637
+ has no replay, so a screen on the subscription refetches its snapshot on reconnect. The memory hub serves tests and one replica; with more than one api replica the hub is the Redis hub when Redis is declared, else the Postgres hub (stateless, no sticky sessions; BE-REALTIME-7).
339
638
  ports:
340
- - {port: RealtimeHub.publish (platform/realtime), adapter: "the memory hub of the api process retains the frame (bounded) and hands it to the open SSE streams; the socket.io server emits to the namespace room of the topic (no Redis adapter: one instance)"}
639
+ - {port: RealtimeHub.publish (platform/realtime), adapter: "the memory hub (one replica, tests) retains the frame (bounded) in the api process; the Postgres hub (replicas > 1) writes the realtime_events row and NOTIFYs, every replica hands it to its open SSE streams; the socket.io server emits to the namespace room of the topic, across replicas through @socket.io/postgres-adapter (websocket only)"}
341
640
  - {port: RealtimeHub.subscribe, adapter: "the SSE controller streams hub.stream(topic, { after }) (the subscription through eventStream); the gateway's stream joins the room built from the principal, as full"}
342
641
  - {port: topic scope, adapter: "the topic and the room name built from @CurrentPrincipal (BE_REALTIME_TOPIC_SCOPE), as full"}
343
- forbids: [redis, ioredis, "@socket.io/redis-adapter", graphql-ws, graphql-subscriptions]
642
+ forbids: []
344
643
  proof: >-
345
644
  The door's own unit spec (<channel>.controller.spec.ts or <channel>.gateway.spec.ts) and the hub adapter's spec, as full;
346
645
  declaring realtime requires integration specs titled `realtime/<scenario>: ...` for every declared scenario on
347
646
  `starci supabase start` (R284), using a real client (an HTTP event-stream read or a socket.io client). Lite has no e2e world.
348
- see: [knowledge/patterns/be/supabase-queues.yaml, knowledge/patterns/fe/realtime.yaml]
647
+ see: [knowledge/patterns/fe/realtime.yaml]
@@ -147,7 +147,7 @@ files:
147
147
  optional: true
148
148
  - path: src/modules/platform/saga/
149
149
  slot: be.platform
150
- role: 'The shared store: `saga_states` (status and version fence) and the event claims, with the state machine in `saga.service.ts` — a measured file of a `coverage: required` slot, held at 100 per file with its spec beside it.'
150
+ role: 'The fenced `saga_states` store and the state machine in `saga.service.ts`, which claims deliveries through the shared `platform/inbox` port (BE-IDEMPOTENCY-2). The service is a measured file of a `coverage: required` slot, held at 100 per file with its spec beside it.'
151
151
  rules: [R170, R80]
152
152
  optional: true
153
153
  - path: src/tests/e2e/<area>/<name>.e2e-spec.ts
@@ -275,4 +275,4 @@ liteAdapter:
275
275
  The orchestrator's unit spec (<saga>.saga.service.spec.ts) as full; declaring saga requires integration specs titled
276
276
  `saga/<scenario>: ...` for every declared scenario on `starci supabase start` (R284), including each compensation path with
277
277
  failure injection and the final state read back. The e2e world of R173 remains full-only.
278
- see: [knowledge/patterns/be/supabase-queues.yaml]
278
+ see: [knowledge/patterns/be/event-bus.yaml]
@@ -129,7 +129,7 @@ enforces:
129
129
  - {rule: R21, code: HFS_DUPLICATE_CODE, enforcer: duplicated-lines-density, condition: duplication}
130
130
  - {rule: R21, code: HFS_DUPLICATE_SYMBOL, enforcer: duplicated-lines-density, condition: duplication}
131
131
  # Ops that write product code: each runs sonar-local on its own change and cannot settle done while the gate is red.
132
- enforcedOps: [backend.implement, interface.implement, code.refactor]
132
+ enforcedOps: [backend.implement, interface.implement, code.refactor, code.fix]
133
133
  reasoning: |
134
134
  Chosen against the four projects the local server holds on 2026-09-29 (whole-project duplicated lines
135
135
  1.6 / 2.1 / 1.7 / 0.0 percent), so new code that meets them is at or below what main already carries.