From d7a05f56e516a7e1f9bf393cfabb8140fc611d43 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Mon, 27 Apr 2026 12:23:18 +0200 Subject: [PATCH 1/8] fix(windows): json unmarshal errors in telemetry and pg-delta declarative sync (#5128) * fix(windows): json unmarshal errors in telemetry and pg-delta declarative sync Three Windows-only failures, all surfacing as JSON parse errors: 1. telemetry: any field-level unmarshal error (e.g. session_last_active stored as a number) now recreates state instead of propagating, since identity fields aren't worth surfacing an error for. 2. pg-delta declarative sync: containerRef now normalises Windows path separators with filepath.ToSlash so paths like supabase\.temp\pgdelta\catalog-baseline.json resolve correctly inside the Linux edge-runtime container. 3. pg-delta export/diff: parse callers (DeclarativeExportPgDeltaRef, ExportCatalogPgDelta, pgcache.exportCatalog) now surface stderr when stdout is empty, instead of failing later with "unexpected end of JSON input". DiffPgDeltaRef intentionally still accepts empty stdout as a legitimate "no schema changes" result. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: only run test on windows and fix lint --------- Co-authored-by: Claude Opus 4.7 (1M context) --- internal/db/diff/pgdelta.go | 16 ++++++++++++--- internal/db/diff/pgdelta_test.go | 34 ++++++++++++++++++++++++++++++++ internal/db/pgcache/cache.go | 7 ++++++- internal/telemetry/state.go | 14 +++++++++++-- internal/telemetry/state_test.go | 32 ++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 internal/db/diff/pgdelta_test.go diff --git a/internal/db/diff/pgdelta.go b/internal/db/diff/pgdelta.go index 70b30a95a1..8ba8fb0fe9 100644 --- a/internal/db/diff/pgdelta.go +++ b/internal/db/diff/pgdelta.go @@ -6,6 +6,7 @@ import ( _ "embed" "encoding/json" "os" + "path/filepath" "strings" "github.com/go-errors/errors" @@ -47,12 +48,14 @@ func isPostgresURL(ref string) bool { // containerRef translates a host-relative catalog file path into the absolute // path where it appears inside the edge runtime container (CWD mounted at -// /workspace). Postgres URLs and empty strings pass through unchanged. +// /workspace). Postgres URLs and empty strings pass through unchanged. Path +// separators are normalised to forward slashes so Windows paths (with `\`) +// resolve correctly inside the Linux container. func containerRef(ref string) string { if ref == "" || isPostgresURL(ref) { return ref } - return "/workspace/" + ref + return "/workspace/" + filepath.ToSlash(ref) } // pgDeltaFormatOptions returns the experimental.pgdelta.format_options config for @@ -143,6 +146,9 @@ func DeclarativeExportPgDeltaRef(ctx context.Context, sourceRef, targetRef strin if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeExportScript, binds, "error exporting declarative schema", &stdout, &stderr); err != nil { return DeclarativeOutput{}, err } + if stdout.Len() == 0 { + return DeclarativeOutput{}, errors.Errorf("error exporting declarative schema: edge-runtime script produced no output:\n%s", stderr.String()) + } var result DeclarativeOutput if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { return DeclarativeOutput{}, errors.Errorf("failed to parse declarative export output: %w", err) @@ -176,5 +182,9 @@ func ExportCatalogPgDelta(ctx context.Context, targetRef, role string, options . if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportScript, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } - return strings.TrimSpace(stdout.String()), nil + snapshot := strings.TrimSpace(stdout.String()) + if len(snapshot) == 0 { + return "", errors.Errorf("error exporting pg-delta catalog: edge-runtime script produced no output:\n%s", stderr.String()) + } + return snapshot, nil } diff --git a/internal/db/diff/pgdelta_test.go b/internal/db/diff/pgdelta_test.go new file mode 100644 index 0000000000..671414a069 --- /dev/null +++ b/internal/db/diff/pgdelta_test.go @@ -0,0 +1,34 @@ +package diff + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContainerRef(t *testing.T) { + t.Run("passes empty string through", func(t *testing.T) { + assert.Equal(t, "", containerRef("")) + }) + + t.Run("passes postgres URLs through", func(t *testing.T) { + assert.Equal(t, "postgresql://user@host:5432/db", containerRef("postgresql://user@host:5432/db")) + assert.Equal(t, "postgres://user@host:5432/db", containerRef("postgres://user@host:5432/db")) + }) + + t.Run("normalises Windows path separators", func(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("path separator behaviour is Windows-only") + } + // On Windows, filepath.Join produces backslashes which the Linux + // container cannot read; containerRef must convert them. + ref := `supabase\.temp\pgdelta\catalog-baseline-17.6.1.106.json` + assert.Equal(t, "/workspace/supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json", containerRef(ref)) + }) + + t.Run("leaves unix paths untouched", func(t *testing.T) { + ref := "supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json" + assert.Equal(t, "/workspace/supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json", containerRef(ref)) + }) +} diff --git a/internal/db/pgcache/cache.go b/internal/db/pgcache/cache.go index db12199475..c6881c84cc 100644 --- a/internal/db/pgcache/cache.go +++ b/internal/db/pgcache/cache.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/go-errors/errors" "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" "github.com/spf13/afero" @@ -255,5 +256,9 @@ func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.C if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportTS, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } - return strings.TrimSpace(stdout.String()), nil + snapshot := strings.TrimSpace(stdout.String()) + if len(snapshot) == 0 { + return "", errors.Errorf("error exporting pg-delta catalog: edge-runtime script produced no output:\n%s", stderr.String()) + } + return snapshot, nil } diff --git a/internal/telemetry/state.go b/internal/telemetry/state.go index 58096c5de4..8d69996468 100644 --- a/internal/telemetry/state.go +++ b/internal/telemetry/state.go @@ -17,6 +17,12 @@ const SchemaVersion = 1 const sessionRotationThreshold = 30 * time.Minute +// errMalformedState marks any read where the file existed but couldn't be +// decoded into a State — covers JSON syntax errors, unexpected types +// (e.g. session_last_active stored as a number), and field-level unmarshal +// failures from time.Time / uuid. Used to trigger fresh-state creation. +var errMalformedState = errors.New("malformed telemetry state") + type State struct { Enabled bool `json:"enabled"` DeviceID string `json:"device_id"` @@ -48,7 +54,7 @@ func LoadState(fsys afero.Fs) (State, error) { } var state State if err := json.Unmarshal(contents, &state); err != nil { - return State{}, errors.Errorf("failed to parse telemetry file: %w", err) + return State{}, errors.Errorf("%w: %v", errMalformedState, err) } return state, nil } @@ -74,7 +80,11 @@ func LoadOrCreateState(fsys afero.Fs, now time.Time) (State, bool, error) { state.SessionLastActive = now.UTC() return state, false, SaveState(state, fsys) } - if !errors.Is(err, os.ErrNotExist) { + // Treat a missing file OR an unparseable file as "no existing state" and + // recreate. Identity fields (device_id, session_id) are not worth + // surfacing an error for — losing them is harmless. We only propagate + // genuine I/O errors (permissions, disk full) so the user can act. + if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, errMalformedState) { return State{}, false, err } state = State{ diff --git a/internal/telemetry/state_test.go b/internal/telemetry/state_test.go index a2a07a5b35..9cd03dd967 100644 --- a/internal/telemetry/state_test.go +++ b/internal/telemetry/state_test.go @@ -79,6 +79,38 @@ func TestLoadOrCreateState(t *testing.T) { assert.Equal(t, now, state.SessionLastActive) }) + t.Run("recovers from corrupted state file", func(t *testing.T) { + // Each entry simulates a real-world corruption shape we've observed. + corruptions := map[string][]byte{ + "empty file": {}, + "truncated json": []byte(`{"enabled":tru`), + "session_last_active is a number (not a string)": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":1776770348993,"schema_version":1}`), + "session_last_active is a malformed string": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":"not-a-time","schema_version":1}`), + } + for label, contents := range corruptions { + t.Run(label, func(t *testing.T) { + t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") + fsys := afero.NewMemMapFs() + path, err := telemetryPath() + require.NoError(t, err) + require.NoError(t, fsys.MkdirAll("/tmp/supabase-home", 0755)) + require.NoError(t, afero.WriteFile(fsys, path, contents, 0644)) + + state, created, err := LoadOrCreateState(fsys, now) + + require.NoError(t, err) + assert.True(t, created) + assert.True(t, state.Enabled) + assert.Equal(t, SchemaVersion, state.SchemaVersion) + assert.NoError(t, uuid.Validate(state.DeviceID)) + assert.NoError(t, uuid.Validate(state.SessionID)) + saved, err := LoadState(fsys) + require.NoError(t, err) + assert.Equal(t, state, saved) + }) + } + }) + t.Run("rotates stale session after inactivity threshold", func(t *testing.T) { t.Setenv("SUPABASE_HOME", "/tmp/supabase-home") fsys := afero.NewMemMapFs() From 7285baba6eb272f3d3b63756030353f424022279 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Tue, 28 Apr 2026 18:33:32 +0200 Subject: [PATCH 2/8] chore: update codegen api (#5143) --- api/overlay.yaml | 3 ++ pkg/api/client.gen.go | 8 +-- pkg/api/types.gen.go | 110 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/api/overlay.yaml b/api/overlay.yaml index 7843f3d86b..b59831268b 100644 --- a/api/overlay.yaml +++ b/api/overlay.yaml @@ -41,6 +41,9 @@ actions: - target: $.components.schemas.DiskRequestBody.properties.attributes.discriminator description: Replaces discriminated union with concrete type remove: true +- target: $.components.schemas.JitStateResponse.discriminator + description: Replaces discriminated union with concrete type + remove: true - target: $.paths.*.*.parameters[?(@.name=='branch_id_or_ref')] update: schema: diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go index 5d7941a128..ed33fb4585 100644 --- a/pkg/api/client.gen.go +++ b/pkg/api/client.gen.go @@ -14260,7 +14260,7 @@ func (r V1GetServicesHealthResponse) StatusCode() int { type V1GetJitAccessConfigResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *JitAccessResponse + JSON200 *JitStateResponse } // Status returns HTTPResponse.Status @@ -14282,7 +14282,7 @@ func (r V1GetJitAccessConfigResponse) StatusCode() int { type V1UpdateJitAccessConfigResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *JitAccessResponse + JSON200 *JitStateResponse } // Status returns HTTPResponse.Status @@ -19925,7 +19925,7 @@ func ParseV1GetJitAccessConfigResponse(rsp *http.Response) (*V1GetJitAccessConfi switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAccessResponse + var dest JitStateResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -19951,7 +19951,7 @@ func ParseV1UpdateJitAccessConfigResponse(rsp *http.Response) (*V1UpdateJitAcces switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest JitAccessResponse + var dest JitStateResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 637dcad9b5..89211a3a2e 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -492,8 +492,26 @@ const ( // Defines values for JitAccessRequestRequestState. const ( - Disabled JitAccessRequestRequestState = "disabled" - Enabled JitAccessRequestRequestState = "enabled" + JitAccessRequestRequestStateDisabled JitAccessRequestRequestState = "disabled" + JitAccessRequestRequestStateEnabled JitAccessRequestRequestState = "enabled" +) + +// Defines values for JitStateResponse0State. +const ( + JitStateResponse0StateDisabled JitStateResponse0State = "disabled" + JitStateResponse0StateEnabled JitStateResponse0State = "enabled" +) + +// Defines values for JitStateResponse1State. +const ( + Unavailable JitStateResponse1State = "unavailable" +) + +// Defines values for JitStateResponse1UnavailableReason. +const ( + ManualMigrationRequired JitStateResponse1UnavailableReason = "manual_migration_required" + PostgresUpgradeRequired JitStateResponse1UnavailableReason = "postgres_upgrade_required" + TemporarilyUnavailable JitStateResponse1UnavailableReason = "temporarily_unavailable" ) // Defines values for ListActionRunResponseRunStepsName. @@ -2856,6 +2874,32 @@ type JitListAccessResponse struct { } `json:"items"` } +// JitStateResponse defines model for JitStateResponse. +type JitStateResponse struct { + union json.RawMessage +} + +// JitStateResponse0 defines model for . +type JitStateResponse0 struct { + AppliedSuccessfully *bool `json:"appliedSuccessfully,omitempty"` + State JitStateResponse0State `json:"state"` +} + +// JitStateResponse0State defines model for JitStateResponse.0.State. +type JitStateResponse0State string + +// JitStateResponse1 defines model for . +type JitStateResponse1 struct { + State JitStateResponse1State `json:"state"` + UnavailableReason JitStateResponse1UnavailableReason `json:"unavailableReason"` +} + +// JitStateResponse1State defines model for JitStateResponse.1.State. +type JitStateResponse1State string + +// JitStateResponse1UnavailableReason defines model for JitStateResponse.1.UnavailableReason. +type JitStateResponse1UnavailableReason string + // LegacyApiKeysResponse defines model for LegacyApiKeysResponse. type LegacyApiKeysResponse struct { Enabled bool `json:"enabled"` @@ -5938,6 +5982,68 @@ func (t *DiskResponse_Attributes) UnmarshalJSON(b []byte) error { return err } +// AsJitStateResponse0 returns the union data inside the JitStateResponse as a JitStateResponse0 +func (t JitStateResponse) AsJitStateResponse0() (JitStateResponse0, error) { + var body JitStateResponse0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromJitStateResponse0 overwrites any union data inside the JitStateResponse as the provided JitStateResponse0 +func (t *JitStateResponse) FromJitStateResponse0(v JitStateResponse0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeJitStateResponse0 performs a merge with any union data inside the JitStateResponse, using the provided JitStateResponse0 +func (t *JitStateResponse) MergeJitStateResponse0(v JitStateResponse0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsJitStateResponse1 returns the union data inside the JitStateResponse as a JitStateResponse1 +func (t JitStateResponse) AsJitStateResponse1() (JitStateResponse1, error) { + var body JitStateResponse1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromJitStateResponse1 overwrites any union data inside the JitStateResponse as the provided JitStateResponse1 +func (t *JitStateResponse) FromJitStateResponse1(v JitStateResponse1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeJitStateResponse1 performs a merge with any union data inside the JitStateResponse, using the provided JitStateResponse1 +func (t *JitStateResponse) MergeJitStateResponse1(v JitStateResponse1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t JitStateResponse) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *JitStateResponse) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsListProjectAddonsResponseAvailableAddonsVariantsId0 returns the union data inside the ListProjectAddonsResponse_AvailableAddons_Variants_Id as a ListProjectAddonsResponseAvailableAddonsVariantsId0 func (t ListProjectAddonsResponse_AvailableAddons_Variants_Id) AsListProjectAddonsResponseAvailableAddonsVariantsId0() (ListProjectAddonsResponseAvailableAddonsVariantsId0, error) { var body ListProjectAddonsResponseAvailableAddonsVariantsId0 From be971cd991e3eeeca8ca9f0d1a7d83f5a9ebf375 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 28 Apr 2026 18:42:44 +0200 Subject: [PATCH 3/8] fix: encode auth external url explicitly (#5092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: encode auth external url explicitly * chore(deps): upgrade pg-delta to alpha.17 (#5110) Closes: #5094 * chore(deps): bump the actions-major group across 1 directory with 4 updates (#5108) Bumps the actions-major group with 4 updates in the / directory: [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata), [t1m0thyj/unlock-keyring](https://github.com/t1m0thyj/unlock-keyring), [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) and [actions/setup-node](https://github.com/actions/setup-node). Updates `dependabot/fetch-metadata` from 3.0.0 to 3.1.0 - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/ffa630c65fa7e0ecfa0625b5ceda64399aea1b36...25dd0e34f4fe68f24cc83900b1fe3fe149efef98) Updates `t1m0thyj/unlock-keyring` from 1.1.0 to 1.2.0 - [Release notes](https://github.com/t1m0thyj/unlock-keyring/releases) - [Commits](https://github.com/t1m0thyj/unlock-keyring/compare/728cc718a07b5e7b62c269fc89295e248b24cba7...cbcf205c879ebd86add70bab3a6abfcce59a5cae) Updates `goreleaser/goreleaser-action` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/ec59f474b9834571250b370d4735c50f8e2d1e29...e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c) Updates `actions/setup-node` from 6.3.0 to 6.4.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: t1m0thyj/unlock-keyring dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: goreleaser/goreleaser-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: actions/setup-node dependency-version: 6.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Valleteau * fix(docker): bump the docker-minor group across 1 directory with 6 updates (#5079) * fix(docker): bump the docker-minor group across 1 directory with 6 updates Bumps the docker-minor group with 6 updates in the /pkg/config/templates directory: | Package | From | To | | --- | --- | --- | | postgrest/postgrest | `v14.8` | `v14.9` | | supabase/studio | `2026.04.08-sha-205cbe7` | `2026.04.13-sha-e95f1cc` | | supabase/edge-runtime | `v1.73.3` | `v1.73.5` | | supabase/realtime | `v2.82.0` | `v2.83.1` | | supabase/storage-api | `v1.48.28` | `v1.51.0` | | supabase/logflare | `1.37.1` | `1.38.2` | Updates `postgrest/postgrest` from v14.8 to v14.9 Updates `supabase/studio` from 2026.04.08-sha-205cbe7 to 2026.04.13-sha-e95f1cc Updates `supabase/edge-runtime` from v1.73.3 to v1.73.5 Updates `supabase/realtime` from v2.82.0 to v2.83.1 Updates `supabase/storage-api` from v1.48.28 to v1.51.0 Updates `supabase/logflare` from 1.37.1 to 1.38.2 --- updated-dependencies: - dependency-name: postgrest/postgrest dependency-version: v14.9 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/studio dependency-version: 2026.04.13-sha-e95f1cc dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/edge-runtime dependency-version: v1.73.5 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/realtime dependency-version: v2.83.1 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/storage-api dependency-version: v1.51.0 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/logflare dependency-version: 1.38.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-minor ... Signed-off-by: dependabot[bot] * Downgrade postgrest version from 14.9 to 14.8 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Valleteau * chore(workflows): enable install scripts for supabase package in Yarn (#5111) chore(workflows): enable install scripts for supabase package in Yarn Berry setup This change sets the YARN_ENABLE_SCRIPTS environment variable to true during the installation of the supabase package, allowing its postinstall script to run as required by Yarn Berry 4.14+. This adjustment ensures the necessary binary is fetched correctly. * feat: --diff-engine flag on db pull * fix(docker): bump the docker-minor group in /pkg/config/templates with 6 updates (#5113) fix(docker): bump the docker-minor group Bumps the docker-minor group in /pkg/config/templates with 6 updates: | Package | From | To | | --- | --- | --- | | postgrest/postgrest | `v14.8` | `v14.10` | | supabase/studio | `2026.04.13-sha-e95f1cc` | `2026.04.20-sha-b721a2d` | | supabase/edge-runtime | `v1.73.5` | `v1.73.13` | | supabase/realtime | `v2.83.1` | `v2.86.3` | | supabase/storage-api | `v1.51.0` | `v1.54.1` | | supabase/logflare | `1.38.2` | `1.39.1` | Updates `postgrest/postgrest` from v14.8 to v14.10 Updates `supabase/studio` from 2026.04.13-sha-e95f1cc to 2026.04.20-sha-b721a2d Updates `supabase/edge-runtime` from v1.73.5 to v1.73.13 Updates `supabase/realtime` from v2.83.1 to v2.86.3 Updates `supabase/storage-api` from v1.51.0 to v1.54.1 Updates `supabase/logflare` from 1.38.2 to 1.39.1 --- updated-dependencies: - dependency-name: postgrest/postgrest dependency-version: v14.10 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/studio dependency-version: 2026.04.20-sha-b721a2d dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/edge-runtime dependency-version: v1.73.13 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/realtime dependency-version: v2.86.3 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/storage-api dependency-version: v1.54.1 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/logflare dependency-version: 1.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Valleteau * feat: exposing new api keys to functions (#4946) Co-authored-by: Andrew Valleteau * chore: upgrade pg-delta to alpha.20 in multiple templates * fix: remove version comparison check for storage image updates (#5118) fix: honor pinned storage version offline Remove the version comparison that only pinned storage when the local version was newer than the default. This prevented `supabase start` from using an already-downloaded image offline, since Docker would still try to pull the default newer image. Fixes CLI-1393. Co-authored-by: Claude * fix: improve error handling and output formatting in pg-delta apply process (#5120) - Updated the `runDeclarativeSync` function to avoid wrapping SQL output with `utils.Bold`, preventing excessive whitespace in multi-line SQL. - Changed the result accumulation in `migra.ts` from string concatenation to an array for better performance and clarity. - Enhanced the `ApplyResult` struct to include `ValidationErrors` and `Diagnostics`, allowing for more detailed error reporting. - Modified the `formatApplyFailure` function to include validation errors and diagnostics in the output, improving user feedback on apply failures. - Added tests for validation error handling in `apply_test.go` to ensure robustness against various error scenarios. * fix(start): guard db_logs vector transform against null regex capture (#5126) The `db_logs` transform aborts with `expected string, got null` in `upcase!()` when `parse_regex` matches an event message but the `level` named group resolves to null. The fallback branch only covers regex failure (`err != null || parsed == null`), leaving a third path where the match succeeds but the capture is null. That path overwrites the would-be fallback with null and crashes on upcase. Observed under routine local dev load (Next.js dev server issuing service-role Postgres queries): 3,000+ aborted transforms in two minutes, cascading into Vector retry storms and Logflare `ErlSysMon` message-queue backpressure. Extend the fallback condition to also fire when `parsed.level` is null, and guard the assignment in the match branch, so `error_severity` always has a non-null string before `upcase!`. Co-authored-by: rebasecase * Update Dockerfile for Studio image 2026-04-27 * chore: resync develop with main (#5123) Prod deploy (#5109) * fix(pg-delta): declarative-sync-no-declarative-dir-set (#5078) * feat(declarative): add tests for skipping config updates when PgDelta is enabled - These tests verify that the configuration remains unchanged when PgDelta is enabled, ensuring the declarative directory is the source of truth. - Updated the WriteDeclarativeSchemas function to reflect the new behavior regarding PgDelta configuration. * fix(declarative): DSL change due to upgrade * feat(auth): add support for configuring passkeys and webauthn (#5077) * fix: atomic parser (#5064) * fix * test --------- * fix(pg-delta): declarative apply error results (#5082) * fix(pg-delta): declarative apply error results Improve readability report for decalrative appy errors wrapping * chore: upgrade pg-delta to alpha 13 * feat(telemetry): attach org/project groups to all CLI events Only ~19% of CLI events had PostHog group properties ($group_0, $group_1) because groups were only set during `supabase link`. Commands using --project-ref without linking sent events invisible to group analytics. Add EnsureProjectGroupsCached which resolves and caches project metadata (including org ID) in linked-project.json when a project ref is available. The cache is checked before every cli_command_executed event, so the API call only happens once per unique project ref. Closes GROWTH-761 * fix: address code review feedback - Guard against log.Fatalln crash: check auth token before calling GetSupabase(), and move the API call to cmd/root.go where it belongs - Don't overwrite existing linked-project.json cache — supabase link is the authoritative source, we only fill the gap when no cache exists - Fire GroupIdentify for org and project after caching, matching the link flow so PostHog has group metadata - Restructure so telemetry package has no API dependencies (pure caching + PostHog calls), making tests reliable without gock/mocks * fix: adds etl to managed schema (#5090) * chore: sync API types from infrastructure (#5093) * chore(deps): bump the actions-major group across 1 directory with 5 updates (#5088) Bumps the actions-major group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `3.0.0` | `3.1.1` | | [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) | `8.1.0` | `8.1.1` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` | | [github/codeql-action](https://github.com/github/codeql-action) | `4.35.1` | `4.35.2` | | [docker/build-push-action](https://github.com/docker/build-push-action) | `7.0.0` | `7.1.0` | Updates `actions/create-github-app-token` from 3.0.0 to 3.1.1 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/f8d387b68d61c58ab83c6c016672934102569859...1b10c78c7865c340bc4f6099eb2f838309f1e8c3) Updates `peter-evans/create-pull-request` from 8.1.0 to 8.1.1 - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/c0f553fe549906ede9cf27b5156039d195d2ece0...5f6978faf089d4d20b00c7766989d076bb2fc7f1) Updates `actions/upload-artifact` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) Updates `github/codeql-action` from 4.35.1 to 4.35.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225) Updates `docker/build-push-action` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: peter-evans/create-pull-request dependency-version: 8.1.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-major - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-major - dependency-name: github/codeql-action dependency-version: 4.35.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions-major - dependency-name: docker/build-push-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major ... * fix: functions download (#5096) * fix * test --------- * feat(db): strengthen RLS advisory message for stronger agent compliance * chore(deps): upgrade pg-delta to alpha.17 (#5110) Closes: #5094 * chore(deps): bump the actions-major group across 1 directory with 4 updates (#5108) Bumps the actions-major group with 4 updates in the / directory: [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata), [t1m0thyj/unlock-keyring](https://github.com/t1m0thyj/unlock-keyring), [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) and [actions/setup-node](https://github.com/actions/setup-node). Updates `dependabot/fetch-metadata` from 3.0.0 to 3.1.0 - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/ffa630c65fa7e0ecfa0625b5ceda64399aea1b36...25dd0e34f4fe68f24cc83900b1fe3fe149efef98) Updates `t1m0thyj/unlock-keyring` from 1.1.0 to 1.2.0 - [Release notes](https://github.com/t1m0thyj/unlock-keyring/releases) - [Commits](https://github.com/t1m0thyj/unlock-keyring/compare/728cc718a07b5e7b62c269fc89295e248b24cba7...cbcf205c879ebd86add70bab3a6abfcce59a5cae) Updates `goreleaser/goreleaser-action` from 7.0.0 to 7.1.0 - [Release notes](https://github.com/goreleaser/goreleaser-action/releases) - [Commits](https://github.com/goreleaser/goreleaser-action/compare/ec59f474b9834571250b370d4735c50f8e2d1e29...e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c) Updates `actions/setup-node` from 6.3.0 to 6.4.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: t1m0thyj/unlock-keyring dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: goreleaser/goreleaser-action dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major - dependency-name: actions/setup-node dependency-version: 6.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions-major ... * fix(docker): bump the docker-minor group across 1 directory with 6 updates (#5079) * fix(docker): bump the docker-minor group across 1 directory with 6 updates Bumps the docker-minor group with 6 updates in the /pkg/config/templates directory: | Package | From | To | | --- | --- | --- | | postgrest/postgrest | `v14.8` | `v14.9` | | supabase/studio | `2026.04.08-sha-205cbe7` | `2026.04.13-sha-e95f1cc` | | supabase/edge-runtime | `v1.73.3` | `v1.73.5` | | supabase/realtime | `v2.82.0` | `v2.83.1` | | supabase/storage-api | `v1.48.28` | `v1.51.0` | | supabase/logflare | `1.37.1` | `1.38.2` | Updates `postgrest/postgrest` from v14.8 to v14.9 Updates `supabase/studio` from 2026.04.08-sha-205cbe7 to 2026.04.13-sha-e95f1cc Updates `supabase/edge-runtime` from v1.73.3 to v1.73.5 Updates `supabase/realtime` from v2.82.0 to v2.83.1 Updates `supabase/storage-api` from v1.48.28 to v1.51.0 Updates `supabase/logflare` from 1.37.1 to 1.38.2 --- updated-dependencies: - dependency-name: postgrest/postgrest dependency-version: v14.9 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/studio dependency-version: 2026.04.13-sha-e95f1cc dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/edge-runtime dependency-version: v1.73.5 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/realtime dependency-version: v2.83.1 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/storage-api dependency-version: v1.51.0 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/logflare dependency-version: 1.38.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-minor ... * Downgrade postgrest version from 14.9 to 14.8 --------- * chore(workflows): enable install scripts for supabase package in Yarn (#5111) chore(workflows): enable install scripts for supabase package in Yarn Berry setup This change sets the YARN_ENABLE_SCRIPTS environment variable to true during the installation of the supabase package, allowing its postinstall script to run as required by Yarn Berry 4.14+. This adjustment ensures the necessary binary is fetched correctly. * feat: --diff-engine flag on db pull * fix(docker): bump the docker-minor group in /pkg/config/templates with 6 updates (#5113) fix(docker): bump the docker-minor group Bumps the docker-minor group in /pkg/config/templates with 6 updates: | Package | From | To | | --- | --- | --- | | postgrest/postgrest | `v14.8` | `v14.10` | | supabase/studio | `2026.04.13-sha-e95f1cc` | `2026.04.20-sha-b721a2d` | | supabase/edge-runtime | `v1.73.5` | `v1.73.13` | | supabase/realtime | `v2.83.1` | `v2.86.3` | | supabase/storage-api | `v1.51.0` | `v1.54.1` | | supabase/logflare | `1.38.2` | `1.39.1` | Updates `postgrest/postgrest` from v14.8 to v14.10 Updates `supabase/studio` from 2026.04.13-sha-e95f1cc to 2026.04.20-sha-b721a2d Updates `supabase/edge-runtime` from v1.73.5 to v1.73.13 Updates `supabase/realtime` from v2.83.1 to v2.86.3 Updates `supabase/storage-api` from v1.51.0 to v1.54.1 Updates `supabase/logflare` from 1.38.2 to 1.39.1 --- updated-dependencies: - dependency-name: postgrest/postgrest dependency-version: v14.10 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/studio dependency-version: 2026.04.20-sha-b721a2d dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/edge-runtime dependency-version: v1.73.13 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/realtime dependency-version: v2.86.3 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/storage-api dependency-version: v1.54.1 dependency-type: direct:production dependency-group: docker-minor - dependency-name: supabase/logflare dependency-version: 1.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-minor ... * feat: exposing new api keys to functions (#4946) * chore: upgrade pg-delta to alpha.20 in multiple templates * fix: remove version comparison check for storage image updates (#5118) fix: honor pinned storage version offline Remove the version comparison that only pinned storage when the local version was newer than the default. This prevented `supabase start` from using an already-downloaded image offline, since Docker would still try to pull the default newer image. Fixes CLI-1393. * fix: improve error handling and output formatting in pg-delta apply process (#5120) - Updated the `runDeclarativeSync` function to avoid wrapping SQL output with `utils.Bold`, preventing excessive whitespace in multi-line SQL. - Changed the result accumulation in `migra.ts` from string concatenation to an array for better performance and clarity. - Enhanced the `ApplyResult` struct to include `ValidationErrors` and `Diagnostics`, allowing for more detailed error reporting. - Modified the `formatApplyFailure` function to include validation errors and diagnostics in the output, improving user feedback on apply failures. - Added tests for validation error handling in `apply_test.go` to ensure robustness against various error scenarios. --------- Signed-off-by: dependabot[bot] Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Co-authored-by: fadymak Co-authored-by: Vaibhav <117663341+7ttp@users.noreply.github.com> Co-authored-by: Sean Oliver <882952+seanoliver@users.noreply.github.com> Co-authored-by: Han Qiao Co-authored-by: Julien Goux Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mert YEREKAPAN Co-authored-by: Mert YEREKAPAN <33198490+myerekapan@users.noreply.github.com> Co-authored-by: Kalleby Santos <105971119+kallebysantos@users.noreply.github.com> Co-authored-by: Claude * Revert "Update Dockerfile for Studio image 2026-04-27" (#5132) * Revert "Revert "Update Dockerfile for Studio image 2026-04-27"" (#5134) Revert "Revert "Update Dockerfile for Studio image 2026-04-27" (#5132)" This reverts commit 9251eaf45dfd190aa188764999b0024eae58b5ab. * fix(windows): json unmarshal errors in telemetry and pg-delta declarative sync (#5128) * fix(windows): json unmarshal errors in telemetry and pg-delta declarative sync Three Windows-only failures, all surfacing as JSON parse errors: 1. telemetry: any field-level unmarshal error (e.g. session_last_active stored as a number) now recreates state instead of propagating, since identity fields aren't worth surfacing an error for. 2. pg-delta declarative sync: containerRef now normalises Windows path separators with filepath.ToSlash so paths like supabase\.temp\pgdelta\catalog-baseline.json resolve correctly inside the Linux edge-runtime container. 3. pg-delta export/diff: parse callers (DeclarativeExportPgDeltaRef, ExportCatalogPgDelta, pgcache.exportCatalog) now surface stderr when stdout is empty, instead of failing later with "unexpected end of JSON input". DiffPgDeltaRef intentionally still accepts empty stdout as a legitimate "no schema changes" result. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: only run test on windows and fix lint --------- Co-authored-by: Claude Opus 4.7 (1M context) --------- Signed-off-by: dependabot[bot] Co-authored-by: Andrew Valleteau Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kalleby Santos <105971119+kallebysantos@users.noreply.github.com> Co-authored-by: avallete Co-authored-by: Claude Co-authored-by: It's Me! <192345912+rebasecase@users.noreply.github.com> Co-authored-by: rebasecase Co-authored-by: Joshen Lim Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Co-authored-by: fadymak Co-authored-by: Vaibhav <117663341+7ttp@users.noreply.github.com> Co-authored-by: Sean Oliver <882952+seanoliver@users.noreply.github.com> Co-authored-by: Han Qiao Co-authored-by: Mert YEREKAPAN Co-authored-by: Mert YEREKAPAN <33198490+myerekapan@users.noreply.github.com> --- internal/db/start/start.go | 2 +- internal/start/start.go | 188 +++++++++--------- internal/start/start_test.go | 65 ++++++ internal/start/templates/kong.yml | 8 +- pkg/config/auth.go | 8 + pkg/config/config.go | 9 +- pkg/config/config_test.go | 37 +++- pkg/config/templates/config.toml | 6 +- .../local_enabled_and_disabled.diff | 16 +- 9 files changed, 228 insertions(+), 111 deletions(-) diff --git a/internal/db/start/start.go b/internal/db/start/start.go index a30619398f..cf9cdc8dfd 100644 --- a/internal/db/start/start.go +++ b/internal/db/start/start.go @@ -320,7 +320,7 @@ func initAuthJob(host string) utils.DockerJob { return utils.DockerJob{ Image: utils.Config.Auth.Image, Env: []string{ - "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl, + "API_EXTERNAL_URL=" + utils.Config.AuthExternalURL(), "GOTRUE_LOG_LEVEL=error", "GOTRUE_DB_DRIVER=postgres", fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host), diff --git a/internal/start/start.go b/internal/start/start.go index 846e87e4b7..dd195cfb66 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -573,78 +573,7 @@ EOF // Start GoTrue. if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) { - var testOTP bytes.Buffer - if len(utils.Config.Auth.Sms.TestOTP) > 0 { - formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP) - } - - env := []string{ - "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl, - - "GOTRUE_API_HOST=0.0.0.0", - "GOTRUE_API_PORT=9999", - - "GOTRUE_DB_DRIVER=postgres", - fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), - - "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, - "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","), - fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup), - - "GOTRUE_JWT_ADMIN_ROLES=service_role", - "GOTRUE_JWT_AUD=authenticated", - "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated", - fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry), - "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, - "GOTRUE_JWT_ISSUER=" + utils.Config.Auth.JwtIssuer, - - fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup), - fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges), - fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations), - fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength), - fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry), - "GOTRUE_MAILER_TEMPLATE_RELOADING_ENABLED=true", - - fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns), - - fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency), - - fmt.Sprintf("GOTRUE_MAILER_URLPATHS_INVITE=%s/verify", utils.Config.Auth.JwtIssuer), - fmt.Sprintf("GOTRUE_MAILER_URLPATHS_CONFIRMATION=%s/verify", utils.Config.Auth.JwtIssuer), - fmt.Sprintf("GOTRUE_MAILER_URLPATHS_RECOVERY=%s/verify", utils.Config.Auth.JwtIssuer), - fmt.Sprintf("GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=%s/verify", utils.Config.Auth.JwtIssuer), - "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000", - - fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup), - fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations), - fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency), - "GOTRUE_SMS_OTP_EXP=6000", - "GOTRUE_SMS_OTP_LENGTH=6", - fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template), - "GOTRUE_SMS_TEST_OTP=" + testOTP.String(), - - fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength), - fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()), - fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation), - fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval), - fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking), - fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange), - fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled), - fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled), - fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled), - fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled), - fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled), - fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled), - fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors), - - // Add rate limit configurations - fmt.Sprintf("GOTRUE_RATE_LIMIT_ANONYMOUS_USERS=%v", utils.Config.Auth.RateLimit.AnonymousUsers), - fmt.Sprintf("GOTRUE_RATE_LIMIT_TOKEN_REFRESH=%v", utils.Config.Auth.RateLimit.TokenRefresh), - fmt.Sprintf("GOTRUE_RATE_LIMIT_OTP=%v", utils.Config.Auth.RateLimit.SignInSignUps), - fmt.Sprintf("GOTRUE_RATE_LIMIT_VERIFY=%v", utils.Config.Auth.RateLimit.TokenVerifications), - fmt.Sprintf("GOTRUE_RATE_LIMIT_SMS_SENT=%v", utils.Config.Auth.RateLimit.SmsSent), - fmt.Sprintf("GOTRUE_RATE_LIMIT_WEB3=%v", utils.Config.Auth.RateLimit.Web3), - } + env := buildGotrueEnv(dbConfig) // Serialise default or custom signing keys if keys, err := json.Marshal(utils.Config.Auth.SigningKeys); err == nil { @@ -817,26 +746,7 @@ EOF ) } - for name, config := range utils.Config.Auth.External { - env = append( - env, - fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled), - fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId), - fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value), - fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck), - fmt.Sprintf("GOTRUE_EXTERNAL_%s_EMAIL_OPTIONAL=%t", strings.ToUpper(name), config.EmailOptional), - ) - - redirectUri := config.RedirectUri - if redirectUri == "" { - redirectUri = utils.Config.Auth.JwtIssuer + "/callback" - } - env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), redirectUri)) - - if config.Url != "" { - env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url)) - } - } + env = appendGotrueExternalProviderEnv(env) env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_WEB3_SOLANA_ENABLED=%v", utils.Config.Auth.Web3.Solana.Enabled), fmt.Sprintf("GOTRUE_EXTERNAL_WEB3_ETHEREUM_ENABLED=%v", utils.Config.Auth.Web3.Ethereum.Enabled), @@ -1368,6 +1278,100 @@ func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) { } } +func buildGotrueEnv(dbConfig pgconn.Config) []string { + var testOTP bytes.Buffer + if len(utils.Config.Auth.Sms.TestOTP) > 0 { + formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP) + } + + return []string{ + "API_EXTERNAL_URL=" + utils.Config.AuthExternalURL(), + + "GOTRUE_API_HOST=0.0.0.0", + "GOTRUE_API_PORT=9999", + + "GOTRUE_DB_DRIVER=postgres", + fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), + + "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, + "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","), + fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup), + + "GOTRUE_JWT_ADMIN_ROLES=service_role", + "GOTRUE_JWT_AUD=authenticated", + "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated", + fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry), + "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value, + "GOTRUE_JWT_ISSUER=" + utils.Config.Auth.JwtIssuer, + + fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup), + fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges), + fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations), + fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength), + fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry), + "GOTRUE_MAILER_TEMPLATE_RELOADING_ENABLED=true", + + fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns), + + fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency), + + "GOTRUE_MAILER_URLPATHS_INVITE=/verify", + "GOTRUE_MAILER_URLPATHS_CONFIRMATION=/verify", + "GOTRUE_MAILER_URLPATHS_RECOVERY=/verify", + "GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/verify", + "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000", + + fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup), + fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations), + fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency), + "GOTRUE_SMS_OTP_EXP=6000", + "GOTRUE_SMS_OTP_LENGTH=6", + fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template), + "GOTRUE_SMS_TEST_OTP=" + testOTP.String(), + + fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength), + fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()), + fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation), + fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval), + fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking), + fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange), + fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled), + fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled), + fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled), + fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled), + fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled), + fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled), + fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors), + + fmt.Sprintf("GOTRUE_RATE_LIMIT_ANONYMOUS_USERS=%v", utils.Config.Auth.RateLimit.AnonymousUsers), + fmt.Sprintf("GOTRUE_RATE_LIMIT_TOKEN_REFRESH=%v", utils.Config.Auth.RateLimit.TokenRefresh), + fmt.Sprintf("GOTRUE_RATE_LIMIT_OTP=%v", utils.Config.Auth.RateLimit.SignInSignUps), + fmt.Sprintf("GOTRUE_RATE_LIMIT_VERIFY=%v", utils.Config.Auth.RateLimit.TokenVerifications), + fmt.Sprintf("GOTRUE_RATE_LIMIT_SMS_SENT=%v", utils.Config.Auth.RateLimit.SmsSent), + fmt.Sprintf("GOTRUE_RATE_LIMIT_WEB3=%v", utils.Config.Auth.RateLimit.Web3), + } +} + +func appendGotrueExternalProviderEnv(env []string) []string { + for name, config := range utils.Config.Auth.External { + env = append( + env, + fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled), + fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId), + fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value), + fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck), + fmt.Sprintf("GOTRUE_EXTERNAL_%s_EMAIL_OPTIONAL=%t", strings.ToUpper(name), config.EmailOptional), + ) + if config.RedirectUri != "" { + env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), config.RedirectUri)) + } + if config.Url != "" { + env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url)) + } + } + return env +} + func printSecurityNotice() { fmt.Fprintln(os.Stderr, utils.Yellow("Local dev security notice")) fmt.Fprintln(os.Stderr, "All services bind to 0.0.0.0 (network-accessible, not just localhost)") diff --git a/internal/start/start_test.go b/internal/start/start_test.go index 56d237e2b9..573c28b092 100644 --- a/internal/start/start_test.go +++ b/internal/start/start_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "regexp" + "strings" "testing" "time" @@ -300,6 +301,59 @@ func TestDatabaseStart(t *testing.T) { }) } +func TestBuildGotrueEnv(t *testing.T) { + original := utils.Config + t.Cleanup(func() { + utils.Config = original + }) + + t.Run("uses auth scoped external url and relative mailer paths", func(t *testing.T) { + utils.Config = config.NewConfig() + utils.Config.Api.ExternalUrl = "http://127.0.0.1:54321" + utils.Config.Auth.ExternalUrl = "http://127.0.0.1:54321/auth/v1" + utils.Config.Auth.JwtIssuer = utils.Config.Auth.ExternalUrl + utils.Config.Auth.SiteUrl = "http://127.0.0.1:3000" + provider := utils.Config.Auth.External["github"] + provider.Enabled = true + provider.ClientId = "client-id" + provider.Secret.Value = "secret" + utils.Config.Auth.External["github"] = provider + + env := envToMap(appendGotrueExternalProviderEnv(buildGotrueEnv(pgconn.Config{ + Host: "db", + Port: 5432, + Database: "postgres", + Password: "postgres", + }))) + + assert.Equal(t, "http://127.0.0.1:54321/auth/v1", env["API_EXTERNAL_URL"]) + assert.Equal(t, "http://127.0.0.1:54321/auth/v1", env["GOTRUE_JWT_ISSUER"]) + assert.Equal(t, "/verify", env["GOTRUE_MAILER_URLPATHS_INVITE"]) + assert.Equal(t, "/verify", env["GOTRUE_MAILER_URLPATHS_CONFIRMATION"]) + assert.Equal(t, "/verify", env["GOTRUE_MAILER_URLPATHS_RECOVERY"]) + assert.Equal(t, "/verify", env["GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE"]) + assert.NotContains(t, env, "GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI") + }) + + t.Run("preserves explicit provider redirect override", func(t *testing.T) { + utils.Config = config.NewConfig() + utils.Config.Api.ExternalUrl = "http://127.0.0.1:54321" + utils.Config.Auth.ExternalUrl = "http://127.0.0.1:54321/auth/v1" + utils.Config.Auth.JwtIssuer = "https://issuer.example.com/auth/v1" + utils.Config.Auth.SiteUrl = "http://127.0.0.1:3000" + provider := utils.Config.Auth.External["azure"] + provider.Enabled = true + provider.RedirectUri = "https://example.com/custom/callback" + utils.Config.Auth.External["azure"] = provider + + env := envToMap(appendGotrueExternalProviderEnv(buildGotrueEnv(pgconn.Config{}))) + + assert.Equal(t, "http://127.0.0.1:54321/auth/v1", env["API_EXTERNAL_URL"]) + assert.Equal(t, "https://issuer.example.com/auth/v1", env["GOTRUE_JWT_ISSUER"]) + assert.Equal(t, "https://example.com/custom/callback", env["GOTRUE_EXTERNAL_AZURE_REDIRECT_URI"]) + }) +} + func TestFormatMapForEnvConfig(t *testing.T) { t.Run("It produces the correct format and removes the trailing comma", func(t *testing.T) { testcases := []struct { @@ -347,3 +401,14 @@ func TestFormatMapForEnvConfig(t *testing.T) { } }) } + +func envToMap(env []string) map[string]string { + result := make(map[string]string, len(env)) + for _, item := range env { + key, value, ok := strings.Cut(item, "=") + if ok { + result[key] = value + } + } + return result +} diff --git a/internal/start/templates/kong.yml b/internal/start/templates/kong.yml index 0c185eb6e4..4cbfc3b1eb 100644 --- a/internal/start/templates/kong.yml +++ b/internal/start/templates/kong.yml @@ -2,7 +2,7 @@ _format_version: "1.1" services: # Tenant project endpoints - name: auth-v1-open - _comment: "GoTrue: /auth/v1/verify* -> http://auth:9999/verify*" + _comment: "GoTrue external /auth/v1/verify* -> internal root /verify*" url: http://{{ .GotrueId }}:9999/verify routes: - name: auth-v1-open @@ -12,7 +12,7 @@ services: plugins: - name: cors - name: auth-v1-open-callback - _comment: "GoTrue: /auth/v1/callback* -> http://auth:9999/callback*" + _comment: "GoTrue external /auth/v1/callback* -> internal root /callback*" url: http://{{ .GotrueId }}:9999/callback routes: - name: auth-v1-open-callback @@ -22,7 +22,7 @@ services: plugins: - name: cors - name: auth-v1-open-authorize - _comment: "GoTrue: /auth/v1/authorize* -> http://auth:9999/authorize*" + _comment: "GoTrue external /auth/v1/authorize* -> internal root /authorize*" url: http://{{ .GotrueId }}:9999/authorize routes: - name: auth-v1-open-authorize @@ -32,7 +32,7 @@ services: plugins: - name: cors - name: auth-v1 - _comment: "GoTrue: /auth/v1/* -> http://auth:9999/*" + _comment: "GoTrue external /auth/v1/* -> internal root /*" url: http://{{ .GotrueId }}:9999/ routes: - name: auth-v1-all diff --git a/pkg/config/auth.go b/pkg/config/auth.go index c1795c8971..4517affb5b 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -150,6 +150,7 @@ type ( Image string `toml:"-" json:"-"` SiteUrl string `toml:"site_url" json:"site_url"` + ExternalUrl string `toml:"external_url" json:"external_url"` AdditionalRedirectUrls []string `toml:"additional_redirect_urls" json:"additional_redirect_urls"` JwtExpiry uint `toml:"jwt_expiry" json:"jwt_expiry"` JwtIssuer string `toml:"jwt_issuer" json:"jwt_issuer"` @@ -397,6 +398,13 @@ type ( } ) +func (a auth) GetExternalURL(apiExternalURL string) string { + if len(a.ExternalUrl) > 0 { + return a.ExternalUrl + } + return strings.TrimRight(apiExternalURL, "/") + "/auth/v1" +} + func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody { body := v1API.UpdateAuthConfigBody{ SiteUrl: nullable.NewNullableWithValue(a.SiteUrl), diff --git a/pkg/config/config.go b/pkg/config/config.go index d17b894416..8e5a4b943f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -445,6 +445,10 @@ func NewConfig(editors ...ConfigEditor) config { return initial } +func (c *config) AuthExternalURL() string { + return c.Auth.GetExternalURL(c.Api.ExternalUrl) +} + var ( //go:embed templates/certs/kong.local.crt kongCert []byte @@ -632,9 +636,12 @@ func (c *config) Load(path string, fsys fs.FS, overrides ...ConfigEditor) error } c.Api.ExternalUrl = apiUrl.String() } + if len(c.Auth.ExternalUrl) == 0 { + c.Auth.ExternalUrl = c.AuthExternalURL() + } // Set default JWT issuer if not configured if len(c.Auth.JwtIssuer) == 0 { - c.Auth.JwtIssuer = c.Api.ExternalUrl + "/auth/v1" + c.Auth.JwtIssuer = c.Auth.ExternalUrl } // Update image versions switch c.Db.MajorVersion { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f019b7cbce..abd581d11e 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -39,14 +39,43 @@ func TestConfigParsing(t *testing.T) { assert.NoError(t, err) }) + t.Run("auth external url defaults from api external url", func(t *testing.T) { + config := NewConfig() + require.NoError(t, config.Load("", fs.MapFS{})) + + assert.Equal(t, strings.TrimRight(config.Api.ExternalUrl, "/")+"/auth/v1", config.Auth.ExternalUrl) + assert.Equal(t, config.Auth.ExternalUrl, config.Auth.JwtIssuer) + }) + + t.Run("auth external url and jwt issuer preserve explicit overrides", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "config.toml": &fs.MapFile{Data: []byte(` +[api] +external_url = "https://api.example.com/" + +[auth] +site_url = "https://app.example.com" +external_url = "https://auth.example.com/custom/" +jwt_issuer = "https://issuer.example.com/custom/" +`)}, + } + + require.NoError(t, config.Load("config.toml", fsys)) + assert.Equal(t, "https://auth.example.com/custom/", config.Auth.ExternalUrl) + assert.Equal(t, "https://issuer.example.com/custom/", config.Auth.JwtIssuer) + }) + t.Run("config file with environment variables", func(t *testing.T) { config := NewConfig() // Setup in-memory fs fsys := fs.MapFS{ - "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, - "supabase/templates/invite.html": &fs.MapFile{}, - "certs/my-cert.pem": &fs.MapFile{}, - "certs/my-key.pem": &fs.MapFile{}, + "supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed}, + "supabase/templates/invite.html": &fs.MapFile{}, + "supabase/templates/password_changed_notification.html": &fs.MapFile{}, + "supabase/signing_keys.json": &fs.MapFile{Data: []byte("[]")}, + "certs/my-cert.pem": &fs.MapFile{}, + "certs/my-key.pem": &fs.MapFile{}, } // Run test t.Setenv("TWILIO_AUTH_TOKEN", "token") diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index 97ed4e5665..d34c36c9ba 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -152,11 +152,13 @@ enabled = true # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used # in emails. site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. additional_redirect_urls = ["https://127.0.0.1:3000"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 -# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# JWT issuer URL. If not set, defaults to auth.external_url. # jwt_issuer = "" # Path to JWT signing key. DO NOT commit your signing keys file to git. # signing_keys_path = "./signing_keys.json" @@ -317,7 +319,7 @@ enabled = false client_id = "" # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. +# Overrides the default auth callback URL derived from auth.external_url. redirect_uri = "" # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, # or any other third-party OIDC providers. diff --git a/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff index 5425d12659..21094fdd40 100644 --- a/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff +++ b/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff @@ -1,11 +1,16 @@ diff remote[auth] local[auth] --- remote[auth] +++ local[auth] -@@ -1,14 +1,14 @@ +@@ -1,16 +1,16 @@ enabled = false -site_url = "" ++site_url = "http://127.0.0.1:3000" + external_url = "" -additional_redirect_urls = ["https://127.0.0.1:3000", "https://ref.supabase.co"] -jwt_expiry = 0 ++additional_redirect_urls = ["https://127.0.0.1:3000"] ++jwt_expiry = 3600 + jwt_issuer = "" -enable_refresh_token_rotation = true -refresh_token_reuse_interval = 0 -enable_manual_linking = true @@ -13,9 +18,6 @@ diff remote[auth] local[auth] -enable_anonymous_sign_ins = true -minimum_password_length = 8 -password_requirements = "letters_digits" -+site_url = "http://127.0.0.1:3000" -+additional_redirect_urls = ["https://127.0.0.1:3000"] -+jwt_expiry = 3600 +enable_refresh_token_rotation = false +refresh_token_reuse_interval = 10 +enable_manual_linking = false @@ -23,6 +25,6 @@ diff remote[auth] local[auth] +enable_anonymous_sign_ins = false +minimum_password_length = 6 +password_requirements = "lower_upper_letters_digits_symbols" - jwt_secret = "" - anon_key = "" - service_role_key = "" + signing_keys_path = "" + publishable_key = "" + secret_key = "" From 73148544d2b1e4e2536577c4ec5fa51a3e614a7d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 29 Apr 2026 14:07:57 +0800 Subject: [PATCH 4/8] Update Dockerfile (#5144) --- pkg/config/templates/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index fb3225da8f..99efdf2cf8 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -5,7 +5,7 @@ FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v14.10 AS postgrest FROM supabase/postgres-meta:v0.96.4 AS pgmeta -FROM supabase/studio:2026.04.27-sha-4afbe9c AS studio +FROM supabase/studio:2026.04.28-sha-89d08a2 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.73.13 AS edgeruntime FROM timberio/vector:0.53.0-alpine AS vector From a11f261c5b5c350a12d931d48c8afeded31ef25c Mon Sep 17 00:00:00 2001 From: wucm667 <109257021+wucm667@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:24:28 +0800 Subject: [PATCH 5/8] fix(functions): set nofile ulimit for Edge Runtime container (#5152) * fix(functions): set nofile ulimit for Edge Runtime container The Edge Runtime container was started with Docker's default nofile ulimit (1024 soft), causing "Too many open files" errors for projects with 200+ Edge Functions running long dev sessions. Set nofile ulimit to 65536 (soft and hard) to allow sufficient file descriptors for Deno isolates handling many concurrent functions. Fixes #5151 Signed-off-by: wucm667 * chore: add comment internal/functions/serve/serve.go * chore: fix comment indent --------- Signed-off-by: wucm667 Co-authored-by: Andrew Valleteau --- internal/functions/serve/serve.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index dc82f532e4..38ca9f7e2e 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -223,6 +223,16 @@ EOF container.HostConfig{ Binds: binds, PortBindings: portBindings, + Resources: container.Resources{ + // Raise nofile to accommodate FD usage from many concurrent Deno isolates (see #5151). + Ulimits: []*container.Ulimit{ + { + Name: "nofile", + Soft: 65536, + Hard: 65536, + }, + }, + }, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ From cd3c3d3fccb49eaac7e9541afda95acb7bb05541 Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Thu, 30 Apr 2026 04:32:52 -0500 Subject: [PATCH 6/8] feat: add SUPABASE_HOSTNAME env var to override local service host (#4947) feat: add SUPABASE_SERVICES_HOSTNAME env var for local service host Allows the CLI to communicate with where supabase services are listening when it differs from where the docker engine API is listening. The concrete use case was running the CLI inside a dev container with the host Docker socket bind-mounted. In that context 127.0.0.1 is the container's own loopback, not the Docker host. Set SUPABASE_SERVICES_HOSTNAME=host.docker.internal to reach sibling containers via Docker Desktop's host gateway without proxying the Docker socket. --- AI assistance: Claude Sonnet 4.6 in Pi Agent - Used to find root cause, propose fix options, draft code/test/docs. - I ran supabase start inside the dev container, confirmed all services healthy, reviewed/edited all code changes/text. Co-authored-by: Joel Martin Co-authored-by: Andrew Valleteau --- docs/supabase/start.md | 2 ++ internal/utils/misc.go | 6 ++++++ internal/utils/misc_test.go | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/docs/supabase/start.md b/docs/supabase/start.md index a590610f11..7db99b8c49 100644 --- a/docs/supabase/start.md +++ b/docs/supabase/start.md @@ -9,3 +9,5 @@ All service containers are started by default. You can exclude those not needed > It is recommended to have at least 7GB of RAM to start all services. Health checks are automatically added to verify the started containers. Use `--ignore-health-check` flag to ignore these errors. + +> If the CLI is running inside a dev container with the Docker socket bind-mounted, set the `SUPABASE_SERVICES_HOSTNAME` environment variable to the hostname reachable from inside that container, such as `host.docker.internal`. diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 937519aa53..149dfee320 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -256,6 +256,12 @@ func ValidateFunctionSlug(slug string) error { } func GetHostname() string { + // Overrides the host for local service connections. Useful when + // running inside a dev container when the Docker host is not + // 127.0.0.1 (container's own loopback). + if h := os.Getenv("SUPABASE_SERVICES_HOSTNAME"); h != "" { + return h + } host := Docker.DaemonHost() if parsed, err := client.ParseHostURL(host); err == nil && parsed.Scheme == "tcp" { if host, _, err := net.SplitHostPort(parsed.Host); err == nil { diff --git a/internal/utils/misc_test.go b/internal/utils/misc_test.go index 0e1b6475d7..e8e075201b 100644 --- a/internal/utils/misc_test.go +++ b/internal/utils/misc_test.go @@ -157,6 +157,18 @@ func TestAssertProjectRefIsValid(t *testing.T) { }) } +func TestGetHostname(t *testing.T) { + t.Run("returns SUPABASE_SERVICES_HOSTNAME when set", func(t *testing.T) { + t.Setenv("SUPABASE_SERVICES_HOSTNAME", "host.docker.internal") + assert.Equal(t, "host.docker.internal", GetHostname()) + }) + + t.Run("returns 127.0.0.1 when SUPABASE_SERVICES_HOSTNAME is not set", func(t *testing.T) { + t.Setenv("SUPABASE_SERVICES_HOSTNAME", "") + assert.Equal(t, "127.0.0.1", GetHostname()) + }) +} + func TestWriteFile(t *testing.T) { t.Run("writes file with directories", func(t *testing.T) { fsys := afero.NewMemMapFs() From 3f01508fd6ac3278470938108bef739b03f229ac Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:08:41 +0200 Subject: [PATCH 7/8] feat: emit Claude Code plugin hint from CLI (#5153) * feat: emit Claude Code plugin hint from CLI Adds a claude-code-hint tag to stderr when running inside Claude Code (CLAUDECODE env var set). Claude Code strips the tag before it reaches the model and shows a one-time prompt to install the Supabase plugin. Emits on every invocation via Execute() for maximum coverage, and again after login success so users in a setup flow are reached too. Co-Authored-By: Claude Sonnet 4.6 * feat: emit Claude Code plugin hint from CLI Adds a claude-code-hint tag to stderr when running inside Claude Code (CLAUDECODE env var set). Claude Code strips the tag before it reaches the model and shows a one-time prompt to install the Supabase plugin. Emits on every invocation via Execute() for maximum coverage, and again after login success so users in a setup flow are reached too. Co-Authored-By: Claude Sonnet 4.6 * refactor: centralize Claude Code detection via IsClaudeCode() Extracts IsClaudeCode() from the existing IsAgent() check in agent.go and reuses it in SuggestClaudePlugin(). This ensures both places check the same env vars (CLAUDECODE and CLAUDE_CODE) consistently. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- cmd/root.go | 3 +++ internal/login/login.go | 6 ++++++ internal/utils/agent/agent.go | 7 ++++++- internal/utils/misc.go | 10 ++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index f8468a3e58..3fc341b370 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -193,6 +193,9 @@ func Execute() { if err != nil { fmt.Fprintln(utils.GetDebugLogger(), err) } + if hint := utils.SuggestClaudePlugin(); hint != "" { + fmt.Fprintln(os.Stderr, hint) + } if semver.Compare(version, "v"+utils.Version) > 0 { fmt.Fprintln(os.Stderr, suggestUpgrade(version)) } diff --git a/internal/login/login.go b/internal/login/login.go index 18ba9ee640..8394db2c6d 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -172,6 +172,9 @@ func Run(ctx context.Context, stdout io.Writer, params RunParams) error { } handleTelemetryAfterLogin(ctx, params) fmt.Println(loggedInMsg) + if hint := utils.SuggestClaudePlugin(); hint != "" { + fmt.Fprintln(os.Stderr, hint) + } return nil } @@ -223,6 +226,9 @@ func Run(ctx context.Context, stdout io.Writer, params RunParams) error { fmt.Fprintf(stdout, "Token %s created successfully.\n\n", utils.Bold(params.TokenName)) fmt.Fprintln(stdout, loggedInMsg) + if hint := utils.SuggestClaudePlugin(); hint != "" { + fmt.Fprintln(os.Stderr, hint) + } return nil } diff --git a/internal/utils/agent/agent.go b/internal/utils/agent/agent.go index 65846ea62a..e3b0d7f88f 100644 --- a/internal/utils/agent/agent.go +++ b/internal/utils/agent/agent.go @@ -5,6 +5,11 @@ import ( "strings" ) +// IsClaudeCode reports whether the CLI is running inside Claude Code. +func IsClaudeCode() bool { + return os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE") != "" +} + // IsAgent checks environment variables to detect if the CLI is being invoked // by an AI coding agent. Based on the detection logic from Vercel's // @vercel/functions/ai package. @@ -37,7 +42,7 @@ func IsAgent() bool { return true } // Claude Code - if os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE") != "" { + if IsClaudeCode() { return true } // Replit diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 149dfee320..9a1c420fc4 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -17,6 +17,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/spf13/afero" "github.com/spf13/viper" + "github.com/supabase/cli/internal/utils/agent" "github.com/supabase/cli/pkg/migration" ) @@ -38,6 +39,15 @@ func ShortContainerImageName(imageName string) string { const SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot the error." +const claudeCodeHint = `` + +func SuggestClaudePlugin() string { + if agent.IsClaudeCode() { + return claudeCodeHint + } + return "" +} + var ( CmdSuggestion string CurrentDirAbs string From 0dc6a4755da01e01ab75f34b955ccc01904afe35 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Thu, 30 Apr 2026 19:36:47 +0200 Subject: [PATCH 8/8] feat(pg-delta): enhance pg-delta version handling (#5154) feat(pg-delta): enhance pg-delta version handling and script interpolation - Updated the configuration to read the pg-delta npm version from the `.temp/pgdelta-version` file, defaulting to "1.0.0-alpha.22" if the file is missing or empty. - Implemented the `InterpolatePgDeltaScript` function to replace version placeholders in embedded TypeScript scripts with the effective pg-delta npm version. - Modified various functions to utilize the new script interpolation method for executing pg-delta related scripts, ensuring consistent version usage across the application. - Added tests to verify the correct behavior of the new version handling and interpolation logic. This change improves the management of pg-delta versions and enhances the flexibility of script execution. --- cmd/db_schema_declarative.go | 7 ++++- internal/db/diff/pgdelta.go | 10 ++++--- internal/db/pgcache/cache.go | 4 ++- internal/pgdelta/apply.go | 4 ++- internal/utils/misc.go | 1 + pkg/config/config.go | 12 +++++++++ pkg/config/config_test.go | 51 +++++++++++++++++++++++++++++++++++ pkg/config/pgdelta_version.go | 28 +++++++++++++++++++ pkg/config/utils.go | 2 ++ 9 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 pkg/config/pgdelta_version.go diff --git a/cmd/db_schema_declarative.go b/cmd/db_schema_declarative.go index 8cb98b9912..896b7bd582 100644 --- a/cmd/db_schema_declarative.go +++ b/cmd/db_schema_declarative.go @@ -52,7 +52,12 @@ var ( // If the user has passed the --experimental flag and pg-delta is not enabled, enable it // so in the rest of the code we can know that we're running pg-delta logic. if viper.GetBool("EXPERIMENTAL") && !utils.IsPgDeltaEnabled() { - utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true} + if utils.Config.Experimental.PgDelta == nil { + utils.Config.Experimental.PgDelta = &config.PgDeltaConfig{Enabled: true} + } else { + // We preserve the version set into `.temp/pgdelta-version` by just enabling pg-delta. + utils.Config.Experimental.PgDelta.Enabled = true + } } if !utils.IsPgDeltaEnabled() { utils.CmdSuggestion = fmt.Sprintf("Either pass %s or add %s with %s to %s", diff --git a/internal/db/diff/pgdelta.go b/internal/db/diff/pgdelta.go index 8ba8fb0fe9..dd6af84924 100644 --- a/internal/db/diff/pgdelta.go +++ b/internal/db/diff/pgdelta.go @@ -14,6 +14,7 @@ import ( "github.com/jackc/pgx/v4" "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/config" ) //go:embed templates/pgdelta.ts @@ -104,7 +105,8 @@ func DiffPgDeltaRef(ctx context.Context, sourceRef, targetRef string, schema []s binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaScript, binds, "error diffing schema", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error diffing schema", &stdout, &stderr); err != nil { return "", err } return stdout.String(), nil @@ -143,7 +145,8 @@ func DeclarativeExportPgDeltaRef(ctx context.Context, sourceRef, targetRef strin binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeExportScript, binds, "error exporting declarative schema", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaDeclarativeExportScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting declarative schema", &stdout, &stderr); err != nil { return DeclarativeOutput{}, err } if stdout.Len() == 0 { @@ -179,7 +182,8 @@ func ExportCatalogPgDelta(ctx context.Context, targetRef, role string, options . binds = append(binds, cwd+":/workspace") } var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportScript, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } snapshot := strings.TrimSpace(stdout.String()) diff --git a/internal/db/pgcache/cache.go b/internal/db/pgcache/cache.go index c6881c84cc..aeb1ebfbce 100644 --- a/internal/db/pgcache/cache.go +++ b/internal/db/pgcache/cache.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/viper" "github.com/supabase/cli/internal/gen/types" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/migration" ) @@ -253,7 +254,8 @@ func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.C } binds := []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"} var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportTS, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { + script := config.InterpolatePgDeltaScript(config.Config(&utils.Config), pgDeltaCatalogExportTS) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil { return "", err } snapshot := strings.TrimSpace(stdout.String()) diff --git a/internal/pgdelta/apply.go b/internal/pgdelta/apply.go index 2c57eb506b..f9009a3202 100644 --- a/internal/pgdelta/apply.go +++ b/internal/pgdelta/apply.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/viper" "github.com/supabase/cli/internal/utils" + pkgconfig "github.com/supabase/cli/pkg/config" ) //go:embed templates/pgdelta_declarative_apply.ts @@ -321,7 +322,8 @@ func ApplyDeclarative(ctx context.Context, config pgconn.Config, fsys afero.Fs) fmt.Fprintln(os.Stderr, "Applying declarative schemas via pg-delta...") var stdout, stderr bytes.Buffer - if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeApplyScript, binds, "error running pg-delta script", &stdout, &stderr); err != nil { + script := pkgconfig.InterpolatePgDeltaScript(pkgconfig.Config(&utils.Config), pgDeltaDeclarativeApplyScript) + if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error running pg-delta script", &stdout, &stderr); err != nil { return err } diff --git a/internal/utils/misc.go b/internal/utils/misc.go index 9a1c420fc4..7ba482d961 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -86,6 +86,7 @@ var ( PgmetaVersionPath = filepath.Join(TempDir, "pgmeta-version") PoolerVersionPath = filepath.Join(TempDir, "pooler-version") RealtimeVersionPath = filepath.Join(TempDir, "realtime-version") + PgDeltaVersionPath = filepath.Join(TempDir, "pgdelta-version") CliVersionPath = filepath.Join(TempDir, "cli-latest") CurrBranchPath = filepath.Join(SupabaseDirPath, ".branches", "_current_branch") // DeclarativeDir is the canonical location for pg-delta declarative schema diff --git a/pkg/config/config.go b/pkg/config/config.go index 8e5a4b943f..e2697ad825 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -229,6 +229,8 @@ type ( Enabled bool `toml:"enabled" json:"enabled"` DeclarativeSchemaPath string `toml:"declarative_schema_path" json:"declarative_schema_path"` FormatOptions string `toml:"format_options" json:"format_options"` + // NpmVersion is set from .temp/pgdelta-version during Load (not from TOML). + NpmVersion string `toml:"-" json:"-"` } inspect struct { @@ -690,6 +692,16 @@ func (c *config) Load(path string, fsys fs.FS, overrides ...ConfigEditor) error if version, err := fs.ReadFile(fsys, builder.LogflareVersionPath); err == nil && len(version) > 0 { c.Analytics.Image = replaceImageTag(Images.Logflare, string(version)) } + v := DefaultPgDeltaNpmVersion + if version, err := fs.ReadFile(fsys, builder.PgDeltaVersionPath); err == nil { + if trimmed := strings.TrimSpace(string(version)); len(trimmed) > 0 { + v = trimmed + } + } + if c.Experimental.PgDelta == nil { + c.Experimental.PgDelta = &PgDeltaConfig{} + } + c.Experimental.PgDelta.NpmVersion = v // TODO: replace derived config resolution with viper decode hooks if err := c.resolve(builder, fsys); err != nil { return err diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index abd581d11e..d7bca3948d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -243,6 +243,57 @@ format_options = "not-json" }) } +func TestPgDeltaNpmVersionPinning(t *testing.T) { + t.Run("defaults when pgdelta-version file missing", func(t *testing.T) { + c := NewConfig() + require.NoError(t, c.Load("", fs.MapFS{})) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, DefaultPgDeltaNpmVersion, c.Experimental.PgDelta.NpmVersion) + assert.Equal(t, DefaultPgDeltaNpmVersion, EffectivePgDeltaNpmVersion(Config(&c))) + }) + + t.Run("EffectivePgDeltaNpmVersion nil config uses default", func(t *testing.T) { + assert.Equal(t, DefaultPgDeltaNpmVersion, EffectivePgDeltaNpmVersion(nil)) + }) + + t.Run("reads trimmed version from supabase/.temp/pgdelta-version", func(t *testing.T) { + c := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[experimental.pgdelta] +enabled = true +`)}, + "supabase/.temp/pgdelta-version": &fs.MapFile{Data: []byte(" 9.9.9-test \n")}, + } + require.NoError(t, c.Load("", fsys)) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, "9.9.9-test", c.Experimental.PgDelta.NpmVersion) + assert.Equal(t, "9.9.9-test", EffectivePgDeltaNpmVersion(Config(&c))) + }) + + t.Run("whitespace-only pgdelta-version keeps default", func(t *testing.T) { + c := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` +[experimental.pgdelta] +enabled = true +`)}, + "supabase/.temp/pgdelta-version": &fs.MapFile{Data: []byte(" \n")}, + } + require.NoError(t, c.Load("", fsys)) + require.NotNil(t, c.Experimental.PgDelta) + assert.Equal(t, DefaultPgDeltaNpmVersion, c.Experimental.PgDelta.NpmVersion) + }) + + t.Run("InterpolatePgDeltaScript substitutes placeholder", func(t *testing.T) { + c := NewConfig() + require.NoError(t, c.Load("", fs.MapFS{})) + // Embedded TS pins use this semver literal before InterpolatePgDeltaScript runs. + got := InterpolatePgDeltaScript(Config(&c), `from "npm:@supabase/pg-delta@1.0.0-alpha.20";`) + assert.Equal(t, `from "npm:@supabase/pg-delta@`+DefaultPgDeltaNpmVersion+`";`, got) + }) +} + func TestRemoteOverride(t *testing.T) { t.Run("load staging override", func(t *testing.T) { config := NewConfig() diff --git a/pkg/config/pgdelta_version.go b/pkg/config/pgdelta_version.go new file mode 100644 index 0000000000..297a4c34b1 --- /dev/null +++ b/pkg/config/pgdelta_version.go @@ -0,0 +1,28 @@ +package config + +import "strings" + +// DefaultPgDeltaNpmVersion is the npm dist-tag/version used for @supabase/pg-delta +// when supabase/.temp/pgdelta-version is absent or empty. +const DefaultPgDeltaNpmVersion = "1.0.0-alpha.22" + +const pgDeltaNpmVersionPlaceholder = "1.0.0-alpha.20" + +// EffectivePgDeltaNpmVersion returns the pg-delta npm version from loaded config, +// or DefaultPgDeltaNpmVersion when unset (e.g. before Load or empty field). +func EffectivePgDeltaNpmVersion(c Config) string { + if c == nil { + return DefaultPgDeltaNpmVersion + } + if c.Experimental.PgDelta != nil { + if v := strings.TrimSpace(c.Experimental.PgDelta.NpmVersion); v != "" { + return v + } + } + return DefaultPgDeltaNpmVersion +} + +// InterpolatePgDeltaScript substitutes pg delta npm version placeholders in embedded TS. +func InterpolatePgDeltaScript(c Config, script string) string { + return strings.ReplaceAll(script, pgDeltaNpmVersionPlaceholder, EffectivePgDeltaNpmVersion(c)) +} diff --git a/pkg/config/utils.go b/pkg/config/utils.go index 2bb6db6f4b..4c004d4eeb 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -28,6 +28,7 @@ type pathBuilder struct { RealtimeVersionPath string EdgeRuntimeVersionPath string LogflareVersionPath string + PgDeltaVersionPath string CliVersionPath string CurrBranchPath string SchemasDir string @@ -64,6 +65,7 @@ func NewPathBuilder(configPath string) pathBuilder { PoolerVersionPath: filepath.Join(base, ".temp", "pooler-version"), RealtimeVersionPath: filepath.Join(base, ".temp", "realtime-version"), LogflareVersionPath: filepath.Join(base, ".temp", "logflare-version"), + PgDeltaVersionPath: filepath.Join(base, ".temp", "pgdelta-version"), CliVersionPath: filepath.Join(base, ".temp", "cli-latest"), CurrBranchPath: filepath.Join(base, ".branches", "_current_branch"), SchemasDir: filepath.Join(base, "schemas"),