@@ -16,6 +16,7 @@ import (
16
16
"os"
17
17
"path"
18
18
"path/filepath"
19
+ "reflect"
19
20
"regexp"
20
21
"sort"
21
22
"strconv"
@@ -378,9 +379,64 @@ func (c *config) Eject(w io.Writer) error {
378
379
return nil
379
380
}
380
381
382
+ // Loads custom config file to struct fields tagged with toml.
383
+ func (c * config ) loadFromFile (filename string , fsys fs.FS ) error {
384
+ v := viper .New ()
385
+ v .SetConfigType ("toml" )
386
+ // Load default values
387
+ var buf bytes.Buffer
388
+ if err := initConfigTemplate .Option ("missingkey=zero" ).Execute (& buf , c ); err != nil {
389
+ return errors .Errorf ("failed to initialise template config: %w" , err )
390
+ } else if err := c .loadFromReader (v , & buf ); err != nil {
391
+ return err
392
+ }
393
+ // Load custom config
394
+ if ext := filepath .Ext (filename ); len (ext ) > 0 {
395
+ v .SetConfigType (ext [1 :])
396
+ }
397
+ f , err := fsys .Open (filename )
398
+ if err != nil {
399
+ return errors .Errorf ("failed to read file config: %w" , err )
400
+ }
401
+ defer f .Close ()
402
+ return c .loadFromReader (v , f )
403
+ }
404
+
405
+ func (c * config ) loadFromReader (v * viper.Viper , r io.Reader ) error {
406
+ if err := v .MergeConfig (r ); err != nil {
407
+ return errors .Errorf ("failed to merge config: %w" , err )
408
+ }
409
+ // Manually parse [functions.*] to empty struct for backwards compatibility
410
+ for key , value := range v .GetStringMap ("functions" ) {
411
+ if m , ok := value .(map [string ]any ); ok && len (m ) == 0 {
412
+ v .Set ("functions." + key , function {})
413
+ }
414
+ }
415
+ if err := v .UnmarshalExact (c , viper .DecodeHook (mapstructure .ComposeDecodeHookFunc (
416
+ mapstructure .StringToTimeDurationHookFunc (),
417
+ mapstructure .StringToIPHookFunc (),
418
+ mapstructure .StringToSliceHookFunc ("," ),
419
+ mapstructure .TextUnmarshallerHookFunc (),
420
+ LoadEnvHook ,
421
+ // TODO: include decrypt secret hook
422
+ )), func (dc * mapstructure.DecoderConfig ) {
423
+ dc .TagName = "toml"
424
+ dc .Squash = true
425
+ }); err != nil {
426
+ return errors .Errorf ("failed to parse config: %w" , err )
427
+ }
428
+ return nil
429
+ }
430
+
431
+ // Loads envs prefixed with supabase_ to struct fields tagged with mapstructure.
381
432
func (c * config ) loadFromEnv () error {
382
- // Allow overriding base config object with automatic env
383
- // Ref: https://github.com/spf13/viper/issues/761
433
+ v := viper .New ()
434
+ v .SetEnvPrefix ("SUPABASE" )
435
+ v .SetEnvKeyReplacer (strings .NewReplacer ("." , "_" ))
436
+ v .AutomaticEnv ()
437
+ // Viper does not parse env vars automatically. Instead of calling viper.BindEnv
438
+ // per key, we decode all keys from an existing struct, and merge them to viper.
439
+ // Ref: https://github.com/spf13/viper/issues/761#issuecomment-859306364
384
440
envKeysMap := map [string ]interface {}{}
385
441
if dec , err := mapstructure .NewDecoder (& mapstructure.DecoderConfig {
386
442
Result : & envKeysMap ,
@@ -389,47 +445,32 @@ func (c *config) loadFromEnv() error {
389
445
return errors .Errorf ("failed to create decoder: %w" , err )
390
446
} else if err := dec .Decode (c .baseConfig ); err != nil {
391
447
return errors .Errorf ("failed to decode env: %w" , err )
392
- }
393
- v := viper .New ()
394
- v .SetEnvPrefix ("SUPABASE" )
395
- v .SetEnvKeyReplacer (strings .NewReplacer ("." , "_" ))
396
- v .AutomaticEnv ()
397
- if err := v .MergeConfigMap (envKeysMap ); err != nil {
398
- return errors .Errorf ("failed to merge config: %w" , err )
399
- } else if err := v .Unmarshal (c ); err != nil {
400
- return errors .Errorf ("failed to parse env to config: %w" , err )
448
+ } else if err := v .MergeConfigMap (envKeysMap ); err != nil {
449
+ return errors .Errorf ("failed to merge env config: %w" , err )
450
+ }
451
+ // Writes viper state back to config struct, with automatic env substitution
452
+ if err := v .UnmarshalExact (c , viper .DecodeHook (mapstructure .ComposeDecodeHookFunc (
453
+ mapstructure .StringToTimeDurationHookFunc (),
454
+ mapstructure .StringToIPHookFunc (),
455
+ mapstructure .StringToSliceHookFunc ("," ),
456
+ mapstructure .TextUnmarshallerHookFunc (),
457
+ // TODO: include decrypt secret hook
458
+ ))); err != nil {
459
+ return errors .Errorf ("failed to parse env override: %w" , err )
401
460
}
402
461
return nil
403
462
}
404
463
405
464
func (c * config ) Load (path string , fsys fs.FS ) error {
406
465
builder := NewPathBuilder (path )
407
- // Load default values
408
- var buf bytes.Buffer
409
- if err := initConfigTemplate .Option ("missingkey=zero" ).Execute (& buf , c ); err != nil {
410
- return errors .Errorf ("failed to initialise config template: %w" , err )
411
- }
412
- dec := toml .NewDecoder (& buf )
413
- if _ , err := dec .Decode (c ); err != nil {
414
- return errors .Errorf ("failed to decode config template: %w" , err )
415
- }
416
- if metadata , err := toml .DecodeFS (fsys , builder .ConfigPath , c ); err != nil {
417
- cwd , osErr := os .Getwd ()
418
- if osErr != nil {
419
- cwd = "current directory"
420
- }
421
- return errors .Errorf ("cannot read config in %s: %w" , cwd , err )
422
- } else if undecoded := metadata .Undecoded (); len (undecoded ) > 0 {
423
- for _ , key := range undecoded {
424
- if key [0 ] != "remotes" {
425
- fmt .Fprintf (os .Stderr , "Unknown config field: [%s]\n " , key )
426
- }
427
- }
428
- }
429
466
// Load secrets from .env file
430
467
if err := loadDefaultEnv (); err != nil {
431
468
return err
432
- } else if err := c .loadFromEnv (); err != nil {
469
+ }
470
+ if err := c .loadFromFile (builder .ConfigPath , fsys ); err != nil {
471
+ return err
472
+ }
473
+ if err := c .loadFromEnv (); err != nil {
433
474
return err
434
475
}
435
476
// Generate JWT tokens
@@ -619,17 +660,16 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
619
660
case 15 :
620
661
if len (c .Experimental .OrioleDBVersion ) > 0 {
621
662
c .Db .Image = "supabase/postgres:orioledb-" + c .Experimental .OrioleDBVersion
622
- var err error
623
- if c .Experimental .S3Host , err = maybeLoadEnv (c .Experimental .S3Host ); err != nil {
663
+ if err := assertEnvLoaded (c .Experimental .S3Host ); err != nil {
624
664
return err
625
665
}
626
- if c . Experimental . S3Region , err = maybeLoadEnv (c .Experimental .S3Region ); err != nil {
666
+ if err := assertEnvLoaded (c .Experimental .S3Region ); err != nil {
627
667
return err
628
668
}
629
- if c . Experimental . S3AccessKey , err = maybeLoadEnv (c .Experimental .S3AccessKey ); err != nil {
669
+ if err := assertEnvLoaded (c .Experimental .S3AccessKey ); err != nil {
630
670
return err
631
671
}
632
- if c . Experimental . S3SecretKey , err = maybeLoadEnv (c .Experimental .S3SecretKey ); err != nil {
672
+ if err := assertEnvLoaded (c .Experimental .S3SecretKey ); err != nil {
633
673
return err
634
674
}
635
675
}
@@ -666,7 +706,6 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
666
706
} else if parsed .Host == "" || parsed .Host == c .Hostname {
667
707
c .Studio .ApiUrl = c .Api .ExternalUrl
668
708
}
669
- c .Studio .OpenaiApiKey , _ = maybeLoadEnv (c .Studio .OpenaiApiKey )
670
709
}
671
710
// Validate smtp config
672
711
if c .Inbucket .Enabled {
@@ -679,12 +718,11 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
679
718
if c .Auth .SiteUrl == "" {
680
719
return errors .New ("Missing required field in config: auth.site_url" )
681
720
}
682
- var err error
683
- if c .Auth .SiteUrl , err = maybeLoadEnv (c .Auth .SiteUrl ); err != nil {
721
+ if err := assertEnvLoaded (c .Auth .SiteUrl ); err != nil {
684
722
return err
685
723
}
686
724
for i , url := range c .Auth .AdditionalRedirectUrls {
687
- if c . Auth . AdditionalRedirectUrls [ i ], err = maybeLoadEnv (url ); err != nil {
725
+ if err := assertEnvLoaded (url ); err != nil {
688
726
return errors .Errorf ("Invalid config for auth.additional_redirect_urls[%d]: %v" , i , err )
689
727
}
690
728
}
@@ -749,18 +787,24 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
749
787
return nil
750
788
}
751
789
752
- func maybeLoadEnv (s string ) (string , error ) {
753
- matches := envPattern .FindStringSubmatch (s )
754
- if len (matches ) == 0 {
755
- return s , nil
790
+ func assertEnvLoaded (s string ) error {
791
+ if matches := envPattern .FindStringSubmatch (s ); len (matches ) > 1 {
792
+ return errors .Errorf (`Error evaluating "%s": environment variable %s is unset.` , s , matches [1 ])
756
793
}
794
+ return nil
795
+ }
757
796
758
- envName := matches [ 1 ]
759
- if value := os . Getenv ( envName ); value != "" {
760
- return value , nil
797
+ func LoadEnvHook ( f reflect. Kind , t reflect. Kind , data interface {}) ( interface {}, error ) {
798
+ if f != reflect . String || t != reflect . String {
799
+ return data , nil
761
800
}
762
-
763
- return "" , errors .Errorf (`Error evaluating "%s": environment variable %s is unset.` , s , envName )
801
+ value := data .(string )
802
+ if matches := envPattern .FindStringSubmatch (value ); len (matches ) > 1 {
803
+ if v , exists := os .LookupEnv (matches [1 ]); exists {
804
+ value = v
805
+ }
806
+ }
807
+ return value , nil
764
808
}
765
809
766
810
func truncateText (text string , maxLen int ) string {
@@ -874,7 +918,7 @@ func (e *email) validate(fsys fs.FS) (err error) {
874
918
if len (e .Smtp .AdminEmail ) == 0 {
875
919
return errors .New ("Missing required field in config: auth.email.smtp.admin_email" )
876
920
}
877
- if e . Smtp . Pass , err = maybeLoadEnv (e .Smtp .Pass ); err != nil {
921
+ if err := assertEnvLoaded (e .Smtp .Pass ); err != nil {
878
922
return err
879
923
}
880
924
}
@@ -893,7 +937,7 @@ func (s *sms) validate() (err error) {
893
937
if len (s .Twilio .AuthToken ) == 0 {
894
938
return errors .New ("Missing required field in config: auth.sms.twilio.auth_token" )
895
939
}
896
- if s . Twilio . AuthToken , err = maybeLoadEnv (s .Twilio .AuthToken ); err != nil {
940
+ if err := assertEnvLoaded (s .Twilio .AuthToken ); err != nil {
897
941
return err
898
942
}
899
943
case s .TwilioVerify .Enabled :
@@ -906,7 +950,7 @@ func (s *sms) validate() (err error) {
906
950
if len (s .TwilioVerify .AuthToken ) == 0 {
907
951
return errors .New ("Missing required field in config: auth.sms.twilio_verify.auth_token" )
908
952
}
909
- if s . TwilioVerify . AuthToken , err = maybeLoadEnv (s .TwilioVerify .AuthToken ); err != nil {
953
+ if err := assertEnvLoaded (s .TwilioVerify .AuthToken ); err != nil {
910
954
return err
911
955
}
912
956
case s .Messagebird .Enabled :
@@ -916,7 +960,7 @@ func (s *sms) validate() (err error) {
916
960
if len (s .Messagebird .AccessKey ) == 0 {
917
961
return errors .New ("Missing required field in config: auth.sms.messagebird.access_key" )
918
962
}
919
- if s . Messagebird . AccessKey , err = maybeLoadEnv (s .Messagebird .AccessKey ); err != nil {
963
+ if err := assertEnvLoaded (s .Messagebird .AccessKey ); err != nil {
920
964
return err
921
965
}
922
966
case s .Textlocal .Enabled :
@@ -926,7 +970,7 @@ func (s *sms) validate() (err error) {
926
970
if len (s .Textlocal .ApiKey ) == 0 {
927
971
return errors .New ("Missing required field in config: auth.sms.textlocal.api_key" )
928
972
}
929
- if s . Textlocal . ApiKey , err = maybeLoadEnv (s .Textlocal .ApiKey ); err != nil {
973
+ if err := assertEnvLoaded (s .Textlocal .ApiKey ); err != nil {
930
974
return err
931
975
}
932
976
case s .Vonage .Enabled :
@@ -939,10 +983,10 @@ func (s *sms) validate() (err error) {
939
983
if len (s .Vonage .ApiSecret ) == 0 {
940
984
return errors .New ("Missing required field in config: auth.sms.vonage.api_secret" )
941
985
}
942
- if s . Vonage . ApiKey , err = maybeLoadEnv (s .Vonage .ApiKey ); err != nil {
986
+ if err := assertEnvLoaded (s .Vonage .ApiKey ); err != nil {
943
987
return err
944
988
}
945
- if s . Vonage . ApiSecret , err = maybeLoadEnv (s .Vonage .ApiSecret ); err != nil {
989
+ if err := assertEnvLoaded (s .Vonage .ApiSecret ); err != nil {
946
990
return err
947
991
}
948
992
case s .EnableSignup :
@@ -969,16 +1013,16 @@ func (e external) validate() (err error) {
969
1013
if ! sliceContains ([]string {"apple" , "google" }, ext ) && provider .Secret == "" {
970
1014
return errors .Errorf ("Missing required field in config: auth.external.%s.secret" , ext )
971
1015
}
972
- if provider . ClientId , err = maybeLoadEnv (provider .ClientId ); err != nil {
1016
+ if err := assertEnvLoaded (provider .ClientId ); err != nil {
973
1017
return err
974
1018
}
975
- if provider . Secret , err = maybeLoadEnv (provider .Secret ); err != nil {
1019
+ if err := assertEnvLoaded (provider .Secret ); err != nil {
976
1020
return err
977
1021
}
978
- if provider . RedirectUri , err = maybeLoadEnv (provider .RedirectUri ); err != nil {
1022
+ if err := assertEnvLoaded (provider .RedirectUri ); err != nil {
979
1023
return err
980
1024
}
981
- if provider . Url , err = maybeLoadEnv (provider .Url ); err != nil {
1025
+ if err := assertEnvLoaded (provider .Url ); err != nil {
982
1026
return err
983
1027
}
984
1028
e [ext ] = provider
@@ -1033,7 +1077,7 @@ func (h *hookConfig) validate(hookType string) (err error) {
1033
1077
case "http" , "https" :
1034
1078
if len (h .Secrets ) == 0 {
1035
1079
return errors .Errorf ("Missing required field in config: auth.hook.%s.secrets" , hookType )
1036
- } else if h . Secrets , err = maybeLoadEnv (h .Secrets ); err != nil {
1080
+ } else if err := assertEnvLoaded (h .Secrets ); err != nil {
1037
1081
return err
1038
1082
}
1039
1083
for _ , secret := range strings .Split (h .Secrets , "|" ) {
@@ -1119,13 +1163,13 @@ func (c *tpaCognito) issuerURL() string {
1119
1163
func (c * tpaCognito ) validate () (err error ) {
1120
1164
if c .UserPoolID == "" {
1121
1165
return errors .New ("Invalid config: auth.third_party.cognito is enabled but without a user_pool_id." )
1122
- } else if c . UserPoolID , err = maybeLoadEnv (c .UserPoolID ); err != nil {
1166
+ } else if err := assertEnvLoaded (c .UserPoolID ); err != nil {
1123
1167
return err
1124
1168
}
1125
1169
1126
1170
if c .UserPoolRegion == "" {
1127
1171
return errors .New ("Invalid config: auth.third_party.cognito is enabled but without a user_pool_region." )
1128
- } else if c . UserPoolRegion , err = maybeLoadEnv (c .UserPoolRegion ); err != nil {
1172
+ } else if err := assertEnvLoaded (c .UserPoolRegion ); err != nil {
1129
1173
return err
1130
1174
}
1131
1175
0 commit comments