@starci/hfs 4.9.11 → 4.10.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 (219) hide show
  1. package/CHANGELOG.md +13 -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 +177 -155
  18. package/runtime/knowledge/hfs/slots.yaml +164 -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/{supabase-be.mjs → identity.mjs} +45 -15
  33. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  34. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  35. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  36. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  37. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  38. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  39. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  40. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  41. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  42. package/runtime/scripts/hfs/check.mjs +6 -6
  43. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  44. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  45. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  46. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  47. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  48. package/runtime/scripts/hfs/manifest-shape.mjs +4 -4
  49. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  50. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  51. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  52. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  53. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  54. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  55. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  56. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  57. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  58. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  59. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  60. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  61. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  62. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  63. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  64. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  65. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  66. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  67. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  68. package/runtime/scripts/hfs/slots.mjs +8 -2
  69. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  70. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  71. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  72. package/scaffold/add-app.mjs +2 -10
  73. package/scaffold/add-cli-lite.mjs +6 -6
  74. package/scaffold/add-compose.mjs +1 -1
  75. package/scaffold/add-inbox-lite.mjs +4 -8
  76. package/scaffold/add-plan.mjs +1 -1
  77. package/scaffold/add-queue-lite.mjs +16 -24
  78. package/scaffold/add-table.mjs +34 -26
  79. package/scaffold/add.mjs +2 -2
  80. package/scaffold/app.mjs +19 -86
  81. package/scaffold/lite-exports.mjs +1 -1
  82. package/src/main.mjs +19 -20
  83. package/stack/target-binding.mjs +0 -60
  84. package/stack/urgent-floor.mjs +2 -2
  85. package/sync/index.mjs +32 -8
  86. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  87. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  88. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  89. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  90. package/templates/app/gitignore-lite +0 -3
  91. package/templates/app/package-scripts/db.json +9 -0
  92. package/templates/app/package-scripts/package.json +0 -1
  93. package/templates/app/package-scripts-lite/package.json +1 -1
  94. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  95. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  96. package/templates/app/skeleton/README.md +1 -1
  97. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  98. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  99. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  100. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  101. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  102. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  103. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  104. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  105. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  106. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  107. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  108. package/templates/app/skeleton-lite/README.md +11 -11
  109. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  110. package/templates/app/tool-config-lite/prettierignore +0 -1
  111. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  112. package/templates/be/inbox/database-client.ts.tpl +13 -4
  113. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  114. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  115. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  116. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  117. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  118. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  119. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  120. package/templates/be/inbox/service.ts.tpl +1 -2
  121. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  122. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  123. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  124. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  125. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  126. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  127. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  128. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  129. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  130. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  131. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  132. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  133. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  134. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  135. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  136. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  137. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  138. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  139. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  140. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  141. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  142. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  143. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
  145. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  146. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  147. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  148. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  149. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  150. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  151. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  152. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  153. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  154. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  158. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  159. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  160. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  161. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  162. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  163. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  164. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  165. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  166. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  167. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  168. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
  169. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  170. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  171. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  172. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  173. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  174. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  175. package/templates/be/table/entities.index.ts.tpl +4 -0
  176. package/templates/be/table/entity.ts.tpl +21 -0
  177. package/templates/be/table/index.ts.tpl +1 -0
  178. package/templates/be/table/module.ts.tpl +1 -1
  179. package/templates/be/table/service.api.ts.tpl +10 -11
  180. package/templates/be/table/service.ts.tpl +5 -6
  181. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  182. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  183. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  184. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  185. package/db/local-migrate.mjs +0 -194
  186. package/db/reset.mjs +0 -125
  187. package/emit/db-types.mjs +0 -271
  188. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  189. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  190. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  191. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  192. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  193. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  194. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  195. package/scaffold/supabase-ports.mjs +0 -26
  196. package/templates/app/package-scripts/supabase.json +0 -7
  197. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  198. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  199. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  200. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  201. package/templates/be/inbox/sql.ts.tpl +0 -16
  202. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  203. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  204. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  205. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  206. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  207. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  208. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  209. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  210. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  211. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  212. package/templates/be/table/rows.ts.tpl +0 -4
  213. package/templates/be/table/sql.api.ts.tpl +0 -7
  214. package/templates/be/table/sql.ts.tpl +0 -4
  215. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  216. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  217. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  218. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  219. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
@@ -0,0 +1,114 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import { InjectClock } from "@modules/platform/clock"
4
+ import type { Clock } from "@modules/platform/clock"
5
+ import { JobsError, JobsErrorCode } from "./errors/jobs.error"
6
+ import { callDbFunction } from "@modules/platform/database"
7
+ import { isRunKey } from "./jobs.contracts"
8
+ import type {
9
+ AdvanceWrite,
10
+ ClaimedJob,
11
+ ClaimParams,
12
+ ClaimResult,
13
+ FailWrite,
14
+ GuardedWrite,
15
+ RunKey,
16
+ } from "./jobs.contracts"
17
+ import { InjectJobsManagers } from "./jobs.decorators"
18
+ import type { JobClaims } from "./jobs.port"
19
+
20
+ /** The record job_claim returns (a bigint arrives as text). */
21
+ interface ClaimedRecord {
22
+ /** The id of the job row. */
23
+ readonly id: string
24
+ /** The kind of the job. */
25
+ readonly kind: string
26
+ /** The fencing token after the claim. */
27
+ readonly fencing_token: string
28
+ /** The last recorded step, or null. */
29
+ readonly current_step: string | null
30
+ /** The payload, as the database returns the jsonb value. */
31
+ readonly payload: object
32
+ }
33
+
34
+ @Injectable()
35
+ /**
36
+ * The one owner of the job row, through the job_* functions of its migration (R334). A claim is one atomic upsert that bumps the fencing token in the store, so two workers can never
37
+ * hold the same token; every other write is `WHERE fencing_token = <expected>`, so a worker that lost its claim (a zombie) changes
38
+ * nothing and learns it from `JobFencedOut`.
39
+ */
40
+ export class JobClaimService implements JobClaims {
41
+ constructor(
42
+ @InjectJobsManagers() private readonly managers: ReadonlyArray<EntityManager>,
43
+ @InjectClock() private readonly clock: Clock,
44
+ ) {}
45
+
46
+ /** Claims the delivery; null when the job is done or another worker holds a claim that has not expired. */
47
+ async claim(params: ClaimParams): Promise<ClaimResult> {
48
+ const now = this.clock.now()
49
+ const rows = await callDbFunction<ClaimedRecord>(this.writer(), "job_claim", [
50
+ params.kind,
51
+ params.jobKey,
52
+ params.workerId,
53
+ new Date(now.getTime() + params.leaseMs),
54
+ JSON.stringify(params.payload),
55
+ now,
56
+ ])
57
+ const row = rows[0]
58
+ if (row === undefined) return null
59
+ // The durable delivery supplies the same request id on every retry; the job fence identifies a claim separately.
60
+ return {
61
+ jobId: row.id,
62
+ correlationId: params.correlationId,
63
+ kind: row.kind,
64
+ fencingToken: Number(row.fencing_token),
65
+ currentStep: row.current_step,
66
+ payload: row.payload,
67
+ }
68
+ }
69
+
70
+ /** Records a finished step under the token the caller holds. */
71
+ advance(write: AdvanceWrite): Promise<void> {
72
+ return this.guarded("job_advance", [write.jobId, write.expectedFencingToken, write.step, this.clock.now()], write)
73
+ }
74
+
75
+ /** Completes the job under the token the caller holds. */
76
+ complete(write: GuardedWrite): Promise<void> {
77
+ return this.guarded("job_complete", [write.jobId, write.expectedFencingToken, this.clock.now()], write)
78
+ }
79
+
80
+ /** Marks the job failed under the token the caller holds, so BullMQ can retry it. */
81
+ fail(write: FailWrite): Promise<void> {
82
+ return this.guarded("job_fail", [write.jobId, write.expectedFencingToken, write.reason, this.clock.now()], write)
83
+ }
84
+
85
+ /** The idempotency key of one step of this claim: it includes the token, so a zombie's repeat is a different key than the live worker's. */
86
+ runKey(job: ClaimedJob, step: string): RunKey {
87
+ const key = `${job.jobId}:${step}:${job.fencingToken}`
88
+ if (!isRunKey(key))
89
+ throw new JobsError({ code: JobsErrorCode.RunKeyInvalid, params: { jobId: job.jobId, step } })
90
+ return key
91
+ }
92
+
93
+ /** The entity manager of the connection that holds the job table. */
94
+ private writer(): EntityManager {
95
+ const [manager] = this.managers
96
+ if (manager === undefined) throw new JobsError({ code: JobsErrorCode.ConnectionMissing })
97
+ return manager
98
+ }
99
+
100
+ /** Runs a guarded function that updates only when the token still matches; no row back means a newer worker owns the job. */
101
+ private async guarded(
102
+ fn: "job_advance" | "job_complete" | "job_fail",
103
+ parameters: Array<unknown>,
104
+ write: GuardedWrite,
105
+ ): Promise<void> {
106
+ const rows = await callDbFunction<object>(this.writer(), fn, parameters)
107
+ if (rows.length === 0) {
108
+ throw new JobsError({
109
+ code: JobsErrorCode.FencedOut,
110
+ params: { jobId: write.jobId, token: write.expectedFencingToken },
111
+ })
112
+ }
113
+ }
114
+ }
@@ -42,3 +42,54 @@ with check (kind is not null);
42
42
 
43
43
  revoke all on table public.jobs from public, anon, authenticated;
44
44
  grant select, insert, update on table public.jobs to app_be;
45
+
46
+ -- The claim and the guarded writes, called through callDbFunction of platform/database (R334). A claim inserts a new key with
47
+ -- token 1 or claims a failed or expired one again, bumping the token; a done job or a live claim returns no row. Every other
48
+ -- write matches only the token the caller holds, so a worker that lost its claim changes nothing and gets no row back.
49
+ create function public.job_claim(p_kind varchar, p_job_key varchar, p_worker varchar, p_lease_end timestamptz, p_payload jsonb, p_now timestamptz)
50
+ returns table (id uuid, kind varchar, fencing_token bigint, current_step varchar, payload jsonb)
51
+ language sql
52
+ as $$
53
+ insert into public.jobs as j (kind, job_key, status, fencing_token, claimed_by, lease_expires_at, payload, created_at, updated_at)
54
+ values (p_kind, p_job_key, 'running', 1, p_worker, p_lease_end, p_payload, p_now, p_now)
55
+ on conflict (job_key) do update
56
+ set fencing_token = j.fencing_token + 1, claimed_by = p_worker, lease_expires_at = p_lease_end, status = 'running', error = null, updated_at = p_now
57
+ where j.status = 'failed' or (j.status = 'running' and j.lease_expires_at < p_now)
58
+ returning j.id, j.kind, j.fencing_token, j.current_step, j.payload
59
+ $$;
60
+
61
+ create function public.job_advance(p_id uuid, p_token bigint, p_step varchar, p_now timestamptz)
62
+ returns table (id uuid)
63
+ language sql
64
+ as $$
65
+ update public.jobs as j set current_step = p_step, updated_at = p_now
66
+ where j.id = p_id and j.fencing_token = p_token and j.status = 'running'
67
+ returning j.id
68
+ $$;
69
+
70
+ create function public.job_complete(p_id uuid, p_token bigint, p_now timestamptz)
71
+ returns table (id uuid)
72
+ language sql
73
+ as $$
74
+ update public.jobs as j set status = 'done', lease_expires_at = null, updated_at = p_now
75
+ where j.id = p_id and j.fencing_token = p_token and j.status = 'running'
76
+ returning j.id
77
+ $$;
78
+
79
+ create function public.job_fail(p_id uuid, p_token bigint, p_reason text, p_now timestamptz)
80
+ returns table (id uuid)
81
+ language sql
82
+ as $$
83
+ update public.jobs as j set status = 'failed', error = p_reason, lease_expires_at = null, updated_at = p_now
84
+ where j.id = p_id and j.fencing_token = p_token and j.status = 'running'
85
+ returning j.id
86
+ $$;
87
+
88
+ revoke all on function public.job_claim(varchar, varchar, varchar, timestamptz, jsonb, timestamptz) from public;
89
+ grant execute on function public.job_claim(varchar, varchar, varchar, timestamptz, jsonb, timestamptz) to app_be;
90
+ revoke all on function public.job_advance(uuid, bigint, varchar, timestamptz) from public;
91
+ grant execute on function public.job_advance(uuid, bigint, varchar, timestamptz) to app_be;
92
+ revoke all on function public.job_complete(uuid, bigint, timestamptz) from public;
93
+ grant execute on function public.job_complete(uuid, bigint, timestamptz) to app_be;
94
+ revoke all on function public.job_fail(uuid, bigint, text, timestamptz) from public;
95
+ grant execute on function public.job_fail(uuid, bigint, text, timestamptz) to app_be;
@@ -0,0 +1,62 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { FakeClock, fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { CLOCK } from "@modules/platform/clock"
4
+ import { QueueOutboxService } from "./queue-outbox.service"
5
+
6
+ // The statement callDbFunction of platform/database sends for queue_write.
7
+ const INSERT_QUEUE_ROW = "SELECT * FROM queue_write($1, $2, $3, $4)"
8
+ import { QueueErrorCode } from "./errors/queue.error"
9
+
10
+ const AT = "2026-02-03T04:05:06.000Z"
11
+
12
+ const build = async () => {
13
+ const moduleRef = await Test.createTestingModule({
14
+ providers: [QueueOutboxService, { provide: CLOCK, useValue: new FakeClock(AT) }],
15
+ }).compile()
16
+ return { outbox: moduleRef.get(QueueOutboxService) }
17
+ }
18
+
19
+ describe("QueueOutboxService", () => {
20
+ describe("write", () => {
21
+ it("writes the job as a row of the outbox of the transaction it is given", async () => {
22
+ const { outbox } = await build()
23
+ const tx = fakeTransaction(mockEntityManager({ query: [INSERT_QUEUE_ROW, []] }))
24
+
25
+ await tx.em.transaction((manager) => outbox.write(manager, "mail", { id: "m-1" }, "request-1"))
26
+
27
+ expect(tx.em.query).toHaveBeenCalledWith(INSERT_QUEUE_ROW, [
28
+ "mail",
29
+ JSON.stringify({ id: "m-1" }),
30
+ new Date(AT),
31
+ "request-1",
32
+ ])
33
+ expect(tx.commits).toBe(1)
34
+ })
35
+
36
+ it("leaves no row behind when the transaction rolls back", async () => {
37
+ const { outbox } = await build()
38
+ const tx = fakeTransaction(mockEntityManager({ query: [INSERT_QUEUE_ROW, []] }))
39
+ const failure = new Error("the change failed")
40
+
41
+ await expect(
42
+ tx.em.transaction(async (manager) => {
43
+ await outbox.write(manager, "mail", { id: "m-1" }, "request-1")
44
+ return Promise.reject(failure)
45
+ }),
46
+ ).rejects.toBe(failure)
47
+
48
+ expect(tx.rollbacks).toBe(1)
49
+ expect(tx.committedWrites).toEqual([])
50
+ })
51
+
52
+ it("rejects an unsafe correlation id before writing any SQL", async () => {
53
+ const { outbox } = await build()
54
+ const tx = fakeTransaction(mockEntityManager())
55
+ await expect(
56
+ tx.em.transaction((manager) => outbox.write(manager, "mail", {}, "bad\nid")),
57
+ ).rejects.toMatchObject({ code: QueueErrorCode.CorrelationInvalid })
58
+ expect(tx.em.query).not.toHaveBeenCalled()
59
+ expect(tx.rollbacks).toBe(1)
60
+ })
61
+ })
62
+ })
@@ -0,0 +1,20 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import { InjectClock } from "@modules/platform/clock"
4
+ import type { Clock } from "@modules/platform/clock"
5
+ import { isCorrelationId } from "@modules/platform/logging"
6
+ import type { QueueOutbox } from "./queue.port"
7
+ import { QueueError, QueueErrorCode } from "./errors/queue.error"
8
+ import { callDbFunction } from "@modules/platform/database"
9
+
10
+ @Injectable()
11
+ /** The writing side of the queues: a job is a row of the outbox of the caller's transaction, so it exists exactly when the change that needs it commits. */
12
+ export class QueueOutboxService implements QueueOutbox {
13
+ constructor(@InjectClock() private readonly clock: Clock) {}
14
+
15
+ /** Writes the job in the outbox of the transaction `tx`; the relay adds it to BullMQ after the commit. */
16
+ async write(tx: EntityManager, queue: string, payload: object, correlationId: string): Promise<void> {
17
+ if (!isCorrelationId(correlationId)) throw new QueueError({ code: QueueErrorCode.CorrelationInvalid })
18
+ await callDbFunction(tx, "queue_write", [queue, JSON.stringify(payload), this.clock.now(), correlationId])
19
+ }
20
+ }
@@ -30,3 +30,38 @@ with check (queue is not null);
30
30
 
31
31
  revoke all on table public.queue from public, anon, authenticated;
32
32
  grant select, insert, update on table public.queue to app_be;
33
+
34
+ -- The outbox write, the relay's locked read of the waiting rows and its mark, called through callDbFunction of platform/database
35
+ -- (R334). The read locks the rows it returns for the caller's transaction and skips rows another relay holds.
36
+ create function public.queue_write(p_queue varchar, p_payload jsonb, p_created_at timestamptz, p_correlation_id varchar)
37
+ returns void
38
+ language sql
39
+ as $$
40
+ insert into public.queue (queue, payload, created_at, correlation_id) values (p_queue, p_payload, p_created_at, p_correlation_id)
41
+ $$;
42
+
43
+ create function public.queue_waiting(p_batch integer)
44
+ returns table (id uuid, queue varchar, payload jsonb, "correlationId" varchar)
45
+ language sql
46
+ as $$
47
+ select q.id, q.queue, q.payload, coalesce(q.correlation_id, q.id::text)
48
+ from public.queue as q
49
+ where q.sent_at is null
50
+ order by q.created_at, q.id
51
+ limit p_batch
52
+ for update skip locked
53
+ $$;
54
+
55
+ create function public.queue_mark_sent(p_ids uuid[], p_sent_at timestamptz)
56
+ returns void
57
+ language sql
58
+ as $$
59
+ update public.queue set sent_at = p_sent_at where id = any(p_ids)
60
+ $$;
61
+
62
+ revoke all on function public.queue_write(varchar, jsonb, timestamptz, varchar) from public;
63
+ grant execute on function public.queue_write(varchar, jsonb, timestamptz, varchar) to app_be;
64
+ revoke all on function public.queue_waiting(integer) from public;
65
+ grant execute on function public.queue_waiting(integer) to app_be;
66
+ revoke all on function public.queue_mark_sent(uuid[], timestamptz) from public;
67
+ grant execute on function public.queue_mark_sent(uuid[], timestamptz) to app_be;
@@ -9,7 +9,10 @@ import { OutboxLogEvent } from "@modules/platform/outbox"
9
9
  import type { QueueOptions } from "./queue.options"
10
10
  import { QueueRelayService } from "./queue-relay.service"
11
11
  import type { QueueTransport } from "./queue-transport.port"
12
- import { MARK_ROWS_SENT, SELECT_WAITING_ROWS } from "./persistence/queue.sql"
12
+
13
+ // The statements callDbFunction of platform/database sends for the relay's functions.
14
+ const SELECT_WAITING_ROWS = "SELECT * FROM queue_waiting($1)"
15
+ const MARK_ROWS_SENT = "SELECT * FROM queue_mark_sent($1, $2)"
13
16
 
14
17
  const AT = "2026-02-03T04:05:06.000Z"
15
18
 
@@ -9,8 +9,19 @@ import { OutboxRelayPolicy } from "@modules/platform/outbox"
9
9
  import { InjectQueueOptions, InjectQueueRelayManagers, InjectQueueTransport } from "./queue.decorators"
10
10
  import type { QueueOptions } from "./queue.options"
11
11
  import type { QueueTransport } from "./queue-transport.port"
12
- import { MARK_ROWS_SENT, SELECT_WAITING_ROWS } from "./persistence/queue.sql"
13
- import type { QueueRow } from "./persistence/queue.rows"
12
+ import { callDbFunction } from "@modules/platform/database"
13
+
14
+ /** One waiting row queue_waiting returns. */
15
+ interface QueueRow {
16
+ /** The correlation id recorded by the producer's transaction. */
17
+ readonly correlationId: string
18
+ /** The row id: it is the BullMQ job id, so a repeated relay pass adds nothing. */
19
+ readonly id: string
20
+ /** The queue the job goes to. */
21
+ readonly queue: string
22
+ /** The payload, as the database returns the jsonb value. */
23
+ readonly payload: object
24
+ }
14
25
 
15
26
  @Injectable()
16
27
  /**
@@ -32,8 +43,8 @@ export class QueueRelayService extends OutboxRelayPolicy<QueueRow> {
32
43
  super()
33
44
  }
34
45
 
35
- protected select(tx: EntityManager): Promise<Array<QueueRow>> {
36
- return tx.query(SELECT_WAITING_ROWS, [this.options.relayBatch])
46
+ protected async select(tx: EntityManager): Promise<Array<QueueRow>> {
47
+ return [...(await callDbFunction<QueueRow>(tx, "queue_waiting", [this.options.relayBatch]))]
37
48
  }
38
49
 
39
50
  protected async deliver(rows: ReadonlyArray<QueueRow>): Promise<void> {
@@ -42,7 +53,7 @@ export class QueueRelayService extends OutboxRelayPolicy<QueueRow> {
42
53
  }
43
54
 
44
55
  protected async mark(tx: EntityManager, rows: ReadonlyArray<QueueRow>): Promise<void> {
45
- await tx.query(MARK_ROWS_SENT, [rows.map((row) => row.id), this.clock.now()])
56
+ await callDbFunction(tx, "queue_mark_sent", [rows.map((row) => row.id), this.clock.now()])
46
57
  }
47
58
 
48
59
  protected idle(): Promise<void> {
@@ -1,8 +1,8 @@
1
1
  import { Test } from "@nestjs/testing"
2
2
  import { MESSAGE_MAPPING_METADATA } from "@nestjs/websockets/constants"
3
3
  import { @@service@@ } from "@@serviceModule@@"
4
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
5
- import { SupabaseErrorCode } from "@modules/integrations/supabase"
4
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
5
+ import { PassportErrorCode } from "@modules/integrations/passport"
6
6
  import type { Principal } from "@modules/platform/cqrs"
7
7
  import { ok, refused } from "@modules/platform/primitives"
8
8
  import { REALTIME_HUB } from "@modules/platform/realtime"
@@ -79,7 +79,7 @@ const socketOf = (id: string, auth: Readonly<Record<string, unknown>> = { access
79
79
  return { socket, pushed }
80
80
  }
81
81
 
82
- const build = async (hub: RealtimeHub, verify: VerifySupabaseAccessToken) => {
82
+ const build = async (hub: RealtimeHub, verify: VerifyAccessToken) => {
83
83
  const moduleRef = await Test.createTestingModule({
84
84
  imports: [@@Channel@@WebsocketModule.register({ verifyAccessToken: verify })],
85
85
  })
@@ -96,7 +96,7 @@ const build = async (hub: RealtimeHub, verify: VerifySupabaseAccessToken) => {
96
96
  describe("@@Channel@@Gateway", () => {
97
97
  it("builds the topic only from the verified principal, ignoring handshake principal and topic claims", async () => {
98
98
  const { hub, topics } = hubAnswering()
99
- const verify: VerifySupabaseAccessToken = jest.fn().mockResolvedValue(ok(principal))
99
+ const verify: VerifyAccessToken = jest.fn().mockResolvedValue(ok(principal))
100
100
  const { transport, adapter } = await build(hub, verify)
101
101
  const { socket } = socketOf("socket-1", { accessToken: "signed-token", principal: { id: "p-2" }, topic: "foreign" })
102
102
 
@@ -111,7 +111,7 @@ describe("@@Channel@@Gateway", () => {
111
111
 
112
112
  it("isolates another authenticated principal's topic", async () => {
113
113
  const { hub, topics } = hubAnswering()
114
- const verify: VerifySupabaseAccessToken = jest.fn().mockResolvedValue(ok({ id: "p-2", role: "authenticated" }))
114
+ const verify: VerifyAccessToken = jest.fn().mockResolvedValue(ok({ id: "p-2", role: "authenticated" }))
115
115
  const { transport, adapter } = await build(hub, verify)
116
116
  const { socket } = socketOf("socket-2")
117
117
 
@@ -126,7 +126,7 @@ describe("@@Channel@@Gateway", () => {
126
126
  it("pushes a hub frame automatically and releases the iterator on disconnect", async () => {
127
127
  const frame = { id: "x-1" }
128
128
  const { hub, released } = hubAnswering(frame)
129
- const verify: VerifySupabaseAccessToken = jest.fn().mockResolvedValue(ok(principal))
129
+ const verify: VerifyAccessToken = jest.fn().mockResolvedValue(ok(principal))
130
130
  const { transport, adapter } = await build(hub, verify)
131
131
  const { socket, pushed } = socketOf("socket-1")
132
132
 
@@ -142,7 +142,7 @@ describe("@@Channel@@Gateway", () => {
142
142
 
143
143
  it.each([{}, { accessToken: "" }, { accessToken: 42 }])("refuses a missing or malformed access token without invoking the verifier", async (auth) => {
144
144
  const { hub, topics } = hubAnswering()
145
- const verify: VerifySupabaseAccessToken = jest.fn()
145
+ const verify: VerifyAccessToken = jest.fn()
146
146
  const { adapter } = await build(hub, verify)
147
147
  const { socket } = socketOf("socket-1", auth)
148
148
  socket.data.principal = principal
@@ -156,7 +156,7 @@ describe("@@Channel@@Gateway", () => {
156
156
 
157
157
  it("refuses a rejected token and clears a prior principal", async () => {
158
158
  const { hub, topics } = hubAnswering()
159
- const verify: VerifySupabaseAccessToken = jest.fn().mockResolvedValue(refused(SupabaseErrorCode.AccessTokenInvalid))
159
+ const verify: VerifyAccessToken = jest.fn().mockResolvedValue(refused(PassportErrorCode.AccessTokenInvalid))
160
160
  const { adapter } = await build(hub, verify)
161
161
  const { socket } = socketOf("socket-1")
162
162
  socket.data.principal = principal
@@ -1,7 +1,7 @@
1
1
  import { Module } from "@nestjs/common"
2
2
  import type { OnModuleDestroy, OnModuleInit } from "@nestjs/common"
3
3
  import type { @@Channel@@Frame } from "@@serviceModule@@"
4
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
4
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
5
5
  import { injector } from "@modules/platform/composition"
6
6
  import type { Principal } from "@modules/platform/cqrs"
7
7
  import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from "./@@channel@@-websocket.module-definition"
@@ -48,8 +48,8 @@ export class @@Channel@@WebsocketModule extends ConfigurableModuleClass implemen
48
48
 
49
49
  constructor(
50
50
  private readonly gateway: @@Channel@@Gateway,
51
- @injector<VerifySupabaseAccessToken>(ACCESS_TOKEN_VERIFIER)
52
- private readonly verifyAccessToken: VerifySupabaseAccessToken,
51
+ @injector<VerifyAccessToken>(ACCESS_TOKEN_VERIFIER)
52
+ private readonly verifyAccessToken: VerifyAccessToken,
53
53
  ) {
54
54
  super()
55
55
  }
@@ -1,7 +1,7 @@
1
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
1
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
2
2
 
3
3
  /** The options of the @@channel@@ socket transport: the cryptographic verifier that authenticates each connection. */
4
4
  export interface @@Channel@@WebsocketOptions {
5
5
  /** Verifies the access token of a handshake with the same verifier identity uses. */
6
- readonly verifyAccessToken: VerifySupabaseAccessToken
6
+ readonly verifyAccessToken: VerifyAccessToken
7
7
  }
@@ -18,7 +18,6 @@ RUN npm ci --ignore-scripts
18
18
 
19
19
  COPY be/tsconfig.json be/tsconfig.build.json be/
20
20
  COPY be/src be/src
21
- COPY supabase/types supabase/types
22
21
  COPY be/apps/api be/apps/api
23
22
  RUN npm run build:be
24
23
 
@@ -2,7 +2,8 @@ import type { EnvSource } from "@modules/platform/config"
2
2
  import { parseServerConfig } from "@modules/platform/config"
3
3
  import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
4
4
  import { parseHttpSecurityConfig } from "@modules/platform/http-security"
5
- import { parseSupabaseConfig } from "@modules/integrations/supabase"
5
+ import { parsePassportConfig } from "@modules/integrations/passport"
6
+ import { parseObjectStorageConfig } from "@modules/integrations/object-storage"
6
7
 
7
8
  /** The API environment values required before composition. */
8
9
  export interface ApiEnv {
@@ -10,10 +11,12 @@ export interface ApiEnv {
10
11
  readonly server: ReturnType<typeof parseServerConfig>
11
12
  /** The origin allowlist and rate limits. */
12
13
  readonly httpSecurity: ReturnType<typeof parseHttpSecurityConfig>
13
- /** The least-privilege PostgreSQL connection to the Supabase project. */
14
+ /** The least-privilege PostgreSQL connection to the app's own database. */
14
15
  readonly database: ReturnType<typeof parsePrimaryDatabaseConfig>
15
- /** Supabase Auth, Storage and token-verification configuration. */
16
- readonly supabase: ReturnType<typeof parseSupabaseConfig>
16
+ /** Google OAuth, the app's own access tokens and the refresh cookie. */
17
+ readonly passport: ReturnType<typeof parsePassportConfig>
18
+ /** S3-compatible object storage. */
19
+ readonly objectStorage: ReturnType<typeof parseObjectStorageConfig>
17
20
  }
18
21
 
19
22
  /** Reads the API environment; missing or invalid values fail boot, and secrets use `_FILE` first. */
@@ -21,5 +24,6 @@ export const readApiEnv = (env: EnvSource): ApiEnv => ({
21
24
  server: parseServerConfig(env),
22
25
  httpSecurity: parseHttpSecurityConfig(env),
23
26
  database: parsePrimaryDatabaseConfig(env),
24
- supabase: parseSupabaseConfig(env),
27
+ passport: parsePassportConfig(env),
28
+ objectStorage: parseObjectStorageConfig(env),
25
29
  })
@@ -1,4 +1,5 @@
1
- import type { SupabaseOptions } from "@modules/integrations/supabase"
1
+ import type { PassportOptions } from "@modules/integrations/passport"
2
+ import type { ObjectStorageOptions } from "@modules/integrations/object-storage"
2
3
  import type { ServerOptions } from "@modules/platform/config"
3
4
  import type { DatabaseConnectionOptions } from "@modules/platform/database"
4
5
  import type { HttpSecurityOptions } from "@modules/platform/http-security"
@@ -9,8 +10,10 @@ export interface ApiOptions {
9
10
  readonly server: ServerOptions
10
11
  /** The origin allowlist and rate limits. */
11
12
  readonly httpSecurity: HttpSecurityOptions
12
- /** The least-privilege PostgreSQL connection to the Supabase project. */
13
+ /** The least-privilege PostgreSQL connection to the app's own database. */
13
14
  readonly database: DatabaseConnectionOptions
14
- /** Supabase Auth, Storage and token-verification configuration. */
15
- readonly supabase: SupabaseOptions
15
+ /** Google OAuth, the app's own access tokens and the refresh cookie. */
16
+ readonly passport: PassportOptions
17
+ /** S3-compatible object storage. */
18
+ readonly objectStorage: ObjectStorageOptions
16
19
  }
@@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"
2
2
  import type { DynamicModule } from "@nestjs/common"
3
3
  import { APP_FILTER, APP_GUARD } from "@nestjs/core"
4
4
  import { AuthGuard, CredentialGuard } from "@modules/domain/identity"
5
- import { createSupabaseAccessTokenVerifier } from "@modules/integrations/supabase"
5
+ import { tokenKit } from "@modules/integrations/passport"
6
6
  import { ErrorsFilter } from "@modules/platform/errors"
7
7
  import { OriginGuard, RateLimitGuard } from "@modules/platform/http-security"
8
8
  import { platformModules } from "./compose/platform"
@@ -18,18 +18,18 @@ import type { ApiOptions } from "./api.options"
18
18
  export class AppModule {
19
19
  /** Builds the module from options parsed once at boot; nothing here reads the environment. */
20
20
  static register(options: ApiOptions): DynamicModule {
21
- const verifyAccessToken = createSupabaseAccessTokenVerifier(options.supabase)
21
+ const tokens = tokenKit(options.passport.tokens)
22
22
  return {
23
23
  module: AppModule,
24
24
  imports: [
25
25
  ...platformModules(options),
26
- ...domainModules(verifyAccessToken),
26
+ ...domainModules(tokens.verify),
27
27
  ...systemModules(),
28
28
  ...apiModules(),
29
29
  ...webhooksModules(),
30
30
  ],
31
31
  providers: [
32
- ...integrationProviders(options.supabase, verifyAccessToken),
32
+ ...integrationProviders(options, tokens.verify),
33
33
  { provide: APP_FILTER, useClass: ErrorsFilter },
34
34
  { provide: APP_GUARD, useClass: RateLimitGuard },
35
35
  { provide: APP_GUARD, useClass: OriginGuard },
@@ -1,10 +1,10 @@
1
1
  import type { DynamicModule, Type } from "@nestjs/common"
2
2
  import { IdentityModule } from "@modules/domain/identity"
3
3
  import { LivenessModule } from "@modules/domain/liveness"
4
- import type { VerifySupabaseAccessToken } from "@modules/integrations/supabase"
4
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
5
5
 
6
- /** The domain capabilities of the API, with the app's verified Supabase identity adapter. */
7
- export const domainModules = (verifyAccessToken: VerifySupabaseAccessToken): Array<Type<unknown> | DynamicModule> => [
6
+ /** The domain capabilities of the API, with the verifier of the app's own access tokens. */
7
+ export const domainModules = (verifyAccessToken: VerifyAccessToken): Array<Type<unknown> | DynamicModule> => [
8
8
  IdentityModule.register({ isGlobal: true, verifyAccessToken }),
9
9
  LivenessModule.register({ isGlobal: true }),
10
10
  ]
@@ -1,16 +1,12 @@
1
1
  import type { Provider } from "@nestjs/common"
2
- import {
3
- SUPABASE_ACCESS_TOKEN_VERIFIER,
4
- SUPABASE_ADMIN_CLIENT,
5
- createSupabaseAdminClient,
6
- } from "@modules/integrations/supabase"
7
- import type { SupabaseOptions, VerifySupabaseAccessToken } from "@modules/integrations/supabase"
2
+ import { OWN_TOKEN_VERIFIER, PASSPORT_OPTIONS } from "@modules/integrations/passport"
3
+ import type { VerifyAccessToken } from "@modules/integrations/passport"
4
+ import { OBJECT_STORAGE, createObjectStorage } from "@modules/integrations/object-storage"
5
+ import type { ApiOptions } from "../api.options"
8
6
 
9
- /** The Supabase integration ports bound once for this app. */
10
- export const integrationProviders = (
11
- options: SupabaseOptions,
12
- verifyAccessToken: VerifySupabaseAccessToken,
13
- ): Array<Provider> => [
14
- { provide: SUPABASE_ADMIN_CLIENT, useValue: createSupabaseAdminClient(options) },
15
- { provide: SUPABASE_ACCESS_TOKEN_VERIFIER, useValue: verifyAccessToken },
7
+ /** The passport and object-storage integration ports bound once for this app. */
8
+ export const integrationProviders = (options: ApiOptions, verifyAccessToken: VerifyAccessToken): Array<Provider> => [
9
+ { provide: PASSPORT_OPTIONS, useValue: options.passport },
10
+ { provide: OWN_TOKEN_VERIFIER, useValue: verifyAccessToken },
11
+ { provide: OBJECT_STORAGE, useValue: createObjectStorage(options.objectStorage) },
16
12
  ]
@@ -1,10 +1,11 @@
1
1
  import type { DynamicModule } from "@nestjs/common"
2
2
  import { IDENTITY_ERROR_KINDS, IDENTITY_MESSAGES } from "@modules/domain/identity"
3
- import { SUPABASE_ERROR_KINDS } from "@modules/integrations/supabase"
3
+ import { PASSPORT_ERROR_KINDS } from "@modules/integrations/passport"
4
4
  import { ClockModule } from "@modules/platform/clock"
5
5
  import { CONFIG_ERROR_KINDS } from "@modules/platform/config"
6
6
  import { CqrsModule } from "@modules/platform/cqrs"
7
7
  import { DATABASE_ERROR_KINDS, DatabaseModule } from "@modules/platform/database"
8
+ import { PRIMARY_ENTITIES } from "@modules/platform/database/entities"
8
9
  import { ERRORS_MESSAGES, ErrorsModule } from "@modules/platform/errors"
9
10
  import { HTTP_SECURITY_ERROR_KINDS, HTTP_SECURITY_MESSAGES, HttpSecurityModule } from "@modules/platform/http-security"
10
11
  import { I18nModule } from "@modules/platform/i18n"
@@ -37,7 +38,7 @@ export const platformModules = (options: ApiOptions): Array<DynamicModule> => [
37
38
  DATABASE_ERROR_KINDS,
38
39
  HTTP_SECURITY_ERROR_KINDS,
39
40
  IDENTITY_ERROR_KINDS,
40
- SUPABASE_ERROR_KINDS,
41
+ PASSPORT_ERROR_KINDS,
41
42
  ],
42
43
  }),
43
44
  CqrsModule.register({ isGlobal: true }),
@@ -48,5 +49,6 @@ export const platformModules = (options: ApiOptions): Array<DynamicModule> => [
48
49
  DatabaseModule.register({
49
50
  isGlobal: true,
50
51
  connections: [options.database],
52
+ entities: PRIMARY_ENTITIES,
51
53
  }),
52
54
  ]
@@ -5,8 +5,8 @@ import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
5
5
  export interface CliEnv {
6
6
  /** The logical connection name declared in `hfs.json`. */
7
7
  readonly name: string
8
- /** The Supabase PostgreSQL provider. */
9
- readonly provider: "supabase"
8
+ /** The PostgreSQL provider. */
9
+ readonly provider: "postgres"
10
10
  /** The least-privilege database connection URL, read through the secret source. */
11
11
  readonly url: Secret
12
12
  /** The isolated PostgreSQL schema. */
@@ -1,16 +1,17 @@
1
1
  import type { DynamicModule } from "@nestjs/common"
2
2
  import { ClockModule } from "@modules/platform/clock"
3
3
  import { DatabaseModule } from "@modules/platform/database"
4
+ import { PRIMARY_ENTITIES } from "@modules/platform/database/entities"
4
5
  import { IdsModule } from "@modules/platform/ids"
5
6
  import { HTTP_ERROR_KINDS, HTTP_MESSAGES, HttpModule } from "@modules/platform/http"
6
7
  import { LoggingModule } from "@modules/platform/logging"
7
8
  import type { CliAppOptions } from "../cli.options"
8
9
 
9
- /** The shared clock, logger and least-privilege connection used by Supabase CLI commands. */
10
+ /** The shared clock, logger and least-privilege connection used by the operator commands. */
10
11
  export const platformModules = (options: CliAppOptions): Array<DynamicModule> => [
11
12
  ClockModule.register({ isGlobal: true }),
12
13
  IdsModule.register({ isGlobal: true }),
13
14
  LoggingModule.register({ isGlobal: true }),
14
15
  HttpModule.register({ isGlobal: true }),
15
- DatabaseModule.register({ isGlobal: true, connections: options.connections }),
16
+ DatabaseModule.register({ isGlobal: true, connections: options.connections, entities: PRIMARY_ENTITIES }),
16
17
  ]