@starci/hfs 4.9.9 → 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 (241) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -8
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +289 -0
  5. package/db/dump.mjs +12 -11
  6. package/db/entity-list.mjs +9 -0
  7. package/db/migrate.mjs +84 -260
  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/lint/run.mjs +12 -2
  13. package/package.json +5 -3
  14. package/release/gate.mjs +82 -31
  15. package/release/index.mjs +19 -8
  16. package/release/push.mjs +4 -4
  17. package/release/records.mjs +54 -0
  18. package/release/urgent.mjs +129 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +54 -24
  20. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  21. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  22. package/runtime/knowledge/hfs/rules.yaml +250 -157
  23. package/runtime/knowledge/hfs/slots.yaml +199 -174
  24. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  25. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  26. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  27. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  28. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  29. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  30. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  31. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  32. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  33. package/runtime/modules/kernel/command-policy.yaml +1 -1
  34. package/runtime/modules/kernel/failure-codes.yaml +86 -115
  35. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  36. package/runtime/scripts/hfs/app-wiring.mjs +23 -2
  37. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  38. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  39. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  40. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  41. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  42. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  43. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  44. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  45. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  46. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  47. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  48. package/runtime/scripts/hfs/check.mjs +8 -8
  49. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  50. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  51. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  52. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  53. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  54. package/runtime/scripts/hfs/manifest-shape.mjs +6 -5
  55. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  56. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  57. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  58. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  59. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  60. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  61. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  62. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  63. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  64. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  65. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  66. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  67. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  68. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  69. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  70. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  71. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  72. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  73. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  74. package/runtime/scripts/hfs/slots.mjs +8 -2
  75. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  76. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  77. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  78. package/scaffold/add-app.mjs +2 -10
  79. package/scaffold/add-cli-lite.mjs +6 -6
  80. package/scaffold/add-compose.mjs +1 -1
  81. package/scaffold/add-inbox-lite.mjs +4 -8
  82. package/scaffold/add-plan.mjs +1 -1
  83. package/scaffold/add-queue-lite.mjs +16 -24
  84. package/scaffold/add-table.mjs +34 -26
  85. package/scaffold/add.mjs +2 -2
  86. package/scaffold/app.mjs +19 -86
  87. package/scaffold/lite-exports.mjs +1 -1
  88. package/src/main.mjs +24 -22
  89. package/stack/cli.mjs +10 -2
  90. package/stack/deploy.mjs +21 -4
  91. package/stack/target-binding.mjs +0 -60
  92. package/stack/urgent-floor.mjs +147 -0
  93. package/sync/ci-commands.mjs +3 -1
  94. package/sync/index.mjs +32 -8
  95. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +4 -1
  96. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  97. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  98. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +4 -1
  99. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  100. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  101. package/templates/app/gitignore-lite +0 -3
  102. package/templates/app/package-scripts/db.json +9 -0
  103. package/templates/app/package-scripts/package.json +0 -1
  104. package/templates/app/package-scripts-lite/package.json +1 -1
  105. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  106. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  107. package/templates/app/skeleton/README.md +1 -1
  108. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  109. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  110. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  111. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  112. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  113. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  114. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  115. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  116. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  117. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  118. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  119. package/templates/app/skeleton-lite/README.md +11 -11
  120. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  121. package/templates/app/tool-config-lite/prettierignore +0 -1
  122. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  123. package/templates/be/inbox/database-client.ts.tpl +13 -4
  124. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  125. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  126. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  127. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  128. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  129. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  130. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  131. package/templates/be/inbox/service.ts.tpl +1 -2
  132. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  133. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  134. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  135. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  136. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  137. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  138. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  139. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  140. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  141. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  142. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  143. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -7
  144. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +3 -0
  145. package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
  146. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +15 -1
  147. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +10 -9
  148. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +1 -1
  149. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +34 -18
  150. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +29 -8
  151. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +80 -4
  152. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +39 -11
  153. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  154. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  155. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  156. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  157. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  158. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  159. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  160. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  161. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  162. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  163. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  164. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  165. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +23 -13
  166. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  167. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  168. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  169. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  170. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  171. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  172. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  173. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  174. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  175. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  176. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  177. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  178. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  179. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  180. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  181. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  182. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  183. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  184. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  185. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  186. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  187. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  188. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  189. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
  190. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  191. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  192. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  193. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  194. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  195. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  196. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  197. package/templates/be/table/entities.index.ts.tpl +4 -0
  198. package/templates/be/table/entity.ts.tpl +21 -0
  199. package/templates/be/table/index.ts.tpl +1 -0
  200. package/templates/be/table/module.ts.tpl +1 -1
  201. package/templates/be/table/service.api.ts.tpl +10 -11
  202. package/templates/be/table/service.ts.tpl +5 -6
  203. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  204. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  205. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  206. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  207. package/db/local-migrate.mjs +0 -194
  208. package/db/reset.mjs +0 -125
  209. package/emit/db-types.mjs +0 -271
  210. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  211. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  212. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  213. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  214. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  215. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  216. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  217. package/scaffold/supabase-ports.mjs +0 -26
  218. package/templates/app/package-scripts/supabase.json +0 -7
  219. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  220. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  221. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  222. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  223. package/templates/be/inbox/sql.ts.tpl +0 -16
  224. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  225. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  226. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  227. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  228. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  229. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  230. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  231. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  232. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  233. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  234. package/templates/be/table/rows.ts.tpl +0 -4
  235. package/templates/be/table/sql.api.ts.tpl +0 -7
  236. package/templates/be/table/sql.ts.tpl +0 -4
  237. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  238. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  239. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  240. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  241. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
@@ -8,13 +8,29 @@ services:
8
8
  HTTP_SECURITY_ALLOWED_ORIGINS: http://dev.{{project}}.example.invalid
9
9
  PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
10
10
  PRIMARY_DB_SCHEMA: public
11
- SUPABASE_URL: https://{{project}}.supabase.co
12
- SUPABASE_SERVICE_ROLE_KEY_FILE: /run/secrets/supabase-service-role-key
11
+ GOOGLE_CLIENT_ID: REPLACE_WITH_GOOGLE_OAUTH_CLIENT_ID
12
+ GOOGLE_CLIENT_SECRET_FILE: /run/secrets/google-client-secret
13
+ GOOGLE_CALLBACK_URL: http://dev.{{project}}.example.invalid/auth/google/callback
14
+ AUTH_JWT_ISSUER: http://dev.{{project}}.example.invalid
15
+ AUTH_JWT_AUDIENCE: {{project}}
16
+ AUTH_JWT_KEY_ID: dev-1
17
+ AUTH_JWT_PRIVATE_KEY_FILE: /run/secrets/auth-jwt-private-key
18
+ STORAGE_ENDPOINT: https://storage.{{project}}.example.invalid
19
+ STORAGE_REGION: us-east-1
20
+ STORAGE_BUCKET: {{project}}-dev
21
+ STORAGE_ACCESS_KEY_ID_FILE: /run/secrets/storage-access-key-id
22
+ STORAGE_SECRET_ACCESS_KEY_FILE: /run/secrets/storage-secret-access-key
13
23
  secrets:
14
24
  - source: primary-db-url
15
25
  target: primary-db-url
16
- - source: supabase-service-role-key
17
- target: supabase-service-role-key
26
+ - source: google-client-secret
27
+ target: google-client-secret
28
+ - source: auth-jwt-private-key
29
+ target: auth-jwt-private-key
30
+ - source: storage-access-key-id
31
+ target: storage-access-key-id
32
+ - source: storage-secret-access-key
33
+ target: storage-secret-access-key
18
34
  networks: [app]
19
35
  healthcheck:
20
36
  test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
@@ -66,6 +82,38 @@ services:
66
82
  restart_policy:
67
83
  condition: on-failure
68
84
  delay: 5s
85
+ db:
86
+ image: "supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2"
87
+ environment:
88
+ POSTGRES_DB: postgres
89
+ POSTGRES_PASSWORD_FILE: /run/secrets/db-password
90
+ secrets:
91
+ - source: db-password
92
+ target: db-password
93
+ volumes:
94
+ - db-data:/var/lib/postgresql/data
95
+ networks: [app]
96
+ healthcheck:
97
+ test: ["CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
98
+ interval: 10s
99
+ timeout: 5s
100
+ start_period: 30s
101
+ retries: 5
102
+ deploy:
103
+ replicas: 1
104
+ placement:
105
+ constraints: ["node.labels.{{project}} == dev"]
106
+ update_config:
107
+ parallelism: 1
108
+ order: stop-first
109
+ failure_action: rollback
110
+ monitor: 30s
111
+ rollback_config:
112
+ parallelism: 1
113
+ order: stop-first
114
+ restart_policy:
115
+ condition: on-failure
116
+ delay: 5s
69
117
  ingress:
70
118
  image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
71
119
  configs:
@@ -104,13 +152,28 @@ secrets:
104
152
  primary-db-url:
105
153
  external: true
106
154
  name: "{{project}}-dev-primary-db-url-v1"
107
- supabase-service-role-key:
155
+ db-password:
156
+ external: true
157
+ name: "{{project}}-dev-db-password-v1"
158
+ google-client-secret:
159
+ external: true
160
+ name: "{{project}}-dev-google-client-secret-v1"
161
+ auth-jwt-private-key:
108
162
  external: true
109
- name: "{{project}}-dev-supabase-service-role-key-v1"
163
+ name: "{{project}}-dev-auth-jwt-private-key-v1"
164
+ storage-access-key-id:
165
+ external: true
166
+ name: "{{project}}-dev-storage-access-key-id-v1"
167
+ storage-secret-access-key:
168
+ external: true
169
+ name: "{{project}}-dev-storage-secret-access-key-v1"
110
170
  configs:
111
171
  ingress-config:
112
172
  file: ./Caddyfile
113
173
  volumes:
174
+ db-data:
175
+ external: true
176
+ name: "{{project}}-dev-db-data"
114
177
  ingress-data:
115
178
  external: true
116
179
  name: "{{project}}-dev-ingress-data"
@@ -1,18 +1,29 @@
1
1
  # Environment key roster
2
2
 
3
- This roster declares names only. Scaffolding generates the two declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set dev` before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Store product credential values in sealed custody and materialize private KEY files outside the repository.
3
+ This roster declares names only. Scaffolding generates the six declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set dev` before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Store product credential values in sealed custody and materialize private KEY files outside the repository.
4
4
 
5
5
  | Key | Consumer |
6
6
  | --- | --- |
7
7
  | `PORT` | API listener |
8
8
  | `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
9
- | `PRIMARY_DB_URL` | API primary database connection; `.starcistacks/dev/secrets/primary-db-url.enc` maps to external `{{project}}-dev-primary-db-url-v1` and mounted `/run/secrets/primary-db-url` |
9
+ | `PRIMARY_DB_URL` | API least-privilege database connection; `.starcistacks/dev/secrets/primary-db-url.enc` maps to external `{{project}}-dev-primary-db-url-v1` and mounted `/run/secrets/primary-db-url` |
10
10
  | `PRIMARY_DB_SCHEMA` | API primary database schema |
11
- | `SUPABASE_URL` | API Auth and Storage integration |
12
- | `SUPABASE_SERVICE_ROLE_KEY` | API Auth and Storage integration; `.starcistacks/dev/secrets/supabase-service-role-key.enc` maps to external `{{project}}-dev-supabase-service-role-key-v1` and mounted `/run/secrets/supabase-service-role-key` |
11
+ | `POSTGRES_PASSWORD` | The db service's superuser password; `.starcistacks/dev/secrets/db-password.enc` maps to external `{{project}}-dev-db-password-v1` and mounted `/run/secrets/db-password` |
12
+ | `GOOGLE_CLIENT_ID` | API Google OAuth client |
13
+ | `GOOGLE_CLIENT_SECRET` | API Google OAuth client; `.starcistacks/dev/secrets/google-client-secret.enc` maps to external `{{project}}-dev-google-client-secret-v1` and mounted `/run/secrets/google-client-secret` |
14
+ | `GOOGLE_CALLBACK_URL` | API Google OAuth callback door |
15
+ | `AUTH_JWT_ISSUER` | API access-token issuer |
16
+ | `AUTH_JWT_AUDIENCE` | API access-token audience |
17
+ | `AUTH_JWT_KEY_ID` | API signing key id, published in the JWKS |
18
+ | `AUTH_JWT_PRIVATE_KEY` | API ES256 signing key; `.starcistacks/dev/secrets/auth-jwt-private-key.enc` maps to external `{{project}}-dev-auth-jwt-private-key-v1` and mounted `/run/secrets/auth-jwt-private-key` |
19
+ | `STORAGE_ENDPOINT` | API S3-compatible storage endpoint |
20
+ | `STORAGE_REGION` | API storage signing region |
21
+ | `STORAGE_BUCKET` | API storage bucket |
22
+ | `STORAGE_ACCESS_KEY_ID` | API storage credential; `.starcistacks/dev/secrets/storage-access-key-id.enc` maps to external `{{project}}-dev-storage-access-key-id-v1` and mounted `/run/secrets/storage-access-key-id` |
23
+ | `STORAGE_SECRET_ACCESS_KEY` | API storage credential; `.starcistacks/dev/secrets/storage-secret-access-key.enc` maps to external `{{project}}-dev-storage-secret-access-key-v1` and mounted `/run/secrets/storage-secret-access-key` |
24
+ | `DATABASE_URL` | `starci app db` verbs (migrate, dump, plan) and `contract:emit`, from the operator's environment |
13
25
  | `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
26
+ | `NEXT_PUBLIC_API_URL` | Front-end auth doors of the API |
14
27
  | `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
15
- | `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
16
- | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
17
28
 
18
- Public front-end configuration is supplied at build time and stays outside sealed custody. Production database deployment consumes `SUPABASE_ACCESS_TOKEN`, `SUPABASE_DB_PASSWORD` and `SUPABASE_PROJECT_ID` from its environment's sealed custody; the GitHub secret store carries only the age identity.
29
+ Public front-end configuration is supplied at build time and stays outside sealed custody. The database verbs read `DATABASE_URL` from the operator's environment; the GitHub secret store carries only the age identity.
@@ -1,11 +1,11 @@
1
1
  # Production stack
2
2
 
3
3
  The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
4
- Hosted Supabase supplies PostgreSQL, pgvector, Auth and Storage. The app and its other dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
4
+ The `db` service (image supabase/postgres) supplies PostgreSQL with pg_cron, pg_net, pgmq and pgvector on a named volume; sign-in is the API's own (Passport) and storage is the S3-compatible MinIO the environment names. The app and its dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
5
5
  The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. Public records under `.starcistacks/production/infra/swarm/` explicitly mark those owner-run operations as pending.
6
6
  Its application image references contain illustrative all-zero digests; they identify no published image and must be replaced before deployment.
7
7
 
8
- The runtime template tracks no encrypted members. Scaffolding generates the two declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set production` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
8
+ The runtime template tracks no encrypted members. Scaffolding generates the six declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set production` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
9
9
 
10
10
  | Command | Operation |
11
11
  | --- | --- |
@@ -26,13 +26,13 @@ Every item is pending. Swarm initialization, provider URIs, DNS changes and key
26
26
  | Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress volumes. |
27
27
  | DNS | Replace the reserved `example.invalid` hosts consistently in the manifest, Caddyfile and public configuration; publish address records for the web and API hosts and retain a provider receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
28
28
  | TLS | Caddy issues certificates after real DNS resolves. Provision `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data` on the selected node; retain an issuance and renewal receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
29
- | Database | Provision Supabase and the app_be connection; provider backups remain enabled. Record the provider URI and recovery owner without copying credentials here. |
29
+ | Database | Provision `{{project}}-production-db-data` on the stateful node; after the first deploy create the app_be login through the baseline migration (`starci app db migrate --url-env DATABASE_URL --execute`). Record the recovery owner without copying credentials here. |
30
30
  | Secret custody | Replace generated placeholder values with real values at the manifest's declared `.starcistacks/production/secrets/*.enc` paths; record their keys in `.starcistacks/production/runtime/env/KEYS.md`. The app's age identity stays only in the host's untracked `.claude/secret.env`. |
31
- | External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1` and `{{project}}-production-supabase-service-role-key-v1`. The API reads mounted files; rotation creates new versioned names before updating the model. |
32
- | Public build configuration | Build the front end with its declared public site and GraphQL URLs, the Supabase public URL and anonymous key. Public browser values stay outside sealed secret custody. |
31
+ | External secrets | Materialize the sealed members in owner custody and create the `{{project}}-production-<member>-v1` external secret of each declared member. The API reads mounted files; rotation creates new versioned names before updating the model. |
32
+ | Public build configuration | Build the front end with its declared public site, API and GraphQL URLs. Public browser values stay outside sealed secret custody. |
33
33
  | Immutable release | Replace the illustrative all-zero application digests in the manifest, model and `.starcistacks/production/infra/swarm/releases/images.lock.yaml` with digests produced by the verified release. Store each rendered release under `/srv/{{project}}/releases/<digest>`. Retain the last accepted record in `.starcistacks/production/infra/swarm/releases/previous.yaml`, then move the current pointer and update `.starcistacks/production/infra/swarm/releases/current.yaml` after verification. |
34
34
  | Rollback | Retain the previous model and images, redeploy that model if convergence or health fails, and record the resulting convergence receipt. Database changes follow an expand/contract sequence. |
35
- | Backup | Provision the manifest's seven days of database PITR and thirty daily encrypted off-host database exports and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
35
+ | Backup | Provision the manifest's thirty daily encrypted off-host database exports (`starci app db dump`) and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
36
36
  | Restore proof | Perform a restore into isolated infrastructure, verify the restored database and ingress state, and attach a dated receipt with artifact digests to `.starcistacks/production/infra/swarm/recovery/restore-proof.md`. Its initial record is pending; no restore has been run by scaffolding. |
37
37
 
38
38
  No host scheduler is installed. Scheduled application work belongs to the back-end scheduler or an explicitly declared replicated job.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "environment": "production",
3
- "provider": "supabase",
3
+ "provider": "postgres",
4
4
  "project": "{{project}}",
5
5
  "runtime": "docker-swarm"
6
6
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  Status: pending owner execution. No backup or restore has been run by scaffolding.
4
4
 
5
- The production operator owns recovery of the managed Supabase database and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision seven days of provider PITR and thirty daily encrypted off-host database exports. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
5
+ The production operator owns recovery of the db service's volume `{{project}}-production-db-data` and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision thirty daily encrypted off-host database exports through `starci app db dump`. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
6
6
 
7
- Before relying on the backup policy, restore the provider database backup and both volume snapshots into isolated infrastructure. Record the actual off-host destination, encryption custody, backup artifact digests, restoration time and operator. Verify database connectivity and the API readiness endpoint, then verify the restored TLS and ingress configuration. Attach the dated result here without credentials.
7
+ Before relying on the backup policy, restore a `starci app db dump` archive and both volume snapshots into isolated infrastructure. Record the actual off-host destination, encryption custody, backup artifact digests, restoration time and operator. Verify database connectivity and the API readiness endpoint, then verify the restored TLS and ingress configuration. Attach the dated result here without credentials.
@@ -8,13 +8,29 @@ services:
8
8
  HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
9
9
  PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
10
10
  PRIMARY_DB_SCHEMA: public
11
- SUPABASE_URL: https://{{project}}.supabase.co
12
- SUPABASE_SERVICE_ROLE_KEY_FILE: /run/secrets/supabase-service-role-key
11
+ GOOGLE_CLIENT_ID: REPLACE_WITH_GOOGLE_OAUTH_CLIENT_ID
12
+ GOOGLE_CLIENT_SECRET_FILE: /run/secrets/google-client-secret
13
+ GOOGLE_CALLBACK_URL: https://api.{{project}}.example.invalid/auth/google/callback
14
+ AUTH_JWT_ISSUER: https://api.{{project}}.example.invalid
15
+ AUTH_JWT_AUDIENCE: {{project}}
16
+ AUTH_JWT_KEY_ID: production-1
17
+ AUTH_JWT_PRIVATE_KEY_FILE: /run/secrets/auth-jwt-private-key
18
+ STORAGE_ENDPOINT: https://storage.{{project}}.example.invalid
19
+ STORAGE_REGION: us-east-1
20
+ STORAGE_BUCKET: {{project}}-production
21
+ STORAGE_ACCESS_KEY_ID_FILE: /run/secrets/storage-access-key-id
22
+ STORAGE_SECRET_ACCESS_KEY_FILE: /run/secrets/storage-secret-access-key
13
23
  secrets:
14
24
  - source: primary-db-url
15
25
  target: primary-db-url
16
- - source: supabase-service-role-key
17
- target: supabase-service-role-key
26
+ - source: google-client-secret
27
+ target: google-client-secret
28
+ - source: auth-jwt-private-key
29
+ target: auth-jwt-private-key
30
+ - source: storage-access-key-id
31
+ target: storage-access-key-id
32
+ - source: storage-secret-access-key
33
+ target: storage-secret-access-key
18
34
  networks: [app]
19
35
  healthcheck:
20
36
  test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
@@ -66,6 +82,38 @@ services:
66
82
  restart_policy:
67
83
  condition: on-failure
68
84
  delay: 5s
85
+ db:
86
+ image: "supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2"
87
+ environment:
88
+ POSTGRES_DB: postgres
89
+ POSTGRES_PASSWORD_FILE: /run/secrets/db-password
90
+ secrets:
91
+ - source: db-password
92
+ target: db-password
93
+ volumes:
94
+ - db-data:/var/lib/postgresql/data
95
+ networks: [app]
96
+ healthcheck:
97
+ test: ["CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1"]
98
+ interval: 10s
99
+ timeout: 5s
100
+ start_period: 30s
101
+ retries: 5
102
+ deploy:
103
+ replicas: 1
104
+ placement:
105
+ constraints: ["node.labels.{{project}} == production"]
106
+ update_config:
107
+ parallelism: 1
108
+ order: stop-first
109
+ failure_action: rollback
110
+ monitor: 30s
111
+ rollback_config:
112
+ parallelism: 1
113
+ order: stop-first
114
+ restart_policy:
115
+ condition: on-failure
116
+ delay: 5s
69
117
  ingress:
70
118
  image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
71
119
  configs:
@@ -105,13 +153,28 @@ secrets:
105
153
  primary-db-url:
106
154
  external: true
107
155
  name: "{{project}}-production-primary-db-url-v1"
108
- supabase-service-role-key:
156
+ db-password:
157
+ external: true
158
+ name: "{{project}}-production-db-password-v1"
159
+ google-client-secret:
160
+ external: true
161
+ name: "{{project}}-production-google-client-secret-v1"
162
+ auth-jwt-private-key:
109
163
  external: true
110
- name: "{{project}}-production-supabase-service-role-key-v1"
164
+ name: "{{project}}-production-auth-jwt-private-key-v1"
165
+ storage-access-key-id:
166
+ external: true
167
+ name: "{{project}}-production-storage-access-key-id-v1"
168
+ storage-secret-access-key:
169
+ external: true
170
+ name: "{{project}}-production-storage-secret-access-key-v1"
111
171
  configs:
112
172
  ingress-config:
113
173
  file: ./Caddyfile
114
174
  volumes:
175
+ db-data:
176
+ external: true
177
+ name: "{{project}}-production-db-data"
115
178
  ingress-data:
116
179
  external: true
117
180
  name: "{{project}}-production-ingress-data"
@@ -1,6 +1,6 @@
1
1
  # Production environment key roster
2
2
 
3
- This inventory declares names, consumers and mounted file paths. Scaffolding generates the two declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set production` after owner provisioning and before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Scaffolding supplies no deployment credential or successful deployment.
3
+ This inventory declares names, consumers and mounted file paths. Scaffolding generates the six declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set production` after owner provisioning and before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Scaffolding supplies no deployment credential or successful deployment.
4
4
 
5
5
  | Key | Consumer |
6
6
  | --- | --- |
@@ -8,14 +8,22 @@ This inventory declares names, consumers and mounted file paths. Scaffolding gen
8
8
  | `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
9
9
  | `PRIMARY_DB_URL` | API least-privilege database connection; reads `PRIMARY_DB_URL_FILE` at `/run/secrets/primary-db-url` |
10
10
  | `PRIMARY_DB_SCHEMA` | API primary database schema |
11
- | `SUPABASE_URL` | API Auth and Storage integration |
12
- | `SUPABASE_SERVICE_ROLE_KEY` | API Auth and Storage integration; reads `SUPABASE_SERVICE_ROLE_KEY_FILE` at `/run/secrets/supabase-service-role-key` |
11
+ | `POSTGRES_PASSWORD` | The db service's superuser password; reads `POSTGRES_PASSWORD_FILE` at `/run/secrets/db-password` |
12
+ | `GOOGLE_CLIENT_ID` | API Google OAuth client |
13
+ | `GOOGLE_CLIENT_SECRET` | API Google OAuth client; reads `GOOGLE_CLIENT_SECRET_FILE` at `/run/secrets/google-client-secret` |
14
+ | `GOOGLE_CALLBACK_URL` | API Google OAuth callback door |
15
+ | `AUTH_JWT_ISSUER` | API access-token issuer |
16
+ | `AUTH_JWT_AUDIENCE` | API access-token audience |
17
+ | `AUTH_JWT_KEY_ID` | API signing key id, published in the JWKS |
18
+ | `AUTH_JWT_PRIVATE_KEY` | API ES256 signing key; reads `AUTH_JWT_PRIVATE_KEY_FILE` at `/run/secrets/auth-jwt-private-key` |
19
+ | `STORAGE_ENDPOINT` | API S3-compatible storage endpoint |
20
+ | `STORAGE_REGION` | API storage signing region |
21
+ | `STORAGE_BUCKET` | API storage bucket |
22
+ | `STORAGE_ACCESS_KEY_ID` | API storage credential; reads `STORAGE_ACCESS_KEY_ID_FILE` at `/run/secrets/storage-access-key-id` |
23
+ | `STORAGE_SECRET_ACCESS_KEY` | API storage credential; reads `STORAGE_SECRET_ACCESS_KEY_FILE` at `/run/secrets/storage-secret-access-key` |
24
+ | `DATABASE_URL` | `starci app db` verbs (migrate, dump, plan) and `contract:emit`, from the operator's environment |
13
25
  | `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
26
+ | `NEXT_PUBLIC_API_URL` | Front-end auth doors of the API |
14
27
  | `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
15
- | `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
16
- | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
17
- | `SUPABASE_ACCESS_TOKEN` | Production database deployment |
18
- | `SUPABASE_DB_PASSWORD` | Production database deployment |
19
- | `SUPABASE_PROJECT_ID` | Production database deployment |
20
28
 
21
- Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in `.starcistacks/production/secrets/primary-db-url.enc` and `.starcistacks/production/secrets/supabase-service-role-key.enc` at the declared paths. Versioned external application secrets are `{{project}}-production-primary-db-url-v1` and `{{project}}-production-supabase-service-role-key-v1`. Seal deployment-only credentials separately under `.starcistacks/production/secrets/*.enc`; they never enter an API container.
29
+ Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in the six `.starcistacks/production/secrets/*.enc` members at the declared paths; their versioned external secrets are `{{project}}-production-<member>-v1`. The database deploy workflow reads `DATABASE_URL` from the sealed `database-url` member of its environment; it never enters an API container.
@@ -1,20 +1,20 @@
1
1
  # {{project}}
2
2
 
3
- One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and Supabase as the schema authority.
3
+ One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and its own PostgreSQL with SQL migrations in `db/` as the schema authority.
4
4
 
5
5
  ## Overview
6
6
 
7
- The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and uses Supabase only for Auth in the browser (`fe/apps/web/src/modules/auth`); every data read and write goes through the API, which verifies the access token.
7
+ The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and signs in only through the API's own auth doors (`fe/apps/web/src/modules/auth`, which holds the access token in memory while the refresh token stays in an httpOnly cookie); every data read and write goes through the API, which verifies the access token against its own keys.
8
8
 
9
9
  ## Stack
10
10
 
11
- TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, Supabase for Auth and PostgreSQL, npm workspaces, and Turbo for front-end tasks.
11
+ TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, PostgreSQL (the `db` service, image supabase/postgres), Passport for Google and password sign-in, S3-compatible storage (MinIO), npm workspaces, and Turbo for front-end tasks.
12
12
 
13
13
  ## Repository layout
14
14
 
15
- - `supabase/`: local configuration, forward-only SQL migrations, seed data, and generated database types.
16
- - `be/`: the API, shared platform capabilities, verified identity admission, and privileged Supabase integrations.
17
- - `fe/`: the web app; the only Supabase client is the Auth client of `fe/apps/web/src/modules/auth`, data goes through `fe/apps/web/src/modules/api`.
15
+ - `db/`: forward-only SQL migrations, seed data, and generated database types.
16
+ - `be/`: the API, shared platform capabilities, verified identity admission, and the passport and storage integrations.
17
+ - `fe/`: the web app; it holds no credential, signs in through `fe/apps/web/src/modules/auth` and reaches data through `fe/apps/web/src/modules/api`.
18
18
  - `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
19
19
  - `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
20
20
 
@@ -24,7 +24,7 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
24
24
 
25
25
  ## Development
26
26
 
27
- Every development dependency runs in Docker Linux containers. Start local Supabase with `npm run db:start` through the Supabase CLI; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies run on Docker Swarm, except Supabase, which is a hosted service; Supabase pgvector owns vector search.
27
+ Every development dependency runs in Docker Linux containers. Create the local database once with `npm run db:create`, start it with `npm run db:start` (one container of the pinned `db` image, on 127.0.0.1:54322), export `DATABASE_URL=postgresql://supabase_admin@127.0.0.1:54322/postgres` and apply `db/bootstrap.sql` and the migrations with `npm run db:push`; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies, the database included, run on Docker Swarm; pgvector owns vector search.
28
28
 
29
29
  ```sh
30
30
  npm install
@@ -36,17 +36,17 @@ npm run build:be
36
36
  npm run build:fe
37
37
  ```
38
38
 
39
- The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMARY_DB_SCHEMA`, `SUPABASE_URL`, and `SUPABASE_SERVICE_ROLE_KEY`. The web app reads `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, and, once the first GraphQL operation exists, `NEXT_PUBLIC_GRAPHQL_URL`. Missing configuration stops the relevant process with a named error; no template supplies credential defaults.
39
+ The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMARY_DB_SCHEMA`, the Google OAuth keys `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` and `GOOGLE_CALLBACK_URL`, its own token keys `AUTH_JWT_ISSUER`, `AUTH_JWT_AUDIENCE`, `AUTH_JWT_KEY_ID` and `AUTH_JWT_PRIVATE_KEY`, and the storage keys `STORAGE_ENDPOINT`, `STORAGE_REGION`, `STORAGE_BUCKET`, `STORAGE_ACCESS_KEY_ID` and `STORAGE_SECRET_ACCESS_KEY`. The database tooling reads `DATABASE_URL`. The web app reads `NEXT_PUBLIC_SITE_URL`, `NEXT_PUBLIC_API_URL`, and, once the first GraphQL operation exists, `NEXT_PUBLIC_GRAPHQL_URL`. Missing configuration stops the relevant process with a named error; no template supplies credential defaults.
40
40
 
41
- Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.web.buildArgs.values`. Before building deployment images, replace the placeholders with your site's public origin, your Supabase project's public URL, and its public anonymous key. These values reach every browser; keep the service role key in sealed runtime custody.
41
+ Production browser build values live in `.starcistacks/application-stacks.yaml` under `environments.production.deploy.images.web.buildArgs.values`. Before building deployment images, replace the placeholders with your site's and your API's public origins. These values reach every browser; every credential stays in sealed runtime custody.
42
42
 
43
- `npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local Supabase stack (`npm run db:start`); a lite app has no e2e world.
43
+ `npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local database (`npm run db:start`, then `npm run db:push`); a lite app has no e2e world.
44
44
 
45
45
  The public runtime template tracks only sealed member names. Scaffolding creates this app's own age recipient and encrypts generated placeholder values into the declared `.starcistacks/<environment>/secrets/*.enc` members. Keep the matching host age identity in untracked local custody, then use `starci app secret set` to replace each placeholder with an actual value before deployment. Product apps retain sealed member custody. Runtime specs use disposable copies with ephemeral identities and remove them afterwards.
46
46
 
47
47
  ## Database changes
48
48
 
49
- Create a forward-only migration under `supabase/migrations`, keep each table's RLS policies and grants beside the table, and run `npm run contract:emit` to refresh `supabase/types/database.types.ts`. `starci app add table <name> --fe` adds the table with its GraphQL feature on the API and its operations and SWR hooks in the web app; `npm run codegen:graphql` regenerates the web app's wire types from `be/contracts/<app>/schema.graphql`.
49
+ Create a forward-only migration with `npm run db:new -- <kebab-name>` (it lands in `db/migrations`), keep each table's RLS policies and grants beside the table, apply it with `npm run db:push`, and run `npm run contract:emit` to refresh `db/types/database.types.ts`. `starci app add table <name> --fe` adds the table with its GraphQL feature on the API and its operations and SWR hooks in the web app; `npm run codegen:graphql` regenerates the web app's wire types from `be/contracts/<app>/schema.graphql`.
50
50
 
51
51
  ## Work
52
52
 
@@ -6,7 +6,7 @@ begin
6
6
  end;
7
7
  $$;
8
8
 
9
- -- The attributes a Supabase migration role may change: SUPERUSER and BYPASSRLS stay as create role set them (the role migration owner is no superuser).
9
+ -- The attributes a migration may change: SUPERUSER and BYPASSRLS stay as create role set them.
10
10
  alter role app_be login noinherit nocreatedb nocreaterole;
11
11
 
12
12
  create schema if not exists private;
@@ -24,6 +24,26 @@ begin
24
24
  end;
25
25
  $$;
26
26
 
27
+ -- The rotating refresh sessions of integrations/passport (auth.users comes from db/bootstrap.sql).
28
+ -- One row per issued refresh token, stored by its sha256 only. A family is one sign-in; rotation revokes the presented row
29
+ -- and inserts its successor, and presenting a revoked row revokes the whole family (reuse detection).
30
+ create table auth.refresh_sessions (
31
+ id uuid primary key default gen_random_uuid(),
32
+ family_id uuid not null,
33
+ user_id uuid not null references auth.users (id) on delete cascade,
34
+ token_hash char(64) not null unique,
35
+ expires_at timestamptz not null,
36
+ revoked_at timestamptz,
37
+ created_at timestamptz not null default now()
38
+ );
39
+
40
+ create index refresh_sessions_family_idx on auth.refresh_sessions (family_id);
41
+
42
+ grant usage on schema auth to app_be;
43
+ grant select, insert, update on table auth.users to app_be;
44
+ grant select, insert on table auth.identities to app_be;
45
+ grant select, insert, update on table auth.refresh_sessions to app_be;
46
+
27
47
  create table public.profiles (
28
48
  id uuid primary key references auth.users (id) on delete cascade,
29
49
  display_name text not null check (char_length(display_name) between 1 and 120),
@@ -1,2 +1 @@
1
1
  {{> app/tool-config/prettierignore}}
2
- supabase/types/
@@ -1,15 +1,19 @@
1
1
  import { mockEntityManager } from "@starci/jest-preset"
2
2
  import type { EntityManager } from "typeorm"
3
- import { sql } from "./database.sql"
4
3
  import { DatabaseErrorCode } from "./errors/database.error"
5
- import { recordWebhookDelivery } from "./webhook-inbox.client"
4
+ import { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"
6
5
 
7
- // The caller owns the conflict-safe pending-row insert; an opaque function call stands in for it here.
8
- const INSERT = sql`SELECT id FROM record_spec_inbox($1, $2, $3::jsonb)`
6
+ const INSERT = webhookReceiptInsert("spec_inbox")
9
7
  const PAYLOAD = { id: "delivery-1" }
10
8
 
11
9
  const record = (manager: EntityManager, deliveryId: string | undefined) =>
12
- recordWebhookDelivery({ manager, insert: INSERT, provider: "spec", deliveryId, payload: PAYLOAD })
10
+ recordWebhookDelivery({ manager, table: "spec_inbox", provider: "spec", deliveryId, payload: PAYLOAD })
11
+
12
+ describe("webhookReceiptInsert", () => {
13
+ it("refuses a table name that is not a lowercase identifier", () => {
14
+ expect(() => webhookReceiptInsert("spec; drop table x")).toThrow(TypeError)
15
+ })
16
+ })
13
17
 
14
18
  describe("recordWebhookDelivery", () => {
15
19
  it("records a new delivery through the caller's transaction manager", async () => {
@@ -1,10 +1,10 @@
1
1
  import type { EntityManager } from "typeorm"
2
2
  import { DatabaseError, DatabaseErrorCode } from "./errors/database.error"
3
- import type { SqlText } from "./database.sql"
4
3
 
5
4
  interface RecordWebhookDelivery {
6
5
  readonly manager: EntityManager
7
- readonly insert: SqlText
6
+ /** The provider inbox table under public, created by its db migration. */
7
+ readonly table: string
8
8
  readonly provider: string
9
9
  readonly deliveryId: string | undefined
10
10
  readonly payload: object
@@ -12,13 +12,22 @@ interface RecordWebhookDelivery {
12
12
 
13
13
  type WebhookReceiptId = string | undefined
14
14
 
15
+ /** A provider inbox table name: a lowercase identifier, never a value. */
16
+ const TABLE = /^[a-z_][a-z0-9_]*$/
17
+
18
+ /** The conflict-safe insert of one pending delivery into `public.<table>`, answering its id only to the winning transaction. */
19
+ export const webhookReceiptInsert = (table: string): string => {
20
+ if (!TABLE.test(table)) throw new TypeError(`${table} is not a provider inbox table name`)
21
+ return `INSERT INTO public.${table} (provider, delivery_id, payload) VALUES ($1, $2, $3::jsonb) ON CONFLICT (provider, delivery_id) DO NOTHING RETURNING id`
22
+ }
23
+
15
24
  /**
16
25
  * Records one pending delivery using the intake's transaction manager and returns its durable receipt identity.
17
26
  * Business processing and the completion marker belong to the reactor or job that consumes the committed row.
18
27
  */
19
28
  export const recordWebhookDelivery = async ({
20
29
  manager,
21
- insert,
30
+ table,
22
31
  provider,
23
32
  deliveryId,
24
33
  payload,
@@ -27,7 +36,7 @@ export const recordWebhookDelivery = async ({
27
36
  if (!normalizedDeliveryId) {
28
37
  throw new DatabaseError({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
29
38
  }
30
- const rows: Array<{ readonly id: string }> = await manager.query(insert, [
39
+ const rows: Array<{ readonly id: string }> = await manager.query(webhookReceiptInsert(table), [
31
40
  provider,
32
41
  normalizedDeliveryId,
33
42
  JSON.stringify(payload),
@@ -16,3 +16,50 @@ on public.inbox for delete to app_be
16
16
  using (source like 'client:%' and request_fingerprint is not null and outcome is not null);
17
17
 
18
18
  grant update, delete on table public.inbox to app_be;
19
+
20
+ -- The client claim, its saved outcome and the bounded retention sweep, called through callDbFunction of platform/database (R334).
21
+ -- A conflicting claim locks the existing row and returns its committed fingerprint and outcome after the winner commits.
22
+ create function public.inbox_claim_client(p_source text, p_event_id text, p_fingerprint varchar, p_claimed_at timestamptz)
23
+ returns table (request_fingerprint varchar, outcome jsonb, incomplete boolean)
24
+ language sql
25
+ as $$
26
+ insert into public.inbox as i (source, event_id, request_fingerprint, claimed_at)
27
+ values (p_source, p_event_id, p_fingerprint, p_claimed_at)
28
+ on conflict (source, event_id) do update set outcome = coalesce(i.outcome, '{}'::jsonb)
29
+ returning i.request_fingerprint, i.outcome,
30
+ (i.outcome is not null and (jsonb_typeof(i.outcome) <> 'object' or not (i.outcome ? 'value')))
31
+ $$;
32
+
33
+ create function public.inbox_save_client_outcome(p_source text, p_event_id text, p_outcome jsonb)
34
+ returns table (outcome jsonb)
35
+ language sql
36
+ as $$
37
+ update public.inbox as i set outcome = p_outcome
38
+ where i.source = p_source and i.event_id = p_event_id
39
+ returning i.outcome
40
+ $$;
41
+
42
+ -- Retention never deletes delivery rows or an incomplete claim; its bounded batch skips rows held by active retries.
43
+ create function public.inbox_sweep_clients(p_cutoff timestamptz, p_limit integer)
44
+ returns table (deleted_count integer)
45
+ language sql
46
+ as $$
47
+ with expired as (
48
+ select source, event_id from public.inbox
49
+ where source like 'client:%' and request_fingerprint is not null and outcome is not null
50
+ and jsonb_typeof(outcome) = 'object' and outcome ? 'value' and claimed_at < p_cutoff
51
+ order by claimed_at, source, event_id limit p_limit for update skip locked
52
+ ), deleted as (
53
+ delete from public.inbox as claims using expired
54
+ where claims.source = expired.source and claims.event_id = expired.event_id
55
+ returning claims.event_id
56
+ )
57
+ select count(*)::integer from deleted
58
+ $$;
59
+
60
+ revoke all on function public.inbox_claim_client(text, text, varchar, timestamptz) from public;
61
+ grant execute on function public.inbox_claim_client(text, text, varchar, timestamptz) to app_be;
62
+ revoke all on function public.inbox_save_client_outcome(text, text, jsonb) from public;
63
+ grant execute on function public.inbox_save_client_outcome(text, text, jsonb) to app_be;
64
+ revoke all on function public.inbox_sweep_clients(timestamptz, integer) from public;
65
+ grant execute on function public.inbox_sweep_clients(timestamptz, integer) to app_be;
@@ -3,7 +3,9 @@ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
3
  import { EventBusErrorCode } from "./errors/event-bus.error"
4
4
  import { BaseEvent } from "./event-bus.contracts"
5
5
  import { EventBusService } from "./event-bus.service"
6
- import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
6
+
7
+ // The statement callDbFunction of platform/database sends for event_bus_write.
8
+ const INSERT_OUTBOX_EVENT = "SELECT * FROM event_bus_write($1, $2, $3, $4)"
7
9
 
8
10
  class ReceiptRecordedEvent extends BaseEvent {
9
11
  readonly eventId = "receipt-1"
@@ -4,7 +4,7 @@ import { isCorrelationId } from "@modules/platform/logging"
4
4
  import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
5
5
  import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
6
6
  import type { EventBus } from "./event-bus.port"
7
- import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
7
+ import { callDbFunction } from "@modules/platform/database"
8
8
 
9
9
  @Injectable()
10
10
  /** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
@@ -18,6 +18,6 @@ export class EventBusService implements EventBus {
18
18
  eventName: event.eventName,
19
19
  payload: event.payload,
20
20
  }
21
- await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope), correlationId])
21
+ await callDbFunction(tx, "event_bus_write", [event.eventId, event.eventName, JSON.stringify(envelope), correlationId])
22
22
  }
23
23
  }
@@ -7,7 +7,12 @@ import { CLOCK } from "@modules/platform/clock"
7
7
  import { InboxError, InboxErrorCode } from "./errors/inbox.error"
8
8
  import { requestFingerprint } from "./request-fingerprint.policy"
9
9
  import { PostgresInbox } from "./inbox.service"
10
- import { CLAIM_CLIENT, CLAIM_EVENT, SAVE_CLIENT_OUTCOME, SWEEP_CLIENTS } from "./persistence/inbox.sql"
10
+
11
+ // The statements callDbFunction of platform/database sends for each inbox function.
12
+ const CLAIM_EVENT = "SELECT * FROM inbox_claim_event($1, $2)"
13
+ const CLAIM_CLIENT = "SELECT * FROM inbox_claim_client($1, $2, $3, $4)"
14
+ const SAVE_CLIENT_OUTCOME = "SELECT * FROM inbox_save_client_outcome($1, $2, $3)"
15
+ const SWEEP_CLIENTS = "SELECT * FROM inbox_sweep_clients($1, $2)"
11
16
 
12
17
  const runClient = <T>(
13
18
  inbox: PostgresInbox,