@starci/hfs 4.3.0 → 4.4.0

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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,121 @@
1
+ # Illustrative production model. Replace all-zero application digests with verified published release digests before use.
2
+ version: "3.8"
3
+ services:
4
+ api:
5
+ image: "{{project}}/api:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
6
+ environment:
7
+ NODE_ENV: production
8
+ PORT: "3001"
9
+ HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
10
+ PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
11
+ PRIMARY_DB_SCHEMA: public
12
+ SUPABASE_URL: https://{{project}}.supabase.co
13
+ SUPABASE_SERVICE_ROLE_KEY_FILE: /run/secrets/supabase-service-role-key
14
+ secrets:
15
+ - source: primary-db-url
16
+ target: primary-db-url
17
+ - source: supabase-service-role-key
18
+ target: supabase-service-role-key
19
+ networks: [app]
20
+ healthcheck:
21
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
22
+ interval: 30s
23
+ timeout: 5s
24
+ start_period: 20s
25
+ retries: 3
26
+ deploy:
27
+ replicas: 1
28
+ placement:
29
+ constraints: ["node.labels.{{project}} == production"]
30
+ update_config:
31
+ parallelism: 1
32
+ order: start-first
33
+ failure_action: rollback
34
+ monitor: 30s
35
+ rollback_config:
36
+ parallelism: 1
37
+ order: stop-first
38
+ restart_policy:
39
+ condition: on-failure
40
+ delay: 5s
41
+ web:
42
+ image: "{{project}}/web:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
43
+ environment:
44
+ NODE_ENV: production
45
+ PORT: "3000"
46
+ HOSTNAME: 0.0.0.0
47
+ NEXT_PUBLIC_SITE_URL: https://{{project}}.example.invalid
48
+ networks: [app]
49
+ healthcheck:
50
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
51
+ interval: 30s
52
+ timeout: 5s
53
+ start_period: 20s
54
+ retries: 3
55
+ deploy:
56
+ replicas: 1
57
+ placement:
58
+ constraints: ["node.labels.{{project}} == production"]
59
+ update_config:
60
+ parallelism: 1
61
+ order: start-first
62
+ failure_action: rollback
63
+ monitor: 30s
64
+ rollback_config:
65
+ parallelism: 1
66
+ order: stop-first
67
+ restart_policy:
68
+ condition: on-failure
69
+ delay: 5s
70
+ ingress:
71
+ image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
72
+ configs:
73
+ - source: ingress-config
74
+ target: /etc/caddy/Caddyfile
75
+ ports:
76
+ - { target: 80, published: 80, protocol: tcp, mode: ingress }
77
+ - { target: 443, published: 443, protocol: tcp, mode: ingress }
78
+ volumes:
79
+ - ingress-data:/data
80
+ - ingress-config-data:/config
81
+ networks: [app]
82
+ healthcheck:
83
+ test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
84
+ interval: 30s
85
+ timeout: 5s
86
+ retries: 3
87
+ deploy:
88
+ replicas: 1
89
+ placement:
90
+ constraints: ["node.labels.{{project}} == production"]
91
+ update_config:
92
+ parallelism: 1
93
+ order: stop-first
94
+ failure_action: rollback
95
+ monitor: 30s
96
+ rollback_config:
97
+ parallelism: 1
98
+ order: stop-first
99
+ restart_policy:
100
+ condition: on-failure
101
+ delay: 5s
102
+ networks:
103
+ app:
104
+ driver: overlay
105
+ secrets:
106
+ primary-db-url:
107
+ external: true
108
+ name: "{{project}}-production-primary-db-url-v1"
109
+ supabase-service-role-key:
110
+ external: true
111
+ name: "{{project}}-production-supabase-service-role-key-v1"
112
+ configs:
113
+ ingress-config:
114
+ file: ./Caddyfile
115
+ volumes:
116
+ ingress-data:
117
+ external: true
118
+ name: "{{project}}-production-ingress-data"
119
+ ingress-config-data:
120
+ external: true
121
+ name: "{{project}}-production-ingress-config-data"
@@ -0,0 +1,21 @@
1
+ # Production environment key roster
2
+
3
+ This inventory declares names, consumers and mounted file paths. Scaffolding generates the two declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set production` after owner provisioning and before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Scaffolding supplies no deployment credential or successful deployment.
4
+
5
+ | Key | Consumer |
6
+ | --- | --- |
7
+ | `PORT` | API listener |
8
+ | `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
9
+ | `PRIMARY_DB_URL` | API least-privilege database connection; reads `PRIMARY_DB_URL_FILE` at `/run/secrets/primary-db-url` |
10
+ | `PRIMARY_DB_SCHEMA` | API primary database schema |
11
+ | `SUPABASE_URL` | API Auth and Storage integration |
12
+ | `SUPABASE_SERVICE_ROLE_KEY` | API Auth and Storage integration; reads `SUPABASE_SERVICE_ROLE_KEY_FILE` at `/run/secrets/supabase-service-role-key` |
13
+ | `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
14
+ | `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
15
+ | `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
16
+ | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
17
+ | `SUPABASE_ACCESS_TOKEN` | Production database deployment |
18
+ | `SUPABASE_DB_PASSWORD` | Production database deployment |
19
+ | `SUPABASE_PROJECT_ID` | Production database deployment |
20
+
21
+ Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in `.starcistacks/production/secrets/primary-db-url.enc` and `.starcistacks/production/secrets/supabase-service-role-key.enc` at the declared paths. Versioned external application secrets are `{{project}}-production-primary-db-url-v1` and `{{project}}-production-supabase-service-role-key-v1`. Seal deployment-only credentials separately under `.starcistacks/production/secrets/*.enc`; they never enter an API container.
@@ -0,0 +1,12 @@
1
+ schema: work/brand@1
2
+ id: brand
3
+ kind: brand
4
+ state: todo
5
+ rev: 1
6
+ brand:
7
+ identity:
8
+ name: "{{project}}"
9
+ review:
10
+ reviewer: the {{project}} owner
11
+ authority: "The lite scaffold seeds this record so the brand tree exists; the owner settles rev 1 of the brand before the first drawn surface."
12
+ reviewedAt: "2026-10-14T00:00:00Z"
@@ -4,7 +4,7 @@ One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and Supabas
4
4
 
5
5
  ## Overview
6
6
 
7
- The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`src/modules/api`, SWR hooks under `src/hooks/swr`) and uses Supabase only for Auth in the browser (`src/modules/auth`); every data read and write goes through the API, which verifies the access token.
7
+ The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and uses Supabase only for Auth in the browser (`fe/apps/web/src/modules/auth`); every data read and write goes through the API, which verifies the access token.
8
8
 
9
9
  ## Stack
10
10
 
@@ -14,9 +14,9 @@ TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, Supabas
14
14
 
15
15
  - `supabase/`: local configuration, forward-only SQL migrations, seed data, and generated database types.
16
16
  - `be/`: the API, shared platform capabilities, verified identity admission, and privileged Supabase integrations.
17
- - `fe/`: the web app; the only Supabase client is the Auth client of `src/modules/auth`, data goes through `src/modules/api`.
17
+ - `fe/`: the web app; the only Supabase client is the Auth client of `fe/apps/web/src/modules/auth`, data goes through `fe/apps/web/src/modules/api`.
18
18
  - `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
19
- - `.starciwork/`: `.gitignore`, `workspace.yaml` project metadata and repo roles, and optional `brand/` drawing records, references, and assets.
19
+ - `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
20
20
 
21
21
  ## Development
22
22
 
@@ -34,6 +34,8 @@ The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMAR
34
34
 
35
35
  `npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local Supabase stack (`npm run db:start`); a lite app has no e2e world.
36
36
 
37
+ The public runtime template tracks only sealed member names. Scaffolding creates this app's own age recipient and encrypts generated placeholder values into the declared `.starcistacks/<environment>/secrets/*.enc` members. Keep the matching host age identity in untracked local custody, then use `starci app secret set` to replace each placeholder with an actual value before deployment. Product apps retain sealed member custody. Runtime specs use disposable copies with ephemeral identities and remove them afterwards.
38
+
37
39
  ## Database changes
38
40
 
39
41
  Create a forward-only migration under `supabase/migrations`, keep each table's RLS policies and grants beside the table, and run `npm run contract:emit` to refresh `supabase/types/database.types.ts`. `starci app add table <name> --fe` adds the table with its GraphQL feature on the API and its operations and SWR hooks in the web app; `npm run codegen:graphql` regenerates the web app's wire types from `be/contracts/<app>/schema.graphql`.
@@ -3,7 +3,7 @@ project_id = "{{project}}"
3
3
  [api]
4
4
  enabled = true
5
5
  port = {{supabasePortApi}}
6
- schemas = ["public", "graphql_public"]
6
+ schemas = []
7
7
  extra_search_path = ["public", "extensions"]
8
8
  max_rows = 1000
9
9
 
@@ -25,6 +25,13 @@ port = {{supabasePortInbucket}}
25
25
  [analytics]
26
26
  port = {{supabasePortAnalytics}}
27
27
 
28
+ [edge_runtime]
29
+ enabled = false
30
+
31
+ [storage]
32
+ enabled = true
33
+ file_size_limit = "50MiB"
34
+
28
35
  [auth]
29
36
  enabled = true
30
37
  site_url = "http://127.0.0.1:3000"
@@ -1,5 +1,13 @@
1
- drop role if exists app_be;
2
- create role app_be login noinherit;
1
+ do $$
2
+ begin
3
+ if not exists (select 1 from pg_catalog.pg_roles where rolname = 'app_be') then
4
+ create role app_be login noinherit nosuperuser nobypassrls nocreatedb nocreaterole;
5
+ end if;
6
+ end;
7
+ $$;
8
+
9
+ -- The attributes a Supabase migration role may change: SUPERUSER and BYPASSRLS stay as create role set them (the role migration owner is no superuser).
10
+ alter role app_be login noinherit nocreatedb nocreaterole;
3
11
 
4
12
  create schema if not exists private;
5
13
 
@@ -24,6 +32,7 @@ create table public.profiles (
24
32
  );
25
33
 
26
34
  alter table public.profiles enable row level security;
35
+ alter table public.profiles force row level security;
27
36
 
28
37
  create trigger profiles_set_updated_at
29
38
  before update on public.profiles
@@ -34,20 +43,20 @@ create policy profiles_app_be_select
34
43
  on public.profiles
35
44
  for select
36
45
  to app_be
37
- using (id is not null);
46
+ using (current_user = 'app_be');
38
47
 
39
48
  create policy profiles_app_be_insert
40
49
  on public.profiles
41
50
  for insert
42
51
  to app_be
43
- with check (id is not null);
52
+ with check (current_user = 'app_be');
44
53
 
45
54
  create policy profiles_app_be_update
46
55
  on public.profiles
47
56
  for update
48
57
  to app_be
49
- using (id is not null)
50
- with check (id is not null);
58
+ using (current_user = 'app_be')
59
+ with check (current_user = 'app_be');
51
60
 
52
61
  grant usage on schema public to app_be;
53
62
  revoke all on table public.profiles from public, anon, authenticated;
@@ -1,10 +1,16 @@
1
1
  # Development stack
2
2
 
3
- The full-edition development stack keeps PostgreSQL and the API in one declared Docker Compose project.
3
+ The app services may run in a local Docker Swarm; local Supabase is started separately with its CLI and supplies the database and broker. The tracked model contains no self-hosted database.
4
+ Starter release images and external secrets must be prepared before starting the container model. Replace its starter `SUPABASE_URL` with the local Supabase endpoint reachable from the Swarm node, and seal that local database connection and integration credential; a container cannot use the host's loopback address without owner-configured networking. Local source development remains available through the app scripts.
4
5
 
5
- - Prepare: provide `PRIMARY_DB_URL` to the API through the normal secret custody flow.
6
- - Up: `docker compose -f infra/compose/compose.yaml up -d`.
7
- - Status: `docker compose -f infra/compose/compose.yaml ps`.
8
- - Logs: `docker compose -f infra/compose/compose.yaml logs`.
9
- - Down: `docker compose -f infra/compose/compose.yaml down`.
10
- - Verify: call the API health endpoint and run `npm run lint`.
6
+ | Command | Operation |
7
+ | --- | --- |
8
+ | prepare | Run `npm run db:start` at the app root; obtain the local Supabase connection and Auth configuration. An owner who chooses local Swarm initializes it, labels its node `{{project}}=dev` and provisions the ingress volumes. Create the external development secrets named in the model from real sealed members. |
9
+ | doctor | From this directory, run `docker stack config -c infra/swarm/stack.yaml`. Confirm image, secret and local Supabase availability. |
10
+ | up | Run `docker stack deploy -c infra/swarm/stack.yaml {{project}}-dev` from this directory after preparation. |
11
+ | status | Run `docker stack services {{project}}-dev`. |
12
+ | logs | Run `docker service logs {{project}}-dev_api`; never print mounted credentials. |
13
+ | down | Run `docker stack rm {{project}}-dev`. |
14
+ | verification | Confirm the API `/health/live` and `/health/ready` endpoints, then run `npm run lint` from the app root. |
15
+
16
+ Keep real sealed members under `.starcistacks/dev/secrets/*.enc`, add them to the manifest after sealing and inventory their key names in the environment record. The scaffold creates no fake secret or successful proof.
@@ -32,5 +32,5 @@ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
32
32
  COPY --from=build /app/be/dist be/dist
33
33
 
34
34
  USER node
35
- HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD pgrep -f "apps/{{app}}/src/main.js" > /dev/null || exit 1
35
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["node", "-e", "const fs=require('node:fs');const args=fs.readFileSync('/proc/1/cmdline','utf8').split(String.fromCharCode(0));process.exit(args.includes('be/dist/apps/{{app}}/src/main.js')?0:1)"]
36
36
  CMD ["node", "be/dist/apps/{{app}}/src/main.js"]
@@ -1,4 +1,4 @@
1
- import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
1
+ import { mockEntityManager } from "@starci/jest-preset"
2
2
  import type { EntityManager } from "typeorm"
3
3
  import { sql } from "./database.sql"
4
4
  import { DatabaseErrorCode } from "./errors/database.error"
@@ -8,35 +8,34 @@ import { recordWebhookDelivery } from "./webhook-inbox.client"
8
8
  const INSERT = sql`SELECT id FROM record_spec_inbox($1, $2, $3::jsonb)`
9
9
  const PAYLOAD = { id: "delivery-1" }
10
10
 
11
- const record = (entityManager: EntityManager, deliveryId: string | undefined) =>
12
- recordWebhookDelivery({ entityManager, insert: INSERT, provider: "spec", deliveryId, payload: PAYLOAD })
11
+ const record = (manager: EntityManager, deliveryId: string | undefined) =>
12
+ recordWebhookDelivery({ manager, insert: INSERT, provider: "spec", deliveryId, payload: PAYLOAD })
13
13
 
14
14
  describe("recordWebhookDelivery", () => {
15
- it("records a new delivery and commits before any business processing", async () => {
16
- const transaction = fakeTransaction(mockEntityManager({ query: [INSERT, [{ id: "inbox-1" }]] }))
15
+ it("records a new delivery through the caller's transaction manager", async () => {
16
+ const manager = mockEntityManager({ query: [INSERT, [{ id: "inbox-1" }]] })
17
17
 
18
- await record(transaction.em, " delivery-1 ")
18
+ const receiptId = await record(manager, " delivery-1 ")
19
19
 
20
- expect(transaction.em.query).toHaveBeenCalledTimes(1)
21
- expect(transaction.em.query).toHaveBeenCalledWith(INSERT, ["spec", "delivery-1", JSON.stringify(PAYLOAD)])
22
- expect(transaction.commits).toBe(1)
20
+ expect(manager.query).toHaveBeenCalledTimes(1)
21
+ expect(manager.query).toHaveBeenCalledWith(INSERT, ["spec", "delivery-1", JSON.stringify(PAYLOAD)])
22
+ expect(receiptId).toBe("inbox-1")
23
23
  })
24
24
 
25
25
  it("acknowledges a duplicate without any completion or business write", async () => {
26
- const transaction = fakeTransaction(mockEntityManager({ query: [INSERT, []] }))
26
+ const manager = mockEntityManager({ query: [INSERT, []] })
27
27
 
28
- await record(transaction.em, "delivery-1")
28
+ const receiptId = await record(manager, "delivery-1")
29
29
 
30
- expect(transaction.em.query).toHaveBeenCalledTimes(1)
31
- expect(transaction.commits).toBe(1)
30
+ expect(manager.query).toHaveBeenCalledTimes(1)
31
+ expect(receiptId).toBeUndefined()
32
32
  })
33
33
 
34
- it.each([["no", undefined], ["a blank", " "]])("refuses %s delivery id before opening a transaction", async (_label, deliveryId) => {
35
- const transaction = fakeTransaction(mockEntityManager())
34
+ it.each([["no", undefined], ["a blank", " "]])("refuses %s delivery id before writing a receipt", async (_label, deliveryId) => {
35
+ const manager = mockEntityManager()
36
36
 
37
- await expect(record(transaction.em, deliveryId)).rejects.toMatchObject({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
37
+ await expect(record(manager, deliveryId)).rejects.toMatchObject({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
38
38
 
39
- expect(transaction.outcomes).toEqual([])
40
- expect(transaction.em.query).not.toHaveBeenCalled()
39
+ expect(manager.query).not.toHaveBeenCalled()
41
40
  })
42
41
  })
@@ -3,33 +3,34 @@ import { DatabaseError, DatabaseErrorCode } from "./errors/database.error"
3
3
  import type { SqlText } from "./database.sql"
4
4
 
5
5
  interface RecordWebhookDelivery {
6
- readonly entityManager: EntityManager
6
+ readonly manager: EntityManager
7
7
  readonly insert: SqlText
8
8
  readonly provider: string
9
9
  readonly deliveryId: string | undefined
10
10
  readonly payload: object
11
11
  }
12
12
 
13
+ type WebhookReceiptId = string | undefined
14
+
13
15
  /**
14
- * Records one pending delivery in one EntityManager transaction. The conflict-safe insert keeps redelivery a no-op.
16
+ * Records one pending delivery using the intake's transaction manager and returns its durable receipt identity.
15
17
  * Business processing and the completion marker belong to the reactor or job that consumes the committed row.
16
18
  */
17
19
  export const recordWebhookDelivery = async ({
18
- entityManager,
20
+ manager,
19
21
  insert,
20
22
  provider,
21
23
  deliveryId,
22
24
  payload,
23
- }: RecordWebhookDelivery): Promise<void> => {
25
+ }: RecordWebhookDelivery): Promise<WebhookReceiptId> => {
24
26
  const normalizedDeliveryId = deliveryId?.trim()
25
27
  if (!normalizedDeliveryId) {
26
28
  throw new DatabaseError({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
27
29
  }
28
- await entityManager.transaction(async (manager) => {
29
- await manager.query(insert, [
30
- provider,
31
- normalizedDeliveryId,
32
- JSON.stringify(payload),
33
- ])
34
- })
30
+ const rows: Array<{ readonly id: string }> = await manager.query(insert, [
31
+ provider,
32
+ normalizedDeliveryId,
33
+ JSON.stringify(payload),
34
+ ])
35
+ return rows[0]?.id
35
36
  }
@@ -0,0 +1,36 @@
1
+ import { BaseEvent } from "@modules/platform/event-bus"
2
+ import type { ParsedEvent } from "@modules/platform/event-bus"
3
+
4
+ /** A durable receipt reference; processing reads the provider payload after the intake commits. */
5
+ export interface @@Provider@@DeliveryRecordedPayload {
6
+ readonly receiptId: string
7
+ }
8
+
9
+ /** Version 1 of `@@provider@@.delivery-recorded`, published once by the provider inbox intake. */
10
+ export class @@Provider@@DeliveryRecordedEvent extends BaseEvent {
11
+ static readonly eventName = "@@provider@@.delivery-recorded"
12
+ static readonly version = 1
13
+
14
+ readonly eventName = @@Provider@@DeliveryRecordedEvent.eventName
15
+
16
+ private constructor(
17
+ readonly eventId: string,
18
+ readonly payload: @@Provider@@DeliveryRecordedPayload,
19
+ ) {
20
+ super()
21
+ }
22
+
23
+ /** The receipt identity is stable across relay retries and distinct from a consumer's inbox source. */
24
+ static create(payload: @@Provider@@DeliveryRecordedPayload): @@Provider@@DeliveryRecordedEvent {
25
+ return new @@Provider@@DeliveryRecordedEvent(payload.receiptId, payload)
26
+ }
27
+
28
+ /** Reads an outbox envelope; malformed deliveries never reach a processing consumer. */
29
+ static parse(envelope: unknown): ParsedEvent<@@Provider@@DeliveryRecordedEvent> {
30
+ if (typeof envelope !== "object" || envelope === null || !("eventId" in envelope) || !("payload" in envelope)) return null
31
+ const { eventId, payload } = envelope
32
+ if (typeof eventId !== "string" || !eventId.trim() || typeof payload !== "object" || payload === null || !("receiptId" in payload)) return null
33
+ if (typeof payload.receiptId !== "string" || !payload.receiptId.trim()) return null
34
+ return new @@Provider@@DeliveryRecordedEvent(eventId, { receiptId: payload.receiptId })
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ export { @@Provider@@DeliveryRecordedEvent } from "./@@provider@@-delivery-recorded.event"
@@ -0,0 +1,10 @@
1
+ {
2
+ "events": {
3
+ "@@provider@@.delivery-recorded": {
4
+ "payload": { "receiptId": "string" },
5
+ "version": 1
6
+ }
7
+ },
8
+ "schema": "starci/event-contract@1",
9
+ "service": "@@provider@@"
10
+ }
@@ -9,6 +9,7 @@ create table public.@@table@@ (
9
9
  );
10
10
 
11
11
  alter table public.@@table@@ enable row level security;
12
+ alter table public.@@table@@ force row level security;
12
13
 
13
14
  -- The dedicated app_be PostgreSQL role is the only application path. With no anon or authenticated policy,
14
15
  -- RLS denies those PostgREST roles by default; the service-role JWT is deliberately not granted here.
@@ -0,0 +1,42 @@
1
+ /** The base of each typed event in modules/events: its stable identity, name and payload. */
2
+ export abstract class BaseEvent {
3
+ /** A receiver uses this stable identity to deduplicate redelivery. */
4
+ abstract readonly eventId: string
5
+ /** The event name listed by the service's vendored event contract. */
6
+ abstract readonly eventName: string
7
+ /** The fact that a reactor or job processes after commit. */
8
+ abstract readonly payload: object
9
+ }
10
+
11
+ /** Parsing either returns the event or refuses a mismatched envelope. */
12
+ export type ParsedEvent<Event extends BaseEvent> = Event | null
13
+
14
+ /** The class side of a typed event, shared by publishers and later consumers. */
15
+ export interface EventClass<Event extends BaseEvent> {
16
+ /** The contract's event name. */
17
+ readonly eventName: string
18
+ /** The contract's payload version. */
19
+ readonly version: number
20
+ /** Reads the event from its durable envelope. */
21
+ parse(envelope: unknown): ParsedEvent<Event>
22
+ }
23
+
24
+ /** One committed delivery received by a reactor. */
25
+ export interface EventDelivery<Event extends BaseEvent> {
26
+ /** The consumer's idempotency key. */
27
+ readonly eventId: string
28
+ /** The typed event. */
29
+ readonly event: Event
30
+ /** The number of attempts, including this one. */
31
+ readonly attempt: number
32
+ }
33
+
34
+ /** The typed event's durable envelope, read only after the intake transaction commits. */
35
+ export interface EventEnvelope {
36
+ /** The stable event id. */
37
+ readonly eventId: string
38
+ /** The contract's event name. */
39
+ readonly eventName: string
40
+ /** The data of the fact. */
41
+ readonly payload: object
42
+ }
@@ -0,0 +1,9 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { EventBus } from "./event-bus.port"
4
+
5
+ /** Token of the transactional EventBus publisher. */
6
+ export const EVENT_BUS: unique symbol = Symbol("platform.event-bus")
7
+
8
+ /** Injects the EventBus port. Parameter type: EventBus. */
9
+ export const InjectEventBus = (): TypedParameterDecorator<EventBus> => injector<EventBus>(EVENT_BUS)
@@ -0,0 +1,6 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { EventBusOptions } from "./event-bus.options"
3
+
4
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<EventBusOptions>()
5
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
6
+ .build()
@@ -0,0 +1,19 @@
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import { EVENT_BUS } from "./event-bus.decorators"
4
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./event-bus.module-definition"
5
+ import { EventBusService } from "./event-bus.service"
6
+
7
+ @Module({})
8
+ /** Provides the transactional lite EventBus publisher, shared by domain intake services. */
9
+ export class EventBusModule extends ConfigurableModuleClass {
10
+ /** Registers the publishing capability once per app. */
11
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
12
+ const base = super.register(options)
13
+ return {
14
+ ...base,
15
+ providers: [...(base.providers ?? []), { provide: EVENT_BUS, useClass: EventBusService }],
16
+ exports: [EVENT_BUS],
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ /** The lite publisher uses the caller's transaction and has no runtime options. */
2
+ export interface EventBusOptions {}
@@ -0,0 +1,16 @@
1
+ import type { EntityManager } from "typeorm"
2
+ import type { BaseEvent, EventClass, EventDelivery } from "./event-bus.contracts"
3
+
4
+ /** The publishing side of the lite event bus; the reactor or job owns delivery of committed outbox rows. */
5
+ export interface EventBus {
6
+ /** Writes the typed event in the caller's transaction; a rollback writes no durable event. */
7
+ publish(event: BaseEvent, tx: EntityManager): Promise<void>
8
+ }
9
+
10
+ /** A later message transport receives one typed event and forwards its delivery id. */
11
+ export interface EventConsumer<Event extends BaseEvent> {
12
+ /** The event class the consumer reads. */
13
+ readonly event: EventClass<Event>
14
+ /** Applies one delivery after commit; a throw leaves the delivery available for retry. */
15
+ handle(delivery: EventDelivery<Event>): Promise<void>
16
+ }
@@ -0,0 +1,45 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { BaseEvent } from "./event-bus.contracts"
4
+ import { EventBusService } from "./event-bus.service"
5
+ import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
6
+
7
+ class ReceiptRecordedEvent extends BaseEvent {
8
+ readonly eventId = "receipt-1"
9
+ readonly eventName = "spec.delivery-recorded"
10
+ readonly payload = { receiptId: "receipt-1" }
11
+ }
12
+
13
+ describe("EventBusService", () => {
14
+ const build = async () => {
15
+ const module = await Test.createTestingModule({ providers: [EventBusService] }).compile()
16
+ return module.get(EventBusService)
17
+ }
18
+
19
+ it("writes the typed envelope through the receipt transaction", async () => {
20
+ const subject = await build()
21
+ const transaction = fakeTransaction(mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] }))
22
+ const event = new ReceiptRecordedEvent()
23
+
24
+ await transaction.em.transaction((manager) => subject.publish(event, manager))
25
+
26
+ expect(transaction.em.query).toHaveBeenCalledWith(INSERT_OUTBOX_EVENT, [
27
+ "receipt-1",
28
+ "spec.delivery-recorded",
29
+ JSON.stringify({ eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
30
+ ])
31
+ expect(transaction.commits).toBe(1)
32
+ })
33
+
34
+ it("propagates an outbox failure so the receipt transaction rolls back", async () => {
35
+ const subject = await build()
36
+ const manager = mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] })
37
+ jest.spyOn(manager, "query").mockRejectedValue(new Error("outbox unavailable"))
38
+ const transaction = fakeTransaction(manager)
39
+
40
+ await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx))).rejects.toThrow("outbox unavailable")
41
+
42
+ expect(transaction.commits).toBe(0)
43
+ expect(transaction.rollbacks).toBe(1)
44
+ })
45
+ })
@@ -0,0 +1,15 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
4
+ import type { EventBus } from "./event-bus.port"
5
+ import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
6
+
7
+ @Injectable()
8
+ /** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
9
+ export class EventBusService implements EventBus {
10
+ /** The outbox commits or rolls back with the receipt; only a later reactor or job processes it. */
11
+ async publish(event: BaseEvent, tx: EntityManager): Promise<void> {
12
+ const envelope: EventEnvelope = { eventId: event.eventId, eventName: event.eventName, payload: event.payload }
13
+ await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope)])
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ export { BaseEvent } from "./event-bus.contracts"
2
+ export type { EventClass, EventDelivery, EventEnvelope, ParsedEvent } from "./event-bus.contracts"
3
+ export { EVENT_BUS, InjectEventBus } from "./event-bus.decorators"
4
+ export { EventBusModule } from "./event-bus.module"
5
+ export type { EventBus, EventConsumer } from "./event-bus.port"
@@ -0,0 +1,5 @@
1
+ import { sql } from "@modules/platform/database"
2
+
3
+ /** Writes only the typed envelope; a reactor or job reads the committed row afterward. */
4
+ export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload)
5
+ VALUES ($1, $2, $3::jsonb)`