@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
@@ -1,41 +1,50 @@
1
- // Supabase database rules (design 4.2, L02-L09). SQL migrations are parsed with libpg-query through
2
- // database-sql.mjs; config, Git history and generated-type checks are split into bounded sibling modules.
1
+ // The database rules of a lite app's db/ tree (design 4.2, L02-L09). SQL migrations are parsed with libpg-query through
2
+ // database-sql.mjs; Git history and generated-type checks are split into bounded sibling modules.
3
3
  import { found, readJson, readText } from './read.mjs';
4
- import { configFindings, parseToml, typesFindings } from './database-config.mjs';
5
4
  import { defaultGit, migrationShapeFindings } from './database-migrations.mjs';
6
5
  import { migrationSetPolicyFindings, sqlAnalysis } from './database-sql.mjs';
7
6
  import { migrationSetDefinerFindings } from './database-plpgsql.mjs';
8
7
  import { databaseAccessFindings } from './database-access.mjs';
9
- import { CONFIG_FILE, DB_CONFIG_POLICY, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, TYPES_FILE } from './database-constants.mjs';
8
+ import { DB_DIR, DB_MIGRATION_SHAPE, isActiveMigration, LITE_DB_MIGRATION, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
10
9
  import { byCodeUnit } from '../../lib/list.mjs';
11
10
  import { eachInOrder } from '../../lib/in-order.mjs';
12
11
 
13
12
  export { migrationStamp } from './database-migrations.mjs';
14
13
  export {
15
- DB_BE_ONLY_ACCESS, DB_REALTIME_UNLISTED, DB_APP_ROLE, DB_FORCE_RLS_DEFAULT,
16
- DB_POLICY_TAUTOLOGY, DB_VIEW_UNSAFE, DB_STORAGE_PREFIX,
14
+ DB_APP_ROLE, DB_FORCE_RLS_DEFAULT, DB_POLICY_TAUTOLOGY, DB_VIEW_UNSAFE, DB_STORAGE_PREFIX,
17
15
  } from './database-access.mjs';
18
16
  export const DB_DEFINER_REVOKE = 'DB_DEFINER_REVOKE';
19
17
  export {
20
- CONFIG_FILE,
21
- DB_CONFIG_POLICY,
22
18
  DB_DEFINER_SAFE,
23
19
  DB_DYNAMIC_DDL,
24
20
  DB_MIGRATION_SHAPE,
25
21
  DB_POLICY_SHAPE,
26
22
  DB_RLS_REQUIRED,
27
23
  DB_STORAGE_POLICY,
28
- DB_TYPES_DRIFT,
29
- TYPES_FILE,
24
+ LITE_DB_MIGRATION,
30
25
  } from './database-constants.mjs';
31
26
 
32
- const declaresSupabaseConnection = (declaration) => ['be', 'fe'].some((side) => (declaration?.sides?.[side]?.connections ?? []).some((connection) => connection?.provider === 'supabase'));
27
+ /** The schemas the app's tables live in; no schema is exposed to a browser, which reaches data only through the back end. */
28
+ const APP_SCHEMAS = new Set(['public']);
33
29
 
34
- async function configResult(repoRoot, declared) {
35
- const configText = readText(repoRoot, CONFIG_FILE);
36
- const config = configText === null ? null : await parseToml(configText);
37
- if (config === null) return { config, findings: [found(DB_CONFIG_POLICY, CONFIG_FILE, `${CONFIG_FILE} is not readable valid TOML; the config is policy, so it must parse`, {})] };
38
- return { config, findings: configFindings({ file: CONFIG_FILE, text: configText, toml: config, supabase: declared ?? null }) };
30
+ /** A .starcistacks seed is the one SQL file a lite app keeps outside db/. */
31
+ const STACK_SEED = /^\.starcistacks\/[^/]+\/seeds\/.+\.sql$/;
32
+
33
+ /**
34
+ * LITE_DB_MIGRATION: a lite app's schema lives only under db/: no supabase/ tree and no SQL file outside db/ (but a
35
+ * .starcistacks seed), and every SQL file under db/ is a migration, the archive, the seed or nothing.
36
+ */
37
+ export function liteDbPathFindings(files) {
38
+ const findings = [];
39
+ for (const file of files) {
40
+ if (file === 'supabase' || file.startsWith('supabase/')) {
41
+ findings.push(found(LITE_DB_MIGRATION, file, `${file} is a Supabase project file; a lite app keeps its schema in ${MIGRATIONS_DIR}/<ts14>_<kebab>.sql (move the migrations, delete supabase/)`, {}));
42
+ continue;
43
+ }
44
+ if (!file.endsWith('.sql') || STACK_SEED.test(file)) continue;
45
+ if (!file.startsWith(`${DB_DIR}/`)) findings.push(found(LITE_DB_MIGRATION, file, `${file} is SQL outside ${DB_DIR}/; a schema change is one ${MIGRATIONS_DIR}/<ts14>_<kebab>.sql`, {}));
46
+ }
47
+ return findings;
39
48
  }
40
49
 
41
50
  async function migrationAnalyses({ repoRoot, migrations, exposed, forceRls, findings }) {
@@ -54,38 +63,26 @@ async function migrationAnalyses({ repoRoot, migrations, exposed, forceRls, find
54
63
  }
55
64
 
56
65
  /**
57
- * Judge the Supabase paths in one app. Git and type emission are injected for deterministic specs; this function
58
- * starts neither Docker nor the Supabase CLI. A full app is judged when it declares a Supabase connection, while
59
- * lite apps are judged even before their initial Supabase files have all been scaffolded.
66
+ * Judge the db/ tree of one lite app. Git is injected for deterministic specs; this function starts no
67
+ * database and no Docker process. A full app keeps its TypeORM migrations under be/ and is judged here only for a stray db/ tree.
60
68
  */
61
- export async function checkDatabase({ repoRoot, files, base, git, edition, supabase, emitTypes } = {}) {
69
+ export async function checkDatabase({ repoRoot, files, base, git, edition, db } = {}) {
62
70
  const findings = [];
71
+ const declaration = readJson(repoRoot, 'hfs.json');
72
+ const lite = (edition ?? declaration?.edition) === 'lite';
63
73
  const migrations = files.filter(isActiveMigration).sort(byCodeUnit);
74
+ if (!lite && !migrations.length) return findings;
75
+ const declared = db === undefined ? declaration?.db : db;
64
76
  const archived = files.filter((file) => file.startsWith(`${MIGRATIONS_ARCHIVE_DIR}/`));
65
- const declaration = readJson(repoRoot, 'hfs.json');
66
- const declared = supabase === undefined ? declaration?.supabase : supabase;
67
- const hasConfig = files.includes(CONFIG_FILE);
68
- const supabaseConnection = declaresSupabaseConnection(declaration);
69
- if (!migrations.length && !hasConfig && !files.includes(TYPES_FILE) && !declared && !supabaseConnection && (edition ?? declaration?.edition) !== 'lite') return findings;
70
-
71
- let config = null;
72
- if (hasConfig) {
73
- const result = await configResult(repoRoot, declared);
74
- config = result.config;
75
- findings.push(...result.findings);
76
- }
77
-
78
- const apiSchemas = (config?.api?.schemas ?? []).filter((schema) => typeof schema === 'string');
79
- const exposed = new Set(['public', ...apiSchemas]);
77
+ if (lite) findings.push(...liteDbPathFindings(files));
80
78
  const forceRls = new Set((declared?.forceRls ?? []).map((name) => (name.includes('.') ? name : `public.${name}`)));
81
79
  findings.push(...await migrationShapeFindings({ repoRoot, migrations, archived, git: git ?? defaultGit, base }));
82
80
 
83
- const analyses = await migrationAnalyses({ repoRoot, migrations, exposed, forceRls, findings });
81
+ const analyses = await migrationAnalyses({ repoRoot, migrations, exposed: APP_SCHEMAS, forceRls, findings });
84
82
  findings.push(
85
83
  ...migrationSetPolicyFindings(analyses),
86
- ...migrationSetDefinerFindings(analyses, exposed),
87
- ...await databaseAccessFindings({ analyses, exposed, supabase: declared }),
88
- ...await typesFindings({ repoRoot, files, emitTypes, declaration: declared }),
84
+ ...migrationSetDefinerFindings(analyses, APP_SCHEMAS),
85
+ ...await databaseAccessFindings({ analyses, exposed: APP_SCHEMAS, db: declared }),
89
86
  );
90
87
  return findings;
91
88
  }
@@ -75,7 +75,7 @@ function scriptFindings(rel, pkg) {
75
75
  const scripts = isPlainObject(pkg.scripts) ? pkg.scripts : {};
76
76
  return Object.keys(scripts)
77
77
  .filter((name) => LITE_FORBIDDEN_SCRIPT.test(name))
78
- .map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local Supabase stack and optional contract specs, never an e2e, stack or browser run`, { name }));
78
+ .map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local db service and optional contract specs, never an e2e, stack or browser run`, { name }));
79
79
  }
80
80
 
81
81
  /** The backing services of the manifest (ruleParams.be.backing): name -> {clients, patterns, images, editions?}. */
@@ -1,6 +1,6 @@
1
- // supabase-secrets.mjs - L17 extends the one R06 secret scan for edition lite. It keeps the ordinary
1
+ // lite-secrets.mjs - L17 extends the one R06 secret scan for edition lite. It keeps the ordinary
2
2
  // HFS_PLAINTEXT_SECRET judgement, then adds the reduced lite custody tree and provider-shaped literals
3
- // which are meaningful only in the Supabase/FE surface. Findings name shapes, never captured values.
3
+ // in the trees that hold no secret at all (fe/ and db/). Findings name shapes, never captured values.
4
4
  import { Buffer } from 'node:buffer';
5
5
  import { secretFindings } from './secrets.mjs';
6
6
  import { found, readText } from './read.mjs';
@@ -40,11 +40,11 @@ const validJwt = candidate => {
40
40
 
41
41
  const liteSecretPatterns = line => {
42
42
  const names = LITE_SECRET_PATTERNS.filter(pattern => pattern.re.test(line)).map(pattern => pattern.name);
43
- if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('supabase-eyj-service-token');
43
+ if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('signed-jwt');
44
44
  return names;
45
45
  };
46
46
 
47
- const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('supabase/');
47
+ const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('db/');
48
48
 
49
49
  function custodyPathFinding(file, repo, findings) {
50
50
  if (repo.profile === 'app') {
@@ -71,7 +71,7 @@ function providerSecretFindings(repoRoot, file, repo, findings) {
71
71
  for (const pattern of liteSecretPatterns(line)) {
72
72
  if (seen.has(pattern)) continue;
73
73
  seen.add(pattern);
74
- findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or supabase/.`, { line: index + 1, pattern }));
74
+ findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or db/.`, { line: index + 1, pattern }));
75
75
  }
76
76
  }
77
77
  }
@@ -90,7 +90,7 @@ export function liteSecretCustodyFindings({ repoRoot, files, repo }) {
90
90
  * Drop-in extension of secretFindings for scripts/hfs/check.mjs. When an L17 finding identifies the same file/line as
91
91
  * the generic R06 scanner (a complete JWT, for example), the specific lite code wins so one physical issue is emitted once.
92
92
  */
93
- export function supabaseSecretFindings(input) {
93
+ export function liteSecretFindings(input) {
94
94
  const ordinary = secretFindings(input);
95
95
  const lite = liteSecretCustodyFindings(input);
96
96
  const liteJwtAuthority = (input.repo.edition ?? 'full') === 'lite';
@@ -1,12 +1,10 @@
1
- // R252: inspect executable source for plaintext loads, tracing and service-role custody.
1
+ // R252: inspect executable source for plaintext loads and tracing.
2
2
  // Only source is read. Secret files and sealed envelopes are excluded before any read,
3
3
  // and findings carry a path and a fixed explanation, never source text or values.
4
4
  import { found, readJson, readText } from './read.mjs';
5
5
 
6
6
  const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
7
7
  const XTRACE = 'HFS_SCRIPT_XTRACE';
8
- const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
9
- const SUPABASE_OWNER = 'be.integrations.supabase';
10
8
  const SECRET_ENV_FILE = /(?:^|\/)(?:secrets?\.env|\.env)(?:\.[^/]*)?/iu;
11
9
  const SECRET_NIVO_FILE = /(?:^|\/)\.nivo-lite(?:\/|$)/iu;
12
10
  const SECRET_ENC_FILE = /\.enc$/iu;
@@ -36,7 +34,6 @@ const isLoad = (line) => LOAD_PATTERNS.some((pattern) => pattern.test(line));
36
34
  const READ_CALL = /\b(?:readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|load_dotenv|loadEnvFile|dotenv\s*\.\s*(?:config|parse))\s*\(/giu;
37
35
  const TRACE_COMMAND = /(?:^|[\s;&|('"`]|#!)(?:set|\/?(?:[\w.-]+\/)*(?:bash|sh|dash|zsh|ksh))\s+/giu;
38
36
  const SHELL_OPTIONS = /\bSHELLOPTS\s*=\s*['"]?([^\s;'"]*)/giu;
39
- const SERVICE_ROLE_NAME = /(?:^|[^a-z0-9])(?:[a-z0-9]+[_-])*service[_-]?role(?:[_-][a-z0-9]+)*(?=$|[^a-z0-9])/iu;
40
37
 
41
38
  /** Retain executable strings, including remote shell commands, but discard comment-only lines. */
42
39
  function executableText(text) {
@@ -130,11 +127,6 @@ function enablesTracing(text) {
130
127
  return false;
131
128
  }
132
129
 
133
- function integrationOwns(file, resolver) {
134
- if (!file.startsWith('be/src/modules/integrations/supabase/')) return false;
135
- return !resolver?.ownerOf || resolver.ownerOf(file)?.slot === SUPABASE_OWNER;
136
- }
137
-
138
130
  function isSource(file) {
139
131
  if (isSecretFile(file) || EXCLUDED_TREE.test(file) || TEST_FILE.test(file)) return false;
140
132
  return SOURCE.test(file) || WORKFLOW.test(file) || DOCKER.test(file)
@@ -161,7 +153,6 @@ export function scriptHygieneFindings({ repoRoot, files = [], all = files, repo,
161
153
  const text = executableText(raw);
162
154
  if (plaintextLoad(text)) findings.push(found(PLAINTEXT_LOAD, file, 'Executable source loads a plaintext secret file; use starci app secret with sealed envelopes.'));
163
155
  if (enablesTracing(text)) findings.push(found(XTRACE, file, 'Executable source enables shell tracing or secret reveal; remove xtrace and --reveal.'));
164
- if (SERVICE_ROLE_NAME.test(text.replace(/([a-z0-9])([A-Z])/gu, '$1_$2')) && !integrationOwns(file, resolver)) findings.push(found(SERVICE_ROLE, file, 'Service-role access belongs only to be/src/modules/integrations/supabase/ and its configuration.'));
165
156
  }
166
157
  return findings;
167
158
  }
@@ -1,9 +1,10 @@
1
- // R276: lite keeps Postgres and pgvector on managed Supabase, admits only the backing services available to its edition,
1
+ // R276: lite runs exactly its own PostgreSQL (the db component, image supabase/postgres) with pgvector, admits only the backing services available to its edition,
2
2
  // and runs a declared Redis as one service with AOF fsynced at least every second on a named volume and no eviction
3
3
  // (ruling:R212/case-10, owner:lite-vector-is-pgvector); sidecar SQLite persists on declared named volumes.
4
4
  import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
5
5
  import { composeEntries } from '../../lib/compose-values.mjs';
6
6
  import { repositoryOf } from '../../lib/stack-services.mjs';
7
+ import { isLiteDatabaseImage } from '../../lib/swarm-stack.mjs';
7
8
  import { loadSlotManifest } from '../slots.mjs';
8
9
  import { judgedInEdition } from '../edition-slots.mjs';
9
10
  import { readJson } from './read.mjs';
@@ -43,12 +44,12 @@ function imageIssues(components, services, { catalog, declared }) {
43
44
  for (const [id, entry] of Object.entries(inventory)) {
44
45
  if (!present(entry?.image)) continue;
45
46
  const service = serviceOfImage(catalog, entry.image);
46
- if (DATABASE_IMAGE.test(repositoryOf(entry.image)))
47
+ if (DATABASE_IMAGE.test(repositoryOf(entry.image)) && !isLiteDatabaseImage(entry.image))
47
48
  errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
48
- message: 'lite must not run a self-hosted Postgres or Mongo database image; use managed Supabase backing' });
49
+ message: 'lite runs one database, the db component of image supabase/postgres; no other Postgres or Mongo image' });
49
50
  else if (service !== null && !judgedInEdition(catalog[service], 'lite'))
50
51
  errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
51
- message: `lite must not run ${service}; vector search uses Supabase pgvector (owner:lite-vector-is-pgvector)` });
52
+ message: `lite must not run ${service}; vector search uses pgvector in the db service (owner:lite-vector-is-pgvector)` });
52
53
  else if (service !== null && !declared.has(service))
53
54
  errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
54
55
  message: `lite runs ${service} only when hfs.json sides.be.backing declares it for the capability that needs it (ruling:R212/case-10)` });
@@ -81,7 +81,8 @@ function stateful(component, service) {
81
81
  function recoveryComponents({ doc, spec, model }) {
82
82
  const components = mapping(doc.components), services = mapping(model?.services);
83
83
  const selected = new Set([...Object.keys(mapping(spec.components)), ...Object.keys(services)]);
84
- const required = new Set(Object.entries(mapping(doc.backing)).filter(([, entry]) => entry?.managed === true).map(([id]) => id));
84
+ // Every declared backing (the lite db: backing.db naming its component) owes a backup, as every stateful component does.
85
+ const required = new Set(Object.entries(mapping(doc.backing)).filter(([, entry]) => typeof entry?.provider === 'string').map(([id]) => id));
85
86
  for (const id of selected) if (stateful(mapping(components[id]), mapping(services[id]))) required.add(id);
86
87
  return [...required];
87
88
  }
@@ -290,7 +291,7 @@ function sqlTemplateText(node) {
290
291
  }
291
292
 
292
293
  function recurringObservations({ doc, repoRoot, files, file, issues }) {
293
- const sql = file.startsWith('supabase/migrations/') && file.endsWith('.sql') && !file.includes('/_archive/');
294
+ const sql = file.startsWith('db/migrations/') && file.endsWith('.sql') && !file.startsWith('db/migrations/archive/');
294
295
  const sourceFile = SOURCE.test(file) && !TEST.test(file);
295
296
  if ((!sql && !sourceFile) || !publicReference(repoRoot, files, file)) return [];
296
297
  const text = readText(repoRoot, file);
@@ -92,8 +92,8 @@ function swarmDeploymentFiles(repoRoot, files) {
92
92
  function liteEnvironmentFindings({ doc, environments, deployments }) {
93
93
  const findings = [];
94
94
  const delivery = deployments.length || Object.values(environments).some(spec => Array.isArray(spec?.composeFiles) && spec.composeFiles.length);
95
- if (delivery && (doc.backing?.supabase?.provider !== 'supabase' || doc.backing?.supabase?.managed !== true))
96
- findings.push(found(CODE, DECLARATION, `${DECLARATION}: lite Swarm delivery must declare backing.supabase with provider: supabase and managed: true`));
95
+ if (delivery && (doc.backing?.db?.provider !== 'postgres' || doc.backing?.db?.component !== 'db'))
96
+ findings.push(found(CODE, DECLARATION, `${DECLARATION}: lite Swarm delivery must declare backing.db with provider: postgres and component: db (its db service)`));
97
97
  for (const [environment, spec] of Object.entries(environments)) {
98
98
  if (!Array.isArray(spec?.composeFiles) || !spec.composeFiles.length) continue;
99
99
  if (spec.runtime !== 'docker-swarm' || spec.composeFiles.some(file => typeof file !== 'string' || !file.startsWith('infra/swarm/')))
@@ -5,7 +5,7 @@
5
5
  // `deployment.json` and `k8s/` are not in the list, so they fall out of it;
6
6
  // - the declaration (read by scripts/lib/stack-declaration.mjs, the one reader of it) states a sonar service, a local
7
7
  // Sonar is owned by the host (`stack.owner: host`, root `.claude/ext/sonar`).
8
- // The lite overlay admits Swarm compute and managed Supabase backing. Runtime rules are shared with the stack gates.
8
+ // The lite overlay admits Swarm compute and its own db service. Runtime rules are shared with the stack gates.
9
9
  // The declaration's quality/delivery services contract (custody, CI wiring, project keys) stays check-starcistacks's.
10
10
  import { braceVariants, globExpression } from '../../lib/glob.mjs';
11
11
  import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../../lib/stack-declaration.mjs';
@@ -49,7 +49,7 @@ function sideCrossing(repo, fromSide, toSide, toPath) {
49
49
  function importAllowedApp({ repo, root, sides }, fromPath, toPath) {
50
50
  const from = splitSide(fromPath);
51
51
  const to = splitSide(toPath);
52
- // A side file may read an app-root path (supabase/types/) only through a declared read, exactly as it reads the other side.
52
+ // A side file may read an app-root path (db/types/) only through a declared read, exactly as it reads the other side.
53
53
  if (from && from.side !== to?.side) return sideCrossing(repo, from.side, to?.side ?? null, toPath);
54
54
  if (from && to) return sides[from.side].importAllowed(from.rest, to.rest);
55
55
  return root.importAllowed(fromPath, toPath);
@@ -120,6 +120,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
120
120
  connections: Object.freeze((s.connections ?? []).map(connectionView)),
121
121
  providers,
122
122
  reads: Object.freeze([...(s.reads ?? [])]),
123
+ dbFunctions: Object.freeze((s.dbFunctions ?? []).map((entry) => Object.freeze({ ...entry }))),
123
124
  ...(s.grammar === undefined ? {} : { grammar: s.grammar }),
124
125
  ...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map(entry => Object.freeze({ ...entry }))) }),
125
126
  manifestVersion: manifest.version,
@@ -221,7 +222,8 @@ function createScopeResolver(manifest, repo) {
221
222
  trackingOf,
222
223
  /** True only for a path a slot owns and requires to be committed. */
223
224
  isTracked: (p) => trackingOf(p) === 'tracked',
224
- ruleParams: () => ruleParams(manifest, profile, edition),
225
+ // The app's own database functions (hfs.json sides.be.dbFunctions) join the canon's list (R334).
226
+ ruleParams: () => withDeclaredDbFunctions(ruleParams(manifest, profile, edition), repo.dbFunctions),
225
227
  allowedImports: (tier) => manifest.tiers[profile]?.[tier]?.mayImport ?? null,
226
228
  });
227
229
  }
@@ -235,13 +237,17 @@ function createScopeResolver(manifest, repo) {
235
237
  */
236
238
  export function createSlotResolver(manifest, repo) {
237
239
  if (repo.profile !== APP_SCOPE) return createScopeResolver(manifest, repo);
238
- // The root declares no connection of its own, yet a provider slot of the app profile (app.supabase*) is enabled by a
240
+ // The root declares no connection of its own, yet a provider slot of the app profile is enabled by a
239
241
  // side's connection with that provider: the root scope reads the union of the sides' connections.
240
242
  const root = createScopeResolver(manifest, { ...repo, connections: SIDES.flatMap((side) => repo.sides[side].connections) });
241
243
  const sides = Object.fromEntries(PROFILES.map((side) => [side, createScopeResolver(manifest, repo.sides[side])]));
242
244
  return appResolver(repo, root, sides);
243
245
  }
244
246
 
247
+ /** The rule parameters with the app's declared database functions appended to `dbFunctions` (be only), frozen. */
248
+ const withDeclaredDbFunctions = (params, declared = []) => (params.dbFunctions === undefined && !declared.length ? params
249
+ : Object.freeze({ ...params, dbFunctions: Object.freeze([...(params.dbFunctions ?? []), ...declared]) }));
250
+
245
251
  /** The rule parameters of one profile (be: infraOwners, suffixes, bannedSuffixes and the rest over the shared ruleParams.common fileLines and duplicateBlock; fe: common alone or with its overrides; runtime: ruleParams.runtime of a runtime manifest), as a frozen deep copy. Under `edition` lite the `lite` overrides of ruleParams.<profile> merge over the base (the `lite` key itself is never returned). */
246
252
  export function ruleParams(manifest, profile, edition = 'full') {
247
253
  const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;
@@ -1,6 +1,6 @@
1
1
  // pg-parse.mjs - the one SQL parse entry of the hfs database rules (scripts/hfs/rules/database.mjs): the WASM build
2
2
  // of libpg-query, the real PostgreSQL parser compiled to WebAssembly (no native build, no postgres server). It is
3
- // loaded lazily: a check of an app with no supabase/ tree never pays the module init. parseSql gives the statement
3
+ // loaded lazily: a check of an app with no db/ tree never pays the module init. parseSql gives the statement
4
4
  // tree of a migration (`{version, stmts}`, each stmt `{stmt, stmt_location, stmt_len}` with byte offsets); a
5
5
  // plpgsql body (a DO block's or a function's `as` source) is judged through parsePlpgsqlBody, which wraps it in a
6
6
  // probe CREATE FUNCTION because libpg-query parses plpgsql only in that form. A parse failure throws the
@@ -10,7 +10,7 @@ let loading = null;
10
10
 
11
11
  /**
12
12
  * The libpg-query module (wasm build), initialized once per process. A bare dynamic import so the package can be
13
- * absent until a supabase/ tree exists to judge; the caller reports the load failure, not this module.
13
+ * absent until a db/ tree exists to judge; the caller reports the load failure, not this module.
14
14
  */
15
15
  function loadPgParser() {
16
16
  loading ??= (async () => {
@@ -100,7 +100,7 @@ const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Z
100
100
  const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
101
101
 
102
102
  /** True when a bare variable `name` holds secret material as an identifier segment (DB_PASSWORD, dbPassword,
103
- * SUPABASE_SERVICE_ROLE_KEY) and does not end in a config suffix (PASSWORD_MIN_LENGTH, SECRET_FILE,
103
+ * STORAGE_SECRET_ACCESS_KEY) and does not end in a config suffix (PASSWORD_MIN_LENGTH, SECRET_FILE,
104
104
  * tokenBudget). The same name judgement the `assigned-secret` patterns make, for callers that read keys. */
105
105
  export const secretNameLike = (name) => SECRET_NAME_TEST.test(name) && !BENIGN_TAIL_TEST.test(name);
106
106
 
@@ -117,7 +117,7 @@ export const SECRET_PATTERNS = [
117
117
  { name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
118
118
  { name: 'age-secret-key', re: /\bAGE-SECRET-KEY-1[0-9A-Z]{20,}\b/ },
119
119
  { name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
120
- // A value written `env(NAME)` (the Supabase config.toml reference form) names a variable, not a secret.
120
+ // A value written `env(NAME)` (a config reference form) names a variable, not a secret.
121
121
  // A keyword-assigned value that names itself a stand-in (a test stub's `accessToken: "fixture-..."`) is no
122
122
  // candidate; only this heuristic takes the exemption, never a provider-shaped token above.
123
123
  // A spec file's keyword-assigned values are test inputs (a spec's sign-in
@@ -10,6 +10,9 @@ const BACKING_IMAGE_SQL_AND_CACHE = 'postgres(?:ql)?|postgresql-repmgr|mysql(?:-
10
10
  const BACKING_IMAGE_BROKERS_AND_OTHERS = '(?:cp-)?kafka(?:-native)?|keycloak|minio|rabbitmq';
11
11
  const BACKING_IMAGE = new RegExp(`^(?:${BACKING_IMAGE_SQL_AND_CACHE}|${BACKING_IMAGE_BROKERS_AND_OTHERS})$`, 'i');
12
12
  const mapping = value => plain(value) ? value : {};
13
+
14
+ /** True for the one database image a lite app runs (its db component): supabase/postgres, pinned by digest. */
15
+ export const isLiteDatabaseImage = image => typeof image === 'string' && /^supabase\/postgres(?::[^@]+)?@sha256:[a-f0-9]{64}$/i.test(image);
13
16
  const present = value => typeof value === 'string' && Boolean(value.trim());
14
17
  const configured = value => Object.keys(mapping(value)).length > 0;
15
18
  const schedulerImage = image => present(image) && /(?:^|\/)swarm-cronjob(?:[:@]|$)/.test(image);
@@ -49,8 +52,8 @@ export function swarmInventoryIssues({ components, lite = false }) {
49
52
  const add = (at, message) => errors.push({ at, message });
50
53
  for (const [id, entry] of Object.entries(catalog)) {
51
54
  const component = mapping(entry);
52
- if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
53
- add(`components.${id}.image`, 'lite must use managed Supabase backing services; this image duplicates a Supabase-provided service');
55
+ if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)) && !isLiteDatabaseImage(component.image))
56
+ add(`components.${id}.image`, 'lite runs no backing image but its db component (supabase/postgres); storage is the external S3 endpoint, identity the API');
54
57
  jobDeclarationIssues(id, component, catalog, add);
55
58
  }
56
59
  return errors;
@@ -117,8 +120,8 @@ export function swarmModelIssues({ model, components, bindings, lite = false })
117
120
  add(`services.${id}.ports`, 'Swarm ports must be an array of published port definitions');
118
121
  else if (service.ports?.length && component.role !== 'ingress')
119
122
  add(`services.${id}.ports`, 'only a component with role ingress may publish Swarm ports');
120
- if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)))
121
- add(`services.${id}.image`, 'lite stack must not run an image that duplicates a Supabase-provided backing service');
123
+ if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)) && !isLiteDatabaseImage(service.image))
124
+ add(`services.${id}.image`, 'lite stack runs no backing image but its db service (supabase/postgres)');
122
125
  jobServiceIssues(id, service, component, services, bound, add);
123
126
  }
124
127
  for (const id of Object.keys(bound)) {
@@ -7,8 +7,7 @@ import {
7
7
  } from "../runtime/scripts/hfs/slots.mjs";
8
8
  import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
9
9
  import { ScaffoldError, pascalOf } from "./service.mjs";
10
- import { bundledPins } from "../runtime/scripts/hfs/canon-pins.mjs";
11
- import { PARTIAL_VARIABLES, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
10
+ import { PARTIAL_VARIABLES, coreFeApps, jsonText, packageJsonText } from "./app.mjs";
12
11
 
13
12
  /** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
14
13
  const CORE_DIRS = Object.freeze({
@@ -66,8 +65,7 @@ function planAppTemplates(repo, name) {
66
65
  if (!fs.existsSync(dir)) continue;
67
66
  for (const relative of listFiles(dir)) byPath.set(relative, { relative: `${targetBase}/${relative}`, body: readTemplate(dir, relative) });
68
67
  }
69
- return callbackSourceFiles([...byPath.values()].map(file => ({ path: file.relative, content: file.body })), repo)
70
- .map(file => ({ relative: file.path, body: file.content }));
68
+ return [...byPath.values()];
71
69
  }
72
70
 
73
71
  /** The package and TypeScript manifests of a new Next workspace. */
@@ -85,12 +83,6 @@ function planWorkspaceManifests(root, repo, name) {
85
83
  );
86
84
  const workspacePackage = JSON.parse(fs.readFileSync(firstPackage, "utf8"));
87
85
  workspacePackage.name = `@${repo.project}/${name}`;
88
- if (hasAuthCallback(repo)) {
89
- const pins = bundledPins();
90
- workspacePackage.dependencies ??= {};
91
- for (const dependency of ["@supabase/ssr", "@supabase/supabase-js", "server-only"])
92
- workspacePackage.dependencies[dependency] = pins[dependency].version;
93
- }
94
86
  const appTemplates = path.join(TEMPLATES_DIR, "fe", "app");
95
87
  return [
96
88
  {
@@ -21,7 +21,7 @@ const managedScripts = (repo, source) => {
21
21
 
22
22
  const CLI_OPTIONS = `import type { DatabaseConnectionOptions } from "@modules/platform/database"
23
23
 
24
- /** Everything the lite cli needs: the least-privilege Supabase PostgreSQL connection. */
24
+ /** Everything the lite cli needs: the least-privilege PostgreSQL connection. */
25
25
  export interface CliAppOptions {
26
26
  /** The connections the built-in migrate and seed groups operate on. */
27
27
  readonly connections: ReadonlyArray<DatabaseConnectionOptions>
@@ -36,8 +36,8 @@ export const createCliAppOptions = (primary: DatabaseConnectionOptions): CliAppO
36
36
  const MIGRATE_RUN = `import { CommandRunner, SubCommand } from "nest-commander"
37
37
  import { MigrationRunnerService } from "@modules/platform/database"
38
38
 
39
- @SubCommand({ name: "run", description: "Push the pending Supabase migrations" })
40
- /** \`cli migrate run\`: delegates schema authority to the managed Supabase db:push script. */
39
+ @SubCommand({ name: "run", description: "Apply the pending db migrations" })
40
+ /** \`cli migrate run\`: delegates schema authority to the managed db:push script (starci app db migrate). */
41
41
  export class RunCli extends CommandRunner {
42
42
  constructor(private readonly migrations: MigrationRunnerService) {
43
43
  super()
@@ -53,7 +53,7 @@ export class RunCli extends CommandRunner {
53
53
  const SEED_RUN = `import { CommandRunner, SubCommand } from "nest-commander"
54
54
  import { SeedRunnerService } from "@modules/platform/database"
55
55
 
56
- @SubCommand({ name: "run", description: "Run the tracked Supabase seed through the application role" })
56
+ @SubCommand({ name: "run", description: "Run the tracked seed through the application role" })
57
57
  /** \`cli seed run\`: executes the one tracked seed through the shared least-privilege EntityManager. */
58
58
  export class RunSeedsCli extends CommandRunner {
59
59
  constructor(private readonly seeds: SeedRunnerService) {
@@ -91,7 +91,7 @@ import { SeedCli } from "./seed.cli"
91
91
  import { RunSeedsCli } from "./subs/run.cli"
92
92
 
93
93
  @Module({ providers: [SeedCli, RunSeedsCli] })
94
- /** The lite seed group: the command and its one tracked Supabase seed runner. */
94
+ /** The lite seed group: the command and its one tracked seed runner. */
95
95
  export class SeedModule {}
96
96
  `;
97
97
 
@@ -144,7 +144,7 @@ function enableLiteCliDatabase(root) {
144
144
  }
145
145
 
146
146
  /**
147
- * Creates the optional lite cli app, its typed CliEnv/readCliEnv boundary and Supabase migrate/seed groups on first `add cli`.
147
+ * Creates the optional lite cli app, its typed CliEnv/readCliEnv boundary and its migrate/seed groups on first `add cli`.
148
148
  * Existing cli trees are never repaired or overwritten: drift remains a check finding.
149
149
  */
150
150
  export function ensureLiteCli({ root, manifest, repo }) {
@@ -36,7 +36,7 @@ export function readComposeGroup({ repoRoot, repo, app, area, noun }) {
36
36
  const skeleton = repo.edition === 'lite' ? 'skeleton-lite/apps/api' : 'skeleton/apps/__app__';
37
37
  const template = path.join(TEMPLATES_DIR, 'be', ...skeleton.split('/'), 'src', 'compose', `${area}.ts`);
38
38
  const text = fs.readFileSync(exists ? file : template, 'utf8').replace(/\r\n/g, '\n');
39
- const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken: VerifySupabaseAccessToken' : '';
39
+ const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken: VerifyAccessToken' : '';
40
40
  const marker = `export const ${area}Modules = (${argument}): Array<Type<unknown> | DynamicModule> => [`;
41
41
  const declarationAt = compositionCode(text).indexOf(`\n${marker}`);
42
42
  const active = declarationAt >= 0;
@@ -72,8 +72,8 @@ const databasePatches = (root) => {
72
72
  "lite add webhook needs the scaffolded platform/database capability",
73
73
  );
74
74
  let index = fs.readFileSync(indexFile, "utf8").replace(/\r\n/g, "\n");
75
- if (!index.includes('export { recordWebhookDelivery } from "./webhook-inbox.client"'))
76
- index = `${index.trimEnd()}\nexport { recordWebhookDelivery } from "./webhook-inbox.client"\n`;
75
+ if (!index.includes('export { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"'))
76
+ index = `${index.trimEnd()}\nexport { recordWebhookDelivery, webhookReceiptInsert } from "./webhook-inbox.client"\n`;
77
77
  if (!index.split("\n").some((line) => line.startsWith("export { ") && line.includes("DatabaseErrorCode") && line.endsWith(' } from "./errors/database.error"')))
78
78
  index = `${index.trimEnd()}\nexport { DatabaseError, DatabaseErrorCode } from "./errors/database.error"\n`;
79
79
  let error = withSqlIdentifierError(fs.readFileSync(errorFile, "utf8").replace(/\r\n/g, "\n"));
@@ -169,7 +169,7 @@ export function addLiteWebhookInbox({
169
169
  validateProcessingOwner({ root, ...values, provider });
170
170
  const Provider = pascalOf(provider);
171
171
  const table = `${snakeOf(provider)}_inbox`;
172
- const migrationsDir = path.join(root, "supabase", "migrations");
172
+ const migrationsDir = path.join(root, "db", "migrations");
173
173
  const migrations = fs.existsSync(migrationsDir)
174
174
  ? fs.readdirSync(migrationsDir).filter((file) => MIGRATION.test(file)).sort(byCodeUnit)
175
175
  : [];
@@ -197,7 +197,7 @@ export function addLiteWebhookInbox({
197
197
  const events = eventFiles(root, provider, renderValues);
198
198
  const planned = [
199
199
  {
200
- relative: `supabase/migrations/${stamp}_${suffix}`,
200
+ relative: `db/migrations/${stamp}_${suffix}`,
201
201
  text: template("migration.sql.tpl", renderValues),
202
202
  },
203
203
  { relative: `${base}/index.ts`, text: template("index.ts.tpl", renderValues) },
@@ -221,10 +221,6 @@ export function addLiteWebhookInbox({
221
221
  relative: `${base}/${provider}-inbox.service.spec.ts`,
222
222
  text: template("service.spec.ts.tpl", renderValues),
223
223
  },
224
- {
225
- relative: `${base}/persistence/${provider}-inbox.sql.ts`,
226
- text: template("sql.ts.tpl", renderValues),
227
- },
228
224
  {
229
225
  relative: `be/src/modules/events/${provider}/${provider}-delivery-recorded.event.ts`,
230
226
  text: template("delivery-recorded.event.ts.tpl", renderValues),
@@ -162,7 +162,7 @@ function appendEntries(entries, context, planned) {
162
162
 
163
163
  /** Planned app migrations append after every active migration, before any generated file is written. */
164
164
  function requireMigrationOrder(repoRoot, planned) {
165
- const directory = path.join(repoRoot, 'supabase', 'migrations');
165
+ const directory = path.join(repoRoot, 'db', 'migrations');
166
166
  const migrations = planned.filter(file => path.dirname(file.absolute) === directory);
167
167
  if (migrations.length === 0) return;
168
168
  const existing = fs.existsSync(directory) ? fs.readdirSync(directory) : [];