@starci/hfs 4.9.11 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -9
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +1 -1
  5. package/db/dump.mjs +11 -10
  6. package/db/entity-list.mjs +1 -1
  7. package/db/migrate.mjs +83 -259
  8. package/db/new.mjs +46 -0
  9. package/db/post-cutover.mjs +13 -17
  10. package/db/postgres.mjs +80 -0
  11. package/emit/contracts.mjs +5 -9
  12. package/package.json +1 -1
  13. package/release/gate.mjs +3 -15
  14. package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
  15. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  16. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  17. package/runtime/knowledge/hfs/rules.yaml +179 -157
  18. package/runtime/knowledge/hfs/slots.yaml +185 -172
  19. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  20. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  21. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  22. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  23. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  24. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  25. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  26. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  28. package/runtime/modules/kernel/command-policy.yaml +1 -1
  29. package/runtime/modules/kernel/failure-codes.yaml +65 -115
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  31. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/entity-registry.mjs +53 -0
  33. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  34. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  35. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  36. package/runtime/scripts/hfs/architecture/owners.mjs +11 -0
  37. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  38. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  39. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  40. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  41. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  42. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  43. package/runtime/scripts/hfs/architecture/tiers.mjs +14 -3
  44. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  45. package/runtime/scripts/hfs/check.mjs +6 -6
  46. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  47. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  48. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  49. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  50. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  51. package/runtime/scripts/hfs/manifest-shape.mjs +9 -5
  52. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  53. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  54. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  55. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  56. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  57. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  58. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  59. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  60. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  61. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  62. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  63. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  64. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  65. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  66. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  67. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  68. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  69. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  70. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  71. package/runtime/scripts/hfs/slots.mjs +8 -2
  72. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  73. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  74. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  75. package/scaffold/add-app.mjs +2 -10
  76. package/scaffold/add-cli-lite.mjs +6 -6
  77. package/scaffold/add-compose.mjs +1 -1
  78. package/scaffold/add-inbox-lite.mjs +4 -8
  79. package/scaffold/add-plan.mjs +1 -1
  80. package/scaffold/add-queue-lite.mjs +16 -24
  81. package/scaffold/add-table.mjs +34 -26
  82. package/scaffold/add.mjs +2 -2
  83. package/scaffold/app.mjs +19 -86
  84. package/scaffold/lite-exports.mjs +1 -1
  85. package/src/main.mjs +19 -20
  86. package/stack/target-binding.mjs +0 -60
  87. package/stack/urgent-floor.mjs +2 -2
  88. package/sync/index.mjs +32 -8
  89. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  90. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  91. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  92. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  93. package/templates/app/gitignore-lite +0 -3
  94. package/templates/app/package-scripts/db.json +9 -0
  95. package/templates/app/package-scripts/package.json +0 -1
  96. package/templates/app/package-scripts-lite/package.json +1 -1
  97. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  98. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  99. package/templates/app/skeleton/README.md +1 -1
  100. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  101. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  102. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  103. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  104. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  105. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  106. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  107. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  108. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  109. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  110. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  111. package/templates/app/skeleton-lite/README.md +11 -11
  112. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  113. package/templates/app/tool-config-lite/prettierignore +0 -1
  114. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  115. package/templates/be/inbox/database-client.ts.tpl +13 -4
  116. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  117. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  118. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  119. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  120. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  121. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  122. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  123. package/templates/be/inbox/service.ts.tpl +1 -2
  124. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  125. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  126. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  127. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  128. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  129. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  130. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  131. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  132. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  133. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  134. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  135. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  136. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  137. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  138. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  139. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  140. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  141. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  142. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  143. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  145. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  146. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  147. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
  148. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  149. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  150. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  151. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  152. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  153. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  154. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  158. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  159. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  160. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  161. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  162. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  163. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  164. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  165. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  166. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  167. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  168. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  169. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  170. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  171. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +7 -0
  172. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  173. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  174. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  175. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  176. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  177. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  178. package/templates/be/table/entities.index.ts.tpl +4 -0
  179. package/templates/be/table/entity.ts.tpl +21 -0
  180. package/templates/be/table/index.ts.tpl +1 -0
  181. package/templates/be/table/module.ts.tpl +1 -1
  182. package/templates/be/table/service.api.ts.tpl +10 -11
  183. package/templates/be/table/service.ts.tpl +5 -6
  184. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  185. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  186. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  187. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  188. package/db/local-migrate.mjs +0 -194
  189. package/db/reset.mjs +0 -125
  190. package/emit/db-types.mjs +0 -271
  191. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  192. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  193. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  194. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  195. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  196. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  197. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  198. package/scaffold/supabase-ports.mjs +0 -26
  199. package/templates/app/package-scripts/supabase.json +0 -7
  200. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  201. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  202. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  203. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  204. package/templates/be/inbox/sql.ts.tpl +0 -16
  205. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  206. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  207. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  208. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  209. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  210. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  211. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  212. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  213. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  214. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  215. package/templates/be/table/rows.ts.tpl +0 -4
  216. package/templates/be/table/sql.api.ts.tpl +0 -7
  217. package/templates/be/table/sql.ts.tpl +0 -4
  218. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  219. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  220. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  221. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  222. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
@@ -288,14 +288,14 @@ files:
288
288
  role: "The consumed event's e2e flow; titles event-bus/<scenario>: ... prove the pattern."
289
289
  spec: event-bus/atomic-with-transaction, event-bus/relay-only-after-commit, event-bus/redelivery-then-dead-letter, event-bus/duplicate-delivery-is-a-noop
290
290
  rules: [BE_PATTERN_SPEC_MISSING]
291
- # Edition lite (R212): the same ports and rules, a Supabase adapter behind them. ruling:R212/case-10 moved the lite queue to Redis
291
+ # Edition lite (R212): the same ports and rules, a self-hosted Postgres adapter behind them. ruling:R212/case-10 moved the lite queue to Redis
292
292
  # because full's queue is Redis; full's event bus is Kafka, so the lite event-bus adapter below is unchanged by that ruling.
293
293
  liteAdapter:
294
294
  summary: >-
295
295
  Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction, the outbox,
296
296
  the inbox and thin consumers; Kafka is replaced by pgmq-backed topics, or by in-process delivery after commit for a back end
297
297
  with a single api app. Each consumer group of a topic has its own pgmq queue `<service>__<event>__<group>`, created by a
298
- supabase migration, named by a literal `<GROUP>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258) and
298
+ db migration, named by a literal `<GROUP>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258) and
299
299
  granted only to the application role, so each group keeps its own cursor and dead letter.
300
300
  ports:
301
301
  - {port: EventPublisher (platform/event-bus), adapter: the same outbox row in the domain transaction}
@@ -304,4 +304,4 @@ liteAdapter:
304
304
  forbids: [kafkajs]
305
305
  proof: >-
306
306
  Unit specs as full; declaring event-bus requires integration specs titled `event-bus/<scenario>: ...` for every declared
307
- scenario on `starci supabase start` (R284). Lite has no e2e world.
307
+ scenario on the local db service (`npm run db:start`, R284). Lite has no e2e world.
@@ -81,7 +81,7 @@ rules:
81
81
  (`ON CONFLICT (source, event_id) DO NOTHING RETURNING event_id`) returns `true` for a new row and `false` for a
82
82
  duplicate without aborting the caller's transaction. It never reads first and writes after, which races two concurrent
83
83
  deliveries of the same event. Lite uses its existing `public.inbox` table with the same client receipt columns,
84
- owned by a forward-only Supabase migration. A client receipt extends this table; no capability creates another
84
+ owned by a forward-only db migration. A client receipt extends this table; no capability creates another
85
85
  dedupe table or unique request-key constraint to compete with it.
86
86
  rationale: |-
87
87
  One shared table with one owner keeps the guarantee "this (source, event id) claims at most once" in one place, and
@@ -250,7 +250,7 @@ files:
250
250
  liteAdapter:
251
251
  summary: >-
252
252
  Lite keeps the Inbox port and transaction-owned client receipt API. Its existing public.inbox
253
- table uses Supabase migration authority; the be/inbox platform templates carry the lite adapter and the
253
+ table uses db migration authority; the be/inbox platform templates carry the lite adapter and the
254
254
  platform/client-operation-outcomes.sql.tpl forward migration. The files tree above describes the full TypeORM adapter.
255
255
  Delivery-only webhook scaffolds do not install a client marker. When adopting this client pattern, add the shared
256
256
  idempotency/platform/inbox/effectful.decorators.ts.tpl as src/modules/platform/inbox/effectful.decorators.ts and
@@ -261,5 +261,5 @@ liteAdapter:
261
261
  - {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a pg_cron tick that enqueues through the queue outbox}
262
262
  proof: >-
263
263
  The same inbox and fingerprint unit specs apply; declaring idempotency requires both client receipt scenarios in
264
- lite's local Supabase integration lane.
264
+ lite's local db integration lane.
265
265
  see: [knowledge/patterns/be/queues.yaml]
@@ -277,18 +277,18 @@ files:
277
277
  role: "Titles fenced-job/<scenario>: ... prove the pattern."
278
278
  spec: fenced-job/claim-bumps-token, fenced-job/zombie-fenced-out, fenced-job/redispatch-isolated
279
279
  rules: [BE_PATTERN_SPEC_MISSING]
280
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
280
+ # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
281
281
  liteAdapter:
282
282
  summary: >-
283
283
  Under lite a fenced job is unchanged: the job row with its fencing token, the atomic claim, guarded writes and run keys. The
284
- row's table is a supabase migration, and the processor is fed by full's BullMQ queue on the declared Redis, so a lite app
284
+ row's table is a db migration, and the processor is fed by full's BullMQ queue on the declared Redis, so a lite app
285
285
  that declares fenced-job declares `backing: [redis]`. Without a declared Redis a lite app has no fenced job; a long task is
286
286
  a declared replicated-job running a cli command.
287
287
  ports:
288
- - {port: job claim (platform/jobs), adapter: "the same claim SQL through the shared EntityManager; the table comes from supabase/migrations"}
288
+ - {port: job claim (platform/jobs), adapter: "the claim and guarded writes as the job_claim, job_advance, job_complete and job_fail functions of the jobs migration in db/migrations, called through callDbFunction of platform/database (R334)"}
289
289
  - {port: processor delivery, adapter: "full's BullMQ worker (attempts and exponential backoff from the queue policy, failed jobs kept as the dead letter)"}
290
290
  proof: >-
291
291
  Unit specs as full (job-claim.service.spec.ts); declaring fenced-job requires integration specs titled
292
- `fenced-job/<scenario>: ...` for every declared scenario on the local Supabase stack (R284, which runs no container of its
292
+ `fenced-job/<scenario>: ...` for every declared scenario on the local db service (R284, which runs no container of its
293
293
  own): the claim, the fencing token and the guarded writes are Postgres facts, driven there by the real runner without BullMQ.
294
294
  see: [knowledge/patterns/be/queues.yaml]
@@ -176,15 +176,17 @@ files:
176
176
  role: "Titles projection/<scenario>: ... prove the pattern."
177
177
  spec: projection/recompute-is-idempotent, projection/replay-rebuilds
178
178
  rules: [BE_PATTERN_SPEC_MISSING]
179
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
179
+ # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
180
180
  liteAdapter:
181
181
  summary: >-
182
182
  Under lite a projection is unchanged in behaviour (only it writes its table, recompute is idempotent and replayable, readers
183
- use get*); its table is a supabase migration with a rows type, so the slot carries no projection entity (be.projections lite
184
- overlay), and it is kept current by a reactor on the lite event bus or by a job on the queue (BullMQ on the declared Redis).
183
+ use get*); its table is a db migration mapped by a projection entity under persistence/entities/ (named in PRIMARY_ENTITIES of
184
+ platform/database/entities.ts) and read and written through the EntityManager, never raw SQL (R334, ruling
185
+ lite-persistence-typeorm-entities), and it is kept current by a reactor on the lite event bus or by a job on the queue (BullMQ on
186
+ the declared Redis).
185
187
  ports:
186
- - {port: read model table, adapter: "a supabase migration plus <name>.sql.ts and <name>.rows.ts under persistence/"}
188
+ - {port: read model table, adapter: "a db migration plus its entity under persistence/entities/ and the capability's persistence/entities/index.ts list; no <name>.sql.ts or <name>.rows.ts"}
187
189
  proof: >-
188
190
  Unit specs as full (the projection is a logic role at 100 per file); declaring projection requires integration specs titled
189
- `projection/<scenario>: ...` for every declared scenario on `starci supabase start` (R284).
191
+ `projection/<scenario>: ...` for every declared scenario on the local db service (`npm run db:start`, R284).
190
192
  see: [knowledge/patterns/be/event-bus.yaml, knowledge/patterns/be/queues.yaml]
@@ -83,7 +83,7 @@ rules:
83
83
  hfsRules: [R90, R258]
84
84
  requirement: |-
85
85
  A recurring task is a job scheduler (`upsertJobScheduler`) that the queue module declares in its `schedulers` list and
86
- `platform/queue` registers at boot, or, in an app on Supabase, a uniquely named pg_cron job of a migration whose command
86
+ `platform/queue` registers at boot, or, in a lite app, a uniquely named pg_cron job of a migration whose command
87
87
  (directly or through one `*_tick` function) only inserts one row of a queue constant into the queue outbox table of
88
88
  `ruleParams.db.queueOutbox` (`public.queue`), which the relay hands to BullMQ, or by calling the enqueue function that the app declares with its own outbox table in `hfs.json` `sides.be.queueOutbox` (redis declared in `sides.be.backing`; the function's active definition only inserts into that table); a tick has exactly one of the two owners. A job name is scheduled once while it is active: a later migration that replaces a tick's command calls `cron.unschedule` of the name first. Its job is processed by the same
89
89
  `features/jobs/<job>/<job>.processor.ts` as any enqueued job. `@nestjs/schedule`, `@Cron`, `@Interval`, `@Timeout` and
@@ -96,7 +96,7 @@ rules:
96
96
  when: Expire unpaid orders every five minutes
97
97
  write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
98
98
  - id: case-2
99
- when: A lite app on Supabase sends booking reminders every fifteen minutes
99
+ when: A lite app sends booking reminders every fifteen minutes
100
100
  write: "A migration declares private.booking_reminder_tick() as one insert into public.queue (queue, payload, created_at) values ('booking_reminder', jsonb_build_object('tick', true), now()) and schedules it with cron.schedule('booking_reminder_tick', '*/15 * * * *', ...); the send-reminder processor is the queue's."
101
101
  verification:
102
102
  automated:
@@ -275,19 +275,19 @@ liteAdapter:
275
275
  full's adapter: the relay marks an outbox row sent only after the transport's persist step confirms Redis wrote the job to
276
276
  its append-only file (WAITAOF) and still holds it, so a job Redis loses inside the at most one second fsync window leaves
277
277
  its row waiting and is added again; that is why a lite queue does not lose a job. Without a declared Redis there is no queue
278
- pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. Supabase Queues
279
- (pgmq) is not a queue transport. `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
280
- tables as one supabase migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
278
+ pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. pgmq
279
+ is not a queue transport. `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
280
+ tables as one db migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
281
281
  ports:
282
- - {port: QueueOutbox (platform/queue), adapter: "full's outbox row written through the caller's tx; the table comes from a supabase migration (no TypeORM entity under lite)"}
282
+ - {port: QueueOutbox (platform/queue), adapter: "the outbox row written through the caller's tx by queue_write, and the relay's queue_waiting and queue_mark_sent, functions of the queue migration in db/migrations called through callDbFunction of platform/database (R334)"}
283
283
  - {port: queue transport (queue-transport.port.ts), adapter: "full's bullmq-queue-transport.client.ts over the declared Redis (host, port, db and the password read from its sealed secret file), plus persist(jobs): WAITAOF 1 0 on each queue's connection, then each job's state, refusing with QUEUE_NOT_PERSISTED or QUEUE_JOB_LOST (errors/queue.error.ts)"}
284
284
  - {port: outbox relay (queue-relay.service.ts), adapter: "full's relay, which calls persist after the adds and before it marks the rows sent, so a refusal rolls the mark back and the next pass adds the rows again"}
285
285
  - {port: QueueWorkerRegistry (platform/queue), adapter: "full's BullMQ worker per queue: attempts and exponential backoff from queue.policy.ts, failed jobs kept as the dead letter"}
286
- - {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a supabase migration whose tick inserts one row of the queue constant into the queue outbox"}
286
+ - {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a db migration whose tick inserts one row of the queue constant into the queue outbox"}
287
287
  forbids: [kafkajs, "@nestjs/schedule"]
288
288
  proof: >-
289
289
  Unit specs as full (queue-outbox, relay and worker services, the BullMQ client against a mocked factory); declaring queue
290
- requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local Supabase stack (R284,
290
+ requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local db service (R284,
291
291
  which runs no container of its own): the outbox, its commit boundary, the relay identity and the pg_cron tick are Postgres
292
292
  facts proved there with the real outbox writer and relay, and the queue transport records what the relay hands to BullMQ
293
293
  (the job id is the outbox id). BullMQ itself is covered by the unit spec of the BullMQ client, which also proves the
@@ -154,7 +154,7 @@ files:
154
154
  - path: src/tests/e2e/<area>/<event>.e2e-spec.ts
155
155
  slot: be.tests.e2e
156
156
  role: The reactor's event flow through the bus, redelivery included.
157
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
157
+ # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
158
158
  liteAdapter:
159
159
  summary: >-
160
160
  Under lite a reactor is unchanged: a thin consumer of another context's events that dispatches one command. Its deliveries
@@ -163,5 +163,5 @@ liteAdapter:
163
163
  - {port: EventConsumer delivery, adapter: "the reactor's pgmq queue (see event-bus.yaml 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
- its scenario-titled integration specs on `starci supabase start` (R284), including redelivery through the reactor.
166
+ its scenario-titled integration specs on the local db service (`npm run db:start`, R284), including redelivery through the reactor.
167
167
  see: [knowledge/patterns/be/event-bus.yaml]
@@ -290,8 +290,8 @@ rules:
290
290
  the memory hub (STACKS_REALTIME_SCALED) and the two-replica scenarios are the realtime scale rule BE-REALTIME-7.
291
291
 
292
292
  Lite (ruling:R212/case-10, owner:lite-vector-is-pgvector):
293
- ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on Postgres/Supabase plus
294
- 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
293
+ ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on its own Postgres plus
294
+ Redis when a declared capability needs it. Vector search uses pgvector in the db service. The three doors above are served from that api (the GraphQL subscription door is new
295
295
  in lite, ruling:R212/case-7). A lite api with more than one replica uses the Redis hub when Redis is
296
296
  declared, else the Postgres hub. The example is the SSE `booking-status` channel.
297
297
 
@@ -373,7 +373,7 @@ rules:
373
373
  snapshot with a query on every reconnect. The engines (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`,
374
374
  `graphql-ws`, `graphql-subscriptions`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`
375
375
  (`BE_INFRA_OWNER`). The tables are migrations of the app (`realtime_events` and, for the Postgres socket adapter,
376
- `socket_io_attachments`: a Supabase migration in lite, a TypeORM migration of `platform/realtime` in full). Declaring realtime
376
+ `socket_io_attachments`: a db migration in lite, a TypeORM migration of `platform/realtime` in full). Declaring realtime
377
377
  adds two scenarios, proven with two api replicas: `realtime/push-crosses-replicas` (published through replica A, received on
378
378
  replica B) and `realtime/resume-after-reconnect` (`Last-Event-ID` sent to the other replica receives the missed frames).
379
379
  rationale: |-
@@ -406,7 +406,7 @@ rules:
406
406
  publish writes the realtime_events row and NOTIFYs; every replica LISTENs and writes the frame to its local streams; a
407
407
  reconnect to ANY replica sends Last-Event-ID and the hub reads the missed rows from the table. A domain service that owns the
408
408
  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
409
- not buffered or compressed and the idle timeout is above the heartbeat. Migration: supabase/migrations/<ts>_realtime-events.sql
409
+ not buffered or compressed and the idle timeout is above the heartbeat. Migration: db/migrations/<ts>_realtime-events.sql
410
410
  (lite) or platform/realtime/persistence/migrations/<epochMs>-create-realtime-events.ts (full).
411
411
  - id: case-3
412
412
  when: SSE, scale on Redis (Redis declared, lite or full)
@@ -668,9 +668,9 @@ files:
668
668
  slot: be.persistence
669
669
  role: Full edition migration of the realtime_events replay table and the socket_io_attachments table of the Postgres adapter.
670
670
  optional: true
671
- - path: supabase/migrations/<timestamp>_realtime-events.sql
671
+ - path: db/migrations/<timestamp>_realtime-events.sql
672
672
  slot: repo.side-root-forbidden
673
- 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.
673
+ role: Lite edition migration (created with `npm run db:new`) of the same two tables, forced row level security for app_be, and the pg_cron tick of the retention queue.
674
674
  optional: true
675
675
  - path: src/features/reactors/<name>/transport/message/<event>.consumer.ts
676
676
  slot: be.transport.message
@@ -262,7 +262,7 @@ rules:
262
262
  manual:
263
263
  - Check the injected fault hits the compensation, not only the step before it.
264
264
  relatedExamples: [saga]
265
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
265
+ # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
266
266
  liteAdapter:
267
267
  summary: >-
268
268
  Under lite a saga is unchanged: the orchestrator service, the versioned state behind the fenced store, steps and their
@@ -270,9 +270,9 @@ liteAdapter:
270
270
  consumer group).
271
271
  ports:
272
272
  - {port: saga events, adapter: "the lite event-bus adapter (pgmq) - see event-bus.yaml liteAdapter"}
273
- - {port: saga state store, adapter: "the same table and version fence, created by a supabase migration (no TypeORM entity under lite)"}
273
+ - {port: saga state store, adapter: "the same table and version fence, created by a db migration"}
274
274
  proof: >-
275
275
  The orchestrator's unit spec (<saga>.saga.service.spec.ts) as full; declaring saga requires integration specs titled
276
- `saga/<scenario>: ...` for every declared scenario on `starci supabase start` (R284), including each compensation path with
276
+ `saga/<scenario>: ...` for every declared scenario on the local db service (`npm run db: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
278
  see: [knowledge/patterns/be/event-bus.yaml]
@@ -370,7 +370,7 @@ sourceMaintenance: >-
370
370
  verifies first and calls the supplied domain service's record<Provider>Delivery once. Implement that method as the
371
371
  transactional inbox receipt/outbox recording intake above, then add its reactor or job separately.
372
372
  In lite, the generated POST verifies first and calls <Provider>InboxService.record<Provider>Delivery. This record-only
373
- domain intake uses recordWebhookDelivery to insert a pending provider inbox row atomically in Supabase Postgres, unique on
373
+ domain intake uses recordWebhookDelivery to insert a pending provider inbox row atomically in the app's Postgres, unique on
374
374
  (provider, delivery_id), without processing it or marking it complete. Add a reactor or job separately to consume the
375
375
  committed pending row and dispatch the processing command (reactors.yaml, jobs.yaml). The --service option validates the
376
376
  future domain processing owner; the webhook writer does not import or call that owner synchronously and does not
@@ -182,7 +182,7 @@ raw-tools:
182
182
  podman: {use: 'starci docker up, starci docker down, starci docker build, or starci docker ps'}
183
183
  kubectl: {use: 'starci app stack'}
184
184
  k3d: {use: 'starci app stack'}
185
- supabase: {use: "starci supabase start, starci supabase stop, or starci supabase status for the app's own stack"}
185
+ supabase: {use: "starci app db new|plan|migrate|dump, and npm run db:start or db:stop for the app's own database"}
186
186
  gh: {use: 'starci release cut (the GitHub release is created from the CHANGELOG section of the tag); anything else is the owner''s'}
187
187
  schtasks: {use: 'starci task register, starci task show, or starci task list for runtime scheduled tasks'}
188
188
  shutdown: {use: 'ask the owner: a worker never shuts the host down'}
@@ -870,7 +870,7 @@ BE_JSON_PARSE_UNGUARDED:
870
870
  BE_JWT_ALG_PINNED:
871
871
  title: "JWT verification has no pinned asymmetric algorithm policy"
872
872
  title_vi: "Xác minh JWT thiếu chính sách thuật toán bất đối xứng cố định"
873
- meaning_vi: "Bộ xác minh chưa cố định thuật toán an toàn, nguồn cấu hình của issuer hoặc vai trò authenticated của principal Supabase."
873
+ meaning_vi: "Bộ xác minh chưa cố định thuật toán an toàn, nguồn cấu hình của issuer hoặc vai trò authenticated của principal."
874
874
  causes_vi:
875
875
  - "Danh sách algorithms rỗng, động hoặc chứa none hay HS256"
876
876
  - "Issuer viết trực tiếp hoặc principal chấp nhận vai trò khác authenticated"
@@ -1497,25 +1497,7 @@ BE_SQL_TABLE_OWNER:
1497
1497
  owner: op-retry
1498
1498
  kind: check-finding
1499
1499
 
1500
- BE_SUPABASE_CLIENT_OWNER:
1501
- title: "A back-end Supabase client, import or call is outside `be.integrations.supabase`, or is not typed with `Database`"
1502
- title_vi: "Supabase client phía back end sai chủ sở hữu hoặc thiếu kiểu Database"
1503
- meaning_vi: "Import/call Supabase ở `<path>` không thuộc `be.integrations.supabase`, hoặc client được tạo mà không mang kiểu `Database`."
1504
- causes_vi:
1505
- - "Vi phạm L10: back end dùng EntityManager cho dữ liệu; chỉ integration Supabase sở hữu Auth/Storage client và kiểu Database."
1506
- nextStep_vi: "Chuyển Auth/Storage client vào `be.integrations.supabase`, dùng API công khai của integration và thêm kiểu `Database`; dữ liệu thường đi qua EntityManager."
1507
- owner: op-retry
1508
- kind: check-finding
1509
1500
 
1510
- BE_SUPABASE_JWT_VERIFIED:
1511
- title: "A Supabase principal is derived without pinned JWKS verification, or authorization claims come from the request body"
1512
- title_vi: "JWT Supabase chưa được xác minh đầy đủ hoặc claim lấy từ request body"
1513
- meaning_vi: "`<path>` decode JWT không verify, gọi `jwtVerify` ngoài integration/thiếu JWKS, issuer, audience `authenticated` hay algorithm pin, hoặc đọc role/membership từ body."
1514
- causes_vi:
1515
- - "Vi phạm L16: principal chỉ đến từ access token đã verify signature/iss/aud/exp/algorithm hoặc được đọc lại từ DB."
1516
- nextStep_vi: "Dùng `createRemoteJWKSet` + `jwtVerify` trong `be.integrations.supabase`, pin issuer/audience/algorithms và bỏ mọi authorization claim từ body."
1517
- owner: op-retry
1518
- kind: check-finding
1519
1501
 
1520
1502
  BE_TEST_BUILDER_ARRANGES:
1521
1503
  title: "A test data builder asserts or has a non-deterministic default"
@@ -1811,15 +1793,6 @@ CLI_ONLY_ENTRY:
1811
1793
  owner: op-retry
1812
1794
  kind: check-finding
1813
1795
 
1814
- DB_API_EXPOSED:
1815
- title: "The database API never exposes pgmq and lite data access is closed"
1816
- title_vi: "API cơ sở dữ liệu mở schema bị cấm"
1817
- meaning_vi: "config.toml mở pgmq hoặc pgmq_public, hoặc ứng dụng lite mở API dữ liệu mà không có policy public_read rõ ràng."
1818
- causes_vi:
1819
- - "Vi phạm R259: cửa dữ liệu không khớp quy tắc front end chỉ dùng Supabase Auth."
1820
- nextStep_vi: "Loại pgmq và pgmq_public khỏi schema API; đóng API dữ liệu lite hoặc khai báo policy public_read giới hạn cần thiết."
1821
- owner: op-retry
1822
- kind: check-finding
1823
1796
 
1824
1797
  DB_APP_ROLE:
1825
1798
  title: "The back-end database role has explicit least privileges"
@@ -1852,26 +1825,7 @@ DB_BASELINE_SHAPE:
1852
1825
  owner: op-retry
1853
1826
  kind: check-finding
1854
1827
 
1855
- DB_BE_ONLY_ACCESS:
1856
- title: "Exposed product data is accessed through the back end"
1857
- title_vi: "Dữ liệu exposed được mở trực tiếp cho trình duyệt"
1858
- meaning_vi: "Migration tại <path> cấp quyền bảng hoặc sequence cho authenticated, khai báo policy cho authenticated, hoặc mở quyền anon ngoài policy select có tên kết thúc _public_read; đối tượng chưa được khai báo ngoại lệ supabase.directAccess."
1859
- causes_vi:
1860
- - "Vi phạm R254: Supabase phía front end chỉ phục vụ Auth; dữ liệu sản phẩm đi qua back end."
1861
- - "Grant hoặc policy mới mở lại cửa dữ liệu trong schema exposed mà không có ngoại lệ rõ ràng."
1862
- nextStep_vi: "Gỡ grant và policy truy cập trực tiếp; cho back end truy cập bằng app role. Nếu chủ ý mở đối tượng, khai báo bảng trong hfs.json supabase.directAccess; anon chỉ đọc qua policy select *_public_read."
1863
- owner: op-retry
1864
- kind: check-finding
1865
1828
 
1866
- DB_CONFIG_POLICY:
1867
- title: "`supabase/config.toml` holds no literal credential (`env(NAME)` only), `auth.jwt_expiry <= 3600`, and signup/site_url/redirects equal the Supabase block of hfs.json"
1868
- title_vi: "config.toml lệch policy"
1869
- meaning_vi: "`<path>` (supabase/config.toml) chứa giá trị literal dưới key credential (secret|password|token|client_id|*_key trừ anon/publishable), `auth.jwt_expiry` quá 3600, hoặc `enable_signup`/`site_url`/`additional_redirect_urls` khác với block Supabase mà hfs.json khai báo."
1870
- causes_vi:
1871
- - "Vi phạm luật L08: secret trong config.toml viết `env(NAME)`; jwt_expiry tối đa 3600; posture auth là fact được check, không phải claim trong doc."
1872
- nextStep_vi: "Đổi credential thành `env(TEN_BIEN)`, hạ `jwt_expiry` về <= 3600, và chỉnh block Supabase của hfs.json hoặc config.toml cho khớp nhau."
1873
- owner: op-retry
1874
- kind: check-finding
1875
1829
 
1876
1830
  DB_CRON_OWNED:
1877
1831
  title: "A uniquely owned migration cron job only enqueues through the queue outbox"
@@ -1930,7 +1884,7 @@ DB_FORCE_RLS_DEFAULT:
1930
1884
  meaning_vi: "Bảng tại <path> thuộc app role nhưng thiếu force row level security và không có ngoại lệ forceRls kèm lý do không rỗng trong hfs.json."
1931
1885
  causes_vi:
1932
1886
  - "Vi phạm R255: chủ sở hữu bảng có thể bỏ qua RLS khi không có force row level security."
1933
- nextStep_vi: "Thêm alter table <schema>.<table> force row level security trong migration; nếu cần miễn, khai báo đúng bảng và lý do trong hfs.json supabase.forceRls."
1887
+ nextStep_vi: "Thêm alter table <schema>.<table> force row level security trong migration; nếu cần miễn, khai báo đúng bảng và lý do trong hfs.json db.forceRls."
1934
1888
  owner: op-retry
1935
1889
  kind: check-finding
1936
1890
 
@@ -1949,8 +1903,8 @@ DB_MIGRATION_SHAPE:
1949
1903
  title_vi: "Migration sai hình dạng hoặc bị sửa"
1950
1904
  meaning_vi: "`<path>` vi phạm luật migration: tên phải là `<ts14>_<kebab>.sql`, stamp là thời điểm UTC thật và không trùng giữa các migration đang hoạt động; migration mới xếp sau chuỗi đang hoạt động đã commit trên HEAD. Archive ghi nhận migration đã nghỉ khỏi chuỗi, nên baseline thay thế có thể mang stamp trước archive. Migration đã lên base là bất biến (sửa hay xoá mà không archive đều là finding)."
1951
1905
  causes_vi:
1952
- - "Vi phạm luật L02: `supabase/migrations/<ts14>_<kebab>.sql`; stamp mới tăng nghiêm ngặt so với chuỗi đang hoạt động đã commit trên HEAD, không so với đồng hồ, thời gian commit hay stamp trên remote; file đã lên base không được đổi."
1953
- nextStep_vi: "Tạo migration mới bằng the Supabase CLI new-migration command, không sửa/đổi tên/xoá migration đã lên base; nếu cần đổi nội dung, viết migration mới."
1906
+ - "Vi phạm luật L02: `db/migrations/<ts14>_<kebab>.sql`; stamp mới tăng nghiêm ngặt so với chuỗi đang hoạt động đã commit trên HEAD, không so với đồng hồ, thời gian commit hay stamp trên remote; file đã lên base không được đổi."
1907
+ nextStep_vi: "Tạo migration mới bằng `npm run db:new -- <ten>`, không sửa/đổi tên/xoá migration đã lên base; nếu cần đổi nội dung, viết migration mới."
1954
1908
  owner: op-retry
1955
1909
  kind: check-finding
1956
1910
 
@@ -1987,22 +1941,13 @@ DB_POLICY_TAUTOLOGY:
1987
1941
  owner: op-retry
1988
1942
  kind: check-finding
1989
1943
 
1990
- DB_REALTIME_UNLISTED:
1991
- title: "Realtime publication tables require declared direct access"
1992
- title_vi: "Bảng Realtime chưa được khai báo truy cập trực tiếp"
1993
- meaning_vi: "Migration tại <path> thêm bảng vào publication supabase_realtime nhưng bảng không có trong hfs.json supabase.directAccess."
1994
- causes_vi:
1995
- - "Vi phạm R254: publication Realtime mở một cửa dữ liệu trực tiếp chưa được khai báo."
1996
- nextStep_vi: "Gỡ bảng khỏi publication supabase_realtime hoặc khai báo ngoại lệ có chủ ý trong hfs.json supabase.directAccess."
1997
- owner: op-retry
1998
- kind: check-finding
1999
1944
 
2000
1945
  DB_RLS_REQUIRED:
2001
1946
  title: "Every `create table` in an exposed schema is followed by `enable row level security` in the same migration"
2002
1947
  title_vi: "Bảng trong schema public thiếu RLS"
2003
1948
  meaning_vi: "`<path>` tạo bảng trong schema exposed (`public` và `[api] schemas` của config.toml) mà không có `alter table ... enable row level security` ngay trong cùng migration; bảng khai báo `forceRls` cần cả `force row level security`."
2004
1949
  causes_vi:
2005
- - "Vi phạm luật L03: mọi `create table`/`create table as` trong schema exposed phải có enable (và force khi hfs.json `supabase.forceRls` khai báo) trong cùng migration; partition con và bảng temp được miễn."
1950
+ - "Vi phạm luật L03: mọi `create table`/`create table as` trong schema exposed phải có enable (và force khi hfs.json `db.forceRls` khai báo) trong cùng migration; partition con và bảng temp được miễn."
2006
1951
  nextStep_vi: "Viết `alter table <schema>.<table> enable row level security;` ngay sau create trong cùng migration; không dùng vòng lặp dynamic SQL."
2007
1952
  owner: op-retry
2008
1953
  kind: check-finding
@@ -2027,15 +1972,6 @@ DB_STORAGE_PREFIX:
2027
1972
  owner: op-retry
2028
1973
  kind: check-finding
2029
1974
 
2030
- DB_TYPES_DRIFT:
2031
- title: "`supabase/types/database.types.ts` is committed and equals what `contract:emit` regenerates from the migrations"
2032
- title_vi: "database.types.ts lệch với migrations"
2033
- meaning_vi: "`supabase/types/database.types.ts` chưa được commit, hoặc khác với text mà `contract:emit` sinh lại từ migrations hiện tại (hoặc emit thất bại). Pattern giống R23 HFS_CONTRACT_SNAPSHOT_DRIFT."
2034
- causes_vi:
2035
- - "Vi phạm luật L09: file types là artefact sinh; commit bản mới nhất, không sửa tay."
2036
- nextStep_vi: "Chạy `starci app emit` và commit `supabase/types/database.types.ts` kết quả."
2037
- owner: op-retry
2038
- kind: check-finding
2039
1975
 
2040
1976
  DB_VAULT_WRITE:
2041
1977
  title: "Migrations contain no vault writes or role passwords"
@@ -2197,16 +2133,6 @@ FE_BUTTON_NO_VARIANT:
2197
2133
  owner: op-retry
2198
2134
  kind: check-finding
2199
2135
 
2200
- FE_CALLBACK_SHAPE:
2201
- title: "An auth callback performs more than code exchange and redirect"
2202
- title_vi: "Callback xác thực làm thêm việc ngoài đổi mã và chuyển hướng"
2203
- meaning_vi: "Handler của fe.route.callback chứa operation dữ liệu, logic sản phẩm, handler bổ sung hoặc phản hồi JSON thay vì chỉ đổi mã xác thực rồi chuyển hướng."
2204
- causes_vi:
2205
- - "Callback gọi operation API hoặc xử lý dữ liệu sản phẩm"
2206
- - "Callback trả JSON hoặc định nghĩa thêm handler"
2207
- nextStep_vi: "Giữ callback ở việc đọc mã, tạo client xác thực, đổi mã và chuyển hướng; chuyển logic sản phẩm về back end hoặc hook phù hợp."
2208
- owner: op-retry
2209
- kind: check-finding
2210
2136
 
2211
2137
  FE_CLIENT_BOUNDARY:
2212
2138
  title: "Server first; `\"use client\"` only where allowed"
@@ -2925,15 +2851,6 @@ FE_SERVER_ONLY_MARK:
2925
2851
  owner: op-retry
2926
2852
  kind: check-finding
2927
2853
 
2928
- FE_SERVICE_ROLE_FORBIDDEN:
2929
- title: "A service-role or credential-shaped environment value enters the front end"
2930
- title_vi: "Service role hoặc biến môi trường bí mật đi vào front end"
2931
- meaning_vi: "`<path>` chứa identifier/string service_role hoặc đọc biến `*_SECRET`, `*_PRIVATE`, `*_TOKEN`, `*_PASSWORD`; public config không được khai báo qua chủ `modules/config`."
2932
- causes_vi:
2933
- - "Vi phạm L13: front end chỉ giữ URL, anon/publishable key và public config; service role luôn ở back end."
2934
- nextStep_vi: "Xóa credential khỏi fe/, chuyển thao tác đặc quyền qua use case back end, và khai báo public config tại `modules/config`."
2935
- owner: op-retry
2936
- kind: check-finding
2937
2854
 
2938
2855
  FE_SINGLE_COMMIT_PER_RENDER:
2939
2856
  title: "A component renders more than one commit"
@@ -3077,15 +2994,6 @@ FE_SUBSCRIPTION_NO_SNAPSHOT:
3077
2994
  owner: op-retry
3078
2995
  kind: check-finding
3079
2996
 
3080
- FE_SUPABASE_CLIENT_OWNER:
3081
- title: "A front-end Supabase import or call is outside `fe.modules.auth`, or reaches past the auth API"
3082
- title_vi: "Supabase phía front end ngoài modules/auth hoặc dùng ngoài API auth"
3083
- meaning_vi: "Import/call Supabase ở `<path>` không thuộc ô `fe.modules.auth`, hoặc gọi API ngoài auth (`.from`, `.rpc`, `.storage`, `.channel`): front end không đọc hay ghi dữ liệu qua Supabase."
3084
- causes_vi:
3085
- - "Vi phạm luật R217: Supabase trên front end chỉ là xác thực (đăng nhập, phiên, access token) trong `modules/auth`; dữ liệu đi qua `modules/api` (Apollo GraphQL) và hook SWR tới back end (R212 case-2)."
3086
- nextStep_vi: "Chuyển client Supabase vào `modules/auth` và chỉ giữ API auth; đọc/ghi dữ liệu bằng query/mutation GraphQL trong `modules/api` và hook `hooks/swr`; không thêm ngoại lệ."
3087
- owner: op-retry
3088
- kind: check-finding
3089
2997
 
3090
2998
  FE_SUSPENSE_NULL_FALLBACK:
3091
2999
  title: "Suspense fallback renders something"
@@ -3689,15 +3597,6 @@ HFS_EDITION_INVALID:
3689
3597
  owner: op-retry
3690
3598
  kind: check-finding
3691
3599
 
3692
- HFS_EMIT_DB_TYPES_STALE:
3693
- title: "Database types require current migration history"
3694
- title_vi: "Lịch sử migration chưa khớp để sinh kiểu"
3695
- meaning_vi: "Tập phiên bản đã áp dụng trong DB cục bộ khác dấu thời gian tệp supabase/migrations."
3696
- causes_vi:
3697
- - "Tập phiên bản đã áp dụng trong DB cục bộ khác dấu thời gian tệp supabase/migrations."
3698
- nextStep_vi: "Chạy npm run db:reset rồi starci app emit; kiểm tra cả phiên bản thiếu và phiên bản thừa."
3699
- owner: op-retry
3700
- kind: verb-refusal
3701
3600
 
3702
3601
  HFS_EMPTY_DIR:
3703
3602
  title: "No empty directory outside an `ignored` slot"
@@ -4302,15 +4201,6 @@ HFS_SERVICE_PLACEMENT:
4302
4201
  owner: op-retry
4303
4202
  kind: check-finding
4304
4203
 
4305
- HFS_SERVICE_ROLE_SCOPE:
4306
- title: "Service-role access belongs to the Supabase integration"
4307
- title_vi: "Service role được dùng ngoài integration Supabase"
4308
- meaning_vi: "Mã tại <path> tham chiếu service_role ngoài integration Supabase của back end, kể cả trong script triển khai."
4309
- causes_vi:
4310
- - "Vi phạm R252: sử dụng quyền service role ngoài thành phần sở hữu kết nối Supabase."
4311
- nextStep_vi: "Đưa quyền service role vào be/src/modules/integrations/supabase và truy cập qua port của integration; script gọi CLI của ứng dụng."
4312
- owner: op-retry
4313
- kind: check-finding
4314
4204
 
4315
4205
  HFS_SERVICE_STACK_DECLARATION:
4316
4206
  title: "Each service app is a service component of the stack"
@@ -5650,3 +5540,63 @@ BE_ENTITY_SCHEMA_DRIFT:
5650
5540
  nextStep_vi: "Viết migration mới trong db/migrations để schema có đúng cột đó, hoặc sửa entity cho khớp schema hiện tại, rồi chạy lại starci app db drift cho đến khi xanh."
5651
5541
  owner: op-retry
5652
5542
  kind: check-finding
5543
+
5544
+ BE_AUTH_PASSPORT_OWNER:
5545
+ title: "An identity library is imported outside integrations/passport"
5546
+ title_vi: "Thư viện định danh bị import ngoài integrations/passport"
5547
+ meaning_vi: "File `<path>` import @nestjs/passport, passport, passport-google-oauth20, passport-local, @nestjs/jwt, jose hoặc bcryptjs mà không thuộc `be.integrations.passport`, hoặc import bcrypt hay client Supabase."
5548
+ causes_vi:
5549
+ - "Vi phạm R328: chỉ integration passport sở hữu chiến lược Google, đăng nhập email/mật khẩu và token riêng của app."
5550
+ nextStep_vi: "Chuyển mã vào `src/modules/integrations/passport/` và gọi qua API công khai của integration; dùng bcryptjs thay bcrypt, bỏ client Supabase."
5551
+ owner: op-retry
5552
+ kind: check-finding
5553
+
5554
+ BE_AUTH_JWT_OWN_JWKS:
5555
+ title: "A lite principal is not built from the app own verified access token"
5556
+ title_vi: "Principal của app lite không đến từ access token riêng đã xác minh"
5557
+ meaning_vi: "`<path>` gọi jwtVerify ngoài integrations/passport, xác minh với createRemoteJWKSet thay vì key set riêng (createLocalJWKSet), thiếu issuer/audience/algorithms, decode token không xác minh hoặc đọc role/membership từ request body."
5558
+ causes_vi:
5559
+ - "Vi phạm R329: principal chỉ đến từ token do chính app ký và xác minh bằng JWKS riêng."
5560
+ nextStep_vi: "Dùng `createTokenKit` của integrations/passport (jwtVerify trên createLocalJWKSet, pin issuer/audience/algorithms) và để domain/identity dựng principal từ kết quả đó."
5561
+ owner: op-retry
5562
+ kind: check-finding
5563
+
5564
+ BE_AUTH_REFRESH_COOKIE:
5565
+ title: "A refresh token leaves the httpOnly cookie"
5566
+ title_vi: "Refresh token rời khỏi cookie httpOnly"
5567
+ meaning_vi: "`<path>` đặt cookie refresh thiếu httpOnly, secure, sameSite strict hoặc path của cửa refresh, hoặc đưa refresh token vào body của request/response."
5568
+ causes_vi:
5569
+ - "Vi phạm R330: refresh token chỉ sống trong cookie httpOnly Secure SameSite=Strict và được xoay vòng mỗi lần dùng."
5570
+ nextStep_vi: "Đặt và xoá cookie qua `setRefreshCookie`/`clearRefreshCookie` của integrations/passport; bỏ trường refreshToken khỏi DTO và kết quả trả về."
5571
+ owner: op-retry
5572
+ kind: check-finding
5573
+
5574
+ BE_STORAGE_OWNER:
5575
+ title: "An S3 SDK is imported outside integrations/object-storage"
5576
+ title_vi: "S3 SDK bị import ngoài integrations/object-storage"
5577
+ meaning_vi: "File `<path>` import @aws-sdk/client-s3 hoặc @aws-sdk/s3-request-presigner ngoài `be.integrations.object-storage`, hoặc import minio."
5578
+ causes_vi:
5579
+ - "Vi phạm R331: chỉ integration storage sở hữu client S3 (MinIO trong lite) và URL presigned."
5580
+ nextStep_vi: "Gọi port ObjectStorage của integrations/object-storage (presignPut, presignGet, remove); bỏ minio."
5581
+ owner: op-retry
5582
+ kind: check-finding
5583
+
5584
+ LITE_DB_MIGRATION:
5585
+ title: "A lite schema change lives outside db/migrations"
5586
+ title_vi: "Thay đổi schema của app lite nằm ngoài db/migrations"
5587
+ meaning_vi: "`<path>` là tệp của thư mục supabase/ hoặc một tệp SQL ngoài db/ (trừ seed của .starcistacks)."
5588
+ causes_vi:
5589
+ - "Vi phạm R332: schema của app lite chỉ nằm trong db/migrations/<ts14>_<kebab>.sql, archive và seed trong db/."
5590
+ nextStep_vi: "Chuyển migration sang db/migrations (giữ nguyên stamp), seed sang db/seed.sql, types sang db/types/; xoá supabase/."
5591
+ owner: op-retry
5592
+ kind: check-finding
5593
+
5594
+ BE_PERSISTENCE_NO_RAW_SQL:
5595
+ title: "Lite domain code runs raw SQL instead of TypeORM entities"
5596
+ title_vi: "Mã domain của app lite chạy SQL thô thay vì entity TypeORM"
5597
+ meaning_vi: "File `<path>` gọi `.query(` hoặc dùng template `sql` ngoài platform/database và ngoài danh sách hàm ruleParams.be.lite.rawSqlFunctions."
5598
+ causes_vi:
5599
+ - "Vi phạm R334: dữ liệu của app lite đi qua entity TypeORM trên EntityManager (find, save, update, upsert, createQueryBuilder)."
5600
+ nextStep_vi: "Khai entity ở persistence/entities/<table>.entity.ts và đọc/ghi bằng EntityManager hoặc createQueryBuilder với tham số có tên; hàm pg/SECURITY DEFINER không diễn đạt được thì khai vào rawSqlFunctions kèm lý do."
5601
+ owner: op-retry
5602
+ kind: check-finding
@@ -85,16 +85,16 @@ properties:
85
85
  command: {type: string, minLength: 1}
86
86
  backing:
87
87
  description: >-
88
- Supabase backing: local Docker containers managed by the Supabase CLI in development and a hosted
89
- service in production. Every other dependency is a Docker component, on Swarm in production.
88
+ The self-hosted database of a lite app: its db component (image supabase/postgres), a Docker container in
89
+ development and a Swarm service in production. Every other dependency is a Docker component too.
90
90
  type: object
91
91
  additionalProperties:
92
92
  type: object
93
93
  additionalProperties: false
94
- required: [provider, managed]
94
+ required: [provider, component]
95
95
  properties:
96
- provider: {const: supabase}
97
- managed: {const: true}
96
+ provider: {const: postgres}
97
+ component: {type: string, minLength: 1}
98
98
  environments:
99
99
  type: object
100
100
  minProperties: 1
@@ -154,7 +154,7 @@ function readCall(env, call, managers) {
154
154
  function ownedRead(env, statement) {
155
155
  const { ts } = env;
156
156
  const call = statementCall(ts, statement);
157
- if (!call || !ts.isCallExpression(call) || !imported(env, call.expression, 'requireOwnedRow', '@modules/platform/database') || call.arguments.length !== 5) return false;
157
+ if (!call || !ts.isCallExpression(call) || !imported(env, call.expression, 'requireOwnedEntity', '@modules/platform/database') || call.arguments.length !== 5) return false;
158
158
  if (!call.arguments.slice(0, 4).every(argument => mapping(env, argument))) return false;
159
159
  const callback = unwrap(ts, call.arguments[4]);
160
160
  if (!ts.isArrowFunction(callback) || callback.parameters.length || !ts.isBlock(callback.body) || callback.body.statements.length !== 1) return false;
@@ -0,0 +1,53 @@
1
+ import { braceVariants, globExpression } from '../../lib/glob.mjs';
2
+
3
+ /**
4
+ * The entity registry (persistence.md BE-PERSISTENCE-3): the one file whose slot plays `role: entity-registry`
5
+ * (`be.platform.database.entities`, lite's `src/modules/platform/database/entities.ts`). It lists every capability's `<c>Entities`
6
+ * array as `PRIMARY_ENTITIES`, so it is the one platform file that imports domain and platform capability barrels, and only for
7
+ * that name; and it is imported by deep path only from the files its slot's `importedBy` names (app composition and the test world).
8
+ * The tier matrix (tiers.mjs), the public-entry check (owners.mjs) and the lint rule `import-owner-entry` read it here.
9
+ */
10
+ export const ENTITY_REGISTRY_ROLE = 'entity-registry';
11
+
12
+ /** The tiers whose capabilities contribute an entity list to the registry. */
13
+ const CONTRIBUTING_TIERS = new Set(['domain', 'platform']);
14
+
15
+ /** The entity list a capability exports: `<c>Entities`, `<c>` its folder name in camelCase. */
16
+ export const entityListName = capability => `${capability.replace(/-([a-z0-9])/g, (_, next) => next.toUpperCase())}Entities`;
17
+
18
+ /** The slot of `file` (side-relative or absolute) when it plays the entity-registry role, else null. */
19
+ export function registrySlot(resolver, file) {
20
+ const slot = resolver?.slotOf?.(file);
21
+ return slot?.role === ENTITY_REGISTRY_ROLE ? slot : null;
22
+ }
23
+
24
+ /**
25
+ * Whether `file` is one of the importers the registry slot admits by deep path (`importedBy`, side-relative patterns); an app-wide
26
+ * resolver answers for a side-prefixed path (`be/apps/...`), which is read without its side folder.
27
+ */
28
+ export function registryImporter(resolver, slot, file) {
29
+ const side = resolver?.sideOf?.(file);
30
+ const rel = side ? file.slice(side.length + 1) : file;
31
+ return (slot.importedBy ?? []).some(pattern => braceVariants(pattern).some(variant => globExpression(variant).test(rel)));
32
+ }
33
+
34
+ /** The names an import declaration binds by name; null for a default, namespace or side-effect import and for any re-export. */
35
+ function namedImports(declaration) {
36
+ const clause = declaration?.importClause;
37
+ if (!clause || clause.name || !Array.isArray(clause.namedBindings?.elements) || !clause.namedBindings.elements.length) return null;
38
+ return clause.namedBindings.elements.map(element => (element.propertyName ?? element.name).text);
39
+ }
40
+
41
+ /**
42
+ * Whether an owner-graph edge (side-relative `from` and `to`, `edge.declaration` the TypeScript statement) is the registry naming a
43
+ * capability's entity list: it leaves the registry, imports the public entry (`index.ts`) of a domain or platform capability, and
44
+ * binds that capability's `<c>Entities` and nothing else.
45
+ */
46
+ export function registryListImport(resolver, edge) {
47
+ if (!registrySlot(resolver, edge.from)) return false;
48
+ const owner = resolver.ownerOf(edge.to);
49
+ if (!owner || !CONTRIBUTING_TIERS.has(resolver.slot(owner.slot)?.tier) || edge.to !== `${owner.root}/index.ts`) return false;
50
+ const names = namedImports(edge.edge?.declaration ?? edge.declaration);
51
+ const listName = entityListName(owner.root.split('/').at(-1));
52
+ return names !== null && names.every(name => name === listName);
53
+ }