@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g06dbe5b

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 (120) hide show
  1. package/dist/PostgresBackendDriver.d.ts +49 -2
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +40 -0
  4. package/dist/{src-BbFOPJ1S.js → auth-users-columns-CBEOeYqa.js} +1118 -220
  5. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  6. package/dist/backup/backup-service.d.ts +10 -1
  7. package/dist/backup/pg-tools.d.ts +47 -0
  8. package/dist/backup-service-Bww-Lg0s.js +8999 -0
  9. package/dist/backup-service-Bww-Lg0s.js.map +1 -0
  10. package/dist/cli-helpers.d.ts +96 -1
  11. package/dist/connection-BuZ97wsr.js +250 -0
  12. package/dist/connection-BuZ97wsr.js.map +1 -0
  13. package/dist/connection.d.ts +42 -0
  14. package/dist/data-transformer.d.ts +7 -2
  15. package/dist/data_driver-ULAyJEi9.js +193 -0
  16. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  17. package/dist/ensure-collection-policies-B_JMGa5K.js +57 -0
  18. package/dist/ensure-collection-policies-B_JMGa5K.js.map +1 -0
  19. package/dist/ensure-collection-tables-DzeTEvMv.js +800 -0
  20. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  21. package/dist/index.es.js +2460 -10233
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  24. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  25. package/dist/schema/auth-schema.d.ts +185 -144
  26. package/dist/schema/auth-users-columns.d.ts +97 -0
  27. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  28. package/dist/schema/doctor.d.ts +23 -25
  29. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  30. package/dist/schema/ensure-collection-tables.d.ts +99 -3
  31. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  32. package/dist/schema/generate-postgres-ddl-logic.d.ts +185 -3
  33. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  34. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  35. package/dist/schema/introspect-db-logic.d.ts +94 -5
  36. package/dist/schema/introspect-db-queries.d.ts +119 -0
  37. package/dist/schema/introspect-db-structure.d.ts +263 -0
  38. package/dist/schema/introspect-db-types.d.ts +11 -0
  39. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  40. package/dist/schema/search-column.d.ts +248 -0
  41. package/dist/security/rls-enforcement.d.ts +61 -5
  42. package/dist/services/FetchService.d.ts +38 -8
  43. package/dist/services/RelationService.d.ts +54 -1
  44. package/dist/services/channel-bus/index.d.ts +1 -7
  45. package/dist/services/collection-helpers.d.ts +62 -2
  46. package/dist/services/dataService.d.ts +8 -1
  47. package/dist/services/realtimeService.d.ts +131 -21
  48. package/dist/services/row-pipeline.d.ts +1 -1
  49. package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
  50. package/dist/src-C_wvdMnl.js.map +1 -0
  51. package/dist/utils/connection-string.d.ts +29 -0
  52. package/dist/utils/drizzle-conditions.d.ts +129 -6
  53. package/dist/utils/pg-error-utils.d.ts +35 -0
  54. package/dist/websocket-D1qbmLZ2.js +587 -0
  55. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  56. package/package.json +17 -16
  57. package/src/PostgresAdapter.ts +21 -2
  58. package/src/PostgresBackendDriver.ts +176 -6
  59. package/src/PostgresBootstrapper.ts +245 -37
  60. package/src/auth/ensure-tables.ts +357 -81
  61. package/src/auth/services.ts +93 -7
  62. package/src/backup/backup-cli.ts +41 -2
  63. package/src/backup/backup-service.ts +38 -5
  64. package/src/backup/pg-tools.ts +96 -3
  65. package/src/cli-helpers.ts +199 -10
  66. package/src/cli.ts +276 -56
  67. package/src/collections/validate-relations.ts +136 -14
  68. package/src/connection.ts +73 -0
  69. package/src/data-transformer.ts +129 -20
  70. package/src/databasePoolManager.ts +5 -2
  71. package/src/history/ensure-history-table.ts +7 -0
  72. package/src/schema/auth-schema.ts +46 -19
  73. package/src/schema/auth-users-columns.ts +131 -0
  74. package/src/schema/doctor-cli.ts +12 -63
  75. package/src/schema/doctor-policy-checks.ts +105 -0
  76. package/src/schema/doctor.ts +135 -76
  77. package/src/schema/ensure-collection-policies.ts +105 -0
  78. package/src/schema/ensure-collection-tables.test.ts +105 -9
  79. package/src/schema/ensure-collection-tables.ts +572 -42
  80. package/src/schema/generate-drizzle-schema-logic.ts +169 -43
  81. package/src/schema/generate-postgres-ddl-logic.ts +670 -29
  82. package/src/schema/generate-postgres-ddl.ts +25 -2
  83. package/src/schema/generated-schema-staleness.ts +169 -0
  84. package/src/schema/introspect-db-constraints.ts +385 -0
  85. package/src/schema/introspect-db-inference.ts +18 -8
  86. package/src/schema/introspect-db-logic.ts +446 -100
  87. package/src/schema/introspect-db-queries.ts +326 -0
  88. package/src/schema/introspect-db-structure.ts +670 -0
  89. package/src/schema/introspect-db-types.ts +56 -0
  90. package/src/schema/introspect-db.ts +37 -80
  91. package/src/schema/introspect-runtime.test.ts +56 -8
  92. package/src/schema/introspect-runtime.ts +31 -9
  93. package/src/schema/non-sql-collections.test.ts +131 -0
  94. package/src/schema/rls-bootstrap-sql.ts +288 -0
  95. package/src/schema/search-column.ts +643 -0
  96. package/src/security/anonymous-grants.test.ts +4 -2
  97. package/src/security/policy-drift.test.ts +11 -3
  98. package/src/security/rls-enforcement.ts +149 -6
  99. package/src/services/BranchService.ts +71 -28
  100. package/src/services/FetchService.ts +265 -122
  101. package/src/services/PersistService.ts +57 -7
  102. package/src/services/RelationService.ts +358 -114
  103. package/src/services/channel-bus/index.ts +0 -9
  104. package/src/services/channel-history.ts +14 -0
  105. package/src/services/channel-presence.ts +13 -0
  106. package/src/services/collection-helpers.ts +123 -4
  107. package/src/services/dataService.ts +8 -1
  108. package/src/services/realtimeService.ts +346 -81
  109. package/src/services/row-pipeline.ts +1 -1
  110. package/src/utils/connection-string.ts +58 -0
  111. package/src/utils/drizzle-conditions.ts +396 -29
  112. package/src/utils/pg-error-utils.ts +73 -3
  113. package/src/websocket.ts +130 -24
  114. package/dist/chunk-DSJWtz9O.js +0 -40
  115. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  116. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  117. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  118. package/dist/src-BbFOPJ1S.js.map +0 -1
  119. package/dist/src-Zqwaw3P5.js.map +0 -1
  120. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -37,7 +37,7 @@ import { ensureHistoryTableExists } from "./history/ensure-history-table";
37
37
  import { patchPgArrayNullSafety } from "./utils/pg-array-null-patch";
38
38
  import { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from "./schema/introspect-runtime";
39
39
  import { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from "./schema/dynamic-tables";
40
- import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
40
+ import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
41
41
  import { provisionTriggerCdc, type CdcTableRef } from "./services/cdc/trigger-cdc";
42
42
  import { collectJunctionLinks } from "./services/cdc/junction-tables";
43
43
  import { createChannelBus, resolveChannelBusSetting } from "./services/channel-bus";
@@ -102,6 +102,40 @@ export interface PostgresDriverInternals {
102
102
 
103
103
  // Re-export from shared CLI error utilities
104
104
  import { isEconnrefused } from "./cli-errors";
105
+ import { classifyConnectFailure } from "./utils/pg-error-utils";
106
+
107
+ /**
108
+ * Which table name the boot-time drift check should look for, for one collection.
109
+ *
110
+ * A declared `table` IS the table name, not a hint to be second-guessed. This
111
+ * used to ask the registry whether it had indexed the declared name and fall
112
+ * back to the SLUG when it had not — but "the registry does not know this table"
113
+ * is exactly the condition the drift check exists to report, so the fallback
114
+ * fired precisely when it was most harmful.
115
+ *
116
+ * A collection with `slug: "usage-daily", table: "usage_daily"` was reported as
117
+ * missing table `usage-daily`: a name that does not exist, should never exist,
118
+ * and that nobody can find by looking. Worse, the remediation the caller prints
119
+ * says to run `rebase db push` — which would then CREATE that invented table
120
+ * beside the correct one, the same "second copy" hazard the misplaced-schema
121
+ * branch further down exists to prevent. Seen in production, where a correctly
122
+ * migrated database reported drift on every boot.
123
+ *
124
+ * The slug is used only when nothing was declared, which is the config shape
125
+ * where the slug genuinely is the table name.
126
+ *
127
+ * Exported for its own test: the caller needs a live pool and a real database,
128
+ * and this is the part that was wrong.
129
+ */
130
+ export function resolveDriftCheckName(
131
+ col: CollectionConfig,
132
+ registeredTableNames: string[]
133
+ ): string {
134
+ const declaredTable = isRelationalCollectionConfig(col) ? col.table : undefined;
135
+ return declaredTable
136
+ ?? registeredTableNames.find((k) => k === col.slug)
137
+ ?? col.slug;
138
+ }
105
139
 
106
140
  /**
107
141
  * Default PostgreSQL bootstrapper.
@@ -260,9 +294,45 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
260
294
  throw new Error(`Cannot connect to PostgreSQL at ${hostInfo}: connection refused. Is the database running?`);
261
295
  }
262
296
 
263
- // For other errors (timeouts, auth failures, etc.) warn but continue —
264
- // the pool may recover on subsequent attempts.
265
- logger.error("❌ Failed to connect to PostgreSQL", { error: err });
297
+ /*
298
+ * Everything else.
299
+ *
300
+ * Two problems with what used to happen here. First, the logged
301
+ * error was Drizzle's wrapper — `Failed query: SELECT 1` and a
302
+ * stack through drizzle internals — while the sentence that
303
+ * actually says what is wrong ("password authentication failed
304
+ * for user …", "database … does not exist") sits in `.cause`
305
+ * and was never printed. A developer with a typo in their
306
+ * DATABASE_URL got two walls of stack trace and no cause.
307
+ *
308
+ * Second, "continuing… the pool may recover" is only true for
309
+ * a transient fault. A wrong password or a missing database is
310
+ * settled: the next query fails the same way, so the process
311
+ * died seconds later anyway — after printing a reassurance.
312
+ * Those now fail here, where the message can be about the
313
+ * cause rather than about whichever query ran next.
314
+ */
315
+ const { fatal, reason, code } = classifyConnectFailure(err);
316
+ const detail = code ? ` [${code}]` : "";
317
+
318
+ if (fatal) {
319
+ logger.error(
320
+ `\n` +
321
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
322
+ ` ❌ PostgreSQL refused the connection\n` +
323
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
324
+ `\n` +
325
+ ` ${reason}${detail}\n` +
326
+ `\n` +
327
+ ` The server is reachable, so this is the credentials or the\n` +
328
+ ` database name in DATABASE_URL — check them in your .env.\n` +
329
+ `\n` +
330
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`
331
+ );
332
+ throw new Error(`PostgreSQL refused the connection: ${reason}${detail}`);
333
+ }
334
+
335
+ logger.error(`❌ Failed to connect to PostgreSQL: ${reason}${detail}`, { error: err });
266
336
  logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
267
337
  }
268
338
 
@@ -294,20 +364,33 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
294
364
  // bypasses RLS. Detect the posture and, when privileged, provision
295
365
  // the restricted `rebase_user` role and route authenticated
296
366
  // requests (reads AND writes) through it. The server context (base
297
- // driver / dataAsAdmin / auth flows) stays on the owner connection
298
- // and bypasses. Default-on: a privileged connection that cannot be
367
+ // driver / auth flows / `rebase.sql`) stays on the owner connection
368
+ // and bypasses; `rebase.dataAsAdmin` does NOT — it is scoped with
369
+ // `withAuth({ uid: "service", roles: ["admin"] })` at boot, so it
370
+ // runs as `rebase_user` and its policies are evaluated.
371
+ // Default-on: a privileged connection that cannot be
299
372
  // isolated fails the boot rather than serving unenforced requests.
300
373
  {
301
374
  const runSql: RawSqlRunner = async (text) => {
302
375
  const res = await schemaAwareDb.execute(sql.raw(text));
303
376
  return (res.rows ?? []) as Record<string, unknown>[];
304
377
  };
378
+ // Said before anything else touches the schema: if the role and
379
+ // a schema share a name, unqualified SQL from any tool that does
380
+ // not pin `search_path` has been landing in the wrong place, and
381
+ // that is worth knowing before reading the drift report below.
382
+ await warnOnRoleSchemaCollision(runSql);
383
+
305
384
  const posture = await detectConnectionPosture(runSql);
306
385
  if (posture.privileged) {
307
386
  const collectionSchemas = registry.getCollections()
308
387
  .map((c) => (c as { schema?: string }).schema)
309
388
  .filter((s): s is string => typeof s === "string");
310
- await ensureAppRole(runSql, ["public", "rebase", "auth", ...collectionSchemas]);
389
+ // `auth` is deliberately absent: the RLS helpers moved into
390
+ // `rebase`, and granting USAGE on a schema Rebase does not
391
+ // own would, on a Supabase database, hand the end-user role
392
+ // access to theirs.
393
+ await ensureAppRole(runSql, ["public", "rebase", ...collectionSchemas]);
311
394
  driver.rlsUserRole = REBASE_USER_ROLE;
312
395
  realtimeService.rlsUserRole = REBASE_USER_ROLE;
313
396
  logger.info(`🔐 RLS enforcement active: authenticated requests run as "${REBASE_USER_ROLE}" (connection "${posture.role}" bypasses RLS: ${posture.superuser ? "superuser" : posture.bypassRLS ? "BYPASSRLS" : "table owner"})`);
@@ -336,6 +419,11 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
336
419
  // a rule that reads as "signed in only" but is true for every
337
420
  // caller grants the data away rather than hiding it.
338
421
  warnOnAnonymousGrants(registry.getCollections() as never);
422
+
423
+ // Raw policy SQL written against the pre-1.0 helper schema. It
424
+ // is rewritten on compile, so this is the only place the project
425
+ // is ever told the spelling moved.
426
+ warnOnLegacyRlsFunctions(registry.getCollections() as never);
339
427
  }
340
428
 
341
429
  // Ensure branch metadata table exists when branching is available
@@ -487,22 +575,28 @@ table: link.table });
487
575
  try {
488
576
  const registeredCollections = registry.getCollections();
489
577
  if (registeredCollections.length > 0) {
490
- const schemasToCheck = Array.from(new Set(
491
- registeredCollections.map(c => "schema" in c && c.schema ? c.schema : "public")
492
- ));
493
- const schemasList = schemasToCheck.map(s => `'${s}'`).join(",");
578
+ // Deliberately unfiltered by schema: a table that is missing
579
+ // from where the collection says it lives is very often
580
+ // sitting in another schema entirely (see the misplaced-table
581
+ // report below), and knowing *which* is the whole answer.
494
582
  const result = await schemaAwareDb.execute(sql.raw(`
495
583
  SELECT table_name, table_schema
496
584
  FROM information_schema.tables
497
- WHERE table_schema IN (${schemasList})
585
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
498
586
  AND table_type = 'BASE TABLE'
499
587
  `));
588
+ const tablesByName = new Map<string, string[]>();
589
+ for (const row of result.rows as Array<{ table_name: string; table_schema: string }>) {
590
+ const schemas = tablesByName.get(row.table_name) ?? [];
591
+ schemas.push(row.table_schema);
592
+ tablesByName.set(row.table_name, schemas);
593
+ }
500
594
  const dbTables = new Set(
501
595
  (result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>
502
596
  r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
503
597
  )
504
598
  );
505
- const missing: Array<{ slug: string; table: string }> = [];
599
+ const missing: Array<{ slug: string; table: string; foundIn: string[] }> = [];
506
600
  for (const col of registeredCollections) {
507
601
  // Auth owns its table and creates it later in this same
508
602
  // boot (initializeAuth → ensureAuthTablesExist), so it is
@@ -513,41 +607,72 @@ table: link.table });
513
607
  if ((col as { auth?: { enabled?: boolean } }).auth?.enabled) continue;
514
608
 
515
609
  const schemaName = "schema" in col && col.schema ? col.schema : "public";
516
- const declaredTable = isRelationalCollectionConfig(col) ? col.table : undefined;
517
- const tableName = registry.hasTableForCollection(
518
- declaredTable ?? col.slug
519
- )
520
- ? (declaredTable ?? col.slug)
521
- : col.slug;
522
- // Resolve the actual table name the registry stored
523
- const resolvedTable = registry.getTableNames().find((k) =>
524
- k === tableName ||
525
- k === col.slug
526
- );
527
- const checkName = resolvedTable ?? tableName;
610
+ const checkName = resolveDriftCheckName(col, registry.getTableNames());
528
611
  const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
529
612
  if (!dbTables.has(fullCheckName)) {
530
613
  // Report what was actually looked up: an unqualified
531
614
  // "users" sends people hunting for public.users.
532
615
  missing.push({ slug: col.slug,
533
- table: fullCheckName });
616
+ table: fullCheckName,
617
+ foundIn: (tablesByName.get(checkName) ?? []).filter(s => s !== schemaName) });
534
618
  }
535
619
  }
536
620
  if (missing.length > 0) {
537
621
  const lines = missing.map(
538
- m => ` • collection "${m.slug}" → table "${m.table}"`
622
+ m => ` • collection "${m.slug}" → table "${m.table}"` +
623
+ (m.foundIn.length > 0
624
+ ? ` — but a table of that name exists in ${m.foundIn.map(s => `"${s}"`).join(", ")}`
625
+ : "")
539
626
  );
627
+ // A table that exists under the same name in another
628
+ // schema is not ordinary drift: it is almost always this
629
+ // framework's own `search_path` hazard. Postgres resolves
630
+ // unqualified SQL through `"$user", public`, and this
631
+ // project creates a schema named `rebase` while every
632
+ // template names the database role `rebase` too — so an
633
+ // unqualified CREATE TABLE (a hand-written migration, the
634
+ // SQL editor, drizzle-kit) lands in `rebase`, and the
635
+ // runtime, which now pins `search_path=public`, cannot see
636
+ // it. Say so, because "missing table" sends people to
637
+ // re-run a push that will create a *second* copy.
638
+ const misplaced = missing.filter(m => m.foundIn.length > 0);
639
+ const misplacedHelp = misplaced.length === 0 ? [] : [
640
+ " Those tables exist — in the wrong schema. Unqualified SQL run by a",
641
+ " role whose name matches a schema (`rebase` is both, by default)",
642
+ " resolves through search_path's \"$user\" and creates there. Move them:",
643
+ ...misplaced.map(m =>
644
+ ` ALTER TABLE "${m.foundIn[0]}"."${m.table.split(".").pop()}" SET SCHEMA "${m.table.includes(".") ? m.table.split(".")[0] : "public"}";`
645
+ ),
646
+ " and qualify the SQL that created them, or pin search_path in DATABASE_URL",
647
+ " (`?options=-c%20search_path%3Dpublic`).",
648
+ ""
649
+ ];
650
+ // This runtime creates collection tables (and their RLS)
651
+ // at boot unless REBASE_MIGRATE_ON_BOOT=none, so a drift
652
+ // this late means that step was disabled, could not run,
653
+ // or failed — the guidance names a path for both a managed
654
+ // tenant (whose in-cluster database `pnpm db:push` cannot
655
+ // reach) and a self-host. Naming only the pnpm scripts, as
656
+ // this once did, told a managed operator to run a command
657
+ // that structurally cannot touch their database.
540
658
  logger.warn([
541
659
  "",
542
- "┌──────────────────────────────────────────────────────────────┐",
543
- "│ ⚠️ SCHEMA DRIFT — Missing tables in database │",
544
- "├──────────────────────────────────────────────────────────────┤",
545
- ...lines.map(l => `│ ${l.padEnd(60)}│`),
546
- "├──────────────────────────────────────────────────────────────┤",
547
- "│ Run one of: │",
548
- "│ pnpm db:push (dev — fast, no migration files) │",
549
- "│ pnpm db:migrate (prod — creates migration files) │",
550
- "└──────────────────────────────────────────────────────────────┘",
660
+ "⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:",
661
+ ...lines,
662
+ "",
663
+ ...misplacedHelp,
664
+ " This runtime applies the collection schema at boot unless",
665
+ " REBASE_MIGRATE_ON_BOOT=none. Check the \"Collection schema\" / \"policies\"",
666
+ " log lines above — this drift means that step was off, skipped, or failed.",
667
+ " • Managed cloud: redeploy with REBASE_MIGRATE_ON_BOOT unset or",
668
+ " \"ensure\"; the runtime applies the schema to the tenant DB.",
669
+ " If the log above says the driver does not implement collection-table",
670
+ " creation, THIS driver is too old to do it. A driver is installed from",
671
+ " your bundle's dependencies, not supplied by the platform image, so a",
672
+ " newer runtime will not update it: bump \"@rebasepro/server-postgres\"",
673
+ " in your project's package.json and redeploy.",
674
+ " • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)",
675
+ " against DATABASE_URL.",
551
676
  ""
552
677
  ].join("\n"));
553
678
  }
@@ -711,7 +836,90 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
711
836
  collections as Parameters<typeof ensureCollectionTables>[1],
712
837
  log
713
838
  );
714
- return { applied: plan.actions.length };
839
+ for (const failure of plan.failures) {
840
+ logger.warn(
841
+ failure.kind === "comment-column"
842
+ // The stamp records which `search` block the generated
843
+ // column was built from. Without it the next boot cannot
844
+ // tell a changed block from an unchanged one and adopts
845
+ // the column again instead of refusing.
846
+ ? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, ` +
847
+ `but a later change to the \`search\` block will not be detected: ${failure.error}`
848
+ : `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the ` +
849
+ `collection still serves, but rows are not policed by this constraint: ${failure.error}`
850
+ );
851
+ }
852
+ return { applied: plan.actions.length - plan.failures.length };
853
+ },
854
+
855
+ /**
856
+ * Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the
857
+ * `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned
858
+ * database serves data instead of denying every user-context read.
859
+ *
860
+ * The companion to {@link ensureCollectionSchema}: that creates the
861
+ * tables, this makes them servable. Boot runs it *after* auth
862
+ * initialization, because the generated policies call `auth.uid()` /
863
+ * `auth.roles()`, and `CREATE POLICY` validates those functions exist.
864
+ *
865
+ * Runs through the same drizzle handle, one statement at a time (that
866
+ * handle speaks the extended query protocol, which rejects multi-command
867
+ * strings). Failures are per-table and non-fatal: a table left un-policed
868
+ * stays RLS-enabled, so it denies rather than leaks.
869
+ */
870
+ async ensureCollectionPolicies(
871
+ collections: unknown[],
872
+ driverResult: InitializedDriver,
873
+ log?: (message: string) => void
874
+ ): Promise<{ applied: number }> {
875
+ const internals = driverResult.internals as PostgresDriverInternals;
876
+ const { ensureCollectionPolicies } = await import("./schema/ensure-collection-policies");
877
+ const queryable = {
878
+ async query<T>(text: string): Promise<{ rows: T[] }> {
879
+ const result = await internals.db.execute(sql.raw(text));
880
+ const rows = (result as unknown as { rows?: T[] }).rows;
881
+ return { rows: rows ?? (Array.isArray(result) ? (result as T[]) : []) };
882
+ }
883
+ };
884
+ const outcome = await ensureCollectionPolicies(
885
+ queryable,
886
+ collections as CollectionConfig[],
887
+ log
888
+ );
889
+
890
+ for (const skip of outcome.skipped) {
891
+ logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
892
+ }
893
+ for (const failure of outcome.failures) {
894
+ logger.warn(
895
+ `🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`
896
+ );
897
+ }
898
+
899
+ // Retire the pre-1.0 `auth` schema now that the policies above no
900
+ // longer call into it. Deliberately after, and deliberately quiet:
901
+ // Postgres refuses to drop a function an RLS policy still
902
+ // references, so on a database where some table has not been
903
+ // recompiled yet this is expected to fail and succeed on a later
904
+ // boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it
905
+ // off a Supabase `auth` schema.
906
+ try {
907
+ const { dropLegacyAuthSchema } = await import("./schema/rls-bootstrap-sql");
908
+ await dropLegacyAuthSchema(
909
+ async (text) => {
910
+ const res = await internals.db.execute(sql.raw(text));
911
+ return (res.rows ?? []) as Record<string, unknown>[];
912
+ },
913
+ { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
914
+ );
915
+ } catch (err) {
916
+ logger.info(
917
+ "Left the legacy `auth` schema in place: " +
918
+ (err instanceof Error ? err.message : String(err))
919
+ );
920
+ }
921
+
922
+ return { applied: outcome.policiesApplied };
715
923
  },
716
924
 
717
925
  getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {