From 66dc64fb6b375a0d9a768e1f499d934fd21575e0 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:27:54 +0530 Subject: [PATCH] fix(stack): apply roles.sql once and as postgres --- .../src/command-internal/stack-bootstrap.ts | 2 +- .../stack-catalog-setup.integration.test.ts | 4 +-- .../command-internal/stack-catalog-setup.ts | 21 ++++++++++----- .../command-internal/stack-shadow-cache.ts | 2 +- .../experimental/stack/start/SIDE_EFFECTS.md | 5 ++-- .../experimental/stack/start/start.handler.ts | 5 ++-- .../stack/start/start.integration.test.ts | 26 +++++++++++++++++-- 7 files changed, 48 insertions(+), 17 deletions(-) diff --git a/apps/cli/src/command-internal/stack-bootstrap.ts b/apps/cli/src/command-internal/stack-bootstrap.ts index efb82990b2..ae1d450bbe 100644 --- a/apps/cli/src/command-internal/stack-bootstrap.ts +++ b/apps/cli/src/command-internal/stack-bootstrap.ts @@ -126,7 +126,7 @@ const applyStackMigrateAndSeed = ( export const projectCatalogOverlay = ( toml: Pick, workdir: string, -): StackCatalogSetupInput["overlay"] => ({ +): NonNullable => ({ webhooks: "config", webhooksEnabled: toml.webhooksEnabled, apiAutoExposeNewTables: toml.baseline.apiAutoExposeNewTables, diff --git a/apps/cli/src/command-internal/stack-catalog-setup.integration.test.ts b/apps/cli/src/command-internal/stack-catalog-setup.integration.test.ts index a750f7389b..c5ad4cff40 100644 --- a/apps/cli/src/command-internal/stack-catalog-setup.integration.test.ts +++ b/apps/cli/src/command-internal/stack-catalog-setup.integration.test.ts @@ -128,7 +128,7 @@ describe("stack catalog setup", { timeout: 180_000 }, () => { args: ["--dbname", databaseUrl, "-At"], stdin: Stream.make( new TextEncoder().encode( - "select coalesce(to_regclass('auth.users')::text,'missing'), coalesce(to_regclass('auth.sessions')::text,'missing'), coalesce(to_regclass('storage.objects')::text,'missing'), coalesce(to_regclass('storage.s3_multipart_uploads')::text,'missing'), coalesce(to_regclass('realtime.messages')::text,'missing'), coalesce(to_regclass('realtime.subscription')::text,'missing'), coalesce(to_regclass('public.catalog_overlay')::text,'missing'), coalesce(to_regprocedure('supabase_functions.http_request()')::text,'missing');", + "select coalesce(to_regclass('auth.users')::text,'missing'), coalesce(to_regclass('auth.sessions')::text,'missing'), coalesce(to_regclass('storage.objects')::text,'missing'), coalesce(to_regclass('storage.s3_multipart_uploads')::text,'missing'), coalesce(to_regclass('realtime.messages')::text,'missing'), coalesce(to_regclass('realtime.subscription')::text,'missing'), coalesce(to_regclass('public.catalog_overlay')::text,'missing'), coalesce(to_regprocedure('supabase_functions.http_request()')::text,'missing'), (select tableowner from pg_tables where tablename = 'catalog_overlay');", ), ), stdout: (bytes) => @@ -139,7 +139,7 @@ describe("stack catalog setup", { timeout: 180_000 }, () => { ); expect(query.exitCode, errors.join("")).toBe(0); expect(rows.join("").trim()).toBe( - "users|sessions|storage.objects|storage.s3_multipart_uploads|realtime.messages|realtime.subscription|catalog_overlay|supabase_functions.http_request()", + "users|sessions|storage.objects|storage.s3_multipart_uploads|realtime.messages|realtime.subscription|catalog_overlay|supabase_functions.http_request()|postgres", ); // pg_net queues webhook requests under the image's function policy. const pgNet: Array = []; diff --git a/apps/cli/src/command-internal/stack-catalog-setup.ts b/apps/cli/src/command-internal/stack-catalog-setup.ts index a55c0a0a3c..42d39fe090 100644 --- a/apps/cli/src/command-internal/stack-catalog-setup.ts +++ b/apps/cli/src/command-internal/stack-catalog-setup.ts @@ -64,7 +64,8 @@ export interface StackCatalogSetupInput { readonly databaseServices: ReadonlyArray; readonly selection: ArtifactSelection; }; - readonly overlay: StackCatalogOverlay; + /** Project SQL for a first setup; omitted when only the catalog changes. */ + readonly overlay?: StackCatalogOverlay; } const catalogError = (cause: unknown): StackCatalogSetupError => @@ -185,12 +186,18 @@ const applyCatalog = Effect.fn("StackCatalogSetup.apply")(function* ( .makeTempDirectoryScoped({ prefix: "supabase-stack-catalog-sql-" }) .pipe(Effect.mapError(catalogError)); yield* ensureStackWebhookSchema(session, fs, path, sqlPath); - yield* applyDatabaseOverlay(session, fs, path, input.overlay.workdir, { - webhooksEnabled: input.overlay.webhooksEnabled, - apiAutoExposeNewTables: input.overlay.apiAutoExposeNewTables, - vault: input.overlay.vault, - webhooks: input.overlay.webhooks, - announceRoles: input.overlay.announceRoles, + const { overlay } = input; + if (overlay === undefined) return; + const projectSession = yield* dbConn.connect( + { ...connection, user: "postgres" }, + { isLocal: true, dnsResolver: "native" }, + ); + yield* applyDatabaseOverlay(projectSession, fs, path, overlay.workdir, { + webhooksEnabled: overlay.webhooksEnabled, + apiAutoExposeNewTables: overlay.apiAutoExposeNewTables, + vault: overlay.vault, + webhooks: overlay.webhooks, + announceRoles: overlay.announceRoles, }).pipe(Effect.withSpan("StackCatalogSetup.applyOverlay")); }), ); diff --git a/apps/cli/src/command-internal/stack-shadow-cache.ts b/apps/cli/src/command-internal/stack-shadow-cache.ts index 6c49ddfddd..39dcdfbe83 100644 --- a/apps/cli/src/command-internal/stack-shadow-cache.ts +++ b/apps/cli/src/command-internal/stack-shadow-cache.ts @@ -52,7 +52,7 @@ export const stackShadowCacheEntry = Effect.fn("StackShadowCache.entry")(functio enabled ? pinKey(pin) : Effect.succeed(""); const keyInputs: ShadowCacheKeyInputs = { postgresImage: yield* encodeKeyParts([ - "stack-v3", + "stack-v4", runtime, platform, arch, diff --git a/apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md b/apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md index b65ea3a418..8e9c81b2f6 100644 --- a/apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md @@ -400,8 +400,9 @@ reset of database data removes the record. A service on an external database has The first configured startup prepares the database catalog, temporarily runs configured schema-owning services, creates the `supabase_functions` schema, which migrations must not recreate, applies the -database overlay, and runs project migrations and seeds. Membership changes -apply needed catalog and webhook setup without replaying project migrations or seeds. An unchanged +database overlay (`roles.sql`, vault, API grants) as `postgres`, like legacy `start`, and runs project +migrations and seeds. Membership changes apply needed catalog and webhook setup without replaying +the overlay, project migrations or seeds. An unchanged composition reapplies the webhook setting before activation, first creating the `supabase_functions` schema when the database lacks it. diff --git a/apps/cli/src/commands/experimental/stack/start/start.handler.ts b/apps/cli/src/commands/experimental/stack/start/start.handler.ts index 1ab862f09c..0e628095c1 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.handler.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.handler.ts @@ -1106,8 +1106,9 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags databaseServices: catalogDatabaseServices(requested), selection: config.selection, }, - overlay: projectCatalogOverlay(toml, target.projectRoot), - ...(migrations === undefined ? {} : { migrations }), + ...(migrations === undefined + ? {} + : { overlay: projectCatalogOverlay(toml, target.projectRoot), migrations }), }).pipe( Effect.tapError((error) => starting.fail(error.message)), Effect.mapError(stackError), diff --git a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts index 4e8a203abf..4a73319d07 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts @@ -342,6 +342,7 @@ const fakeStack = (compositionStart?: Stack["composition"]["start"]) => { let hostDestroyed = 0; let composed = 0; let catalogApplied = 0; + const catalogOverlays: Array = []; let compositionStarts = 0; let lifecycle: "stopped" | "running" = "stopped"; let activations = new Map(); @@ -498,8 +499,12 @@ const fakeStack = (compositionStart?: Stack["composition"]["start"]) => { get compositionStarts() { return compositionStarts; }, - applyCatalog() { + get catalogOverlays() { + return catalogOverlays; + }, + applyCatalog(overlay: boolean) { catalogApplied += 1; + catalogOverlays.push(overlay); }, get savedCredentials() { return savedCredentials; @@ -635,7 +640,7 @@ const layers = ( ciUnsetLayer, Layer.succeed(CliArgs, { args: ["stack", "start"] }), Layer.succeed(StackCatalogSetup, { - apply: () => Effect.sync(() => fixture.applyCatalog()), + apply: (input) => Effect.sync(() => fixture.applyCatalog(input.overlay !== undefined)), }), Layer.succeed(DbConnection, { connect: () => Effect.scoped(Effect.succeed(session)) }), Layer.succeed(YesFlag, false), @@ -1404,6 +1409,23 @@ describe("experimental stack start", () => { }).pipe(Effect.provide([BunServices.layer, BunCrypto.layer])), ); + it.effect("applies the project overlay once when a later start adds a service", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "stack-start-overlay-once-" }); + yield* fs.makeDirectory(`${root}/supabase`, { recursive: true }); + yield* fs.writeFileString(`${root}/supabase/config.toml`, 'project_id = "overlay-once"\n'); + const fixture = fakeStack(); + + yield* stackStart(flags(["storage"])).pipe(Effect.provide(layers(root, fixture))); + yield* fixture.stack.composition.stop; + yield* stackStart(flags()).pipe(Effect.provide(layers(root, fixture))); + + expect(fixture.members.some(({ service }) => service === "storage")).toBe(true); + expect(fixture.catalogOverlays).toEqual([true, false]); + }).pipe(Effect.provide([BunServices.layer, BunCrypto.layer])), + ); + it.effect("creates the supabase_functions schema once when restarting a stack without it", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem;