@starci/hfs 4.9.11 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -9
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +1 -1
  5. package/db/dump.mjs +11 -10
  6. package/db/entity-list.mjs +1 -1
  7. package/db/migrate.mjs +83 -259
  8. package/db/new.mjs +46 -0
  9. package/db/post-cutover.mjs +13 -17
  10. package/db/postgres.mjs +80 -0
  11. package/emit/contracts.mjs +5 -9
  12. package/package.json +1 -1
  13. package/release/gate.mjs +3 -15
  14. package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
  15. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  16. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  17. package/runtime/knowledge/hfs/rules.yaml +179 -157
  18. package/runtime/knowledge/hfs/slots.yaml +185 -172
  19. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  20. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  21. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  22. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  23. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  24. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  25. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  26. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  28. package/runtime/modules/kernel/command-policy.yaml +1 -1
  29. package/runtime/modules/kernel/failure-codes.yaml +65 -115
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  31. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/entity-registry.mjs +53 -0
  33. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  34. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  35. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  36. package/runtime/scripts/hfs/architecture/owners.mjs +11 -0
  37. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  38. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  39. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  40. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  41. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  42. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  43. package/runtime/scripts/hfs/architecture/tiers.mjs +14 -3
  44. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  45. package/runtime/scripts/hfs/check.mjs +6 -6
  46. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  47. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  48. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  49. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  50. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  51. package/runtime/scripts/hfs/manifest-shape.mjs +9 -5
  52. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  53. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  54. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  55. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  56. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  57. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  58. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  59. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  60. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  61. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  62. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  63. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  64. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  65. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  66. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  67. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  68. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  69. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  70. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  71. package/runtime/scripts/hfs/slots.mjs +8 -2
  72. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  73. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  74. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  75. package/scaffold/add-app.mjs +2 -10
  76. package/scaffold/add-cli-lite.mjs +6 -6
  77. package/scaffold/add-compose.mjs +1 -1
  78. package/scaffold/add-inbox-lite.mjs +4 -8
  79. package/scaffold/add-plan.mjs +1 -1
  80. package/scaffold/add-queue-lite.mjs +16 -24
  81. package/scaffold/add-table.mjs +34 -26
  82. package/scaffold/add.mjs +2 -2
  83. package/scaffold/app.mjs +19 -86
  84. package/scaffold/lite-exports.mjs +1 -1
  85. package/src/main.mjs +19 -20
  86. package/stack/target-binding.mjs +0 -60
  87. package/stack/urgent-floor.mjs +2 -2
  88. package/sync/index.mjs +32 -8
  89. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  90. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  91. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  92. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  93. package/templates/app/gitignore-lite +0 -3
  94. package/templates/app/package-scripts/db.json +9 -0
  95. package/templates/app/package-scripts/package.json +0 -1
  96. package/templates/app/package-scripts-lite/package.json +1 -1
  97. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  98. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  99. package/templates/app/skeleton/README.md +1 -1
  100. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  101. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  102. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  103. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  104. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  105. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  106. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  107. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  108. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  109. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  110. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  111. package/templates/app/skeleton-lite/README.md +11 -11
  112. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  113. package/templates/app/tool-config-lite/prettierignore +0 -1
  114. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  115. package/templates/be/inbox/database-client.ts.tpl +13 -4
  116. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  117. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  118. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  119. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  120. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  121. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  122. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  123. package/templates/be/inbox/service.ts.tpl +1 -2
  124. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  125. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  126. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  127. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  128. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  129. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  130. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  131. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  132. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  133. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  134. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  135. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  136. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  137. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  138. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  139. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  140. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  141. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  142. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  143. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  145. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  146. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  147. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
  148. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  149. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  150. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  151. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  152. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  153. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  154. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  158. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  159. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  160. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  161. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  162. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  163. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  164. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  165. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  166. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  167. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  168. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  169. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  170. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  171. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +7 -0
  172. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  173. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  174. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  175. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  176. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  177. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  178. package/templates/be/table/entities.index.ts.tpl +4 -0
  179. package/templates/be/table/entity.ts.tpl +21 -0
  180. package/templates/be/table/index.ts.tpl +1 -0
  181. package/templates/be/table/module.ts.tpl +1 -1
  182. package/templates/be/table/service.api.ts.tpl +10 -11
  183. package/templates/be/table/service.ts.tpl +5 -6
  184. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  185. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  186. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  187. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  188. package/db/local-migrate.mjs +0 -194
  189. package/db/reset.mjs +0 -125
  190. package/emit/db-types.mjs +0 -271
  191. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  192. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  193. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  194. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  195. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  196. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  197. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  198. package/scaffold/supabase-ports.mjs +0 -26
  199. package/templates/app/package-scripts/supabase.json +0 -7
  200. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  201. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  202. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  203. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  204. package/templates/be/inbox/sql.ts.tpl +0 -16
  205. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  206. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  207. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  208. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  209. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  210. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  211. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  212. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  213. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  214. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  215. package/templates/be/table/rows.ts.tpl +0 -4
  216. package/templates/be/table/sql.api.ts.tpl +0 -7
  217. package/templates/be/table/sql.ts.tpl +0 -4
  218. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  219. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  220. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  221. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  222. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
package/src/main.mjs CHANGED
@@ -49,7 +49,7 @@
49
49
  // starci app ci secret set <NAME> [--env E | --dependabot] [--repo owner/name] reads the value from stdin only.
50
50
  // starci app ci var set <NAME> [--env E] [--value v] | var sync [--execute] | verify [--json]
51
51
  // declared GitHub CI custody; verify compares names, scopes and public values.
52
- // starci app db baseline | restamp --map <restamp.yaml> | plan
52
+ // starci app db new <name> | baseline | restamp --map <restamp.yaml> | plan | migrate | post-cutover | dump
53
53
  // preview the database cutover or compare its state with the repo; baseline and restamp mutate only with --apply.
54
54
  // starci app add <api|webhook|realtime|saga|job|reactor|queue|projection> <name> [--event <event> --from <service> --service <Class>=<module>] [--owner <service> --failed <event> --done <event>] [--connection <name>] [--cwd <dir>]
55
55
  // exactly that kind's file tree, generated FROM the files: tree of its pattern topic (knowledge/patterns/be) with the
@@ -79,7 +79,6 @@ import { main as syncMain } from '../sync/cli.mjs';
79
79
  import { SyncError, bundledPresets } from '../sync/index.mjs';
80
80
  import { managedFindings } from '../sync/managed.mjs';
81
81
  import { emitContracts } from '../emit/contracts.mjs';
82
- import { dbTypesEmitter } from '../emit/db-types.mjs';
83
82
  import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
84
83
  import { EditionRefusal } from '../scaffold/edition-gate.mjs';
85
84
  import { addKind } from '../scaffold/add.mjs';
@@ -95,26 +94,26 @@ import { releaseMain } from '../release/index.mjs';
95
94
  import { dbMigrateMain } from '../db/migrate.mjs';
96
95
  import { dbPostCutoverMain } from '../db/post-cutover.mjs';
97
96
  import { dbDumpMain } from '../db/dump.mjs';
98
- import { dbResetMain } from '../db/reset.mjs';
97
+ import { dbNewMain } from '../db/new.mjs';
99
98
  import { dbDriftMain } from '../db/drift.mjs';
100
99
  import { codemodMain } from '../codemod/button-intent.mjs';
101
100
 
102
101
  export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'release', 'explain', 'emit', 'new', 'secret', 'ci', 'db', 'hygiene', 'coverage', 'codemod']);
103
- const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain], ['dump', dbDumpMain], ['reset', dbResetMain], ['drift', dbDriftMain]]);
102
+ const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain], ['dump', dbDumpMain], ['new', dbNewMain], ['drift', dbDriftMain]]);
104
103
 
105
104
  const USAGE = `starci app scaffold <name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private] [--cwd <dir>]
106
105
  starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app|block|composite|branch|leaf|page|overlay|layout> <name> [options] [--cwd <dir>]
107
106
  starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--format text|json] [--sonar <file>]
108
107
  starci app codemod button-intent [--write] [--format json|markdown] [--report <file.json|file.md>] [--cwd <dir>]
109
108
  starci app sync (--check | --write) [--cwd <dir>]
110
- starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--db-types] [--wiring] [--coverage-from <lcov>]
111
- starci app db baseline [--stamp <version>] [--local] [--apply] [--cwd <dir>]
112
- starci app db restamp --map <restamp.yaml> [--local] [--apply] [--cwd <dir>]
113
- starci app db plan [--local] [--json] [--cwd <dir>]
114
- starci app db dump (--linked | --local | --url-env <NAME>) --out <dir> [--schema-only] [--json] [--cwd <dir>]
115
- starci app db reset --linked --auth keep|drop --storage keep|drop --backup <receipt.json> [--execute] [--json] [--cwd <dir>]
116
- starci app db migrate [--execute] [--local | --linked | --url-env <NAME>] [--json] [--cwd <dir>]
117
- starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
109
+ starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--wiring] [--coverage-from <lcov>]
110
+ starci app db new <kebab-name> [--json] [--cwd <dir>]
111
+ starci app db baseline [--stamp <version>] [--url-env <NAME>] [--apply] [--cwd <dir>]
112
+ starci app db restamp --map <restamp.yaml> [--url-env <NAME>] [--apply] [--cwd <dir>]
113
+ starci app db plan [--url-env <NAME>] [--json] [--cwd <dir>]
114
+ starci app db dump [--url-env <NAME>] --out <dir> [--schema-only] [--json] [--cwd <dir>]
115
+ starci app db migrate [--execute] [--url-env <NAME>] [--json] [--cwd <dir>]
116
+ starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--url-env <NAME>] [--json] [--cwd <dir>]
118
117
  starci app db drift [--url-env <NAME>] [--json] [--cwd <dir>]
119
118
  starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--backfill <tag>] [--json] [--cwd <dir>]
120
119
  starci app release push [--execute] [--remote <name>] [--urgent --reason <text>] [--json] [--cwd <dir>]
@@ -147,7 +146,7 @@ const PER_CODE_LIMIT = 25;
147
146
  const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility', '--shapes', '--read', '--app', '--kind', '--blocks', '--tabs']);
148
147
  /** Flags that may repeat: their values are collected in order. */
149
148
  const LIST_FLAGS = new Set(['--inject']);
150
- const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types', '--wiring', '--form']);
149
+ const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--wiring', '--form']);
151
150
 
152
151
  function coverageArgument(arg, args, opts) {
153
152
  const inline = arg.startsWith('--coverage-from=');
@@ -250,11 +249,11 @@ function printExplain(e, out) {
250
249
  * The app pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface, root and sides.
251
250
  * `only` limits the formatter to those (app-relative) files.
252
251
  */
253
- async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier, dbTypes = false }) {
252
+ async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier }) {
254
253
  const tracked = trackedFiles(repoRoot);
255
254
  // Managed files of the root and both sides, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier
256
255
  // through the app's own install (R19, never under --fast).
257
- const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...await checkDatabase({ repoRoot, files: tracked, base, ...(dbTypes ? { emitTypes: dbTypesEmitter() } : {}) }), ...(fast ? [] : await formatFindings({ repoRoot, files: only ?? tracked, prettier }))];
256
+ const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...await checkDatabase({ repoRoot, files: tracked, base }), ...(fast ? [] : await formatFindings({ repoRoot, files: only ?? tracked, prettier }))];
258
257
  // R23 against the be side itself (full pass only): committed snapshots equal what `starci app emit` writes now.
259
258
  let contracts = null;
260
259
  let be = null;
@@ -345,7 +344,7 @@ async function installedStackMain(cwd) {
345
344
  function databaseMain(argv, io) {
346
345
  const [verb, ...rest] = argv;
347
346
  const dispatch = DATABASE_VERBS.get(verb);
348
- if (!dispatch) throw new Error('starci app db takes baseline, restamp, plan, migrate, post-cutover, dump, reset or drift');
347
+ if (!dispatch) throw new Error('starci app db takes new, baseline, restamp, plan, migrate, post-cutover, dump or drift');
349
348
  return dispatch(rest, io);
350
349
  }
351
350
 
@@ -357,7 +356,7 @@ function printCheckProofs(result, out) {
357
356
  async function checkMain(opts, { cwd: repoRoot, harness, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
358
357
  if (opts.positional.length) throw new Error('starci app check takes no path');
359
358
  if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
360
- const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
359
+ const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier }), presets, prettier });
361
360
  const proof = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
362
361
  const changed = opts.fast === true ? changedSince(repoRoot, proof.fast?.base ?? opts.base).files : null;
363
362
  const existing = changed ? changed.filter(file => fs.existsSync(path.join(repoRoot, file))) : [];
@@ -398,7 +397,7 @@ function newMain(opts, { cwd: repoRoot, stdout: out }) {
398
397
  function addMain(opts, { cwd: repoRoot, stdout: out }) {
399
398
  const [noun, name, ...extra] = opts.positional;
400
399
  if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
401
- const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'], shapes: opts.shapes, read: opts.read, app: opts.app, kind: opts.kind, form: opts.form, blocks: opts.blocks, tabs: opts.tabs } });
400
+ const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, shapes: opts.shapes, read: opts.read, app: opts.app, kind: opts.kind, form: opts.form, blocks: opts.blocks, tabs: opts.tabs } });
402
401
  for (const file of created) out(`created ${file}\n`);
403
402
  const kinds = registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : '';
404
403
  // A noun that registers nothing in hfs.json (a table, an app, a front-end unit) ends with what it wrote.
@@ -437,7 +436,7 @@ function declaredEdition(cwd) {
437
436
  */
438
437
  async function stackAppMain(argv, { cwd, edition, stackMain, stdout: out, stderr: err, env, sops, stackIo }) {
439
438
  if (STACK_ACTIONS.includes(argv[0])) return deployStackMain(argv, { ...stackIo, cwd, stdout: out, stderr: err, env, ...(sops ? { sops } : {}) });
440
- if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local infrastructure with the Supabase CLI');
439
+ if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local database with npm run db:start');
441
440
  const run = stackMain ?? await installedStackMain(cwd);
442
441
  return run(argv, { cwd, out, err });
443
442
  }
@@ -460,7 +459,7 @@ function errorMessage(error) {
460
459
 
461
460
  /**
462
461
  * `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
463
- * scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the Supabase runner, `runLinter` the linter transport, `checkApp`
462
+ * scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the database runner, `runLinter` the linter transport, `checkApp`
464
463
  * the HFS check and `stackIo` Swarm tools; production defaults execute every check.
465
464
  */
466
465
  export async function main(argv, {
@@ -6,7 +6,6 @@ import { refusal } from './errors.mjs';
6
6
  const slug = value => typeof value === 'string' && /^[a-z][a-z0-9-]*$/.test(value);
7
7
  const label = value => typeof value === 'string' && /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(value);
8
8
  const host = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(value);
9
- const port = value => Number.isInteger(value) && value > 0 && value <= 65535;
10
9
  const denied = () => { throw refusal('STACKS_DEPLOY_CONFIG_INVALID', 'the native endpoint binding is invalid or differs from the selected owner target'); };
11
10
  const exact = (value, fields) => value && typeof value === 'object' && !Array.isArray(value)
12
11
  && Object.keys(value).length === fields.length && fields.every(key => Object.hasOwn(value, key));
@@ -28,44 +27,11 @@ function ordinaryInput(root, file) {
28
27
  /** A bound native owner opens only an ordinary envelope below its actual app, without ancestor links. */
29
28
  export const assertBoundCustody = (root, file) => ordinaryInput(root, file);
30
29
 
31
- /** Bound database commands require the app-local launcher; they cannot discover a global replacement. */
32
- export function boundSupabaseLauncher(root) {
33
- const launcher = path.join(root, 'node_modules', 'supabase', 'dist', 'supabase.js');
34
- ordinaryInput(root, launcher);
35
- return launcher;
36
- }
37
-
38
30
  /** Native custody can use AGE; tools that do not decrypt custody must not inherit that identity. */
39
31
  export function nativeToolEnvironment(env) {
40
32
  return Object.fromEntries(Object.entries(env).filter(([key]) => !/^SOPS_/i.test(key)));
41
33
  }
42
34
 
43
- /** Bound database tools receive only their selected private password and native routing, without ambient libpq or provider credentials. */
44
- export function nativeDatabaseEnvironment(env, binding, selected = {}, omit = []) {
45
- const omitted = new Set(omit.filter(value => typeof value === 'string').map(value => value.toUpperCase()));
46
- return { ...Object.fromEntries(Object.entries(nativeToolEnvironment(env)).filter(([key]) => !/^(?:PG[A-Z_]*|SUPABASE_)/i.test(key) && !omitted.has(key.toUpperCase()))),
47
- 'SUPABASE_ENV': binding.environment, ...selected };
48
- }
49
-
50
- /** Preserve a logical linked project while selecting a fixed native SQL endpoint without the CLI's linked Management finalizer. */
51
- export function boundLinkedDatabaseUrl(binding, savedPooler = null) {
52
- if (!binding || binding.mode !== 'linked' || !/^[a-z0-9]{20}$/.test(binding.projectRef)) denied();
53
- if (savedPooler === null) return `postgresql://postgres@db.${binding.projectRef}.supabase.co:5432/postgres?sslmode=require`;
54
- let url;
55
- try {
56
- if (typeof savedPooler !== 'string' || savedPooler.length > 8192) denied();
57
- url = new URL(savedPooler.trim());
58
- if (!['postgres:', 'postgresql:'].includes(url.protocol) || !/^[A-Za-z0-9.-]+\.pooler\.supabase\.com$/.test(url.hostname)
59
- || decodeURIComponent(url.username) !== `postgres.${binding.projectRef}` || url.pathname !== '/postgres'
60
- || url.port && !['5432', '6543'].includes(url.port) || url.hash
61
- || [...url.searchParams].some(([key, value]) => key !== 'sslmode' || !['require', 'verify-full'].includes(value))) denied();
62
- } catch { denied(); }
63
- url.password = '';
64
- url.port = '5432';
65
- if (!url.searchParams.has('sslmode')) url.searchParams.set('sslmode', 'require');
66
- return url.toString();
67
- }
68
-
69
35
  /** Parse the internal public binding; ordinary commands without this flag retain their native behavior. */
70
36
  export function parseTargetBinding(text, kind) {
71
37
  if (text === undefined) return null;
@@ -75,16 +41,6 @@ export function parseTargetBinding(text, kind) {
75
41
  if (kind === 'docker-swarm') {
76
42
  if (!exact(binding, ['kind', 'environment', 'stack', 'host', 'user', 'port']) || !slug(binding.stack)
77
43
  || !host(binding.host) || !label(binding.user) || binding.port !== 22) denied();
78
- } else if (kind === 'supabase-db') {
79
- if (binding.mode === 'local') {
80
- if (!exact(binding, ['kind', 'mode', 'environment', 'localProject', 'port']) || !label(binding.localProject) || !port(binding.port)) denied();
81
- } else if (binding.mode === 'linked') {
82
- if (!exact(binding, ['kind', 'mode', 'environment', 'projectRef']) || !/^[a-z0-9]{20}$/.test(binding.projectRef ?? '')) denied();
83
- } else if (binding.mode === 'url-env') {
84
- if (!exact(binding, ['kind', 'mode', 'environment', 'urlEnv', 'host', 'port', 'database', 'user'])
85
- || !/^[A-Z][A-Z0-9_]*$/.test(binding.urlEnv ?? '') || !host(binding.host) || !port(binding.port)
86
- || !label(binding.database) || !label(binding.user)) denied();
87
- } else denied();
88
44
  } else denied();
89
45
  return Object.freeze(binding);
90
46
  }
@@ -107,19 +63,3 @@ export function assertStackAccount(binding, account, knownHosts) {
107
63
  if (!binding) return;
108
64
  if (account !== `${binding.user}@${binding.host}` || binding.port !== 22 || !knownHosts.trim()) denied();
109
65
  }
110
-
111
- /** Compare the effective local Supabase identity after its existing config/environment resolver. */
112
- export function assertLocalDatabase(binding, target) {
113
- if (binding && (binding.mode !== 'local' || binding.localProject !== target.project || binding.port !== target.port)) denied();
114
- }
115
-
116
- /** Refuse a selected URL unless its non-secret endpoint exactly matches the accepted target. */
117
- export function assertUrlDatabase(binding, value) {
118
- if (!binding) return;
119
- let url;
120
- try { url = new URL(value); } catch { denied(); }
121
- if (binding.mode !== 'url-env' || !['postgres:', 'postgresql:'].includes(url.protocol)
122
- || url.hostname !== binding.host || Number(url.port || '5432') !== binding.port
123
- || decodeURIComponent(url.username) !== binding.user || decodeURIComponent(url.pathname.slice(1)) !== binding.database
124
- || url.search || url.hash || !url.password) denied();
125
- }
@@ -22,8 +22,8 @@ import { refusal } from './errors.mjs';
22
22
  const AGE_HEADER = 'age-encryption.org/v1\n';
23
23
  const SHA = /^[a-f0-9]{64}$/u;
24
24
 
25
- /** The database a floor step reaches: the connection named by --db-url-env, else the linked project of the `database:` block. */
26
- const targetOf = (options) => (options.dbUrlEnv ? { urlEnv: options.dbUrlEnv } : { linked: true });
25
+ /** The database a floor step reaches: the connection named by --db-url-env (default DATABASE_URL). */
26
+ const targetOf = (options) => (options.dbUrlEnv ? { urlEnv: options.dbUrlEnv } : {});
27
27
 
28
28
  /** The backups of an urgent deploy live outside every repository: --backup-out, else ~/.starci-backups/<project>. */
29
29
  const backupDir = (config, options) => options.backupOut ?? path.join(os.homedir(), '.starci-backups', config.project);
package/sync/index.mjs CHANGED
@@ -157,6 +157,8 @@ function trailingNewline(text) {
157
157
  }
158
158
 
159
159
  /** The digest-pinned image of a lite app's declared Redis, in every environment that runs it (ruling:R212/case-10). */
160
+ /** The pinned image of a lite app's db service (contexts/stack/pins.yaml images): PostgreSQL with pg_cron, pg_net, pgmq and pgvector. */
161
+ export const DB_IMAGE = 'supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2';
160
162
  export const REDIS_IMAGE = 'redis:7@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f';
161
163
 
162
164
  const redisDevText = project => render(readBundled('app/package-scripts/redis-dev.json'), { project, redisImage: REDIS_IMAGE });
@@ -286,7 +288,7 @@ export function appScripts(app, source) {
286
288
  const watch = entry => `cd be && ts-node-dev --respawn -r tsconfig-paths/register apps/${entry.name}/src/main.ts`;
287
289
  const built = entry => `node be/dist/apps/${entry.name}/src/main.js`;
288
290
  const workspace = entry => feAppPackageName(app.project, entry.name);
289
- // Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the Supabase types check.
291
+ // Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the database types check.
290
292
  const dev = entry => `npm run codegen --silent && turbo run dev --filter=${workspace(entry)}`;
291
293
  const images = imageEntries(app, source);
292
294
  const imageCommand = ({ side, entry }) => `docker build -f ${dockerfilePath(side, entry.name)} -t ${app.project}/${entry.name}:dev .`;
@@ -347,7 +349,7 @@ export function imageFiles(app, source) {
347
349
  const side = app.sides.fe;
348
350
  const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
349
351
  const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
350
- // The folders outside be/ that the back end declares it reads (the generated Supabase types of a lite back end, which an upgraded app keeps reading) are in every one of its images.
352
+ // The folders outside be/ that the back end declares it reads (the generated database types of a lite back end, db/types/) are in every one of its images.
351
353
  const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
352
354
  const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
353
355
  const entries = imageEntries(app, source).map(({ side: which, entry: item }) => ({ side: which, item }));
@@ -373,8 +375,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
373
375
  project: app.project,
374
376
  appBinary: APP_BINARY,
375
377
  appScripts: appScripts(app, source),
376
- // A full upgrade keeps the app's declared Supabase connection and its local database commands.
377
- supabaseScripts: providers.includes('supabase') ? render(readBundled('app/package-scripts/supabase.json'), { appBinary: APP_BINARY }) : '',
378
+ // A lite app's own database runs in development as one container of the pinned db image (ruling:lite-self-hosted-postgres-passport-minio).
379
+ dbImage: DB_IMAGE,
378
380
  // A lite app's declared Redis (sides.be.backing, ruling:R212/case-10) runs in development as one container of its pinned image.
379
381
  backingScripts: lite && (app.sides.be.backing ?? []).includes('redis') ? redisDevText(app.project) : '',
380
382
  // turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
@@ -555,6 +557,24 @@ export async function renderRepo(root, { presets, parseYaml, notices, manifest =
555
557
  return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root, { parseYaml, notices }) }) };
556
558
  }
557
559
 
560
+ /** The folders a lite app's sync retires (ruling lite-self-hosted-postgres-passport-minio): the generated Supabase row types nothing reads any more. */
561
+ const LITE_RETIRED_DIRS = Object.freeze(['supabase/types']);
562
+
563
+ /** The app-relative files of the retired folders that still exist under `root` of a lite app; sync --write deletes them, --check reports them. */
564
+ export function retiredFiles(root, hfs) {
565
+ if (hfs?.edition !== 'lite') return [];
566
+ const found = [];
567
+ const walk = (rel) => {
568
+ const full = path.join(root, rel);
569
+ const stat = fs.lstatSync(full, { throwIfNoEntry: false });
570
+ if (!stat) return;
571
+ if (stat.isDirectory()) for (const name of fs.readdirSync(full).sort(byCodeUnit)) walk(`${rel}/${name}`);
572
+ else found.push(rel);
573
+ };
574
+ for (const dir of LITE_RETIRED_DIRS) walk(dir);
575
+ return found;
576
+ }
577
+
558
578
  /** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
559
579
  function syncOptions(argv) {
560
580
  const check = argv.includes('--check'), write = argv.includes('--write');
@@ -567,16 +587,17 @@ function syncOptions(argv) {
567
587
  return { write, cwdAt };
568
588
  }
569
589
 
570
- function reportSyncCheck(root, targets, out) {
590
+ function reportSyncCheck(root, targets, out, retired = []) {
571
591
  const results = checkTargets(root, targets);
572
592
  const bad = results.filter(item => item.status !== 'ok');
593
+ for (const file of retired) out(`HFS_SYNC_DRIFT ${file}: retired, the lite edition generates no database types; run "npx starci app sync --write" to delete it`);
573
594
  for (const result of bad) {
574
595
  const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
575
596
  const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
576
597
  out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
577
598
  }
578
599
  out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
579
- return bad.length ? 1 : 0;
600
+ return bad.length || retired.length ? 1 : 0;
580
601
  }
581
602
 
582
603
  export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
@@ -589,15 +610,18 @@ export async function runSync(argv, { cwd = process.cwd(), out = line => process
589
610
  const root = cwdAt >= 0 ? path.resolve(cwd, argv[cwdAt + 1]) : cwd;
590
611
  try {
591
612
  const notices = [];
592
- const { targets } = await renderRepo(root, { presets, parseYaml, notices });
613
+ const { hfs, targets } = await renderRepo(root, { presets, parseYaml, notices });
614
+ const retired = retiredFiles(root, hfs);
593
615
  if (targets.some(target => target.path === 'README.md')) for (const notice of new Set(notices)) out(notice);
594
616
  if (write) {
595
617
  const written = writeTargets(root, targets);
596
618
  for (const file of written) out(`wrote ${file}`);
619
+ for (const file of retired) { fs.rmSync(path.join(root, file)); out(`removed ${file} (retired)`); }
620
+ for (const dir of LITE_RETIRED_DIRS) if (retired.length && fs.existsSync(path.join(root, dir)) && !fs.readdirSync(path.join(root, dir)).length) fs.rmdirSync(path.join(root, dir));
597
621
  out(`starci app sync: ${written.length} written, ${targets.length - written.length} already in sync`);
598
622
  return 0;
599
623
  }
600
- return reportSyncCheck(root, targets, out);
624
+ return reportSyncCheck(root, targets, out, retired);
601
625
  } catch (error) {
602
626
  if (!(error instanceof SyncError)) throw error;
603
627
  out(error.message);
@@ -17,7 +17,7 @@ concurrency:
17
17
  cancel-in-progress: false
18
18
 
19
19
  # The job shape of the StarCi CI standard (knowledge/hfs/ci-standard.yaml): fast, lint per side, the unit run in shards, one coverage merge, build,
20
- # sonar, and the database types of the lite edition. Timeouts and the shard count come from ci.jobs of .starcistacks/application-stacks.yaml (defaults in the standard).
20
+ # and sonar. Timeouts and the shard count come from ci.jobs of .starcistacks/application-stacks.yaml (defaults in the standard).
21
21
  jobs:
22
22
  fast:
23
23
  timeout-minutes: {{timeoutFast}}
@@ -110,19 +110,3 @@ jobs:
110
110
  NODE_OPTIONS: {{nodeOptions}}
111
111
 
112
112
  {{sonarJob}}
113
- db-types:
114
- timeout-minutes: {{timeoutDbTypes}}
115
- runs-on: ubuntu-latest
116
- steps:
117
- {{> app/ci-workflows/install-steps.yml}}
118
- - name: start Supabase
119
- run: npm run db:start
120
- - name: apply migrations
121
- run: npm run db:reset
122
- - name: regenerate database types
123
- run: npm run db:types
124
- - name: database types are committed
125
- run: git diff --exit-code -- supabase/types/database.types.ts
126
- - name: stop Supabase
127
- if: always()
128
- run: npm run db:stop
@@ -5,7 +5,7 @@ on:
5
5
  workflow_dispatch:
6
6
  inputs:
7
7
  environment:
8
- description: Protected environment with sealed Supabase custody under .starcistacks
8
+ description: Protected environment whose sealed custody under .starcistacks holds DATABASE_URL
9
9
  required: true
10
10
  type: environment
11
11
  backup_receipt:
@@ -45,7 +45,7 @@ jobs:
45
45
  uses: mdgreenwald/mozilla-sops-action@bb2d96a13ef438ad122e5827a57ebd75d6ff118d # v2.1.0
46
46
  with:
47
47
  version: v3.13.2
48
- - name: materialize sealed custody and push migrations
48
+ - name: materialize sealed custody and migrate the database
49
49
  env:
50
50
  XDG_DATA_HOME: ${{ runner.temp }}/starci-data
51
51
  shell: bash
@@ -57,9 +57,7 @@ jobs:
57
57
  secret_dir="$XDG_DATA_HOME/StarCi/secrets/{{project}}/$STACK_ENVIRONMENT"
58
58
  trap 'rm -rf -- "$secret_dir"' EXIT
59
59
  npx --no-install starci app secret materialize "$STACK_ENVIRONMENT"
60
- export SUPABASE_ACCESS_TOKEN="$(cat "$secret_dir/SUPABASE_ACCESS_TOKEN")"
61
- export SUPABASE_DB_PASSWORD="$(cat "$secret_dir/SUPABASE_DB_PASSWORD")"
62
- project_id="$(cat "$secret_dir/SUPABASE_PROJECT_ID")"
63
- npx --no-install supabase link --project-ref "$project_id"
64
- npm run db:push -- --dry-run
65
- npm run db:push
60
+ DATABASE_URL="$(cat "$secret_dir/DATABASE_URL")"
61
+ export DATABASE_URL
62
+ npx --no-install starci app db migrate --url-env DATABASE_URL
63
+ STARCI_DB_ARMED=1 npx --no-install starci app db migrate --url-env DATABASE_URL --execute
@@ -1,42 +1,5 @@
1
1
  // Generated by {{appBinary}} sync (app root). Do not edit: run "npx {{appBinary}} sync --write".
2
- // Database types hook, offline: the app compiles against the committed supabase/types/database.types.ts, which `npm run contract:emit`
3
- // regenerates from the local stack and stamps with the digest of the migrations it was generated from. This refuses a missing or
4
- // stale file, so lint, typecheck, dev and build never run against types older than the migrations; it starts nothing and needs no stack.
5
- // `--graphql` runs only the GraphQL wire types below (`npm run codegen:graphql`).
6
- import { createHash } from "node:crypto"
7
- import fs from "node:fs"
8
- import path from "node:path"
9
-
10
- const byCodeUnit = (left, right) => {
11
- if (left === right) return 0
12
- return left < right ? -1 : 1
13
- }
14
-
15
- const MIGRATIONS = path.join("supabase", "migrations")
16
- const TYPES = path.join("supabase", "types", "database.types.ts")
17
- const STAMP = /^\/\/ migrations-sha256: ([0-9a-f]{64})$/
18
-
19
- const fail = (message) => {
20
- console.error(`codegen: ${message}`)
21
- process.exit(1)
22
- }
23
-
24
- if (!process.argv.includes("--graphql")) {
25
- const hash = createHash("sha256")
26
- for (const name of (fs.existsSync(MIGRATIONS) ? fs.readdirSync(MIGRATIONS) : [])
27
- .filter((file) => file.endsWith(".sql"))
28
- .sort(byCodeUnit)) {
29
- hash.update(`${name}\0${fs.readFileSync(path.join(MIGRATIONS, name), "utf8").replace(/\r\n/g, "\n")}\0`)
30
- }
31
- const digest = hash.digest("hex")
32
-
33
- if (!fs.existsSync(TYPES))
34
- fail(`${TYPES} is missing; start the local stack and run \`npm run contract:emit\`, then commit it`)
35
- const stamped = STAMP.exec(fs.readFileSync(TYPES, "utf8").split(/\r?\n/, 1)[0])?.[1]
36
- if (stamped !== digest)
37
- fail(
38
- `${TYPES} is ${stamped === undefined ? "not stamped" : "older than the migrations"}; run \`npm run contract:emit\` and commit the regenerated types`,
39
- )
40
- }
2
+ // The GraphQL wire types of the front end (`npm run codegen`, also `npm run codegen:graphql`); a lite app generates no database types.
3
+ // {{> app/codegen-prelude.mjs.partial}}
41
4
 
42
5
  // {{> app/codegen-graphql.mjs.partial}}
@@ -0,0 +1,139 @@
1
+ -- Managed by starci app sync (slot app.db.bootstrap); do not edit: run "npx starci app sync --write".
2
+ -- The pre-migration bootstrap of a lite app's self-hosted PostgreSQL (ruling lite-self-hosted-postgres-passport-minio). Migrations written
3
+ -- for Supabase reference objects GoTrue and Supabase Storage created: the browser roles, the `auth` schema (auth.users, auth.identities,
4
+ -- auth.sessions, auth.uid/jwt/role) and the `storage` schema (storage.buckets, storage.objects). The supabase/postgres image ships only a partial
5
+ -- copy of them, and a lite app runs neither GoTrue nor storage-api: auth is the app's own (integrations/passport) and storage is the S3 endpoint
6
+ -- of integrations/object-storage. This file gives the migrations those objects, EMPTY, and nothing else. It is idempotent; `starci app db migrate`
7
+ -- applies it as the database owner before the first migration of an empty database.
8
+ --
9
+ -- auth.users keeps the GoTrue column set so restored rows and foreign keys from public tables stay valid; encrypted_password holds bcrypt hashes.
10
+ -- auth.uid(), auth.role() and auth.jwt() read the request.jwt.claims setting; the API connects as app_be and sets none, so they return NULL and
11
+ -- every policy that names them denies the browser roles, which have no door in lite.
12
+
13
+ create schema if not exists extensions;
14
+ create schema if not exists auth;
15
+ create schema if not exists storage;
16
+
17
+ do $roles$
18
+ begin
19
+ if not exists (select 1 from pg_roles where rolname = 'anon') then create role anon nologin noinherit; end if;
20
+ if not exists (select 1 from pg_roles where rolname = 'authenticated') then create role authenticated nologin noinherit; end if;
21
+ if not exists (select 1 from pg_roles where rolname = 'service_role') then create role service_role nologin noinherit bypassrls; end if;
22
+ end
23
+ $roles$;
24
+
25
+ grant usage on schema auth, storage to anon, authenticated, service_role;
26
+ grant usage on schema extensions to anon, authenticated, service_role;
27
+
28
+ -- auth.users: the full GoTrue column set (the image's old copy is extended in place).
29
+ create table if not exists auth.users (id uuid primary key);
30
+ alter table auth.users
31
+ add column if not exists instance_id uuid,
32
+ add column if not exists aud varchar(255),
33
+ add column if not exists role varchar(255),
34
+ add column if not exists email varchar(255),
35
+ add column if not exists encrypted_password varchar(255),
36
+ add column if not exists email_confirmed_at timestamptz,
37
+ add column if not exists invited_at timestamptz,
38
+ add column if not exists confirmation_token varchar(255),
39
+ add column if not exists confirmation_sent_at timestamptz,
40
+ add column if not exists recovery_token varchar(255),
41
+ add column if not exists recovery_sent_at timestamptz,
42
+ add column if not exists email_change_token_new varchar(255),
43
+ add column if not exists email_change varchar(255),
44
+ add column if not exists email_change_sent_at timestamptz,
45
+ add column if not exists last_sign_in_at timestamptz,
46
+ add column if not exists raw_app_meta_data jsonb,
47
+ add column if not exists raw_user_meta_data jsonb,
48
+ add column if not exists is_super_admin boolean,
49
+ add column if not exists created_at timestamptz,
50
+ add column if not exists updated_at timestamptz,
51
+ add column if not exists phone text,
52
+ add column if not exists phone_confirmed_at timestamptz,
53
+ add column if not exists confirmed_at timestamptz,
54
+ add column if not exists banned_until timestamptz,
55
+ add column if not exists deleted_at timestamptz,
56
+ add column if not exists is_anonymous boolean not null default false;
57
+ create unique index if not exists users_email_partial_key on auth.users (email) where is_anonymous = false;
58
+
59
+ create table if not exists auth.identities (
60
+ provider_id text not null,
61
+ user_id uuid not null references auth.users (id) on delete cascade,
62
+ identity_data jsonb not null,
63
+ provider text not null,
64
+ last_sign_in_at timestamptz,
65
+ created_at timestamptz,
66
+ updated_at timestamptz,
67
+ email text generated always as (lower(identity_data ->> 'email')) stored,
68
+ id uuid not null default gen_random_uuid() primary key,
69
+ unique (provider_id, provider)
70
+ );
71
+
72
+ create table if not exists auth.sessions (
73
+ id uuid primary key,
74
+ user_id uuid not null references auth.users (id) on delete cascade,
75
+ created_at timestamptz,
76
+ updated_at timestamptz,
77
+ factor_id uuid,
78
+ aal text,
79
+ not_after timestamptz,
80
+ refreshed_at timestamp,
81
+ user_agent text,
82
+ ip inet,
83
+ tag text
84
+ );
85
+ create index if not exists sessions_user_id_idx on auth.sessions (user_id);
86
+
87
+ create or replace function auth.uid() returns uuid language sql stable as
88
+ $$ select nullif(coalesce(current_setting('request.jwt.claim.sub', true), (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')), '')::uuid $$;
89
+
90
+ create or replace function auth.role() returns text language sql stable as
91
+ $$ select nullif(coalesce(current_setting('request.jwt.claim.role', true), (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')), '')::text $$;
92
+
93
+ create or replace function auth.jwt() returns jsonb language sql stable as
94
+ $$ select coalesce(nullif(current_setting('request.jwt.claim', true), ''), nullif(current_setting('request.jwt.claims', true), ''))::jsonb $$;
95
+
96
+ grant execute on function auth.uid(), auth.role(), auth.jwt() to public;
97
+
98
+ -- storage: the two tables the migrations seed and police; the objects live in MinIO, these stay empty bookkeeping.
99
+ create table if not exists storage.buckets (
100
+ id text primary key,
101
+ name text not null,
102
+ owner uuid,
103
+ created_at timestamptz default now(),
104
+ updated_at timestamptz default now(),
105
+ public boolean default false,
106
+ avif_autodetection boolean default false,
107
+ file_size_limit bigint,
108
+ allowed_mime_types text[],
109
+ owner_id text
110
+ );
111
+ create unique index if not exists bname on storage.buckets (name);
112
+
113
+ create table if not exists storage.objects (
114
+ id uuid not null default gen_random_uuid() primary key,
115
+ bucket_id text references storage.buckets (id),
116
+ name text,
117
+ owner uuid,
118
+ created_at timestamptz default now(),
119
+ updated_at timestamptz default now(),
120
+ last_accessed_at timestamptz default now(),
121
+ metadata jsonb,
122
+ path_tokens text[] generated always as (string_to_array(name, '/')) stored,
123
+ version text,
124
+ owner_id text,
125
+ user_metadata jsonb
126
+ );
127
+ create unique index if not exists bucketid_objname on storage.objects (bucket_id, name);
128
+ alter table storage.objects enable row level security;
129
+ alter table storage.buckets enable row level security;
130
+
131
+ grant select, insert, update, delete on storage.objects, storage.buckets to authenticated, service_role;
132
+ grant select on storage.objects, storage.buckets to anon;
133
+
134
+ -- Replication publication the baseline adds tables to (the image ships it; a bare PostgreSQL does not).
135
+ do $pub$
136
+ begin
137
+ if not exists (select 1 from pg_publication where pubname = 'supabase_realtime') then create publication supabase_realtime; end if;
138
+ end
139
+ $pub$;
@@ -1,9 +1,6 @@
1
1
  {{> common/gitignore.base}}
2
2
  # Jest transforms reused by the CI test job
3
3
  .jest-cache/
4
- # Supabase CLI state is local Docker metadata, never application source.
5
- supabase/.temp/
6
- supabase/.branches/
7
4
  # be: the schema Nest writes when it boots GraphQL
8
5
  schema.gql
9
6
  # fe: Next build output and its generated type file
@@ -0,0 +1,9 @@
1
+ "db:create": "docker create --name {{project}}-dev-db -p 127.0.0.1:54322:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -v {{project}}-dev-db-data:/var/lib/postgresql/data {{dbImage}}",
2
+ "db:start": "docker start {{project}}-dev-db",
3
+ "db:stop": "docker stop {{project}}-dev-db",
4
+ "db:reset": "docker rm -f {{project}}-dev-db && docker volume rm {{project}}-dev-db-data && npm run db:create && npm run db:start",
5
+ "db:new": "{{appBinary}} db new",
6
+ "db:push": "{{appBinary}} db migrate --url-env DATABASE_URL --execute",
7
+ "db:plan": "{{appBinary}} db plan --url-env DATABASE_URL",
8
+ "db:dump": "{{appBinary}} db dump --url-env DATABASE_URL --out ../{{project}}-db-backups",
9
+ "db:lint": "{{appBinary}} check --fast",
@@ -9,7 +9,6 @@
9
9
  "test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
10
10
  "test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
11
11
  "test:stack": "starci app stack",
12
- {{supabaseScripts}}
13
12
  "contract:emit": "starci app emit"
14
13
  }
15
14
  }
@@ -6,7 +6,7 @@
6
6
  "typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
7
7
  "test": "cd be && jest --selectProjects unit --coverage",
8
8
  "test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
9
- {{> app/package-scripts/supabase.json}}
9
+ {{> app/package-scripts/db.json}}
10
10
  {{backingScripts}}
11
11
  "contract:emit": "{{appBinary}} emit"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  # Development stack
2
2
 
3
- Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts, and local Supabase, when declared, runs through the Supabase CLI. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
3
+ Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
4
4
  Starter release images and external secrets must be prepared before starting the container model. Local source development remains available through the app scripts.
5
5
 
6
6
  | Command | Operation |