@starci/hfs 4.9.11 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +7 -9
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +1 -1
  5. package/db/dump.mjs +11 -10
  6. package/db/entity-list.mjs +1 -1
  7. package/db/migrate.mjs +83 -259
  8. package/db/new.mjs +46 -0
  9. package/db/post-cutover.mjs +13 -17
  10. package/db/postgres.mjs +80 -0
  11. package/emit/contracts.mjs +5 -9
  12. package/package.json +1 -1
  13. package/release/gate.mjs +3 -15
  14. package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
  15. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  16. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  17. package/runtime/knowledge/hfs/rules.yaml +177 -155
  18. package/runtime/knowledge/hfs/slots.yaml +164 -172
  19. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  20. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  21. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  22. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  23. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  24. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  25. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  26. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  28. package/runtime/modules/kernel/command-policy.yaml +1 -1
  29. package/runtime/modules/kernel/failure-codes.yaml +65 -115
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  31. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  33. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  34. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  35. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  36. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  37. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  38. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  39. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  40. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  41. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  42. package/runtime/scripts/hfs/check.mjs +6 -6
  43. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  44. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  45. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  46. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  47. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  48. package/runtime/scripts/hfs/manifest-shape.mjs +4 -4
  49. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  50. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  51. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  52. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  53. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  54. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  55. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  56. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  57. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  58. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  59. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  60. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  61. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  62. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  63. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  64. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  65. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  66. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  67. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  68. package/runtime/scripts/hfs/slots.mjs +8 -2
  69. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  70. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  71. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  72. package/scaffold/add-app.mjs +2 -10
  73. package/scaffold/add-cli-lite.mjs +6 -6
  74. package/scaffold/add-compose.mjs +1 -1
  75. package/scaffold/add-inbox-lite.mjs +4 -8
  76. package/scaffold/add-plan.mjs +1 -1
  77. package/scaffold/add-queue-lite.mjs +16 -24
  78. package/scaffold/add-table.mjs +34 -26
  79. package/scaffold/add.mjs +2 -2
  80. package/scaffold/app.mjs +19 -86
  81. package/scaffold/lite-exports.mjs +1 -1
  82. package/src/main.mjs +19 -20
  83. package/stack/target-binding.mjs +0 -60
  84. package/stack/urgent-floor.mjs +2 -2
  85. package/sync/index.mjs +32 -8
  86. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  87. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  88. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  89. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  90. package/templates/app/gitignore-lite +0 -3
  91. package/templates/app/package-scripts/db.json +9 -0
  92. package/templates/app/package-scripts/package.json +0 -1
  93. package/templates/app/package-scripts-lite/package.json +1 -1
  94. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  95. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  96. package/templates/app/skeleton/README.md +1 -1
  97. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  98. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  99. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  100. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  101. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  102. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  103. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  104. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  105. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  106. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  107. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  108. package/templates/app/skeleton-lite/README.md +11 -11
  109. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  110. package/templates/app/tool-config-lite/prettierignore +0 -1
  111. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  112. package/templates/be/inbox/database-client.ts.tpl +13 -4
  113. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  114. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  115. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  116. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  117. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  118. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  119. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  120. package/templates/be/inbox/service.ts.tpl +1 -2
  121. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  122. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  123. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  124. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  125. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  126. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  127. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  128. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  129. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  130. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  131. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  132. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  133. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  134. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  135. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  136. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  137. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  138. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  139. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  140. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  141. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  142. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  143. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
  145. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  146. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  147. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  148. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  149. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  150. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  151. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  152. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  153. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  154. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  158. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  159. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  160. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  161. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  162. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  163. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  164. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  165. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  166. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  167. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  168. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
  169. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  170. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  171. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  172. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  173. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  174. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  175. package/templates/be/table/entities.index.ts.tpl +4 -0
  176. package/templates/be/table/entity.ts.tpl +21 -0
  177. package/templates/be/table/index.ts.tpl +1 -0
  178. package/templates/be/table/module.ts.tpl +1 -1
  179. package/templates/be/table/service.api.ts.tpl +10 -11
  180. package/templates/be/table/service.ts.tpl +5 -6
  181. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  182. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  183. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  184. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  185. package/db/local-migrate.mjs +0 -194
  186. package/db/reset.mjs +0 -125
  187. package/emit/db-types.mjs +0 -271
  188. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  189. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  190. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  191. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  192. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  193. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  194. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  195. package/scaffold/supabase-ports.mjs +0 -26
  196. package/templates/app/package-scripts/supabase.json +0 -7
  197. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  198. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  199. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  200. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  201. package/templates/be/inbox/sql.ts.tpl +0 -16
  202. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  203. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  204. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  205. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  206. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  207. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  208. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  209. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  210. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  211. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  212. package/templates/be/table/rows.ts.tpl +0 -4
  213. package/templates/be/table/sql.api.ts.tpl +0 -7
  214. package/templates/be/table/sql.ts.tpl +0 -4
  215. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  216. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  217. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  218. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  219. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.10.0 - 2026-10-29
4
+
5
+ Breaking for lite apps (ruling lite-self-hosted-postgres-passport-minio; the migration note is contexts/project/lite.md "Moving an app off Supabase").
6
+
7
+ - Changed: the lite edition leaves Supabase. `starci app db` runs against a self-hosted PostgreSQL named by `--url-env <NAME>` (default `DATABASE_URL`) through psql and pg_dump (`db/postgres.mjs`): `migrate` applies each `db/migrations/<ts14>_<kebab>.sql` with its history row in one transaction (the `supabase_migrations.schema_migrations` layout, so a restored history carries over), runs `db/bootstrap.sql` first on an empty database and needs no `STARCI_DB_ARMED` on the loopback; `dump` writes roles, schema and data with pg_dumpall and pg_dump; `plan`, `baseline`, `restamp` and `post-cutover` query and repair the history through psql. New `db new <kebab-name>`. Removed: `db reset`, `--linked`, `--local`, `--target-binding` of the db verbs, the bundled Supabase CLI wrappers and the generated database types (`emit/db-types.mjs`, `app check --db-types`, `add --no-types`).
8
+ - Changed: the lite templates. Managed: the db package scripts (`db:create|start|stop|reset` of the pinned `supabase/postgres` image, `db:new`, `db:push`, `db:plan`, `db:dump`), `scripts/codegen.mjs` (GraphQL only), `ci.yml` (no database-types job), `db-deploy.yml` (`starci app db migrate --url-env DATABASE_URL`), `.gitignore`, `.prettierignore` and `db/bootstrap.sql`; `starci app sync --write` deletes a leftover `supabase/types/` of a lite app. Scaffolded: `db/` instead of `supabase/`, a `db` service in both Swarm stacks with its volume, backup and sealed password, `backing.db`, the passport (Google OAuth with state and PKCE, email and password with bcryptjs, own ES256 tokens and JWKS, the refresh cookie) and storage (S3 client, presigned PUT and GET) integrations, the identity guard on the own-token verifier, `platform/database` entities option, `PRIMARY_ENTITIES` and the `callDbFunction`, `advisoryXactLock` and `notify` helpers, and the front end's `modules/auth` over the app's own auth doors (`auth-door.ts`, `token-store.ts`).
9
+ - Changed: the checks. `LITE_DB_MIGRATION` (R332) refuses a `supabase/` tree or SQL outside `db/`; the database rules read `db/migrations` and hfs.json `db` (`forceRls`, `forceRlsExemptions`, `appRole`, `urlEnv`, `image`) and drop the config.toml, browser-access and realtime-publication checks; R276 admits the lite `db` component of image `supabase/postgres`; `backing` entries are `{provider: postgres, component}`; the architecture machine's identity check (`identity.mjs`) holds R329 and R260; SQL table ownership reads the tables the migrations create; hfs.json `sides.be.dbFunctions` joins `ruleParams.be.dbFunctions`.
10
+ - Changed: `starci app add table` in lite writes a TypeORM entity (`persistence/entities/<table>.entity.ts`, `@Entity({ schema: "public", name })`) and the capability's `persistence/entities/index.ts` (`<c>Entities`), exports it from the capability entry and names it in `PRIMARY_ENTITIES` of `platform/database/entities.ts`; the service reads through `requireOwnedEntity` of `platform/database` (the entity's query builder, named parameters) and inserts with `save`. No `<name>.rows.ts` or `<name>.sql.ts` is written (ruling lite-persistence-typeorm-entities). The machine `schema-owner` (R35) checks that lite shape: the entity list in `persistence/entities/index.ts` and its name in `PRIMARY_ENTITIES`.
11
+ - Changed: the lite queue, jobs and webhook generators write no SQL text outside platform/database (R334): their statements are functions of the generated db migrations (job_claim, job_advance, job_complete, job_fail, queue_write, queue_waiting, queue_mark_sent, inbox_claim_event, inbox_claim_client, inbox_save_client_outcome, inbox_sweep_clients, event_bus_write) called through `callDbFunction`, listed with their reasons in the canon `ruleParams.be.dbFunctions`; no `persistence/*.sql.ts` or `*.rows.ts` is written, and the webhook receipt insert is `webhookReceiptInsert(table)` of `platform/database`. The storage integration is `be.integrations.object-storage` (`src/modules/integrations/object-storage/`, `parseObjectStorageConfig`, `ObjectStorageOptions`, options member `objectStorage`). `db/entity-list.mjs` reads the lite entity list at `be/src/modules/platform/database/entities.ts`, where the skeleton keeps it.
12
+ - Changed: the bundled lite pattern knowledge (event-bus, idempotency, jobs, projections, queues, reactors, realtime, saga, webhooks) names db migrations and the local db service instead of Supabase; the command policy points `supabase` at `starci app db` and the db scripts.
13
+ - Changed: the bundled slot manifest 3.9.0 replaces the lite `app.supabase*` slots with `app.db` (`db/migrations`, `db/migrations/archive`, `db/seed.sql`, the managed `db/bootstrap.sql`), `be.integrations.supabase` with `be.integrations.passport` and `be.integrations.object-storage`, removes `fe.route.callback`, sets lite `schemaAuthority: sql`, gives lite TypeORM entities in `be.persistence`, adds `ruleParams.be.dbFunctions`, and moves the S3 SDKs to `integrations/object-storage` and the identity libraries to `integrations/passport` in `infraOwners` (`@supabase/*`, `minio` and `bcrypt` nowhere); the bundled rule catalog retires R216, R217, R219, R254 and R287 and adds R328-R334; the bundled canon pins name hfs 4.10.0, eslint-canon-be 3.7.0, eslint-canon-fe 9.5.0 and the Grammar family 0.15.11, pin `@nestjs/passport` 11.0.5 (CommonJS on the Nest 11 line), `passport` 0.7.0, `passport-google-oauth20` 2.0.0, `passport-local` 1.0.0, `bcryptjs` 3.0.3 and their types (tokens are signed and verified with `jose`; `@nestjs/jwt` stays allowed in `integrations/passport` but unpinned), and drop `@supabase/ssr`, `@supabase/supabase-js` and the `supabase` CLI.
14
+
15
+
3
16
  ## 4.9.11 - 2026-10-29
4
17
 
5
18
  - Added: `starci app db drift [--url-env <NAME>] [--json]` (`db/drift.mjs`, owner ruling 2026-10-29: lite tables are mapped by TypeORM entities while `db/migrations/*.sql` stays the only DDL and `synchronize` is false). It builds TypeORM's metadata from `be/src/platform/database/entities.ts` with the app's own typescript, typeorm and pg (a `DataSource` that never connects), applies `db/bootstrap.sql` and every active migration to a scratch container of the app's `components.db` image (or the empty database `--url-env` names), and reports every missing table or column, type mismatch and nullability mismatch as `BE_ENTITY_SCHEMA_DRIFT` (R333 in the bundled rule catalog, with its failure-code entry). No Docker, no app packages or an unreadable target is no verdict (`DB_DRIFT_NEEDS_DATABASE`, exit 2), never a green; a migration that does not apply to an empty database is `DB_DRIFT_MIGRATION_FAILED`. An app with an entity list gets a `db-drift` row in its release gate (default or declared gates) and `npx starci app db drift` after `npm run db:lint` in its managed CI's build job. Specs: `src/db-drift.spec.mjs` (real TypeORM metadata over a fixture app, stubbed schema) and `src/db-drift-docker.spec.mjs` (a real postgres:17-alpine scratch database where Docker runs, else the no-verdict answer); typeorm 0.3.31 and pg 8.23.1 are dev dependencies for them.
package/README.md CHANGED
@@ -55,9 +55,9 @@ Lite is the smaller edition of the same HFS standard, not another profile or eng
55
55
  the same slot manifest, rule catalog, lint canons, grammar, security rules and Docker rules apply, while edition fields filter
56
56
  out the test world and event machinery.
57
57
 
58
- `starci app scaffold <name> --edition lite` creates the minimum single-api, single-Next-app Supabase product. It generates the
59
- `app.supabase.types` artifact from the app's local Supabase stack; if that generation cannot run, scaffolding fails and removes
60
- the app it began instead of leaving a partial tree.
58
+ `starci app scaffold <name> --edition lite` creates the minimum single-api, single-Next-app product on its own PostgreSQL (`db/`,
59
+ the `db` service of its stack), its Passport identity and its S3 storage; a failed step removes the app it began instead of leaving a
60
+ partial tree. `starci app db new|plan|migrate|dump|post-cutover --url-env <NAME>` drives that database (default `DATABASE_URL`).
61
61
 
62
62
  The lite `app.starciwork` slot is optional when the app has no brand records. When present, it holds only `.gitignore`,
63
63
  optional `workspace.yaml` metadata and `brand/**` (R212): the existing brand record, drawing records, references and assets.
@@ -67,22 +67,20 @@ and `workspace.yaml` and retains `brand/` only when present.
67
67
 
68
68
  The lite additions are deliberately narrow:
69
69
 
70
- - `starci app add table <name> [--fe] [--no-types]` adds the migration and matching back-end capability. `--fe` also adds the table's
70
+ - `starci app add table <name> [--fe]` adds the migration and matching back-end capability. `--fe` also adds the table's
71
71
  api feature over GraphQL (get-<singular> query, create-<singular> mutation, the GraphQL server capability on first use) and, in
72
72
  every product Next app, one operation file per operation under `modules/api/graphql` and its `useQuery*Swr` / `useMutate*Swr`
73
- hook under `hooks/swr/api/graphql`; never a Server Action or a Supabase reader. Types regenerate by default, while `--no-types` is
74
- the explicit opt-out.
73
+ hook under `hooks/swr/api/graphql`; never a Server Action or a database reader.
75
74
  - `starci app add cli <group>` creates the optional cli app on first use, including its built-in migration and seed groups, then adds
76
75
  the requested group without full-edition specs.
77
76
  - `starci app add app <name>` adds another Next app in either edition: the edition's app skeleton plus the shared app core
78
77
  (modules/api, auth, toast, redux, the providers and the hooks), one fe structure for lite and full (owner ruling 2026-10-13).
79
78
  - An `add` or `new` whose target slot is unavailable in lite stops before writing with `<verb> <noun>: full edition only`.
80
79
 
81
- `starci app emit` continues to emit the enabled back-end snapshots and also writes `supabase/types/database.types.ts` when
82
- the `app.supabase.types` slot is enabled. The local Supabase stack must be running for generation.
80
+ `starci app emit` emits the enabled back-end contract snapshots; a lite app generates no database types.
83
81
 
84
82
  `starci app db drift` holds a lite app's TypeORM entities to the schema its migrations build (R333, `BE_ENTITY_SCHEMA_DRIFT`; `db/drift.mjs`).
85
- It loads the entity list `be/src/platform/database/entities.ts` with the app's own `typescript`, `typeorm` and `pg` (its `be/tsconfig.json`
83
+ It loads the entity list `be/src/modules/platform/database/entities.ts` with the app's own `typescript`, `typeorm` and `pg` (its `be/tsconfig.json`
86
84
  path aliases resolved) and builds TypeORM's metadata through a postgres `DataSource` with `synchronize` false that never connects. It applies
87
85
  `db/bootstrap.sql`, when present, and every active `db/migrations/*.sql`, each file in one transaction, to a scratch database: a throwaway
88
86
  container of the image `.starcistacks/application-stacks.yaml` declares for `components.db`, or with `--url-env <NAME>` the database that
@@ -1,11 +1,10 @@
1
- // backup-receipt.mjs - encrypted database backup custody, shared by dump and reset; tool output never enters diagnostics.
1
+ // backup-receipt.mjs - encrypted database backup custody of `starci app db dump`; tool output never enters diagnostics.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { parseYaml } from '../runtime/engine/yaml.mjs';
6
- import { readDeclaration } from '../release/index.mjs';
7
- import { resolveTarget, runCli as migrationCli, targetArgs } from './migrate.mjs';
8
6
  import { failure, refusal } from '../stack/errors.mjs';
7
+ import { DEFAULT_DB_URL_ENV, dbTarget, pgTool, queryRows as psqlRows } from './postgres.mjs';
9
8
 
10
9
  export const RECEIPT_SCHEMA = 'starci/db-backup@1';
11
10
  export function sha256(file) {
@@ -20,10 +19,9 @@ export function sha256(file) {
20
19
  }
21
20
  const inside = (root, file) => { const rel = path.relative(root, file); return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel)); };
22
21
 
23
- /** Backup/restore SQL can hold row values, so even the migration runner's scrubbed stderr must be withheld. */
24
- export function runCli(session, args) {
25
- try { return migrationCli(session, args); }
26
- catch { throw failure('DB_BACKUP_CLI_FAILED', `supabase ${args.slice(0, 2).join(' ')} failed; output withheld`); }
22
+ /** Backup SQL can hold row values, so a client tool's stderr is withheld whole. */
23
+ export function runPgTool(session, command, args) {
24
+ return pgTool(session.ctx, session.target, command, args, { withhold: true, code: 'DB_BACKUP_CLI_FAILED' });
27
25
  }
28
26
 
29
27
  /** Resolves existing ancestors before checking containment, including symlinked output directories and Git worktree marker files. */
@@ -40,27 +38,17 @@ export function outsideRepository(root, output) {
40
38
  return resolved;
41
39
  }
42
40
 
43
- /** Uses migrate's linked/password/URL resolution; local is explicitly selected and never silently defaults to linked. */
41
+ /** The database of `--url-env` (default DATABASE_URL) and the project name the archive carries. */
44
42
  export function backupSession(ctx, options) {
45
- const database = readDeclaration(ctx.cwd).database ?? {};
46
- const selections = [options.linked === true, options.local === true, options.urlEnv !== undefined].filter(Boolean).length;
47
- if (selections !== 1) throw refusal('DB_USAGE', 'select exactly one of --linked, --local or --url-env');
48
- const target = options.local ? { kind: 'local', ref: null, env: {}, secrets: [], how: 'local database' }
49
- : resolveTarget(ctx, { projectRef: null, password: null, ...database }, options);
50
- if (target.kind === 'linked' && !/^[a-z0-9]{10,40}$/u.test(target.ref)) throw refusal('DB_NOT_LINKED', 'linked project ref is invalid');
43
+ const target = dbTarget(ctx, options.urlEnv ?? DEFAULT_DB_URL_ENV);
51
44
  const project = path.basename(ctx.cwd).replace(/[^A-Za-z0-9_-]/gu, '-') || 'database';
52
- return { ctx, target, database, project };
45
+ return { ctx, target, project };
53
46
  }
54
47
 
55
- export const backupArgs = (session) => session.target.kind === 'local' ? ['--local', '--workdir', session.ctx.cwd] : targetArgs(session.target, session.ctx.cwd);
56
-
57
- /** Queries via the same injected Supabase runner; malformed output is withheld. */
48
+ /** One read-only query through psql; failures and malformed output are withheld. */
58
49
  export function queryRows(session, sql) {
59
- let rows;
60
- try { rows = JSON.parse(runCli(session, ['db', 'query', sql, '--output', 'json', '--agent', 'no', ...backupArgs(session)]).stdout); }
50
+ try { return psqlRows(session.ctx, session.target, sql, { withhold: true }); }
61
51
  catch { throw failure('DB_BACKUP_QUERY_FAILED', 'database inventory query failed; output withheld'); }
62
- if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== 'object' || Array.isArray(row))) throw failure('DB_BACKUP_QUERY_FAILED', 'database inventory is not a rows array');
63
- return rows;
64
52
  }
65
53
 
66
54
  /** Executes local tar/age through ctx.spawn, the same seam used by the stack's age installer; never relays tool errors containing data. */
@@ -102,28 +90,3 @@ export function encryptFile(ctx, input, output, recipients) {
102
90
  return checkArchive(output);
103
91
  }
104
92
 
105
- /** Validates a complete, recent, full-data receipt and its encrypted bytes before any destructive reset call. */
106
- export function validateReceipt(session, receiptPath) {
107
- if (!receiptPath) throw refusal('DB_RESET_BACKUP_MISSING', '--backup must name an encrypted backup receipt');
108
- let receipt;
109
- try { receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')); } catch { throw refusal('DB_RESET_BACKUP_INVALID', 'backup receipt is missing or invalid JSON'); }
110
- const valid = receipt && receipt.schema === RECEIPT_SCHEMA && receipt.target === 'linked' && receipt.schemaOnly === false
111
- && typeof receipt.project === 'string' && typeof receipt.ref === 'string' && typeof receipt.file === 'string'
112
- && path.basename(receipt.file) === receipt.file && receipt.file.endsWith('.age') && Number.isSafeInteger(receipt.bytes) && receipt.bytes >= 100
113
- && /^[a-f0-9]{64}$/u.test(receipt.sha256 ?? '') && Array.isArray(receipt.parts)
114
- && ['roles.sql', 'schema.sql', 'auth-storage-schema.sql', 'data.sql', 'auth-storage-data.sql', 'rowcounts.json'].every((part) => receipt.parts.includes(part))
115
- && Number.isSafeInteger(receipt.rowcounts?.tables) && receipt.rowcounts.tables >= 0 && /^\d+$/u.test(receipt.rowcounts?.rows ?? '')
116
- && typeof receipt.at === 'string' && Number.isFinite(Date.parse(receipt.at));
117
- if (!valid) throw refusal('DB_RESET_BACKUP_INVALID', 'backup receipt is not a full starci/db-backup@1 linked backup');
118
- if (receipt.ref !== session.target.ref) throw refusal('DB_RESET_BACKUP_PROJECT', 'backup targets a different project ref');
119
- const maxAge = session.database.reset?.backupMaxAgeHours ?? 24;
120
- if (!Number.isFinite(maxAge) || maxAge <= 0) throw refusal('DB_CONFIG_INVALID', 'database.reset.backupMaxAgeHours must be positive');
121
- const age = session.ctx.now() - Date.parse(receipt.at);
122
- if (age < 0 || age > maxAge * 3600000) throw refusal('DB_RESET_BACKUP_STALE', 'backup is future-dated or older than the allowed backup age');
123
- const file = path.resolve(path.dirname(receiptPath), receipt.file);
124
- if (!fs.existsSync(file)) throw refusal('DB_RESET_BACKUP_ARCHIVE_MISSING', 'backup archive is absent');
125
- try {
126
- if (checkArchive(file) !== receipt.bytes || sha256(file) !== receipt.sha256) throw new Error('mismatch');
127
- } catch { throw refusal('DB_RESET_BACKUP_HASH', 'backup archive size, header or sha256 differs from the receipt'); }
128
- return receipt;
129
- }
package/db/drift.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // drift.mjs - `starci app db drift`: the TypeORM entities of an app against the schema its SQL migrations build.
2
2
  // The migrations (db/migrations/*.sql, after db/bootstrap.sql when present) are the only DDL and `synchronize` is false, so an entity only
3
3
  // maps tables that already exist and can drift from them. The verb builds TypeORM's metadata from the app's entity list
4
- // (be/src/platform/database/entities.ts, compiled in-process with the app's own typescript and loaded with the app's own typeorm) through a
4
+ // (be/src/modules/platform/database/entities.ts, compiled in-process with the app's own typescript and loaded with the app's own typeorm) through a
5
5
  // DataSource that never connects, applies the migrations to a scratch database (a throwaway container of the image the app's
6
6
  // .starcistacks declares for its db component, or the empty database named by --url-env), reads that schema from
7
7
  // information_schema.columns and pg_catalog, and reports every entity column whose table or column is missing, whose type differs (both
package/db/dump.mjs CHANGED
@@ -1,17 +1,18 @@
1
- // dump.mjs - full Supabase backup, encrypted outside Git; every plaintext part is removed even when a tool fails.
1
+ // dump.mjs - `starci app db dump`: a full backup of the database --url-env names (pg_dumpall roles, pg_dump schema and data),
2
+ // encrypted outside Git; every plaintext part is removed even when a tool fails.
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { runVerb } from '../release/index.mjs';
5
6
  import { failure, refusal } from '../stack/errors.mjs';
6
- import { RECEIPT_SCHEMA, outsideRepository, backupSession, backupArgs, backupRecipients, backupTool, encryptFile, queryRows, sha256, runCli } from './backup-receipt.mjs';
7
+ import { RECEIPT_SCHEMA, outsideRepository, backupSession, backupRecipients, backupTool, encryptFile, queryRows, sha256, runPgTool } from './backup-receipt.mjs';
7
8
 
8
- /** Counts every ordinary table with quoted identifiers in a read-only transaction, including auth and storage. */
9
+ /** Counts every ordinary table with quoted identifiers, every schema but the catalogs (auth included). */
9
10
  export function rowcounts(session) {
10
11
  const tables = queryRows(session, "select schemaname, tablename from pg_catalog.pg_tables where schemaname not in ('pg_catalog', 'information_schema') order by schemaname, tablename;");
11
12
  const quote = (name) => `"${String(name).replaceAll('"', '""')}"`;
12
13
  return tables.map((table) => {
13
14
  if (typeof table.schemaname !== 'string' || typeof table.tablename !== 'string') throw failure('DB_BACKUP_QUERY_FAILED', 'invalid table inventory');
14
- const rows = queryRows(session, `begin read only; select count(*)::text as count from ${quote(table.schemaname)}.${quote(table.tablename)}; commit;`);
15
+ const rows = queryRows(session, `select count(*)::text as count from ${quote(table.schemaname)}.${quote(table.tablename)}`);
15
16
  if (rows.length !== 1 || !/^\d+$/u.test(String(rows[0].count))) throw failure('DB_BACKUP_QUERY_FAILED', 'invalid rowcount');
16
17
  return { schema: table.schemaname, table: table.tablename, count: String(rows[0].count) };
17
18
  });
@@ -48,15 +49,15 @@ export function dumpDatabase(ctx, options = {}) {
48
49
  let complete = false;
49
50
  try {
50
51
  fs.mkdirSync(scratch, { mode: 0o700 });
51
- const jobs = [['roles.sql', ['--role-only']], ['schema.sql', []], ['auth-storage-schema.sql', ['--schema', 'auth,storage']]];
52
- if (!options.schemaOnly) jobs.push(['data.sql', ['--data-only']], ['auth-storage-data.sql', ['--data-only', '--schema', 'auth,storage']]);
53
- for (const [file, flags] of jobs) runCli(session, ['db', 'dump', ...flags, '--file', path.join(scratch, file), ...backupArgs(session)]);
52
+ const jobs = [['roles.sql', 'pg_dumpall', ['--roles-only', '--no-role-passwords']], ['schema.sql', 'pg_dump', ['--schema-only']]];
53
+ if (!options.schemaOnly) jobs.push(['data.sql', 'pg_dump', ['--data-only']]);
54
+ for (const [file, command, flags] of jobs) runPgTool(session, command, [...flags, '--file', path.join(scratch, file), '--dbname', session.target.url]);
54
55
  const counts = rowcounts(session);
55
56
  fs.writeFileSync(path.join(scratch, 'rowcounts.json'), `${JSON.stringify(counts, null, 2)}\n`, { mode: 0o600 });
56
57
  const parts = [...jobs.map(([file]) => file), 'rowcounts.json'];
57
58
  const tar = archiveParts(ctx, scratch, parts);
58
59
  const bytes = encryptFile(ctx, tar, archive, recipients);
59
- const receipt = { schema: RECEIPT_SCHEMA, project: session.project, target: session.target.kind, ref: session.target.ref ?? null,
60
+ const receipt = { schema: RECEIPT_SCHEMA, project: session.project, target: session.target.kind, ref: session.target.urlEnv,
60
61
  at, file: path.basename(archive), bytes, sha256: sha256(archive), parts,
61
62
  rowcounts: { tables: counts.length, rows: counts.reduce((sum, row) => sum + BigInt(row.count), 0n).toString(), tablesByName: counts }, schemaOnly: options.schemaOnly === true };
62
63
  fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
@@ -70,9 +71,9 @@ export function dumpDatabase(ctx, options = {}) {
70
71
 
71
72
  export function dbDumpMain(argv, io = {}) {
72
73
  return runVerb({ label: 'starci app db dump', name: 'db dump', code: 'DB_USAGE', io, argv,
73
- spec: { boolean: ['--linked', '--local', '--schema-only', '--json'], value: ['--url-env', '--out'] },
74
+ spec: { boolean: ['--schema-only', '--json'], value: ['--url-env', '--out'] },
74
75
  run: (ctx, flags, positional) => {
75
76
  if (positional.length) throw refusal('DB_USAGE', 'dump takes flags only');
76
- return dumpDatabase(ctx, { linked: flags.linked, local: flags.local, urlEnv: flags['url-env'], out: flags.out, schemaOnly: flags['schema-only'] });
77
+ return dumpDatabase(ctx, { urlEnv: flags['url-env'], out: flags.out, schemaOnly: flags['schema-only'] });
77
78
  } });
78
79
  }
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
5
  /** The app's one entity list: it re-exports the arrays each capability's persistence/entities/index.ts exports. */
6
- export const ENTITY_LIST = 'be/src/platform/database/entities.ts';
6
+ export const ENTITY_LIST = 'be/src/modules/platform/database/entities.ts';
7
7
 
8
8
  /** True when the app at `root` keeps an entity list: the apps whose entities `starci app db drift` checks and whose release gate runs it. */
9
9
  export const hasEntityList = (root) => fs.existsSync(path.join(root, ENTITY_LIST));