From 63c531ba1e35587cc39582358fe5c608eba93deb Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Mon, 1 Jun 2026 04:16:58 +0530 Subject: [PATCH 1/5] feat --- apps/cli-go/pkg/config/config.go | 42 +++++++++++++++++++- apps/cli-go/pkg/config/templates/config.toml | 2 +- apps/cli-go/pkg/config/testdata/config.toml | 2 +- packages/config/src/base.ts | 4 +- packages/config/src/io.ts | 32 ++++++++++++++- packages/config/src/io.unit.test.ts | 2 + 6 files changed, 77 insertions(+), 7 deletions(-) diff --git a/apps/cli-go/pkg/config/config.go b/apps/cli-go/pkg/config/config.go index e2697ad825..d082a204b7 100644 --- a/apps/cli-go/pkg/config/config.go +++ b/apps/cli-go/pkg/config/config.go @@ -144,7 +144,7 @@ type ( Db db `toml:"db" json:"db"` Realtime realtime `toml:"realtime" json:"realtime"` Studio studio `toml:"studio" json:"studio"` - Inbucket inbucket `toml:"inbucket" json:"inbucket"` + Inbucket inbucket `toml:"local_smtp" json:"local_smtp"` Storage storage `toml:"storage" json:"storage"` Auth auth `toml:"auth" json:"auth"` EdgeRuntime edgeRuntime `toml:"edge_runtime" json:"edge_runtime"` @@ -488,13 +488,17 @@ func (c *config) loadFromFile(filename string, fsys fs.FS) error { viper.ExperimentalBindStruct(), viper.EnvKeyReplacer(strings.NewReplacer(".", "_")), ) + fileConfig := viper.New() v.SetEnvPrefix("SUPABASE") v.AutomaticEnv() if err := c.mergeDefaultValues(v); err != nil { return err } else if err := mergeFileConfig(v, filename, fsys); err != nil { return err + } else if err := mergeFileConfig(fileConfig, filename, fsys); err != nil { + return err } + v = normalizeDeprecatedSMTPConfig(v, fileConfig) // Find [remotes.*] block to override base config idToName := map[string]string{} for name, remote := range v.GetStringMap("remotes") { @@ -514,6 +518,40 @@ func (c *config) loadFromFile(filename string, fsys fs.FS) error { return c.load(v) } +func normalizeDeprecatedSMTPConfig(v, fileConfig *viper.Viper) *viper.Viper { + settings := v.AllSettings() + if fileConfig.IsSet("inbucket") { + fmt.Fprintln(os.Stderr, `WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.`) + if !fileConfig.IsSet("local_smtp") { + settings["local_smtp"] = settings["inbucket"] + } + delete(settings, "inbucket") + } + if remotes, ok := settings["remotes"].(map[string]any); ok { + for name, raw := range remotes { + remote, ok := raw.(map[string]any) + if !ok || !fileConfig.IsSet(fmt.Sprintf("remotes.%s.inbucket", name)) { + continue + } + fmt.Fprintf( + os.Stderr, + "WARN: config section [remotes.%s.inbucket] is deprecated. Please use [remotes.%s.local_smtp] instead.\n", + name, + name, + ) + if !fileConfig.IsSet(fmt.Sprintf("remotes.%s.local_smtp", name)) { + remote["local_smtp"] = remote["inbucket"] + } + delete(remote, "inbucket") + } + } + u := viper.New() + if err := u.MergeConfigMap(settings); err != nil { + return v + } + return u +} + func (c *config) mergeDefaultValues(v *viper.Viper) error { v.SetConfigType("toml") var buf bytes.Buffer @@ -907,7 +945,7 @@ func (c *config) Validate(fsys fs.FS) error { // Validate smtp config if c.Inbucket.Enabled { if c.Inbucket.Port == 0 { - return errors.New("Missing required field in config: inbucket.port") + return errors.New("Missing required field in config: local_smtp.port") } } // Validate auth config diff --git a/apps/cli-go/pkg/config/templates/config.toml b/apps/cli-go/pkg/config/templates/config.toml index c172cc4f4e..1d5f443660 100644 --- a/apps/cli-go/pkg/config/templates/config.toml +++ b/apps/cli-go/pkg/config/templates/config.toml @@ -102,7 +102,7 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] +[local_smtp] enabled = true # Port to use for the email testing server web interface. port = 54324 diff --git a/apps/cli-go/pkg/config/testdata/config.toml b/apps/cli-go/pkg/config/testdata/config.toml index b228a9c073..1c60b8af17 100644 --- a/apps/cli-go/pkg/config/testdata/config.toml +++ b/apps/cli-go/pkg/config/testdata/config.toml @@ -96,7 +96,7 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] +[local_smtp] enabled = true # Port to use for the email testing server web interface. port = 54324 diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index 4a56357209..26e14810e0 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -33,7 +33,7 @@ const baseProjectConfigFields = { db, edge_runtime, functions, - inbucket, + local_smtp: inbucket, realtime, storage, studio, @@ -48,7 +48,7 @@ const remoteProjectConfig = Schema.Struct({ db, edge_runtime, functions, - inbucket, + local_smtp: inbucket, realtime, storage, studio, diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index b981a4bd80..0aff8ea41a 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -151,6 +151,35 @@ function parseProjectConfigDocument(content: string, format: ConfigFormat): unkn return format === "json" ? JSON.parse(content) : SmolToml.parse(content); } +function normalizeDeprecatedSMTPSections(document: unknown): unknown { + if (!isObject(document)) { + return document; + } + const normalized = { ...document }; + if ("inbucket" in normalized) { + if (!("local_smtp" in normalized)) { + normalized.local_smtp = normalized.inbucket; + } + delete normalized.inbucket; + } + if (isObject(normalized.remotes)) { + normalized.remotes = Object.fromEntries( + Object.entries(normalized.remotes).map(([name, remote]) => { + if (!isObject(remote) || !("inbucket" in remote)) { + return [name, remote]; + } + const normalizedRemote = { ...remote }; + if (!("local_smtp" in normalizedRemote)) { + normalizedRemote.local_smtp = normalizedRemote.inbucket; + } + delete normalizedRemote.inbucket; + return [name, normalizedRemote]; + }), + ); + } + return normalized; +} + function getSchemaRef(document: unknown): string | undefined { if (!isObject(document)) { return undefined; @@ -214,6 +243,7 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* (filePath: stri try: () => parseProjectConfigDocument(content, format), catch: (cause) => new ProjectConfigParseError({ path: filePath, format, cause }), }); + const normalized = normalizeDeprecatedSMTPSections(document); // Substitute `env(VAR)` references against `.env`/`.env.local`/ambient env // before schema decode. Required for numeric/boolean fields, which would @@ -227,7 +257,7 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* (filePath: stri baseEnv: process.env, }); const interpolated = interpolateEnvReferencesAgainstSchema( - document, + normalized, projectEnv?.values ?? {}, ProjectConfigSchema, ); diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index e3154e9e05..c5379e7185 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -670,9 +670,11 @@ major_version = 16 const document = Schema.toJsonSchemaDocument(ProjectConfigSchema).schema; const schemaString = JSON.stringify(document); + expect(schemaString).toContain("local_smtp"); expect(schemaString).toContain("remotes"); expect(schemaString).toContain("static_files"); expect(schemaString).toContain("env"); + expect(schemaString).not.toContain("inbucket"); expect(schemaString).not.toContain("versions"); }); From 61f3176971c989af2b3a39f2aa3c7cb434b84c4c Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:03:48 +0530 Subject: [PATCH 2/5] nit --- apps/cli/src/shared/init/project-init.templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index 65213ce06c..19d88457c6 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -102,7 +102,7 @@ openai_api_key = "env(OPENAI_API_KEY)" # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they # are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] +[local_smtp] enabled = true # Port to use for the email testing server web interface. port = 54324 From 2203ec1ec4a828e841ccbec35f00f6bb7dcd881a Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 23 Jun 2026 15:42:06 +0200 Subject: [PATCH 3/5] fix(cli): harden local_smtp deprecation handling Strengthen the inbucket -> local_smtp backward-compat normalization across both the Go and TypeScript config loaders: - Go: only rebuild the viper when an inbucket section is actually present, and preserve the env-binding options (ExperimentalBindStruct, AutomaticEnv, SUPABASE prefix, env key replacer) on the rebuilt instance so env overrides are not silently dropped. - Go: deep-merge a deprecated [inbucket] section over the [local_smtp] template defaults instead of wholesale replacement, so a partial section keeps the defaults it omits (e.g. enabled = true). - TS: emit a deprecation warning on stderr (matching Go) when [inbucket] or [remotes.*.inbucket] is used, without polluting machine-readable stdout. - Add back-compat tests on both sides covering inbucket-only, partial inbucket, local_smtp precedence, remotes normalization, and warnings. Co-Authored-By: Claude Opus 4.8 --- apps/cli-go/pkg/config/config.go | 60 ++++++++++-- apps/cli-go/pkg/config/config_test.go | 84 ++++++++++++++++ packages/config/src/io.ts | 34 ++++++- packages/config/src/io.unit.test.ts | 135 +++++++++++++++++++++++++- 4 files changed, 298 insertions(+), 15 deletions(-) diff --git a/apps/cli-go/pkg/config/config.go b/apps/cli-go/pkg/config/config.go index 96513a074c..b2bf3f4a99 100644 --- a/apps/cli-go/pkg/config/config.go +++ b/apps/cli-go/pkg/config/config.go @@ -525,12 +525,11 @@ func (c *config) loadFromFile(filename string, fsys fs.FS) error { func normalizeDeprecatedSMTPConfig(v, fileConfig *viper.Viper) *viper.Viper { settings := v.AllSettings() + changed := false if fileConfig.IsSet("inbucket") { fmt.Fprintln(os.Stderr, `WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.`) - if !fileConfig.IsSet("local_smtp") { - settings["local_smtp"] = settings["inbucket"] - } - delete(settings, "inbucket") + renameDeprecatedSMTP(settings, !fileConfig.IsSet("local_smtp")) + changed = true } if remotes, ok := settings["remotes"].(map[string]any); ok { for name, raw := range remotes { @@ -544,19 +543,62 @@ func normalizeDeprecatedSMTPConfig(v, fileConfig *viper.Viper) *viper.Viper { name, name, ) - if !fileConfig.IsSet(fmt.Sprintf("remotes.%s.local_smtp", name)) { - remote["local_smtp"] = remote["inbucket"] - } - delete(remote, "inbucket") + renameDeprecatedSMTP(remote, !fileConfig.IsSet(fmt.Sprintf("remotes.%s.local_smtp", name))) + changed = true } } - u := viper.New() + if !changed { + return v + } + // Rebuild the viper from the rewritten settings so the now-removed + // `inbucket` key does not trip UnmarshalExact. Preserve the env-binding + // options from the original instance, otherwise SUPABASE_-prefixed env + // overrides bound via ExperimentalBindStruct would be silently dropped. + u := viper.NewWithOptions( + viper.ExperimentalBindStruct(), + viper.EnvKeyReplacer(strings.NewReplacer(".", "_")), + ) + u.SetEnvPrefix("SUPABASE") + u.AutomaticEnv() if err := u.MergeConfigMap(settings); err != nil { return v } return u } +// renameDeprecatedSMTP removes the deprecated `inbucket` key from settings. When +// promote is true (no explicit `local_smtp` is present), the inbucket values are +// deep-merged over any existing `local_smtp` defaults so a partial `[inbucket]` +// section keeps the template defaults it omits (e.g. `enabled = true`). +func renameDeprecatedSMTP(settings map[string]any, promote bool) { + inbucket, ok := settings["inbucket"] + delete(settings, "inbucket") + if !ok || !promote { + return + } + if existing, ok := settings["local_smtp"].(map[string]any); ok { + if override, ok := inbucket.(map[string]any); ok { + mergeConfigMaps(existing, override) + return + } + } + settings["local_smtp"] = inbucket +} + +// mergeConfigMaps deep-merges src into dst, overwriting leaf values while +// recursing into nested maps, mirroring viper's own config merge semantics. +func mergeConfigMaps(dst, src map[string]any) { + for k, val := range src { + if srcMap, ok := val.(map[string]any); ok { + if dstMap, ok := dst[k].(map[string]any); ok { + mergeConfigMaps(dstMap, srcMap) + continue + } + } + dst[k] = val + } +} + func (c *config) mergeDefaultValues(v *viper.Viper) error { v.SetConfigType("toml") var buf bytes.Buffer diff --git a/apps/cli-go/pkg/config/config_test.go b/apps/cli-go/pkg/config/config_test.go index 2c7e486a05..fa38005152 100644 --- a/apps/cli-go/pkg/config/config_test.go +++ b/apps/cli-go/pkg/config/config_test.go @@ -908,3 +908,87 @@ func TestVersionCompare(t *testing.T) { }) } } + +func TestDeprecatedSMTPConfig(t *testing.T) { + t.Run("maps deprecated [inbucket] to local_smtp", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[inbucket] +enabled = true +port = 12345 +`)}, + } + require.NoError(t, config.Load("", fsys)) + assert.True(t, config.Inbucket.Enabled) + assert.Equal(t, uint16(12345), config.Inbucket.Port) + }) + + t.Run("keeps template defaults for a partial [inbucket] section", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[inbucket] +port = 9999 +`)}, + } + require.NoError(t, config.Load("", fsys)) + // enabled is omitted by the user; the template default (true) must survive + // the inbucket -> local_smtp rewrite via deep merge instead of collapsing + // to the zero value. + assert.True(t, config.Inbucket.Enabled) + assert.Equal(t, uint16(9999), config.Inbucket.Port) + }) + + t.Run("prefers explicit [local_smtp] over deprecated [inbucket]", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[inbucket] +enabled = true +port = 11111 + +[local_smtp] +enabled = true +port = 22222 +`)}, + } + require.NoError(t, config.Load("", fsys)) + assert.Equal(t, uint16(22222), config.Inbucket.Port) + }) + + t.Run("normalizes deprecated [remotes.*.inbucket]", func(t *testing.T) { + config := NewConfig() + config.ProjectId = "abcdefghijklmnopqrst" + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[remotes.staging] +project_id = "abcdefghijklmnopqrst" + +[remotes.staging.inbucket] +enabled = true +port = 33333 +`)}, + } + require.NoError(t, config.Load("", fsys)) + assert.Equal(t, uint16(33333), config.Inbucket.Port) + }) + + t.Run("preserves env overrides when rewriting [inbucket]", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[inbucket] +enabled = true +port = 12345 +`)}, + } + // Env overrides are applied via ExperimentalBindStruct at unmarshal time, not + // captured by AllSettings(). Rebuilding the viper without those options while + // rewriting [inbucket] would silently drop this override. + t.Setenv("SUPABASE_AUTH_SITE_URL", "http://env-override.example/") + require.NoError(t, config.Load("", fsys)) + assert.Equal(t, "http://env-override.example/", config.Auth.SiteUrl) + assert.Equal(t, uint16(12345), config.Inbucket.Port) + }) +} diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 8303b02ed9..85f9ca6201 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -1,4 +1,4 @@ -import { Effect, FileSystem, Path, Schema } from "effect"; +import { Console, Effect, FileSystem, Path, Schema } from "effect"; import * as SmolToml from "smol-toml"; import { ProjectConfigSchema, type ProjectConfig } from "./base.ts"; import { DuplicateRemoteProjectIdError, ProjectConfigParseError } from "./errors.ts"; @@ -272,12 +272,27 @@ function parseProjectConfigDocument(content: string, format: ConfigFormat): unkn return format === "json" ? JSON.parse(content) : SmolToml.parse(content); } -function normalizeDeprecatedSMTPSections(document: unknown): unknown { +interface NormalizedSMTPDocument { + readonly document: unknown; + /** Section paths that used the deprecated `inbucket` key, e.g. `inbucket`, `remotes.staging.inbucket`. */ + readonly deprecatedSections: ReadonlyArray; +} + +/** + * Rewrites the deprecated `[inbucket]` config section (top-level and per + * `[remotes.*]`) to its preferred `[local_smtp]` name, mirroring Go's + * `normalizeDeprecatedSMTPConfig`. When both keys are present the explicit + * `local_smtp` wins and `inbucket` is dropped. The returned `deprecatedSections` + * drive the user-facing deprecation warnings emitted by the caller. + */ +function normalizeDeprecatedSMTPSections(document: unknown): NormalizedSMTPDocument { if (!isObject(document)) { - return document; + return { document, deprecatedSections: [] }; } + const deprecatedSections: Array = []; const normalized = { ...document }; if ("inbucket" in normalized) { + deprecatedSections.push("inbucket"); if (!("local_smtp" in normalized)) { normalized.local_smtp = normalized.inbucket; } @@ -289,6 +304,7 @@ function normalizeDeprecatedSMTPSections(document: unknown): unknown { if (!isObject(remote) || !("inbucket" in remote)) { return [name, remote]; } + deprecatedSections.push(`remotes.${name}.inbucket`); const normalizedRemote = { ...remote }; if (!("local_smtp" in normalizedRemote)) { normalizedRemote.local_smtp = normalizedRemote.inbucket; @@ -298,7 +314,7 @@ function normalizeDeprecatedSMTPSections(document: unknown): unknown { }), ); } - return normalized; + return { document: normalized, deprecatedSections }; } function getSchemaRef(document: unknown): string | undefined { @@ -367,7 +383,15 @@ export const loadProjectConfigFile = Effect.fnUntraced(function* ( try: () => parseProjectConfigDocument(content, format), catch: (cause) => new ProjectConfigParseError({ path: filePath, format, cause }), }); - const normalized = normalizeDeprecatedSMTPSections(document); + const { document: normalized, deprecatedSections } = normalizeDeprecatedSMTPSections(document); + // Warn on stderr (matching Go's normalizeDeprecatedSMTPConfig) so the notice + // never pollutes machine-readable stdout payloads. + for (const section of deprecatedSections) { + const replacement = section.replace(/inbucket$/, "local_smtp"); + yield* Console.error( + `WARN: config section [${section}] is deprecated. Please use [${replacement}] instead.`, + ); + } // Substitute `env(VAR)` references against `.env`/`.env.local`/ambient env // before schema decode. Required for numeric/boolean fields, which would diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index e6012d6750..82ac43d0e1 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import { BunServices } from "@effect/platform-bun"; import { mkdtempSync } from "node:fs"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; @@ -1055,3 +1055,136 @@ max_rows = "env(SUPABASE_REMOTE_MAX_ROWS_TEST)" } }); }); + +describe("config io deprecated [inbucket] back-compat", () => { + let warnings: Array = []; + let errorSpy: ReturnType | undefined; + + function captureWarnings() { + warnings = []; + // loadProjectConfigFile emits the deprecation warning via Console.error, whose + // default implementation delegates to globalThis.console.error (stderr). + errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + warnings.push(args.map((a) => String(a)).join(" ")); + }); + } + + afterEach(() => { + errorSpy?.mockRestore(); + errorSpy = undefined; + }); + + async function loadToml(contents: string) { + const cwd = makeTempProject(); + const path = await runConfigEffect(configTomlPath(cwd)); + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile(path, contents); + try { + return await runConfigEffect(loadProjectConfigFile(path)); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + } + + test("loads a deprecated [inbucket] section as [local_smtp]", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[inbucket] +enabled = true +port = 12345 +`, + ); + + expect(loaded.config.local_smtp.enabled).toBe(true); + expect(loaded.config.local_smtp.port).toBe(12345); + expect("inbucket" in loaded.config).toBe(false); + expect(loaded.document).not.toHaveProperty("inbucket"); + expect(loaded.document).toHaveProperty("local_smtp"); + expect( + warnings.some((m) => + m.includes( + "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", + ), + ), + ).toBe(true); + }); + + test("fills schema defaults when a deprecated [inbucket] section is partial", async () => { + const loaded = await loadToml( + `project_id = "abc123" + +[inbucket] +port = 9999 +`, + ); + + // enabled is omitted by the user; the schema default (true) must survive the + // inbucket -> local_smtp rewrite rather than collapsing to a zero value. + expect(loaded.config.local_smtp.enabled).toBe(true); + expect(loaded.config.local_smtp.port).toBe(9999); + }); + + test("prefers an explicit [local_smtp] when both sections are present", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[inbucket] +enabled = true +port = 11111 + +[local_smtp] +enabled = true +port = 22222 +`, + ); + + expect(loaded.config.local_smtp.port).toBe(22222); + expect(loaded.document).not.toHaveProperty("inbucket"); + // The deprecation warning still fires because the deprecated key was present. + expect(warnings.some((m) => m.includes("[inbucket] is deprecated"))).toBe(true); + }); + + test("normalizes a deprecated [remotes.*.inbucket] section", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[remotes.staging] +project_id = "stagingref" + +[remotes.staging.inbucket] +enabled = true +port = 33333 +`, + ); + + const staging = loaded.config.remotes.staging; + expect(staging?.local_smtp?.port).toBe(33333); + expect(staging).not.toHaveProperty("inbucket"); + expect( + warnings.some((m) => + m.includes( + "WARN: config section [remotes.staging.inbucket] is deprecated. Please use [remotes.staging.local_smtp] instead.", + ), + ), + ).toBe(true); + }); + + test("does not warn when only [local_smtp] is used", async () => { + captureWarnings(); + const loaded = await loadToml( + `project_id = "abc123" + +[local_smtp] +enabled = true +port = 54324 +`, + ); + + expect(loaded.config.local_smtp.port).toBe(54324); + expect(warnings.some((m) => m.includes("is deprecated"))).toBe(false); + }); +}); From c29471aadcd8353049ed80ee8cc9f6392f985ddd Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 23 Jun 2026 15:54:52 +0200 Subject: [PATCH 4/5] chore(cli): sync published config schema for local_smtp rename Rename the `inbucket` property to `local_smtp` in the published JSON schema (top-level and the [remotes.*] copy) so editors validating config.toml against https://supabase.com/docs/cli/config.schema.json recognize the new section key. Mirrors the inbucket -> local_smtp rename in packages/config; the inner schema and descriptions are unchanged, matching the source of truth. Co-Authored-By: Claude Opus 4.8 --- apps/docs/public/cli/config.schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 3977893a6e..92e290c140 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -2921,7 +2921,7 @@ } ] }, - "inbucket": { + "local_smtp": { "type": "object", "properties": { "enabled": { @@ -6466,7 +6466,7 @@ } ] }, - "inbucket": { + "local_smtp": { "type": "object", "properties": { "enabled": { From 6ec4390bedd0272b006ef11c9126c107ace21ab9 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 23 Jun 2026 16:14:34 +0200 Subject: [PATCH 5/5] chore(cli): drop implementation name from local_smtp schema docs The local email server is Mailpit, so the user-facing schema description and documentation link no longer reference the now-renamed "Inbucket". Update the section description to "Enable the local SMTP testing server." and point the link at the Mailpit docs, in both packages/config and the published schema. Co-Authored-By: Claude Opus 4.8 --- apps/docs/public/cli/config.schema.json | 4 ++-- packages/config/src/inbucket.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 92e290c140..28062d0891 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -2926,7 +2926,7 @@ "properties": { "enabled": { "type": "boolean", - "description": "Enable the local Inbucket service.", + "description": "Enable the local SMTP testing server.", "default": true }, "port": { @@ -6471,7 +6471,7 @@ "properties": { "enabled": { "type": "boolean", - "description": "Enable the local Inbucket service.", + "description": "Enable the local SMTP testing server.", "default": true }, "port": { diff --git a/packages/config/src/inbucket.ts b/packages/config/src/inbucket.ts index fc2dfd5a7a..7070b816e0 100644 --- a/packages/config/src/inbucket.ts +++ b/packages/config/src/inbucket.ts @@ -3,8 +3,8 @@ import { Effect, Schema } from "effect"; const links = [ { - name: "Inbucket documentation", - link: "https://www.inbucket.org", + name: "Mailpit documentation", + link: "https://mailpit.axllent.org", }, ]; @@ -16,7 +16,7 @@ const defaultPort = 54324; export const inbucket = Schema.Struct({ enabled: Schema.Boolean.annotate({ default: defaultEnabled, - description: "Enable the local Inbucket service.", + description: "Enable the local SMTP testing server.", tags, links, }).pipe(Schema.withDecodingDefaultKey(Effect.succeed(defaultEnabled))),