• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 9444125855

10 Jun 2024 07:21AM UTC coverage: 60.042% (+0.01%) from 60.03%
9444125855

push

github

web-flow
feat: support custom pooler and realtime image (#2401)

6 of 15 new or added lines in 4 files covered. (40.0%)

2 existing lines in 1 file now uncovered.

6909 of 11507 relevant lines covered (60.04%)

632.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.21
/internal/utils/config.go
1
package utils
2

3
import (
4
        "bytes"
5
        _ "embed"
6
        "fmt"
7
        "net/url"
8
        "os"
9
        "path/filepath"
10
        "regexp"
11
        "strings"
12
        "text/template"
13
        "time"
14

15
        "github.com/BurntSushi/toml"
16
        "github.com/docker/go-units"
17
        "github.com/go-errors/errors"
18
        "github.com/golang-jwt/jwt/v5"
19
        "github.com/joho/godotenv"
20
        "github.com/spf13/afero"
21
        "github.com/spf13/viper"
22
        "golang.org/x/mod/semver"
23
)
24

25
var (
26
        NetId         string
27
        DbId          string
28
        ConfigId      string
29
        KongId        string
30
        GotrueId      string
31
        InbucketId    string
32
        RealtimeId    string
33
        RestId        string
34
        StorageId     string
35
        ImgProxyId    string
36
        DifferId      string
37
        PgmetaId      string
38
        StudioId      string
39
        EdgeRuntimeId string
40
        LogflareId    string
41
        VectorId      string
42
        PoolerId      string
43

44
        DbAliases          = []string{"db", "db.supabase.internal"}
45
        KongAliases        = []string{"kong", "api.supabase.internal"}
46
        GotrueAliases      = []string{"auth"}
47
        InbucketAliases    = []string{"inbucket"}
48
        RealtimeAliases    = []string{"realtime", Config.Realtime.TenantId}
49
        RestAliases        = []string{"rest"}
50
        StorageAliases     = []string{"storage"}
51
        ImgProxyAliases    = []string{"imgproxy"}
52
        PgmetaAliases      = []string{"pg_meta"}
53
        StudioAliases      = []string{"studio"}
54
        EdgeRuntimeAliases = []string{"edge_runtime"}
55
        LogflareAliases    = []string{"analytics"}
56
        VectorAliases      = []string{"vector"}
57
        PoolerAliases      = []string{"pooler"}
58

59
        InitialSchemaSql string
60
        //go:embed templates/initial_schemas/13.sql
61
        InitialSchemaPg13Sql string
62
        //go:embed templates/initial_schemas/14.sql
63
        InitialSchemaPg14Sql string
64

65
        //go:embed templates/init_config.toml
66
        initConfigEmbed    string
67
        initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed))
68
        invalidProjectId   = regexp.MustCompile("[^a-zA-Z0-9_.-]+")
69
        envPattern         = regexp.MustCompile(`^env\((.*)\)$`)
70
)
71

72
func GetId(name string) string {
629✔
73
        return "supabase_" + name + "_" + Config.ProjectId
629✔
74
}
629✔
75

76
func UpdateDockerIds() {
37✔
77
        if NetId = viper.GetString("network-id"); len(NetId) == 0 {
74✔
78
                NetId = GetId("network")
37✔
79
        }
37✔
80
        DbId = GetId(DbAliases[0])
37✔
81
        ConfigId = GetId("config")
37✔
82
        KongId = GetId(KongAliases[0])
37✔
83
        GotrueId = GetId(GotrueAliases[0])
37✔
84
        InbucketId = GetId(InbucketAliases[0])
37✔
85
        RealtimeId = GetId(RealtimeAliases[0])
37✔
86
        RestId = GetId(RestAliases[0])
37✔
87
        StorageId = GetId(StorageAliases[0])
37✔
88
        ImgProxyId = GetId(ImgProxyAliases[0])
37✔
89
        DifferId = GetId("differ")
37✔
90
        PgmetaId = GetId(PgmetaAliases[0])
37✔
91
        StudioId = GetId(StudioAliases[0])
37✔
92
        EdgeRuntimeId = GetId(EdgeRuntimeAliases[0])
37✔
93
        LogflareId = GetId(LogflareAliases[0])
37✔
94
        VectorId = GetId(VectorAliases[0])
37✔
95
        PoolerId = GetId(PoolerAliases[0])
37✔
96
}
97

98
func GetDockerIds() []string {
6✔
99
        return []string{
6✔
100
                KongId,
6✔
101
                GotrueId,
6✔
102
                InbucketId,
6✔
103
                RealtimeId,
6✔
104
                RestId,
6✔
105
                StorageId,
6✔
106
                ImgProxyId,
6✔
107
                PgmetaId,
6✔
108
                StudioId,
6✔
109
                EdgeRuntimeId,
6✔
110
                LogflareId,
6✔
111
                VectorId,
6✔
112
                PoolerId,
6✔
113
        }
6✔
114
}
6✔
115

116
// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
117
type sizeInBytes int64
118

119
func (s *sizeInBytes) UnmarshalText(text []byte) error {
93✔
120
        size, err := units.RAMInBytes(string(text))
93✔
121
        if err == nil {
185✔
122
                *s = sizeInBytes(size)
92✔
123
        }
92✔
124
        return err
93✔
125
}
126

127
func (s sizeInBytes) MarshalText() (text []byte, err error) {
×
128
        return []byte(units.BytesSize(float64(s))), nil
×
129
}
×
130

131
type LogflareBackend string
132

133
const (
134
        LogflarePostgres LogflareBackend = "postgres"
135
        LogflareBigQuery LogflareBackend = "bigquery"
136
)
137

138
type PoolMode string
139

140
const (
141
        TransactionMode PoolMode = "transaction"
142
        SessionMode     PoolMode = "session"
143
)
144

145
type AddressFamily string
146

147
const (
148
        AddressIPv6 AddressFamily = "IPv6"
149
        AddressIPv4 AddressFamily = "IPv4"
150
)
151

152
func ToRealtimeEnv(addr AddressFamily) string {
9✔
153
        if addr == AddressIPv6 {
9✔
154
                return "-proto_dist inet6_tcp"
×
155
        }
×
156
        return "-proto_dist inet_tcp"
9✔
157
}
158

159
type CustomClaims struct {
160
        // Overrides Issuer to maintain json order when marshalling
161
        Issuer string `json:"iss,omitempty"`
162
        Ref    string `json:"ref,omitempty"`
163
        Role   string `json:"role"`
164
        jwt.RegisteredClaims
165
}
166

167
const (
168
        defaultJwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long"
169
        defaultJwtExpiry = 1983812996
170
)
171

172
func (c CustomClaims) NewToken() *jwt.Token {
28✔
173
        if c.ExpiresAt == nil {
56✔
174
                c.ExpiresAt = jwt.NewNumericDate(time.Unix(defaultJwtExpiry, 0))
28✔
175
        }
28✔
176
        if len(c.Issuer) == 0 {
56✔
177
                c.Issuer = "supabase-demo"
28✔
178
        }
28✔
179
        return jwt.NewWithClaims(jwt.SigningMethodHS256, c)
28✔
180
}
181

182
type RequestPolicy string
183

184
const (
185
        PolicyPerWorker RequestPolicy = "per_worker"
186
        PolicyOneshot   RequestPolicy = "oneshot"
187
)
188

189
var Config = config{
190
        Api: api{
191
                Image: PostgrestImage,
192
        },
193
        Db: db{
194
                Image:    Pg15Image,
195
                Password: "postgres",
196
                RootKey:  "d4dc5b6d4a1d6a10b2c1e76112c994d65db7cec380572cc1839624d4be3fa275",
197
                Pooler: pooler{
198
                        Image:         SupavisorImage,
199
                        TenantId:      "pooler-dev",
200
                        EncryptionKey: "12345678901234567890123456789032",
201
                        SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
202
                },
203
        },
204
        Realtime: realtime{
205
                Image:           RealtimeImage,
206
                IpVersion:       AddressIPv4,
207
                MaxHeaderLength: 4096,
208
                TenantId:        "realtime-dev",
209
                EncryptionKey:   "supabaserealtime",
210
                SecretKeyBase:   "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
211
        },
212
        Storage: storage{
213
                Image: StorageImage,
214
                S3Credentials: storageS3Credentials{
215
                        AccessKeyId:     "625729a08b95bf1b7ff351a663f3a23c",
216
                        SecretAccessKey: "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907",
217
                        Region:          "local",
218
                },
219
                ImageTransformation: imageTransformation{
220
                        Enabled: true,
221
                },
222
        },
223
        Auth: auth{
224
                Image: GotrueImage,
225
                Email: email{
226
                        Template: map[string]emailTemplate{
227
                                "invite":       {},
228
                                "confirmation": {},
229
                                "recovery":     {},
230
                                "magic_link":   {},
231
                                "email_change": {},
232
                        },
233
                },
234
                External: map[string]provider{
235
                        "apple":         {},
236
                        "azure":         {},
237
                        "bitbucket":     {},
238
                        "discord":       {},
239
                        "facebook":      {},
240
                        "github":        {},
241
                        "gitlab":        {},
242
                        "google":        {},
243
                        "keycloak":      {},
244
                        "linkedin":      {}, // TODO: remove this field in v2
245
                        "linkedin_oidc": {},
246
                        "notion":        {},
247
                        "twitch":        {},
248
                        "twitter":       {},
249
                        "slack":         {},
250
                        "spotify":       {},
251
                        "workos":        {},
252
                        "zoom":          {},
253
                },
254
                JwtSecret: defaultJwtSecret,
255
        },
256
        Studio: studio{
257
                Image:       StudioImage,
258
                PgmetaImage: PgmetaImage,
259
        },
260
        Analytics: analytics{
261
                ApiKey: "api-key",
262
                // Defaults to bigquery for backwards compatibility with existing config.toml
263
                Backend: LogflareBigQuery,
264
        },
265
}
266

267
// We follow these rules when adding new config:
268
//  1. Update init_config.toml (and init_config.test.toml) with the new key, default value, and comments to explain usage.
269
//  2. Update config struct with new field and toml tag (spelled in snake_case).
270
//  3. Add custom field validations to LoadConfigFS function for eg. integer range checks.
271
//
272
// If you are adding new user defined secrets, such as OAuth provider secret, the default value in
273
// init_config.toml should be an env var substitution. For example,
274
//
275
// > secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
276
//
277
// If you are adding an internal config or secret that doesn't need to be overridden by the user,
278
// exclude the field from toml serialization. For example,
279
//
280
//        type auth struct {
281
//                AnonKey string `toml:"-" mapstructure:"anon_key"`
282
//        }
283
//
284
// Use `mapstructure:"anon_key"` tag only if you want inject values from a predictable environment
285
// variable, such as SUPABASE_AUTH_ANON_KEY.
286
//
287
// Default values for internal configs should be added to `var Config` initializer.
288
type (
289
        config struct {
290
                ProjectId    string              `toml:"project_id"`
291
                Hostname     string              `toml:"-"`
292
                Api          api                 `toml:"api"`
293
                Db           db                  `toml:"db" mapstructure:"db"`
294
                Realtime     realtime            `toml:"realtime"`
295
                Studio       studio              `toml:"studio"`
296
                Inbucket     inbucket            `toml:"inbucket"`
297
                Storage      storage             `toml:"storage"`
298
                Auth         auth                `toml:"auth" mapstructure:"auth"`
299
                EdgeRuntime  edgeRuntime         `toml:"edge_runtime"`
300
                Functions    map[string]function `toml:"functions"`
301
                Analytics    analytics           `toml:"analytics"`
302
                Experimental experimental        `toml:"experimental" mapstructure:"-"`
303
                // TODO
304
                // Scripts   scripts
305
        }
306

307
        api struct {
308
                Enabled         bool     `toml:"enabled"`
309
                Image           string   `toml:"-"`
310
                Port            uint16   `toml:"port"`
311
                Schemas         []string `toml:"schemas"`
312
                ExtraSearchPath []string `toml:"extra_search_path"`
313
                MaxRows         uint     `toml:"max_rows"`
314
        }
315

316
        db struct {
317
                Image        string `toml:"-"`
318
                Port         uint16 `toml:"port"`
319
                ShadowPort   uint16 `toml:"shadow_port"`
320
                MajorVersion uint   `toml:"major_version"`
321
                Password     string `toml:"-"`
322
                RootKey      string `toml:"-" mapstructure:"root_key"`
323
                Pooler       pooler `toml:"pooler"`
324
        }
325

326
        pooler struct {
327
                Enabled          bool     `toml:"enabled"`
328
                Image            string   `toml:"-"`
329
                Port             uint16   `toml:"port"`
330
                PoolMode         PoolMode `toml:"pool_mode"`
331
                DefaultPoolSize  uint     `toml:"default_pool_size"`
332
                MaxClientConn    uint     `toml:"max_client_conn"`
333
                ConnectionString string   `toml:"-"`
334
                TenantId         string   `toml:"-"`
335
                EncryptionKey    string   `toml:"-"`
336
                SecretKeyBase    string   `toml:"-"`
337
        }
338

339
        realtime struct {
340
                Enabled         bool          `toml:"enabled"`
341
                Image           string        `toml:"-"`
342
                IpVersion       AddressFamily `toml:"ip_version"`
343
                MaxHeaderLength uint          `toml:"max_header_length"`
344
                TenantId        string        `toml:"-"`
345
                EncryptionKey   string        `toml:"-"`
346
                SecretKeyBase   string        `toml:"-"`
347
        }
348

349
        studio struct {
350
                Enabled      bool   `toml:"enabled"`
351
                Image        string `toml:"-"`
352
                Port         uint16 `toml:"port"`
353
                ApiUrl       string `toml:"api_url"`
354
                OpenaiApiKey string `toml:"openai_api_key"`
355
                PgmetaImage  string `toml:"-"`
356
        }
357

358
        inbucket struct {
359
                Enabled  bool   `toml:"enabled"`
360
                Port     uint16 `toml:"port"`
361
                SmtpPort uint16 `toml:"smtp_port"`
362
                Pop3Port uint16 `toml:"pop3_port"`
363
        }
364

365
        storage struct {
366
                Enabled             bool                 `toml:"enabled"`
367
                Image               string               `toml:"-"`
368
                FileSizeLimit       sizeInBytes          `toml:"file_size_limit"`
369
                S3Credentials       storageS3Credentials `toml:"-"`
370
                ImageTransformation imageTransformation  `toml:"image_transformation"`
371
        }
372

373
        imageTransformation struct {
374
                Enabled bool `toml:"enabled"`
375
        }
376

377
        storageS3Credentials struct {
378
                AccessKeyId     string `toml:"-"`
379
                SecretAccessKey string `toml:"-"`
380
                Region          string `toml:"-"`
381
        }
382

383
        auth struct {
384
                Enabled                bool     `toml:"enabled"`
385
                Image                  string   `toml:"-"`
386
                SiteUrl                string   `toml:"site_url"`
387
                AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
388

389
                JwtExpiry                  uint `toml:"jwt_expiry"`
390
                EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
391
                RefreshTokenReuseInterval  uint `toml:"refresh_token_reuse_interval"`
392
                EnableManualLinking        bool `toml:"enable_manual_linking"`
393
                Hook                       hook `toml:"hook"`
394

395
                EnableSignup           bool  `toml:"enable_signup"`
396
                EnableAnonymousSignIns bool  `toml:"enable_anonymous_sign_ins"`
397
                Email                  email `toml:"email"`
398
                Sms                    sms   `toml:"sms"`
399
                External               map[string]provider
400

401
                // Custom secrets can be injected from .env file
402
                JwtSecret      string `toml:"-" mapstructure:"jwt_secret"`
403
                AnonKey        string `toml:"-" mapstructure:"anon_key"`
404
                ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
405
        }
406

407
        email struct {
408
                EnableSignup         bool                     `toml:"enable_signup"`
409
                DoubleConfirmChanges bool                     `toml:"double_confirm_changes"`
410
                EnableConfirmations  bool                     `toml:"enable_confirmations"`
411
                Template             map[string]emailTemplate `toml:"template"`
412
                MaxFrequency         time.Duration            `toml:"max_frequency"`
413
        }
414

415
        emailTemplate struct {
416
                Subject     string `toml:"subject"`
417
                ContentPath string `toml:"content_path"`
418
        }
419

420
        sms struct {
421
                EnableSignup        bool              `toml:"enable_signup"`
422
                EnableConfirmations bool              `toml:"enable_confirmations"`
423
                Template            string            `toml:"template"`
424
                Twilio              twilioConfig      `toml:"twilio" mapstructure:"twilio"`
425
                TwilioVerify        twilioConfig      `toml:"twilio_verify" mapstructure:"twilio_verify"`
426
                Messagebird         messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"`
427
                Textlocal           textlocalConfig   `toml:"textlocal" mapstructure:"textlocal"`
428
                Vonage              vonageConfig      `toml:"vonage" mapstructure:"vonage"`
429
                TestOTP             map[string]string `toml:"test_otp"`
430
                MaxFrequency        time.Duration     `toml:"max_frequency"`
431
        }
432

433
        hook struct {
434
                MFAVerificationAttempt      hookConfig `toml:"mfa_verification_attempt"`
435
                PasswordVerificationAttempt hookConfig `toml:"password_verification_attempt"`
436
                CustomAccessToken           hookConfig `toml:"custom_access_token"`
437
                SendSMS                     hookConfig `toml:"send_sms"`
438
                SendEmail                   hookConfig `toml:"send_email"`
439
        }
440

441
        hookConfig struct {
442
                Enabled bool   `toml:"enabled"`
443
                URI     string `toml:"uri"`
444
                Secrets string `toml:"secrets"`
445
        }
446

447
        twilioConfig struct {
448
                Enabled           bool   `toml:"enabled"`
449
                AccountSid        string `toml:"account_sid"`
450
                MessageServiceSid string `toml:"message_service_sid"`
451
                AuthToken         string `toml:"auth_token" mapstructure:"auth_token"`
452
        }
453

454
        messagebirdConfig struct {
455
                Enabled    bool   `toml:"enabled"`
456
                Originator string `toml:"originator"`
457
                AccessKey  string `toml:"access_key" mapstructure:"access_key"`
458
        }
459

460
        textlocalConfig struct {
461
                Enabled bool   `toml:"enabled"`
462
                Sender  string `toml:"sender"`
463
                ApiKey  string `toml:"api_key" mapstructure:"api_key"`
464
        }
465

466
        vonageConfig struct {
467
                Enabled   bool   `toml:"enabled"`
468
                From      string `toml:"from"`
469
                ApiKey    string `toml:"api_key" mapstructure:"api_key"`
470
                ApiSecret string `toml:"api_secret" mapstructure:"api_secret"`
471
        }
472

473
        provider struct {
474
                Enabled        bool   `toml:"enabled"`
475
                ClientId       string `toml:"client_id"`
476
                Secret         string `toml:"secret"`
477
                Url            string `toml:"url"`
478
                RedirectUri    string `toml:"redirect_uri"`
479
                SkipNonceCheck bool   `toml:"skip_nonce_check"`
480
        }
481

482
        edgeRuntime struct {
483
                Enabled       bool          `toml:"enabled"`
484
                Policy        RequestPolicy `toml:"policy"`
485
                InspectorPort uint16        `toml:"inspector_port"`
486
        }
487

488
        function struct {
489
                VerifyJWT *bool  `toml:"verify_jwt"`
490
                ImportMap string `toml:"import_map"`
491
        }
492

493
        analytics struct {
494
                Enabled          bool            `toml:"enabled"`
495
                Port             uint16          `toml:"port"`
496
                Backend          LogflareBackend `toml:"backend"`
497
                VectorPort       uint16          `toml:"vector_port"`
498
                GcpProjectId     string          `toml:"gcp_project_id"`
499
                GcpProjectNumber string          `toml:"gcp_project_number"`
500
                GcpJwtPath       string          `toml:"gcp_jwt_path"`
501
                ApiKey           string          `toml:"-" mapstructure:"api_key"`
502
        }
503

504
        experimental struct {
505
                OrioleDBVersion string `toml:"orioledb_version"`
506
                S3Host          string `toml:"s3_host"`
507
                S3Region        string `toml:"s3_region"`
508
                S3AccessKey     string `toml:"s3_access_key"`
509
                S3SecretKey     string `toml:"s3_secret_key"`
510
        }
511

512
        // TODO
513
        // scripts struct {
514
        //         BeforeMigrations string `toml:"before_migrations"`
515
        //         AfterMigrations  string `toml:"after_migrations"`
516
        // }
517
)
518

519
func (h *hookConfig) HandleHook(hookType string) error {
180✔
520
        // If not enabled do nothing
180✔
521
        if !h.Enabled {
358✔
522
                return nil
178✔
523
        }
178✔
524
        if h.URI == "" {
2✔
525
                return errors.Errorf("missing required field in config: auth.hook.%s.uri", hookType)
×
526
        }
×
527
        if err := validateHookURI(h.URI, hookType); err != nil {
2✔
528
                return err
×
529
        }
×
530
        var err error
2✔
531
        if h.Secrets, err = maybeLoadEnv(h.Secrets); err != nil {
2✔
532
                return errors.Errorf("missing required field in config: auth.hook.%s.secrets", hookType)
×
533
        }
×
534
        return nil
2✔
535
}
536

537
func LoadConfigFS(fsys afero.Fs) error {
51✔
538
        // Load default values
51✔
539
        var buf bytes.Buffer
51✔
540
        if err := initConfigTemplate.Execute(&buf, nil); err != nil {
51✔
541
                return errors.Errorf("failed to initialise config template: %w", err)
×
542
        }
×
543
        dec := toml.NewDecoder(&buf)
51✔
544
        if _, err := dec.Decode(&Config); err != nil {
51✔
545
                return errors.Errorf("failed to decode config template: %w", err)
×
546
        }
×
547
        // Load user defined config
548
        if metadata, err := toml.DecodeFS(afero.NewIOFS(fsys), ConfigPath, &Config); err != nil {
65✔
549
                CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
14✔
550
                cwd, osErr := os.Getwd()
14✔
551
                if osErr != nil {
14✔
552
                        cwd = "current directory"
×
553
                }
×
554
                return errors.Errorf("cannot read config in %s: %w", Bold(cwd), err)
14✔
555
        } else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
37✔
556
                fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
×
557
        }
×
558
        // Load secrets from .env file
559
        if err := loadDefaultEnv(); err != nil {
37✔
560
                return err
×
561
        }
×
562
        if err := viper.Unmarshal(&Config); err != nil {
37✔
563
                return errors.Errorf("failed to parse env to config: %w", err)
×
564
        }
×
565

566
        // Generate JWT tokens
567
        if len(Config.Auth.AnonKey) == 0 {
50✔
568
                anonToken := CustomClaims{Role: "anon"}.NewToken()
13✔
569
                if signed, err := anonToken.SignedString([]byte(Config.Auth.JwtSecret)); err != nil {
13✔
570
                        return errors.Errorf("failed to generate anon key: %w", err)
×
571
                } else {
13✔
572
                        Config.Auth.AnonKey = signed
13✔
573
                }
13✔
574
        }
575
        if len(Config.Auth.ServiceRoleKey) == 0 {
50✔
576
                anonToken := CustomClaims{Role: "service_role"}.NewToken()
13✔
577
                if signed, err := anonToken.SignedString([]byte(Config.Auth.JwtSecret)); err != nil {
13✔
578
                        return errors.Errorf("failed to generate service_role key: %w", err)
×
579
                } else {
13✔
580
                        Config.Auth.ServiceRoleKey = signed
13✔
581
                }
13✔
582
        }
583

584
        // Process decoded TOML.
585
        {
37✔
586
                if Config.ProjectId == "" {
37✔
587
                        return errors.New("Missing required field in config: project_id")
×
588
                }
×
589
                Config.Hostname = GetHostname()
37✔
590
                UpdateDockerIds()
37✔
591
                // Validate api config
37✔
592
                if Config.Api.Port == 0 {
37✔
593
                        return errors.New("Missing required field in config: api.port")
×
594
                }
×
595
                if Config.Api.Enabled {
74✔
596
                        if version, err := afero.ReadFile(fsys, RestVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
37✔
597
                                Config.Api.Image = replaceImageTag(PostgrestImage, string(version))
×
598
                        }
×
599
                }
600
                // Append required schemas if they are missing
601
                Config.Api.Schemas = RemoveDuplicates(append([]string{"public", "storage"}, Config.Api.Schemas...))
37✔
602
                Config.Api.ExtraSearchPath = RemoveDuplicates(append([]string{"public"}, Config.Api.ExtraSearchPath...))
37✔
603
                // Validate db config
37✔
604
                if Config.Db.Port == 0 {
37✔
605
                        return errors.New("Missing required field in config: db.port")
×
606
                }
×
607
                switch Config.Db.MajorVersion {
37✔
608
                case 0:
×
609
                        return errors.New("Missing required field in config: db.major_version")
×
610
                case 12:
×
611
                        return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.")
×
612
                case 13:
×
613
                        Config.Db.Image = Pg13Image
×
614
                        InitialSchemaSql = InitialSchemaPg13Sql
×
615
                case 14:
×
616
                        Config.Db.Image = Pg14Image
×
617
                        InitialSchemaSql = InitialSchemaPg14Sql
×
618
                case 15:
37✔
619
                        if len(Config.Experimental.OrioleDBVersion) > 0 {
39✔
620
                                Config.Db.Image = "supabase/postgres:orioledb-" + Config.Experimental.OrioleDBVersion
2✔
621
                                var err error
2✔
622
                                if Config.Experimental.S3Host, err = maybeLoadEnv(Config.Experimental.S3Host); err != nil {
2✔
623
                                        return err
×
624
                                }
×
625
                                if Config.Experimental.S3Region, err = maybeLoadEnv(Config.Experimental.S3Region); err != nil {
2✔
626
                                        return err
×
627
                                }
×
628
                                if Config.Experimental.S3AccessKey, err = maybeLoadEnv(Config.Experimental.S3AccessKey); err != nil {
2✔
629
                                        return err
×
630
                                }
×
631
                                if Config.Experimental.S3SecretKey, err = maybeLoadEnv(Config.Experimental.S3SecretKey); err != nil {
2✔
632
                                        return err
×
633
                                }
×
634
                        } else if version, err := afero.ReadFile(fsys, PostgresVersionPath); err == nil {
35✔
635
                                if strings.HasPrefix(string(version), "15.") && semver.Compare(string(version[3:]), "1.0.55") >= 0 {
×
636
                                        Config.Db.Image = replaceImageTag(Pg15Image, string(version))
×
637
                                }
×
638
                        }
639
                default:
×
640
                        return errors.Errorf("Failed reading config: Invalid %s: %v.", Aqua("db.major_version"), Config.Db.MajorVersion)
×
641
                }
642
                // Validate pooler config
643
                if Config.Db.Pooler.Enabled {
39✔
644
                        allowed := []PoolMode{TransactionMode, SessionMode}
2✔
645
                        if !SliceContains(allowed, Config.Db.Pooler.PoolMode) {
2✔
646
                                return errors.Errorf("Invalid config for db.pooler.pool_mode. Must be one of: %v", allowed)
×
647
                        }
×
648
                        if version, err := afero.ReadFile(fsys, PoolerVersionPath); err == nil && len(version) > 0 {
2✔
NEW
649
                                Config.Db.Pooler.Image = replaceImageTag(SupavisorImage, string(version))
×
NEW
650
                        }
×
651
                }
652
                if connString, err := afero.ReadFile(fsys, PoolerUrlPath); err == nil && len(connString) > 0 {
37✔
653
                        Config.Db.Pooler.ConnectionString = string(connString)
×
654
                }
×
655
                // Validate realtime config
656
                if Config.Realtime.Enabled {
74✔
657
                        allowed := []AddressFamily{AddressIPv6, AddressIPv4}
37✔
658
                        if !SliceContains(allowed, Config.Realtime.IpVersion) {
37✔
659
                                return errors.Errorf("Invalid config for realtime.ip_version. Must be one of: %v", allowed)
×
660
                        }
×
661
                        if version, err := afero.ReadFile(fsys, RealtimeVersionPath); err == nil && len(version) > 0 {
37✔
NEW
662
                                Config.Realtime.Image = replaceImageTag(RealtimeImage, string(version))
×
NEW
663
                        }
×
664
                }
665
                // Validate storage config
666
                if Config.Storage.Enabled {
74✔
667
                        if version, err := afero.ReadFile(fsys, StorageVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
37✔
668
                                Config.Storage.Image = replaceImageTag(StorageImage, string(version))
×
669
                        }
×
670
                }
671
                // Validate studio config
672
                if Config.Studio.Enabled {
74✔
673
                        if Config.Studio.Port == 0 {
37✔
674
                                return errors.New("Missing required field in config: studio.port")
×
675
                        }
×
676
                        if version, err := afero.ReadFile(fsys, StudioVersionPath); err == nil && len(version) > 0 {
37✔
677
                                Config.Studio.Image = replaceImageTag(StudioImage, string(version))
×
678
                        }
×
679
                        if version, err := afero.ReadFile(fsys, PgmetaVersionPath); err == nil && len(version) > 0 {
37✔
680
                                Config.Studio.PgmetaImage = replaceImageTag(PgmetaImage, string(version))
×
681
                        }
×
682
                        Config.Studio.OpenaiApiKey, _ = maybeLoadEnv(Config.Studio.OpenaiApiKey)
37✔
683
                }
684
                // Validate email config
685
                if Config.Inbucket.Enabled {
74✔
686
                        if Config.Inbucket.Port == 0 {
37✔
687
                                return errors.New("Missing required field in config: inbucket.port")
×
688
                        }
×
689
                }
690

691
                // Validate auth config
692
                if Config.Auth.Enabled {
74✔
693
                        if Config.Auth.SiteUrl == "" {
37✔
694
                                return errors.New("Missing required field in config: auth.site_url")
×
695
                        }
×
696
                        var err error
37✔
697
                        if Config.Auth.SiteUrl, err = maybeLoadEnv(Config.Auth.SiteUrl); err != nil {
37✔
698
                                return err
×
699
                        }
×
700
                        if version, err := afero.ReadFile(fsys, GotrueVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
37✔
701
                                Config.Auth.Image = replaceImageTag(GotrueImage, string(version))
×
702
                        }
×
703
                        // Validate email template
704
                        for _, tmpl := range Config.Auth.Email.Template {
218✔
705
                                if len(tmpl.ContentPath) > 0 {
183✔
706
                                        if _, err := fsys.Stat(tmpl.ContentPath); err != nil {
3✔
707
                                                return errors.Errorf("failed to read file info: %w", err)
1✔
708
                                        }
1✔
709
                                }
710
                        }
711
                        // Validate sms config
712
                        if Config.Auth.Sms.Twilio.Enabled {
37✔
713
                                if len(Config.Auth.Sms.Twilio.AccountSid) == 0 {
1✔
714
                                        return errors.New("Missing required field in config: auth.sms.twilio.account_sid")
×
715
                                }
×
716
                                if len(Config.Auth.Sms.Twilio.MessageServiceSid) == 0 {
1✔
717
                                        return errors.New("Missing required field in config: auth.sms.twilio.message_service_sid")
×
718
                                }
×
719
                                if len(Config.Auth.Sms.Twilio.AuthToken) == 0 {
1✔
720
                                        return errors.New("Missing required field in config: auth.sms.twilio.auth_token")
×
721
                                }
×
722
                                if Config.Auth.Sms.Twilio.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.Twilio.AuthToken); err != nil {
1✔
723
                                        return err
×
724
                                }
×
725
                        }
726
                        if Config.Auth.Sms.TwilioVerify.Enabled {
36✔
727
                                if len(Config.Auth.Sms.TwilioVerify.AccountSid) == 0 {
×
728
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.account_sid")
×
729
                                }
×
730
                                if len(Config.Auth.Sms.TwilioVerify.MessageServiceSid) == 0 {
×
731
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.message_service_sid")
×
732
                                }
×
733
                                if len(Config.Auth.Sms.TwilioVerify.AuthToken) == 0 {
×
734
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.auth_token")
×
735
                                }
×
736
                                if Config.Auth.Sms.TwilioVerify.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.TwilioVerify.AuthToken); err != nil {
×
737
                                        return err
×
738
                                }
×
739
                        }
740
                        if Config.Auth.Sms.Messagebird.Enabled {
36✔
741
                                if len(Config.Auth.Sms.Messagebird.Originator) == 0 {
×
742
                                        return errors.New("Missing required field in config: auth.sms.messagebird.originator")
×
743
                                }
×
744
                                if len(Config.Auth.Sms.Messagebird.AccessKey) == 0 {
×
745
                                        return errors.New("Missing required field in config: auth.sms.messagebird.access_key")
×
746
                                }
×
747
                                if Config.Auth.Sms.Messagebird.AccessKey, err = maybeLoadEnv(Config.Auth.Sms.Messagebird.AccessKey); err != nil {
×
748
                                        return err
×
749
                                }
×
750
                        }
751
                        if Config.Auth.Sms.Textlocal.Enabled {
36✔
752
                                if len(Config.Auth.Sms.Textlocal.Sender) == 0 {
×
753
                                        return errors.New("Missing required field in config: auth.sms.textlocal.sender")
×
754
                                }
×
755
                                if len(Config.Auth.Sms.Textlocal.ApiKey) == 0 {
×
756
                                        return errors.New("Missing required field in config: auth.sms.textlocal.api_key")
×
757
                                }
×
758
                                if Config.Auth.Sms.Textlocal.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Textlocal.ApiKey); err != nil {
×
759
                                        return err
×
760
                                }
×
761
                        }
762
                        if Config.Auth.Sms.Vonage.Enabled {
36✔
763
                                if len(Config.Auth.Sms.Vonage.From) == 0 {
×
764
                                        return errors.New("Missing required field in config: auth.sms.vonage.from")
×
765
                                }
×
766
                                if len(Config.Auth.Sms.Vonage.ApiKey) == 0 {
×
767
                                        return errors.New("Missing required field in config: auth.sms.vonage.api_key")
×
768
                                }
×
769
                                if len(Config.Auth.Sms.Vonage.ApiSecret) == 0 {
×
770
                                        return errors.New("Missing required field in config: auth.sms.vonage.api_secret")
×
771
                                }
×
772
                                if Config.Auth.Sms.Vonage.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiKey); err != nil {
×
773
                                        return err
×
774
                                }
×
775
                                if Config.Auth.Sms.Vonage.ApiSecret, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiSecret); err != nil {
×
776
                                        return err
×
777
                                }
×
778
                        }
779
                        if err := Config.Auth.Hook.MFAVerificationAttempt.HandleHook("mfa_verification_attempt"); err != nil {
36✔
780
                                return err
×
781
                        }
×
782
                        if err := Config.Auth.Hook.PasswordVerificationAttempt.HandleHook("password_verification_attempt"); err != nil {
36✔
783
                                return err
×
784
                        }
×
785
                        if err := Config.Auth.Hook.CustomAccessToken.HandleHook("custom_access_token"); err != nil {
36✔
786
                                return err
×
787
                        }
×
788
                        if err := Config.Auth.Hook.SendSMS.HandleHook("send_sms"); err != nil {
36✔
789
                                return err
×
790
                        }
×
791
                        if err := Config.Auth.Hook.SendEmail.HandleHook("send_email"); err != nil {
36✔
792
                                return err
×
793
                        }
×
794
                        // Validate oauth config
795
                        for ext, provider := range Config.Auth.External {
684✔
796
                                if !provider.Enabled {
1,295✔
797
                                        continue
647✔
798
                                }
799
                                if provider.ClientId == "" {
1✔
800
                                        return errors.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
×
801
                                }
×
802
                                if !SliceContains([]string{"apple", "google"}, ext) && provider.Secret == "" {
1✔
803
                                        return errors.Errorf("Missing required field in config: auth.external.%s.secret", ext)
×
804
                                }
×
805
                                if provider.ClientId, err = maybeLoadEnv(provider.ClientId); err != nil {
1✔
806
                                        return err
×
807
                                }
×
808
                                if provider.Secret, err = maybeLoadEnv(provider.Secret); err != nil {
1✔
809
                                        return err
×
810
                                }
×
811
                                if provider.RedirectUri, err = maybeLoadEnv(provider.RedirectUri); err != nil {
1✔
812
                                        return err
×
813
                                }
×
814
                                if provider.Url, err = maybeLoadEnv(provider.Url); err != nil {
1✔
815
                                        return err
×
816
                                }
×
817
                                Config.Auth.External[ext] = provider
1✔
818
                        }
819
                }
820
        }
821
        // Validate functions config
822
        if Config.EdgeRuntime.Enabled {
72✔
823
                allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot}
36✔
824
                if !SliceContains(allowed, Config.EdgeRuntime.Policy) {
36✔
825
                        return errors.Errorf("Invalid config for edge_runtime.policy. Must be one of: %v", allowed)
×
826
                }
×
827
        }
828
        for name, functionConfig := range Config.Functions {
38✔
829
                if functionConfig.VerifyJWT == nil {
2✔
830
                        functionConfig.VerifyJWT = Ptr(true)
×
831
                        Config.Functions[name] = functionConfig
×
832
                }
×
833
        }
834
        // Validate logflare config
835
        if Config.Analytics.Enabled {
36✔
836
                switch Config.Analytics.Backend {
×
837
                case LogflareBigQuery:
×
838
                        if len(Config.Analytics.GcpProjectId) == 0 {
×
839
                                return errors.New("Missing required field in config: analytics.gcp_project_id")
×
840
                        }
×
841
                        if len(Config.Analytics.GcpProjectNumber) == 0 {
×
842
                                return errors.New("Missing required field in config: analytics.gcp_project_number")
×
843
                        }
×
844
                        if len(Config.Analytics.GcpJwtPath) == 0 {
×
845
                                return errors.New("Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path")
×
846
                        }
×
847
                case LogflarePostgres:
×
848
                        break
×
849
                default:
×
850
                        allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery}
×
851
                        return errors.Errorf("Invalid config for analytics.backend. Must be one of: %v", allowed)
×
852
                }
853
        }
854
        return nil
36✔
855
}
856

857
func maybeLoadEnv(s string) (string, error) {
89✔
858
        matches := envPattern.FindStringSubmatch(s)
89✔
859
        if len(matches) == 0 {
137✔
860
                return s, nil
48✔
861
        }
48✔
862

863
        envName := matches[1]
41✔
864
        if value := os.Getenv(envName); value != "" {
45✔
865
                return value, nil
4✔
866
        }
4✔
867

868
        return "", errors.Errorf(`Error evaluating "%s": environment variable %s is unset.`, s, envName)
37✔
869
}
870

871
func sanitizeProjectId(src string) string {
62✔
872
        // A valid project ID must only contain alphanumeric and special characters _.-
62✔
873
        sanitized := invalidProjectId.ReplaceAllString(src, "_")
62✔
874
        // It must also start with an alphanumeric character
62✔
875
        return strings.TrimLeft(sanitized, "_.-")
62✔
876
}
62✔
877

878
type InitParams struct {
879
        ProjectId   string
880
        UseOrioleDB bool
881
        Overwrite   bool
882
}
883

884
func InitConfig(params InitParams, fsys afero.Fs) error {
57✔
885
        // Defaults to current directory name as project id
57✔
886
        if len(params.ProjectId) == 0 {
109✔
887
                cwd, err := os.Getwd()
52✔
888
                if err != nil {
52✔
889
                        return errors.Errorf("failed to get working directory: %w", err)
×
890
                }
×
891
                params.ProjectId = filepath.Base(cwd)
52✔
892
        }
893
        params.ProjectId = sanitizeProjectId(params.ProjectId)
57✔
894
        // Create config file
57✔
895
        if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
58✔
896
                return err
1✔
897
        }
1✔
898
        flag := os.O_WRONLY | os.O_CREATE
56✔
899
        if params.Overwrite {
56✔
900
                flag |= os.O_TRUNC
×
901
        } else {
56✔
902
                flag |= os.O_EXCL
56✔
903
        }
56✔
904
        f, err := fsys.OpenFile(ConfigPath, flag, 0644)
56✔
905
        if err != nil {
58✔
906
                return errors.Errorf("failed to create config file: %w", err)
2✔
907
        }
2✔
908
        defer f.Close()
54✔
909
        // Update from template
54✔
910
        if err := initConfigTemplate.Execute(f, params); err != nil {
54✔
911
                return errors.Errorf("failed to initialise config: %w", err)
×
912
        }
×
913
        return nil
54✔
914
}
915

916
func WriteConfig(fsys afero.Fs, _test bool) error {
37✔
917
        return InitConfig(InitParams{}, fsys)
37✔
918
}
37✔
919

920
func RemoveDuplicates(slice []string) (result []string) {
80✔
921
        set := make(map[string]struct{})
80✔
922
        for _, item := range slice {
345✔
923
                if _, exists := set[item]; !exists {
456✔
924
                        set[item] = struct{}{}
191✔
925
                        result = append(result, item)
191✔
926
                }
191✔
927
        }
928
        return result
80✔
929
}
930

931
func loadDefaultEnv() error {
37✔
932
        env := viper.GetString("ENV")
37✔
933
        if env == "" {
74✔
934
                env = "development"
37✔
935
        }
37✔
936
        filenames := []string{".env." + env + ".local"}
37✔
937
        if env != "test" {
74✔
938
                filenames = append(filenames, ".env.local")
37✔
939
        }
37✔
940
        filenames = append(filenames, ".env."+env, ".env")
37✔
941
        for _, path := range filenames {
185✔
942
                if err := loadEnvIfExists(path); err != nil {
148✔
943
                        return err
×
944
                }
×
945
        }
946
        return nil
37✔
947
}
948

949
func loadEnvIfExists(path string) error {
148✔
950
        if err := godotenv.Load(path); err != nil && !errors.Is(err, os.ErrNotExist) {
148✔
951
                return errors.Errorf("failed to load %s: %w", Bold(".env"), err)
×
952
        }
×
953
        return nil
148✔
954
}
955

956
func validateHookURI(uri, hookName string) error {
7✔
957
        parsed, err := url.Parse(uri)
7✔
958
        if err != nil {
8✔
959
                return errors.Errorf("failed to parse template url: %w", err)
1✔
960
        }
1✔
961
        if !(parsed.Scheme == "http" || parsed.Scheme == "https" || parsed.Scheme == "pg-functions") {
7✔
962
                return errors.Errorf("Invalid HTTP hook config: auth.hook.%v should be a Postgres function URI, or a HTTP or HTTPS URL", hookName)
1✔
963
        }
1✔
964
        return nil
5✔
965
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc