Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/cli/docs/stack-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,9 @@ are quoted so that sourcing the file performs no shell expansion. The exported v
`PUBLISHABLE_KEY`, `SECRET_KEY`, `ANON_KEY`, and `SERVICE_ROLE_KEY`; `REST_URL` and
`FUNCTIONS_URL` are `<API_URL>/rest/v1` and `<API_URL>/functions/v1`, `MCP_URL` is `<API_URL>/mcp`,
whose port is assigned per project, and `DB_URL` uses the `postgres` role with the saved database
password, URI-encoded, and no query string. `INBUCKET_URL` is also exported alongside
`MAILPIT_URL`, with the same value, as a deprecated alias. `start` and `status` text output shows
only the publishable and secret keys; `ANON_KEY` and `SERVICE_ROLE_KEY` appear only in JSON `env`
and `status --env`. `start` and `status` JSON/stream-JSON results include this same connection map
password, URI-encoded, and no query string. `start` and `status` text output shows only the
publishable and secret keys; `ANON_KEY` and `SERVICE_ROLE_KEY` appear only in JSON `env` and
`status --env`. `start` and `status` JSON/stream-JSON results include this same connection map
under `env`; `status --env` remains the dotenv/variable-map export, and `--override-name` only
applies there. `--override-name` accepts repeated or comma-separated `EXPORTED_VARIABLE=NAME`
entries, requires `--env`, and rejects unknown variables, invalid names, and collisions. The
Expand Down
3 changes: 1 addition & 2 deletions apps/cli/docs/templates/examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ supabase-status:
GraphQL URL: http://127.0.0.1:54321/graphql/v1
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
Studio URL: http://127.0.0.1:54323
Inbucket URL: http://127.0.0.1:54324
Mailpit URL: http://127.0.0.1:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
Expand All @@ -95,7 +95,6 @@ supabase-status:
API_URL="http://127.0.0.1:54321"
DB_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
GRAPHQL_URL="http://127.0.0.1:54321/graphql/v1"
INBUCKET_URL="http://127.0.0.1:54324"
JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long"
SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
STUDIO_URL="http://127.0.0.1:54323"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,7 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu

// Not running → bring up the container natively. `context` (loaded eagerly above) is threaded
// through as `preloadedContext` so this call reuses it instead of calling
// `loadLocalProjectContext` a second time, which would double-print deprecated-config-section
// warnings.
// `loadLocalProjectContext` a second time.
const inputs = yield* buildLocalDbContainerInputs(
spawner,
cliSettings.workdir,
Expand Down
5 changes: 2 additions & 3 deletions apps/cli/src/command-internal/db-config.toml-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,8 @@ export interface DbTomlValues {
/** `[db] major_version`, default 17. */
readonly majorVersion: number;
/**
* `[db] orioledb_version` (env-expanded); the deprecated `[experimental] orioledb_version` is
* already promoted into this path by `normalizeDeprecatedOrioleDBVersion`. Set on a 15/17
* project to rewrite the Postgres image to the OrioleDB tag; `None` for a vanilla project.
* `[db] orioledb_version` (env-expanded). Set on a 15/17 project to rewrite the Postgres image
* to the OrioleDB tag; `None` for a vanilla project.
*/
readonly orioledbVersion: Option.Option<string>;
/**
Expand Down
173 changes: 72 additions & 101 deletions apps/cli/src/command-internal/db-config.toml-read.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1654,59 +1654,35 @@ describe("readDbToml", () => {
);
});

it.effect(
"falls back to a non-empty legacy experimental.orioledb_version when db.orioledb_version is absent",
() => {
return withConfig(
[
"[db]",
"major_version = 17",
"[experimental]",
'orioledb_version = "15.1.0.150"',
"",
].join("\n"),
).pipe(
Effect.flatMap(read),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("15.1.0.150");
}),
),
);
},
);

it.effect(
"falls back to a non-empty legacy experimental.orioledb_version when db.orioledb_version is empty",
() => {
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = ""',
"[experimental]",
'orioledb_version = "15.1.0.150"',
"",
].join("\n"),
).pipe(
Effect.flatMap(read),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("15.1.0.150");
}),
),
);
},
);
it.effect("rejects a non-empty experimental.orioledb_version, naming db.orioledb_version", () => {
return withConfig(
["[db]", "major_version = 17", "[experimental]", 'orioledb_version = "15.1.0.150"', ""].join(
"\n",
),
).pipe(
Effect.flatMap(read),
Effect.exit,
Effect.tap((exit) =>
Effect.sync(() => {
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(Cause.pretty(exit.cause)).toContain(
"experimental.orioledb_version in supabase/config.toml is no longer supported. Use db.orioledb_version instead.",
);
}
}),
),
);
});

it.effect("prefers an explicit db.orioledb_version over a non-empty legacy value", () => {
it.effect("ignores an empty experimental.orioledb_version", () => {
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = "17.0.0.1"',
"[experimental]",
'orioledb_version = "15.1.0.150"',
'orioledb_version = ""',
"",
].join("\n"),
).pipe(
Expand All @@ -1719,60 +1695,52 @@ describe("readDbToml", () => {
);
});

it.effect(
"a matched remote's legacy experimental.orioledb_version overrides the base db.orioledb_version",
() => {
// Matches `@supabase/config`'s loader precedence: each `[remotes.*]` block's own legacy
// value is promoted before the remote merge, so it can override the base canonical value.
const ref = "abcdefghijklmnopqrst";
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = "A"',
"[remotes.prod]",
`project_id = "${ref}"`,
"[remotes.prod.experimental]",
'orioledb_version = "B"',
"",
].join("\n"),
).pipe(
Effect.flatMap((dir) => readRef(dir, ref)),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("B");
}),
),
);
},
);
it.effect("a matched remote's db.orioledb_version overrides the base db.orioledb_version", () => {
const ref = "abcdefghijklmnopqrst";
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = "A"',
"[remotes.prod]",
`project_id = "${ref}"`,
"[remotes.prod.db]",
'orioledb_version = "B"',
"",
].join("\n"),
).pipe(
Effect.flatMap((dir) => readRef(dir, ref)),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("B");
}),
),
);
});

it.effect(
"SUPABASE_DB_ORIOLEDB_VERSION beats a matched remote's legacy experimental.orioledb_version",
() => {
const ref = "abcdefghijklmnopqrst";
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = "A"',
"[remotes.prod]",
`project_id = "${ref}"`,
"[remotes.prod.experimental]",
'orioledb_version = "B"',
"",
].join("\n"),
).pipe(
Effect.flatMap((dir) => readRef(dir, ref)),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("env-value");
}),
),
withEnv({ SUPABASE_DB_ORIOLEDB_VERSION: "env-value" }),
);
},
);
it.effect("SUPABASE_DB_ORIOLEDB_VERSION beats a matched remote's db.orioledb_version", () => {
const ref = "abcdefghijklmnopqrst";
return withConfig(
[
"[db]",
"major_version = 17",
'orioledb_version = "A"',
"[remotes.prod]",
`project_id = "${ref}"`,
"[remotes.prod.db]",
'orioledb_version = "B"',
"",
].join("\n"),
).pipe(
Effect.flatMap((dir) => readRef(dir, ref)),
Effect.tap((v) =>
Effect.sync(() => {
expect(Option.getOrNull(v.orioledbVersion)).toBe("env-value");
}),
),
withEnv({ SUPABASE_DB_ORIOLEDB_VERSION: "env-value" }),
);
});

it.effect("warns (does not fail) for an unset S3 env on an OrioleDB project", () => {
const writes: Array<string> = [];
Expand Down Expand Up @@ -2546,8 +2514,11 @@ describe("readDbToml auth.Enabled validation", () => {
it.effect("exempts apple/google from the external secret requirement", () =>
succeeds(["[auth.external.apple]", "enabled = true", 'client_id = "a"']),
);
it.effect("never validates the deprecated linkedin/slack providers", () =>
succeeds(["[auth.external.linkedin]", "enabled = true"]),
it.effect("rejects the removed linkedin provider, naming linkedin_oidc", () =>
failsWith(
["[auth.external.linkedin]", "enabled = true"],
"[auth.external.linkedin] in supabase/config.toml is no longer supported. Use [auth.external.linkedin_oidc] instead.",
),
);

it.effect("rejects an enabled firebase third_party without project_id", () =>
Expand Down
4 changes: 0 additions & 4 deletions apps/cli/src/command-internal/local-config-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,6 @@ export function resolveAuthSms(sms: CliConfig["auth"]["sms"]): CliConfig["auth"]
};
}

/** Deleted external providers, warned on if still enabled, never validated. */
const DEPRECATED_EXTERNAL_PROVIDERS = new Set(["linkedin", "slack"]);

/** Matches `GotrueExternalProviderInput`'s fields, kept separate to avoid a command-specific import. */
export interface ResolvedAuthExternalProvider {
readonly enabled: boolean;
Expand Down Expand Up @@ -674,7 +671,6 @@ export function resolveAuthExternalProviders(
const decodedProviders = new Map(Object.entries(external));
const providerNames = new Set([...Object.keys(externalDoc ?? {}), "apple"]);
for (const name of providerNames) {
if (DEPRECATED_EXTERNAL_PROVIDERS.has(name)) continue;
const provider = decodedProviders.get(name);
const rawProvider = provider === undefined ? asRecord(externalDoc?.[name]) : undefined;
if (provider === undefined && rawProvider === undefined) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,12 +700,6 @@ describe("resolveLocalConfigValues", () => {
expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).not.toThrow();
});

it("skips deprecated linkedin/slack providers", () => {
const config = baseConfig();
const document = { auth: { external: { slack: { enabled: true } } } };
expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).not.toThrow();
});

it("does not validate a disabled unmodeled external provider", () => {
const config = baseConfig();
const document = { auth: { external: { custom: { enabled: false } } } };
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/command-internal/resolved-config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export const describeConfigLoadFailure = (cause: unknown): string =>
"_tag" in cause &&
(cause._tag === "CliConfigValueError" ||
cause._tag === "CliConfigFlagConflictError" ||
cause._tag === "CliConfigLoadError") &&
cause._tag === "CliConfigLoadError" ||
cause._tag === "CliConfigRemovedKeyError" ||
cause._tag === "CliConfigRemovedEnvError") &&
"message" in cause &&
typeof cause.message === "string"
? cause.message
Expand Down
7 changes: 2 additions & 5 deletions apps/cli/src/command-internal/status-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { resolveLocalConfigValues, type LocalConfigValues } from "./local-config
/**
* A status output field: the dotted key `--override-name <key>=<name>` matches against
* (`fieldKey`), and its default output env-var name (`defaultName`). Deprecated fields
* (`inbucket`, `jwt_secret`, `anon_key`, `service_role_key`) are still emitted.
* (`jwt_secret`, `anon_key`, `service_role_key`) are still emitted.
*/
export interface StatusField {
readonly fieldKey: string;
Expand All @@ -28,7 +28,6 @@ const FUNCTIONS_URL: StatusField = {
};
const DB_URL: StatusField = { fieldKey: "db.url", defaultName: "DB_URL" };
const STUDIO_URL: StatusField = { fieldKey: "studio.url", defaultName: "STUDIO_URL" };
const INBUCKET_URL: StatusField = { fieldKey: "inbucket.url", defaultName: "INBUCKET_URL" };
const MAILPIT_URL: StatusField = { fieldKey: "mailpit.url", defaultName: "MAILPIT_URL" };
const PUBLISHABLE_KEY: StatusField = {
fieldKey: "auth.publishable_key",
Expand All @@ -54,7 +53,7 @@ const STORAGE_S3_REGION: StatusField = {
defaultName: "S3_PROTOCOL_REGION",
};

/** All 18 fields, in declaration order. */
/** All 17 fields, in declaration order. */
export const STATUS_FIELDS: ReadonlyArray<StatusField> = [
API_URL,
REST_URL,
Expand All @@ -64,7 +63,6 @@ export const STATUS_FIELDS: ReadonlyArray<StatusField> = [
FUNCTIONS_URL,
DB_URL,
STUDIO_URL,
INBUCKET_URL,
MAILPIT_URL,
PUBLISHABLE_KEY,
SECRET_KEY,
Expand Down Expand Up @@ -365,7 +363,6 @@ export function statusValuesFromState(
}
if (inbucketEnabled) {
values[names.mailpitUrl] = local.mailpitUrl;
values[overrides.get(INBUCKET_URL.fieldKey) ?? INBUCKET_URL.defaultName] = local.mailpitUrl;
}
if (storageEnabled && storageS3ProtocolEnabled) {
values[names.storageS3Url] = local.storageS3Url;
Expand Down
26 changes: 5 additions & 21 deletions apps/cli/src/command-internal/status-values.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ describe("statusValues", () => {
});

describe("inbucket/mailpit gating", () => {
it("includes MAILPIT_URL and the deprecated INBUCKET_URL alias when local_smtp.enabled", () => {
it("emits MAILPIT_URL and no INBUCKET_URL when local_smtp.enabled", () => {
const { values } = statusValues(
baseConfig(),
CONTAINER_IDS,
Expand All @@ -349,17 +349,16 @@ describe("statusValues", () => {
WORKDIR,
);
expect(values.MAILPIT_URL).toBeDefined();
expect(values.INBUCKET_URL).toBe(values.MAILPIT_URL);
expect(values.INBUCKET_URL).toBeUndefined();
});

it("omits MAILPIT_URL/INBUCKET_URL when local_smtp.enabled is false", () => {
it("omits MAILPIT_URL when local_smtp.enabled is false", () => {
const config = baseConfig({ local_smtp: { enabled: false } });
const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR);
expect(values.MAILPIT_URL).toBeUndefined();
expect(values.INBUCKET_URL).toBeUndefined();
});

it("omits MAILPIT_URL/INBUCKET_URL when the inbucket container id is excluded", () => {
it("omits MAILPIT_URL when the inbucket container id is excluded", () => {
const { values } = statusValues(
baseConfig(),
CONTAINER_IDS,
Expand All @@ -371,7 +370,7 @@ describe("statusValues", () => {
expect(values.MAILPIT_URL).toBeUndefined();
});

it("omits MAILPIT_URL/INBUCKET_URL when the mailpit image short name is excluded", () => {
it("omits MAILPIT_URL when the mailpit image short name is excluded", () => {
const { values } = statusValues(
baseConfig(),
CONTAINER_IDS,
Expand Down Expand Up @@ -508,21 +507,6 @@ describe("statusValues", () => {
expect(values.ANON_KEY).toBeUndefined();
expect(values.SERVICE_ROLE_KEY).toBeUndefined();
});

it("remaps the deprecated inbucket.url key independently of mailpit.url", () => {
const overrides = new Map([["inbucket.url", "CUSTOM_INBUCKET_URL"]]);
const { values } = statusValues(
baseConfig(),
CONTAINER_IDS,
HOSTNAME,
NONE,
overrides,
WORKDIR,
);
expect(values.CUSTOM_INBUCKET_URL).toBeDefined();
expect(values.MAILPIT_URL).toBeDefined();
expect(values.INBUCKET_URL).toBeUndefined();
});
});

it("combines stopped-service exclusions with --exclude flag exclusions", () => {
Expand Down
Loading
Loading