Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fd0b568
fix(cli): render SMS/MFA config templates instead of emitting the raw…
Coly010 Jul 27, 2026
78c8a7f
fix(cli): gate vanity-subdomains and network-restrictions behind --ex…
Coly010 Jul 27, 2026
ceb8181
fix(cli): suppress the --debug hint when a confirmation prompt is dec…
Coly010 Jul 27, 2026
3d62a68
fix(cli): surface db connection-failure detail and fix connect sugges…
Coly010 Jul 27, 2026
c4b4587
fix(cli): honor --yes/SUPABASE_YES and Go prompt semantics consistent…
Coly010 Jul 27, 2026
110a6ed
chore(deps): bump github.com/posthog/posthog-go from 1.19.0 to 1.21.0…
dependabot[bot] Jul 28, 2026
83a55b2
chore(ci): bump the actions-major group with 2 updates (#5963)
dependabot[bot] Jul 28, 2026
6d65c9d
chore(deps): bump the npm-major group across 1 directory with 23 upda…
dependabot[bot] Jul 28, 2026
98a0e93
fix(docker): bump the docker-minor group across 1 directory with 6 up…
dependabot[bot] Jul 28, 2026
0748c93
fix(docker): bump supabase/postgres from 17.6.1.143 to 17.6.1.149 in …
dependabot[bot] Jul 28, 2026
4b62e72
feat(cli): envelope-first plan-gate upgrade hints (#5943)
pamelachia Jul 28, 2026
e7d4fc9
fix(cli): diagnose unrunnable images (#5966)
7ttp Jul 28, 2026
daf3c86
fix(cli): render Go's caret, Detail, and 42704 hint in migration-appl…
Coly010 Jul 28, 2026
76db729
fix(cli): bump default pg-delta version to pick up PUBLIC-privilege r…
Coly010 Jul 28, 2026
56395fc
fix(cli): print Connecting line and relative paths in db pull output …
Coly010 Jul 28, 2026
5cd669c
fix(cli): always send domains in sso update PUT body to match Go (CLI…
Coly010 Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cli-go-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/init@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
Expand Down Expand Up @@ -95,7 +95,7 @@ jobs:
exit 1

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
with:
category: "/language:${{matrix.language}}"
defaults:
Expand Down
2 changes: 1 addition & 1 deletion apps/cli-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ require (
github.com/multigres/multigres v0.0.0-20260126223308-f5a52171bbc4
github.com/oapi-codegen/nullable v1.2.0
github.com/olekukonko/tablewriter v1.1.4
github.com/posthog/posthog-go v1.19.0
github.com/posthog/posthog-go v1.21.0
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -970,8 +970,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posthog/posthog-go v1.19.0 h1:GsPilbAfQo8dbIo2yheTZ8pbmE5yYW3G289kXrxZxFA=
github.com/posthog/posthog-go v1.19.0/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=
github.com/posthog/posthog-go v1.21.0 h1:088VokgDXrx8BmDBfIg/XC/yYqc7Y/wDCEl4inmRQxM=
github.com/posthog/posthog-go v1.21.0/go.mod h1://M430hNH3e8CDv4i8SJesb26816Mpa6GIZaiP4pNQU=
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/internal/branches/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func Run(ctx context.Context, body api.CreateBranchBody, fsys afero.Fs) error {
if err != nil {
return errors.Errorf("failed to create preview branch: %w", err)
} else if resp.JSON201 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, flags.ProjectRef, "branching_limit", resp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "branching_limit", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, flags.ProjectRef, "branching_limit", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected create branch status %d: %s", resp.StatusCode(), string(resp.Body))
}
Expand Down
20 changes: 20 additions & 0 deletions apps/cli-go/internal/branches/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,24 @@ func TestCreateCommand(t *testing.T) {
assert.ErrorContains(t, err, "unexpected create branch status 402")
assert.Contains(t, utils.CmdSuggestion, "/org/test-org/billing")
})

t.Run("suggests upgrade from envelope without entitlements round-trip", func(t *testing.T) {
t.Cleanup(apitest.MockPlatformAPI(t))
t.Cleanup(func() { utils.CmdSuggestion = "" })
gock.New(utils.DefaultApiHost).
Post("/v1/projects/" + flags.ProjectRef + "/branches").
Reply(http.StatusPaymentRequired).
JSON(map[string]interface{}{
"message": "Branching is supported only on the Pro plan or above",
"error": map[string]interface{}{
"code": "entitlement_required",
"feature": "branching_limit",
"upgrade_url": "https://supabase.com/dashboard/org/acme/billing",
},
})
fsys := afero.NewMemMapFs()
err := Run(context.Background(), api.CreateBranchBody{Region: cast.Ptr("sin")}, fsys)
assert.ErrorContains(t, err, "unexpected create branch status 402")
assert.Contains(t, utils.CmdSuggestion, "org/acme/billing")
})
}
4 changes: 2 additions & 2 deletions apps/cli-go/internal/branches/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ func Run(ctx context.Context, branchId string, body api.UpdateBranchBody, fsys a
if err != nil {
return errors.Errorf("failed to update preview branch: %w", err)
} else if resp.JSON200 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "branching_persistent", resp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "branching_persistent", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "branching_persistent", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected update branch status %d: %s", resp.StatusCode(), string(resp.Body))
}
Expand Down
4 changes: 4 additions & 0 deletions apps/cli-go/internal/hostnames/activate/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/hostnames"
"github.com/supabase/cli/internal/telemetry"
"github.com/supabase/cli/internal/utils"
)

Expand All @@ -15,6 +16,9 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error {
if err != nil {
return errors.Errorf("failed to activate custom hostname: %w", err)
} else if resp.JSON201 == nil {
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected activate hostname status %d: %s", resp.StatusCode(), string(resp.Body))
}
hostnames.PrintStatus(resp.JSON201, os.Stderr)
Expand Down
4 changes: 4 additions & 0 deletions apps/cli-go/internal/hostnames/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/hostnames"
"github.com/supabase/cli/internal/telemetry"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/pkg/api"
)
Expand All @@ -24,6 +25,9 @@ func Run(ctx context.Context, projectRef string, customHostname string, fsys afe
if err != nil {
return errors.Errorf("failed to create custom hostname: %w", err)
} else if resp.JSON201 == nil {
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected create hostname status %d: %s", resp.StatusCode(), string(resp.Body))
}
hostnames.PrintStatus(resp.JSON201, os.Stderr)
Expand Down
4 changes: 4 additions & 0 deletions apps/cli-go/internal/hostnames/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/hostnames"
"github.com/supabase/cli/internal/telemetry"
"github.com/supabase/cli/internal/utils"
)

Expand All @@ -15,6 +16,9 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error {
if err != nil {
return errors.Errorf("failed to get custom hostname: %w", err)
} else if resp.JSON200 == nil {
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected get hostname status %d: %s", resp.StatusCode(), string(resp.Body))
}
hostnames.PrintStatus(resp.JSON200, os.Stderr)
Expand Down
4 changes: 4 additions & 0 deletions apps/cli-go/internal/hostnames/reverify/reverify.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/hostnames"
"github.com/supabase/cli/internal/telemetry"
"github.com/supabase/cli/internal/utils"
)

Expand All @@ -15,6 +16,9 @@ func Run(ctx context.Context, projectRef string, fsys afero.Fs) error {
if err != nil {
return errors.Errorf("failed to re-verify custom hostname: %w", err)
} else if resp.JSON201 == nil {
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, projectRef, "", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.Errorf("unexpected re-verify hostname status %d: %s", resp.StatusCode(), string(resp.Body))
}
hostnames.PrintStatus(resp.JSON201, os.Stderr)
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/internal/sso/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func Run(ctx context.Context, params RunParams) error {
}

if resp.JSON201 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", resp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "auth.saml_2", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
if resp.StatusCode() == http.StatusNotFound {
return errors.New("SAML 2.0 support is not enabled for this project. Please enable it through the dashboard")
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/internal/sso/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func Run(ctx context.Context, ref, format string) error {
}

if resp.JSON200 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, ref, "auth.saml_2", resp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "auth.saml_2", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, ref, "auth.saml_2", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
if resp.StatusCode() == http.StatusNotFound {
return errors.New("Looks like SAML 2.0 support is not enabled for this project. Please use the dashboard to enable it.")
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/internal/sso/remove/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func Run(ctx context.Context, ref, providerId, format string) error {
}

if resp.JSON200 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, ref, "auth.saml_2", resp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "auth.saml_2", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, ref, "auth.saml_2", resp.StatusCode(), resp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
if resp.StatusCode() == http.StatusNotFound {
return errors.Errorf("An identity provider with ID %q could not be found.", providerId)
Expand Down
8 changes: 4 additions & 4 deletions apps/cli-go/internal/sso/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func Run(ctx context.Context, params RunParams) error {
}

if getResp.JSON200 == nil {
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", getResp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "auth.saml_2", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", getResp.StatusCode(), getResp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
if getResp.StatusCode() == http.StatusNotFound {
return errors.Errorf("An identity provider with ID %q could not be found.", parsed)
Expand Down Expand Up @@ -119,8 +119,8 @@ func Run(ctx context.Context, params RunParams) error {

if putResp.JSON200 == nil {
// GET branch above early-returns on failure, so this and the GET fire are mutually exclusive (max one event per invocation).
if orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", putResp.StatusCode()); isGated {
telemetry.TrackUpgradeSuggested(ctx, "auth.saml_2", orgSlug)
if feature, orgSlug, isGated := utils.SuggestUpgradeOnError(ctx, params.ProjectRef, "auth.saml_2", putResp.StatusCode(), putResp.Body); isGated {
telemetry.TrackUpgradeSuggested(ctx, feature, orgSlug)
}
return errors.New("unexpected error fetching identity provider: " + string(putResp.Body))
}
Expand Down
76 changes: 60 additions & 16 deletions apps/cli-go/internal/utils/plan_gate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package utils

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/supabase/cli/pkg/api"
)

func GetOrgSlugFromProjectRef(ctx context.Context, projectRef string) (string, error) {
Expand All @@ -20,34 +24,74 @@ func GetOrgBillingURL(orgSlug string) string {
return fmt.Sprintf("%s/org/%s/billing", GetSupabaseDashboardURL(), orgSlug)
}

// SuggestUpgradeOnError checks if a failed API response is due to plan limitations
// by looking up the org's entitlements. Only sets CmdSuggestion when the entitlements
// API confirms the feature is gated (hasAccess == false). Returns the resolved org
// slug and true if a billing suggestion was shown (so callers can fire telemetry).
// Only checks on 4xx client errors; skips 2xx (success) and 5xx (server errors).
func SuggestUpgradeOnError(ctx context.Context, projectRef, featureKey string, statusCode int) (orgSlug string, isGated bool) {
func orgSlugFromBillingURL(billingURL string) string {
const marker = "/org/"
start := strings.Index(billingURL, marker)
if start < 0 {
return ""
}
rest := billingURL[start+len(marker):]
if end := strings.Index(rest, "/"); end >= 0 {
return rest[:end]
}
return rest
}

func parsePlanGateError(body []byte) (feature, upgradeURL string, ok bool) {
var envelope api.PlanGateErrorBody
if err := json.Unmarshal(body, &envelope); err != nil || envelope.Error == nil {
return "", "", false
}
if envelope.Error.Code != api.EntitlementRequired || envelope.Error.Feature == "" {
return "", "", false
}
if envelope.Error.UpgradeUrl == nil || *envelope.Error.UpgradeUrl == "" {
return "", "", false
}
return envelope.Error.Feature, *envelope.Error.UpgradeUrl, true
}

func setUpgradeSuggestion(billingURL string) {
CmdSuggestion = fmt.Sprintf("Your organization does not have access to this feature. Upgrade your plan: %s", Bold(billingURL))
}

// SuggestUpgradeOnError checks whether a failed API response is a plan gate and
// sets CmdSuggestion with the billing URL when it is. The entitlement_required
// envelope on the response body is authoritative and needs no network calls;
// when absent, a non-empty featureKey falls back to the entitlements lookup.
// An empty featureKey disables the fallback (envelope-only sites). Returns the
// effective feature and org slug for telemetry, and whether a suggestion was
// shown. Only fires on 4xx.
func SuggestUpgradeOnError(ctx context.Context, projectRef, featureKey string, statusCode int, body []byte) (gatedFeature, orgSlug string, isGated bool) {
if statusCode < 400 || statusCode >= 500 {
return
return "", "", false
}

if feature, upgradeURL, ok := parsePlanGateError(body); ok {
setUpgradeSuggestion(upgradeURL)
return feature, orgSlugFromBillingURL(upgradeURL), true
}

if featureKey == "" {
return "", "", false
}

orgSlug, err := GetOrgSlugFromProjectRef(ctx, projectRef)
slug, err := GetOrgSlugFromProjectRef(ctx, projectRef)
if err != nil {
return
return "", "", false
}

resp, err := GetSupabase().V1GetOrganizationEntitlementsWithResponse(ctx, orgSlug)
resp, err := GetSupabase().V1GetOrganizationEntitlementsWithResponse(ctx, slug)
if err != nil || resp.JSON200 == nil {
return
return "", slug, false
}

for _, e := range resp.JSON200.Entitlements {
if string(e.Feature.Key) == featureKey && !e.HasAccess {
billingURL := GetOrgBillingURL(orgSlug)
CmdSuggestion = fmt.Sprintf("Your organization does not have access to this feature. Upgrade your plan: %s", Bold(billingURL))
isGated = true
return
setUpgradeSuggestion(GetOrgBillingURL(slug))
return featureKey, slug, true
}
}

return
return "", slug, false
}
Loading
Loading