Skip to content

Prod deploy #3896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
63a8891
feat: support jwt signing keys for local auth (#3841)
cemalkilic Jul 21, 2025
bcbc267
chore: sync API types from infrastructure (#3892)
kiwicopple Jul 21, 2025
6e2f6fb
feat: password-less database login (#3885)
sweatybridge Jul 21, 2025
031f785
chore(deps): bump supabase/realtime from v2.41.3 to v2.41.4 in /pkg/c…
dependabot[bot] Jul 22, 2025
f78564d
chore(deps): bump supabase/postgres from 17.4.1.059 to 17.4.1.061 in …
dependabot[bot] Jul 22, 2025
4679e5a
chore(deps): bump supabase/storage-api from v1.25.9 to v1.25.10 in /p…
dependabot[bot] Jul 22, 2025
ae011d7
fix: Update img proxy env to be consistent with prod (#3901)
itslenny Jul 23, 2025
508c01f
chore: sync API types from infrastructure (#3903)
kiwicopple Jul 24, 2025
d8bddb5
chore(deps): bump google.golang.org/grpc from 1.74.0 to 1.74.2 (#3904)
dependabot[bot] Jul 24, 2025
9cd8094
chore(deps): bump supabase/storage-api from v1.25.10 to v1.25.11 in /…
dependabot[bot] Jul 24, 2025
9f9645c
chore(deps): bump supabase/postgres from 17.4.1.061 to 17.4.1.064 in …
dependabot[bot] Jul 24, 2025
53cbced
chore(deps): bump google.golang.org/grpc from 1.74.0 to 1.74.2 in /pk…
dependabot[bot] Jul 24, 2025
e5ec38b
chore(deps): bump supabase/edge-runtime from v1.68.0 to v1.68.2 in /p…
dependabot[bot] Jul 24, 2025
a1d5f85
fix: flag to create data branch (#3913)
sweatybridge Jul 24, 2025
61b41fd
chore: sync API types from infrastructure (#3914)
kiwicopple Jul 24, 2025
760122f
chore(deps): bump supabase/postgres-meta from v0.91.1 to v0.91.2 in /…
dependabot[bot] Jul 25, 2025
635c79c
chore(deps): bump supabase/realtime from v2.41.4 to v2.41.7 in /pkg/c…
dependabot[bot] Jul 25, 2025
b720b12
chore(deps): bump supabase/storage-api from v1.25.11 to v1.25.12 in /…
dependabot[bot] Jul 25, 2025
4655217
chore(deps): bump supabase/realtime from v2.41.7 to v2.41.8 in /pkg/c…
dependabot[bot] Jul 28, 2025
4814176
chore: sync API types from infrastructure (#3926)
kiwicopple Jul 29, 2025
73fb104
chore(deps): bump supabase/studio from 2025.07.21-sha-88dca02 to 2025…
dependabot[bot] Jul 29, 2025
c499249
chore(deps): bump supabase/postgres from 17.4.1.064 to 17.4.1.067 in …
dependabot[bot] Jul 29, 2025
8f8ddb2
chore(deps): bump supabase/postgres-meta from v0.91.2 to v0.91.3 in /…
dependabot[bot] Jul 29, 2025
9d8ef5b
chore(deps): bump supabase/realtime from v2.41.8 to v2.41.9 in /pkg/c…
dependabot[bot] Jul 29, 2025
1cf9098
fix: handle new tenant api key (#3929)
sweatybridge Jul 29, 2025
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
5 changes: 5 additions & 0 deletions cmd/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
Allowed: awsRegions(),
}
persistent bool
withData bool

branchCreateCmd = &cobra.Command{
Use: "create [name]",
Expand All @@ -53,6 +54,9 @@ var (
if cmdFlags.Changed("persistent") {
body.Persistent = &persistent
}
if cmdFlags.Changed("with-data") {
body.WithData = &withData
}
return create.Run(cmd.Context(), body, afero.NewOsFs())
},
}
Expand Down Expand Up @@ -157,6 +161,7 @@ func init() {
createFlags.Var(&branchRegion, "region", "Select a region to deploy the branch database.")
createFlags.Var(&size, "size", "Select a desired instance size for the branch database.")
createFlags.BoolVar(&persistent, "persistent", false, "Whether to create a persistent branch.")
createFlags.BoolVar(&withData, "with-data", false, "Whether to clone production data to the branch database.")
branchesCmd.AddCommand(branchCreateCmd)
branchesCmd.AddCommand(branchListCmd)
branchesCmd.AddCommand(branchGetCmd)
Expand Down
25 changes: 25 additions & 0 deletions cmd/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/supabase/cli/internal/gen/keys"
"github.com/supabase/cli/internal/gen/signingkeys"
"github.com/supabase/cli/internal/gen/types"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
Expand Down Expand Up @@ -93,6 +94,26 @@ var (
supabase gen types --project-id abc-def-123 --schema public --schema private
supabase gen types --db-url 'postgresql://...' --schema public --schema auth`,
}

algorithm = utils.EnumFlag{
Allowed: signingkeys.GetSupportedAlgorithms(),
Value: string(signingkeys.AlgES256),
}
appendKeys bool

genSigningKeyCmd = &cobra.Command{
Use: "signing-key",
Short: "Generate a JWT signing key",
Long: `Securely generate a private JWT signing key for use in the CLI or to import in the dashboard.
Supported algorithms:
ES256 - ECDSA with P-256 curve and SHA-256 (recommended)
RS256 - RSA with SHA-256
`,
RunE: func(cmd *cobra.Command, args []string) error {
return signingkeys.Run(cmd.Context(), algorithm.Value, appendKeys, afero.NewOsFs())
},
}
)

func init() {
Expand All @@ -111,5 +132,9 @@ func init() {
keyFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
keyFlags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.")
genCmd.AddCommand(genKeysCmd)
signingKeyFlags := genSigningKeyCmd.Flags()
signingKeyFlags.Var(&algorithm, "algorithm", "Algorithm for signing key generation.")
signingKeyFlags.BoolVar(&appendKeys, "append", false, "Append new key to existing keys file instead of overwriting.")
genCmd.AddCommand(genSigningKeyCmd)
rootCmd.AddCommand(genCmd)
}
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ var (
}
}
}
if err := flags.ParseDatabaseConfig(cmd.Flags(), fsys); err != nil {
if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil {
return err
}
// Prepare context
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ require (
golang.org/x/mod v0.26.0
golang.org/x/oauth2 v0.30.0
golang.org/x/term v0.33.0
google.golang.org/grpc v1.74.0
google.golang.org/grpc v1.74.2
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1472,8 +1472,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY=
google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
Expand Down
2 changes: 1 addition & 1 deletion internal/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options ..
return err
}
// 6. Push migrations
config := flags.NewDbConfigWithPassword(flags.ProjectRef)
config := flags.NewDbConfigWithPassword(ctx, flags.ProjectRef)
if err := writeDotEnv(keys, config, fsys); err != nil {
fmt.Fprintln(os.Stderr, "Failed to create .env file:", err)
}
Expand Down
249 changes: 249 additions & 0 deletions internal/gen/signingkeys/signingkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package signingkeys

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"os"
"path/filepath"

"github.com/go-errors/errors"
"github.com/google/uuid"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/cast"
)

type Algorithm string

const (
AlgRS256 Algorithm = "RS256"
AlgES256 Algorithm = "ES256"
)

type JWK struct {
KeyType string `json:"kty"`
KeyID string `json:"kid,omitempty"`
Use string `json:"use,omitempty"`
KeyOps []string `json:"key_ops,omitempty"`
Algorithm string `json:"alg,omitempty"`
Extractable *bool `json:"ext,omitempty"`
// RSA specific fields
Modulus string `json:"n,omitempty"`
Exponent string `json:"e,omitempty"`
// RSA private key fields
PrivateExponent string `json:"d,omitempty"`
FirstPrimeFactor string `json:"p,omitempty"`
SecondPrimeFactor string `json:"q,omitempty"`
FirstFactorCRTExponent string `json:"dp,omitempty"`
SecondFactorCRTExponent string `json:"dq,omitempty"`
FirstCRTCoefficient string `json:"qi,omitempty"`
// EC specific fields
Curve string `json:"crv,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
}

type KeyPair struct {
PublicKey JWK
PrivateKey JWK
}

// GenerateKeyPair generates a new key pair for the specified algorithm
func GenerateKeyPair(alg Algorithm) (*KeyPair, error) {
keyID := uuid.New().String()

switch alg {
case AlgRS256:
return generateRSAKeyPair(keyID)
case AlgES256:
return generateECDSAKeyPair(keyID)
default:
return nil, errors.Errorf("unsupported algorithm: %s", alg)
}
}

func generateRSAKeyPair(keyID string) (*KeyPair, error) {
// Generate RSA key pair (2048 bits for RS256)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, errors.Errorf("failed to generate RSA key: %w", err)
}

publicKey := &privateKey.PublicKey

// Precompute CRT values for completeness
privateKey.Precompute()

// Convert to JWK format
privateJWK := JWK{
KeyType: "RSA",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"sign", "verify"},
Algorithm: "RS256",
Extractable: cast.Ptr(true),
Modulus: base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()),
Exponent: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()),
PrivateExponent: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()),
FirstPrimeFactor: base64.RawURLEncoding.EncodeToString(privateKey.Primes[0].Bytes()),
SecondPrimeFactor: base64.RawURLEncoding.EncodeToString(privateKey.Primes[1].Bytes()),
FirstFactorCRTExponent: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Dp.Bytes()),
SecondFactorCRTExponent: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Dq.Bytes()),
FirstCRTCoefficient: base64.RawURLEncoding.EncodeToString(privateKey.Precomputed.Qinv.Bytes()),
}

publicJWK := JWK{
KeyType: "RSA",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"verify"},
Algorithm: "RS256",
Extractable: cast.Ptr(true),
Modulus: privateJWK.Modulus,
Exponent: privateJWK.Exponent,
}

return &KeyPair{
PublicKey: publicJWK,
PrivateKey: privateJWK,
}, nil
}

func generateECDSAKeyPair(keyID string) (*KeyPair, error) {
// Generate ECDSA key pair (P-256 curve for ES256)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, errors.Errorf("failed to generate ECDSA key: %w", err)
}

publicKey := &privateKey.PublicKey

// Convert to JWK format
privateJWK := JWK{
KeyType: "EC",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"sign", "verify"},
Algorithm: "ES256",
Extractable: cast.Ptr(true),
Curve: "P-256",
X: base64.RawURLEncoding.EncodeToString(publicKey.X.Bytes()),
Y: base64.RawURLEncoding.EncodeToString(publicKey.Y.Bytes()),
PrivateExponent: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()),
}

publicJWK := JWK{
KeyType: "EC",
KeyID: keyID,
Use: "sig",
KeyOps: []string{"verify"},
Algorithm: "ES256",
Extractable: cast.Ptr(true),
Curve: "P-256",
X: privateJWK.X,
Y: privateJWK.Y,
}

return &KeyPair{
PublicKey: publicJWK,
PrivateKey: privateJWK,
}, nil
}

// Run generates a key pair and writes it to the specified file path
func Run(ctx context.Context, algorithm string, appendMode bool, fsys afero.Fs) error {
err := flags.LoadConfig(fsys)
if err != nil {
return err
}
outputPath := utils.Config.Auth.SigningKeysPath

// Generate key pair
keyPair, err := GenerateKeyPair(Algorithm(algorithm))
if err != nil {
return err
}

out := io.Writer(os.Stdout)
var jwkArray []JWK
if len(outputPath) > 0 {
if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(outputPath)); err != nil {
return err
}
f, err := fsys.OpenFile(outputPath, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return errors.Errorf("failed to open signing key: %w", err)
}
defer f.Close()
if appendMode {
// Load existing key and reset file
dec := json.NewDecoder(f)
// Since a new file is empty, we must ignore EOF error
if err := dec.Decode(&jwkArray); err != nil && !errors.Is(err, io.EOF) {
return errors.Errorf("failed to decode signing key: %w", err)
}
if _, err = f.Seek(0, io.SeekStart); err != nil {
return errors.Errorf("failed to seek signing key: %w", err)
}
} else if fi, err := f.Stat(); fi.Size() > 0 {
if err != nil {
fmt.Fprintln(utils.GetDebugLogger(), err)
}
label := fmt.Sprintf("Do you want to overwrite the existing %s file?", utils.Bold(outputPath))
if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, label, true); err != nil {
return err
} else if !shouldOverwrite {
return errors.New(context.Canceled)
}
if err := f.Truncate(0); err != nil {
return errors.Errorf("failed to truncate signing key: %w", err)
}
}
out = f
}
jwkArray = append(jwkArray, keyPair.PrivateKey)

// Write to file
enc := json.NewEncoder(out)
enc.SetIndent("", " ")
if err := enc.Encode(jwkArray); err != nil {
return errors.Errorf("failed to encode signing key: %w", err)
}

if len(outputPath) == 0 {
utils.CmdSuggestion = fmt.Sprintf(`
To enable JWT signing keys in your local project:
1. Save the generated key to %s
2. Update your %s with the new keys path

[auth]
signing_keys_path = "./signing_key.json"
`, utils.Bold(filepath.Join(utils.SupabaseDirPath, "signing_key.json")), utils.Bold(utils.ConfigPath))
return nil
}

fmt.Fprintf(os.Stderr, "JWT signing key appended to: %s (now contains %d keys)\n", utils.Bold(outputPath), len(jwkArray))
if len(jwkArray) == 1 {
if ignored, err := utils.IsGitIgnored(outputPath); err != nil {
fmt.Fprintln(utils.GetDebugLogger(), err)
} else if !ignored {
// Since the output path is user defined, we can't update the managed .gitignore file.
fmt.Fprintln(os.Stderr, utils.Yellow("IMPORTANT:"), "Add your signing key path to .gitignore to prevent committing to version control.")
}
}
return nil
}

// GetSupportedAlgorithms returns a list of supported algorithms
func GetSupportedAlgorithms() []string {
return []string{string(AlgRS256), string(AlgES256)}
}
Loading