Repository: supabase/cli
Pull request #2502 (2024-07-08 13:38:38)

feat: expose config as public package
## What kind of change does this PR introduce?

feature

## What is the new behavior?

Allows 3rd party to load, eject, and validate config.toml.

## Additional context

Add any other context or screenshots.


coveralls: ## Pull Request Test Coverage Report for [Build 9840722067](https://coveralls.io/builds/68513117)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **7** unchanged lines in **2** files lost coverage.
* Overall coverage decreased (**-0.03%**) to **61.304%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/storage/rm/rm.go](https://coveralls.io/builds/68513117/source?filename=internal%2Fstorage%2Frm%2Frm.go#L124) | 2 | 89.53% |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/68513117/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L101) | 5 | 11.76% |
<!-- | **Total:** | **7** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/68513117/badge)](https://coveralls.io/builds/68513117) |
| :-- | --: |
| Change from base [Build 9833754803](https://coveralls.io/builds/68503348): |  -0.03% |
| Covered Lines: | 7126 |
| Relevant Lines: | 11624 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go
index 474b02b7..74b69080 100644
--- a/internal/db/diff/diff.go
+++ b/internal/db/diff/diff.go
@@ -28,7 +28,7 @@ import (
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/parser"
+	"github.com/supabase/cli/pkg/parser"
 )
 
 type DiffFunc func(context.Context, string, string, []string) (string, error)
diff --git a/internal/db/diff/migra.go b/internal/db/diff/migra.go
index 6b7dbd16..4e784269 100644
--- a/internal/db/diff/migra.go
+++ b/internal/db/diff/migra.go
@@ -10,6 +10,7 @@ import (
 	"github.com/docker/docker/api/types/network"
 	"github.com/go-errors/errors"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
 //go:embed templates/migra.sh
@@ -25,7 +26,7 @@ func DiffSchemaMigra(ctx context.Context, source, target string, schema []string
 	if err := utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
-			Image: utils.MigraImage,
+			Image: config.MigraImage,
 			Env:   env,
 			Cmd:   cmd,
 		},
diff --git a/internal/db/diff/pgadmin.go b/internal/db/diff/pgadmin.go
index f1b63ee0..29802358 100644
--- a/internal/db/diff/pgadmin.go
+++ b/internal/db/diff/pgadmin.go
@@ -11,6 +11,7 @@ import (
 	"github.com/supabase/cli/internal/db/start"
 	"github.com/supabase/cli/internal/migration/new"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
 var warnDiff = `WARNING: The diff tool is not foolproof, so you may need to manually rearrange and modify the generated migration.
@@ -84,7 +85,7 @@ func DiffSchemaPgAdmin(ctx context.Context, source, target string, schema []stri
 	if len(schema) == 0 {
 		if err := utils.DockerRunOnceWithStream(
 			ctx,
-			utils.DifferImage,
+			config.DifferImage,
 			nil,
 			args,
 			stream.Stdout(),
@@ -97,7 +98,7 @@ func DiffSchemaPgAdmin(ctx context.Context, source, target string, schema []stri
 		p.Send(utils.StatusMsg("Diffing schema: " + s))
 		if err := utils.DockerRunOnceWithStream(
 			ctx,
-			utils.DifferImage,
+			config.DifferImage,
 			nil,
 			append([]string{"--schema", s}, args...),
 			stream.Stdout(),
diff --git a/internal/db/dump/dump.go b/internal/db/dump/dump.go
index a72ec30b..a52b1dc4 100644
--- a/internal/db/dump/dump.go
+++ b/internal/db/dump/dump.go
@@ -14,6 +14,7 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
+	cliConfig "github.com/supabase/cli/pkg/config"
 )
 
 var (
@@ -170,7 +171,7 @@ func dump(ctx context.Context, config pgconn.Config, script string, env []string
 	return utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
-			Image: utils.Pg15Image,
+			Image: cliConfig.Pg15Image,
 			Env:   allEnvs,
 			Cmd:   []string{"bash", "-c", script, "--"},
 		},
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index 7fb73ecb..7ea4c4e7 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -22,7 +22,7 @@ import (
 	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 var (
@@ -152,7 +152,11 @@ func initDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) error {
 		return err
 	}
 	defer conn.Close(context.Background())
-	return apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.InitialSchemaSql))
+	sql := utils.InitialSchemaPg14Sql
+	if utils.Config.Db.MajorVersion == 13 {
+		sql = utils.InitialSchemaPg13Sql
+	}
+	return apply.BatchExecDDL(ctx, conn, strings.NewReader(sql))
 }
 
 // Recreate postgres database by connecting to template1
diff --git a/internal/db/start/start.go b/internal/db/start/start.go
index 4c9578fb..46ea5a9d 100644
--- a/internal/db/start/start.go
+++ b/internal/db/start/start.go
@@ -206,7 +206,11 @@ func initSchema14(ctx context.Context, conn *pgx.Conn) error {
 	if err := apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.GlobalsSql)); err != nil {
 		return err
 	}
-	return apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.InitialSchemaSql))
+	sql := utils.InitialSchemaPg14Sql
+	if utils.Config.Db.MajorVersion == 13 {
+		sql = utils.InitialSchemaPg13Sql
+	}
+	return apply.BatchExecDDL(ctx, conn, strings.NewReader(sql))
 }
 
 func initRealtimeJob(host string) utils.DockerJob {
@@ -260,7 +264,7 @@ func initAuthJob(host string) utils.DockerJob {
 	return utils.DockerJob{
 		Image: utils.Config.Auth.Image,
 		Env: []string{
-			"API_EXTERNAL_URL=" + utils.GetApiUrl(""),
+			"API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
 			"GOTRUE_LOG_LEVEL=error",
 			"GOTRUE_DB_DRIVER=postgres",
 			fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index d2966297..3f6e1385 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -133,7 +133,7 @@ func bundleFunction(ctx context.Context, slug, hostImportMapPath string, fsys af
 	err = utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
-			Image: utils.EdgeRuntimeImage,
+			Image: utils.Config.EdgeRuntime.Image,
 			Env:   []string{},
 			Cmd:   cmd,
 		},
diff --git a/internal/functions/download/download.go b/internal/functions/download/download.go
index 71720c4f..e3b68cae 100644
--- a/internal/functions/download/download.go
+++ b/internal/functions/download/download.go
@@ -181,7 +181,7 @@ func extractOne(ctx context.Context, slug, eszipPath string) error {
 	return utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
-			Image: utils.EdgeRuntimeImage,
+			Image: utils.Config.EdgeRuntime.Image,
 			Cmd:   []string{"unbundle", "--eszip", dockerEszipPath, "--output", utils.DockerDenoDir},
 		},
 		container.HostConfig{
diff --git a/internal/functions/new/new.go b/internal/functions/new/new.go
index c3bf173b..3656e9b5 100644
--- a/internal/functions/new/new.go
+++ b/internal/functions/new/new.go
@@ -52,7 +52,7 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error {
 			URL:   utils.GetApiUrl("/functions/v1/" + slug),
 			Token: utils.Config.Auth.AnonKey,
 		}
-		if err := indexTemplate.Execute(f, config); err != nil {
+		if err := indexTemplate.Option("missingkey=error").Execute(f, config); err != nil {
 			return errors.Errorf("failed to initialise function entrypoint: %w", err)
 		}
 	}
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index 118818b8..a956d5bd 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -172,7 +172,7 @@ EOF
 	_, err = utils.DockerStart(
 		ctx,
 		container.Config{
-			Image:        utils.EdgeRuntimeImage,
+			Image:        utils.Config.EdgeRuntime.Image,
 			Env:          env,
 			Entrypoint:   entrypoint,
 			ExposedPorts: exposedPorts,
@@ -221,7 +221,7 @@ func populatePerFunctionConfigs(importMapPath string, noVerifyJWT *bool, fsys af
 	}
 
 	binds := []string{}
-	functionsConfig := make(map[string]interface{}, len(slugs))
+	functionsConfig := make(map[string]utils.FunctionConfig, len(slugs))
 	for _, functionName := range slugs {
 		fc := utils.GetFunctionConfig(functionName, importMapPath, noVerifyJWT, fsys)
 		if fc.ImportMap != "" {
diff --git a/internal/inspect/bloat/bloat.go b/internal/inspect/bloat/bloat.go
index 13d31dd0..6a97e41c 100644
--- a/internal/inspect/bloat/bloat.go
+++ b/internal/inspect/bloat/bloat.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed bloat.sql
diff --git a/internal/inspect/blocking/blocking.go b/internal/inspect/blocking/blocking.go
index 77870f94..37baed1f 100644
--- a/internal/inspect/blocking/blocking.go
+++ b/internal/inspect/blocking/blocking.go
@@ -12,7 +12,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed blocking.sql
diff --git a/internal/inspect/cache/cache.go b/internal/inspect/cache/cache.go
index 4b775515..ce30c1f4 100644
--- a/internal/inspect/cache/cache.go
+++ b/internal/inspect/cache/cache.go
@@ -11,7 +11,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed cache.sql
diff --git a/internal/inspect/calls/calls.go b/internal/inspect/calls/calls.go
index 94bd8eaf..2fd0c8df 100644
--- a/internal/inspect/calls/calls.go
+++ b/internal/inspect/calls/calls.go
@@ -12,7 +12,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed calls.sql
diff --git a/internal/inspect/index_sizes/index_sizes.go b/internal/inspect/index_sizes/index_sizes.go
index c2fad6c6..2cfc06e8 100644
--- a/internal/inspect/index_sizes/index_sizes.go
+++ b/internal/inspect/index_sizes/index_sizes.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed index_sizes.sql
diff --git a/internal/inspect/index_usage/index_usage.go b/internal/inspect/index_usage/index_usage.go
index b2165f34..cd8875f7 100644
--- a/internal/inspect/index_usage/index_usage.go
+++ b/internal/inspect/index_usage/index_usage.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed index_usage.sql
diff --git a/internal/inspect/locks/locks.go b/internal/inspect/locks/locks.go
index f7d18f0c..0b2db71c 100644
--- a/internal/inspect/locks/locks.go
+++ b/internal/inspect/locks/locks.go
@@ -12,7 +12,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed locks.sql
diff --git a/internal/inspect/long_running_queries/long_running_queries.go b/internal/inspect/long_running_queries/long_running_queries.go
index 4e7ce6f8..acf4be45 100644
--- a/internal/inspect/long_running_queries/long_running_queries.go
+++ b/internal/inspect/long_running_queries/long_running_queries.go
@@ -11,7 +11,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed long_running_queries.sql
diff --git a/internal/inspect/outliers/outliers.go b/internal/inspect/outliers/outliers.go
index a487c9fb..06f015c1 100644
--- a/internal/inspect/outliers/outliers.go
+++ b/internal/inspect/outliers/outliers.go
@@ -12,7 +12,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed outliers.sql
diff --git a/internal/inspect/replication_slots/replication_slots.go b/internal/inspect/replication_slots/replication_slots.go
index 4ad5db57..927ad052 100644
--- a/internal/inspect/replication_slots/replication_slots.go
+++ b/internal/inspect/replication_slots/replication_slots.go
@@ -11,7 +11,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed replication_slots.sql
diff --git a/internal/inspect/role_connections/role_connections.go b/internal/inspect/role_connections/role_connections.go
index 25a7ff32..5b0a5653 100644
--- a/internal/inspect/role_connections/role_connections.go
+++ b/internal/inspect/role_connections/role_connections.go
@@ -11,7 +11,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed role_connections.sql
diff --git a/internal/inspect/seq_scans/seq_scans.go b/internal/inspect/seq_scans/seq_scans.go
index 04911166..6b52538e 100644
--- a/internal/inspect/seq_scans/seq_scans.go
+++ b/internal/inspect/seq_scans/seq_scans.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed seq_scans.sql
diff --git a/internal/inspect/table_index_sizes/table_index_sizes.go b/internal/inspect/table_index_sizes/table_index_sizes.go
index 420f3fdf..e61f2336 100644
--- a/internal/inspect/table_index_sizes/table_index_sizes.go
+++ b/internal/inspect/table_index_sizes/table_index_sizes.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed table_index_sizes.sql
diff --git a/internal/inspect/table_record_counts/table_record_counts.go b/internal/inspect/table_record_counts/table_record_counts.go
index 7d65b918..e0b39437 100644
--- a/internal/inspect/table_record_counts/table_record_counts.go
+++ b/internal/inspect/table_record_counts/table_record_counts.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed table_record_counts.sql
diff --git a/internal/inspect/table_sizes/table_sizes.go b/internal/inspect/table_sizes/table_sizes.go
index 651b3964..7741f011 100644
--- a/internal/inspect/table_sizes/table_sizes.go
+++ b/internal/inspect/table_sizes/table_sizes.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed table_sizes.sql
diff --git a/internal/inspect/total_index_size/total_index_size.go b/internal/inspect/total_index_size/total_index_size.go
index b5b1b61b..fbc66b25 100644
--- a/internal/inspect/total_index_size/total_index_size.go
+++ b/internal/inspect/total_index_size/total_index_size.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed total_index_size.sql
diff --git a/internal/inspect/total_table_sizes/total_table_sizes.go b/internal/inspect/total_table_sizes/total_table_sizes.go
index 2a11f704..80b1c89a 100644
--- a/internal/inspect/total_table_sizes/total_table_sizes.go
+++ b/internal/inspect/total_table_sizes/total_table_sizes.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed total_table_sizes.sql
diff --git a/internal/inspect/unused_indexes/unused_indexes.go b/internal/inspect/unused_indexes/unused_indexes.go
index cf429ed5..2a30a46d 100644
--- a/internal/inspect/unused_indexes/unused_indexes.go
+++ b/internal/inspect/unused_indexes/unused_indexes.go
@@ -12,7 +12,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed unused_indexes.sql
diff --git a/internal/inspect/vacuum_stats/vacuum_stats.go b/internal/inspect/vacuum_stats/vacuum_stats.go
index f8d2c222..dc9326d7 100644
--- a/internal/inspect/vacuum_stats/vacuum_stats.go
+++ b/internal/inspect/vacuum_stats/vacuum_stats.go
@@ -13,7 +13,7 @@ import (
 	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 //go:embed vacuum_stats.sql
diff --git a/internal/link/link.go b/internal/link/link.go
index eb322d9b..2b6e0ba4 100644
--- a/internal/link/link.go
+++ b/internal/link/link.go
@@ -21,6 +21,7 @@ import (
 	"github.com/supabase/cli/internal/utils/flags"
 	"github.com/supabase/cli/internal/utils/tenant"
 	"github.com/supabase/cli/pkg/api"
+	cliConfig "github.com/supabase/cli/pkg/config"
 )
 
 var updatedConfig ConfigCopy
@@ -232,7 +233,7 @@ func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error {
 func updatePoolerConfig(config api.V1PgbouncerConfigResponse) {
 	copy := utils.Config.Db.Pooler
 	if config.PoolMode != nil {
-		copy.PoolMode = utils.PoolMode(*config.PoolMode)
+		copy.PoolMode = cliConfig.PoolMode(*config.PoolMode)
 	}
 	if config.DefaultPoolSize != nil {
 		copy.DefaultPoolSize = uint(*config.DefaultPoolSize)
diff --git a/internal/migration/history/history.go b/internal/migration/history/history.go
index 0a2f2234..f625dd38 100644
--- a/internal/migration/history/history.go
+++ b/internal/migration/history/history.go
@@ -6,7 +6,7 @@ import (
 	"github.com/go-errors/errors"
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 const (
diff --git a/internal/migration/list/list.go b/internal/migration/list/list.go
index 445239bc..08c0c65b 100644
--- a/internal/migration/list/list.go
+++ b/internal/migration/list/list.go
@@ -15,7 +15,7 @@ import (
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/pgxv5"
+	"github.com/supabase/cli/pkg/pgxv5"
 )
 
 const LIST_MIGRATION_VERSION = "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version"
diff --git a/internal/migration/repair/repair.go b/internal/migration/repair/repair.go
index 60425068..5b953064 100644
--- a/internal/migration/repair/repair.go
+++ b/internal/migration/repair/repair.go
@@ -17,7 +17,7 @@ import (
 	"github.com/supabase/cli/internal/migration/history"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/parser"
+	"github.com/supabase/cli/pkg/parser"
 )
 
 const (
diff --git a/internal/services/services.go b/internal/services/services.go
index c98532b7..1e6dc531 100644
--- a/internal/services/services.go
+++ b/internal/services/services.go
@@ -48,12 +48,11 @@ func GetServiceImages() []string {
 		utils.Config.Api.Image,
 		utils.Config.Realtime.Image,
 		utils.Config.Storage.Image,
-		utils.EdgeRuntimeImage,
+		utils.Config.EdgeRuntime.Image,
 		utils.Config.Studio.Image,
 		utils.Config.Studio.PgmetaImage,
-		utils.LogflareImage,
+		utils.Config.Analytics.Image,
 		utils.Config.Db.Pooler.Image,
-		utils.ImageProxyImage,
 	}
 }
 
diff --git a/internal/start/start.go b/internal/start/start.go
index 4512ea22..166d5e3b 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -28,6 +28,7 @@ import (
 	"github.com/supabase/cli/internal/status"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/flags"
+	"github.com/supabase/cli/pkg/config"
 )
 
 func suggestUpdateCmd(serviceImages map[string]string) string {
@@ -143,7 +144,7 @@ type poolerTenant struct {
 	DbDatabase        string
 	DbPassword        string
 	ExternalId        string
-	ModeType          utils.PoolMode
+	ModeType          config.PoolMode
 	DefaultMaxClients uint
 	DefaultPoolSize   uint
 }
@@ -174,7 +175,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 	p.Send(utils.StatusMsg("Starting containers..."))
 
 	// Start Logflare
-	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.LogflareImage, excluded) {
+	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) {
 		env := []string{
 			"DB_DATABASE=" + dbConfig.Database,
 			"DB_HOSTNAME=" + dbConfig.Host,
@@ -194,7 +195,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 		bind := []string{}
 
 		switch utils.Config.Analytics.Backend {
-		case utils.LogflareBigQuery:
+		case config.LogflareBigQuery:
 			workdir, err := os.Getwd()
 			if err != nil {
 				return errors.Errorf("failed to get working directory: %w", err)
@@ -207,7 +208,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 				"GOOGLE_PROJECT_ID="+utils.Config.Analytics.GcpProjectId,
 				"GOOGLE_PROJECT_NUMBER="+utils.Config.Analytics.GcpProjectNumber,
 			)
-		case utils.LogflarePostgres:
+		case config.LogflarePostgres:
 			env = append(env,
 				fmt.Sprintf("POSTGRES_BACKEND_URL=postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
 				"POSTGRES_BACKEND_SCHEMA=_analytics",
@@ -218,7 +219,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 			ctx,
 			container.Config{
 				Hostname: "127.0.0.1",
-				Image:    utils.LogflareImage,
+				Image:    utils.Config.Analytics.Image,
 				Env:      env,
 				// Original entrypoint conflicts with healthcheck due to 15 seconds sleep:
 				// https://github.com/Logflare/logflare/blob/staging/run.sh#L35
@@ -258,9 +259,9 @@ EOF
 	}
 
 	// Start vector
-	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.VectorImage, excluded) {
+	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.VectorImage, excluded) {
 		var vectorConfigBuf bytes.Buffer
-		if err := vectorConfigTemplate.Execute(&vectorConfigBuf, vectorConfig{
+		if err := vectorConfigTemplate.Option("missingkey=error").Execute(&vectorConfigBuf, vectorConfig{
 			ApiKey:        utils.Config.Analytics.ApiKey,
 			VectorId:      utils.VectorId,
 			LogflareId:    utils.LogflareId,
@@ -291,7 +292,7 @@ EOF
 		if _, err := utils.DockerStart(
 			ctx,
 			container.Config{
-				Image: utils.VectorImage,
+				Image: utils.Config.Analytics.VectorImage,
 				Env:   env,
 				Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/vector/vector.yaml && vector
 ` + vectorConfigBuf.String() + `
@@ -325,9 +326,9 @@ EOF
 	}
 
 	// Start Kong.
-	if !isContainerExcluded(utils.KongImage, excluded) {
+	if !isContainerExcluded(utils.Config.Api.KongImage, excluded) {
 		var kongConfigBuf bytes.Buffer
-		if err := kongConfigTemplate.Execute(&kongConfigBuf, kongConfig{
+		if err := kongConfigTemplate.Option("missingkey=error").Execute(&kongConfigBuf, kongConfig{
 			GotrueId:      utils.GotrueId,
 			RestId:        utils.RestId,
 			RealtimeId:    utils.Config.Realtime.TenantId,
@@ -365,7 +366,7 @@ EOF
 		if _, err := utils.DockerStart(
 			ctx,
 			container.Config{
-				Image: utils.KongImage,
+				Image: utils.Config.Api.KongImage,
 				Env: []string{
 					"KONG_DATABASE=off",
 					"KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml",
@@ -395,6 +396,11 @@ EOF
 ` + status.KongKey + `
 EOF
 `},
+				ExposedPorts: nat.PortSet{
+					"8000/tcp": {},
+					"8443/tcp": {},
+					nat.Port(fmt.Sprintf("%d/tcp", nginxTemplateServerPort)): {},
+				},
 			},
 			container.HostConfig{
 				Binds: binds,
@@ -425,7 +431,7 @@ EOF
 		}
 
 		env := []string{
-			"API_EXTERNAL_URL=" + utils.GetApiUrl(""),
+			"API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
 
 			"GOTRUE_API_HOST=0.0.0.0",
 			"GOTRUE_API_PORT=9999",
@@ -649,7 +655,7 @@ EOF
 	}
 
 	// Start Inbucket.
-	if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.InbucketImage, excluded) {
+	if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.Config.Inbucket.Image, excluded) {
 		inbucketPortBindings := nat.PortMap{"9000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Port), 10)}}}
 		if utils.Config.Inbucket.SmtpPort != 0 {
 			inbucketPortBindings["2500/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.SmtpPort), 10)}}
@@ -660,7 +666,7 @@ EOF
 		if _, err := utils.DockerStart(
 			ctx,
 			container.Config{
-				Image: utils.InbucketImage,
+				Image: utils.Config.Inbucket.Image,
 			},
 			container.HostConfig{
 				Binds: []string{
@@ -832,11 +838,11 @@ EOF
 	}
 
 	// Start Storage ImgProxy.
-	if utils.Config.Storage.Enabled && utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.ImageProxyImage, excluded) {
+	if utils.Config.Storage.Enabled && utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImageTransformation.Image, excluded) {
 		if _, err := utils.DockerStart(
 			ctx,
 			container.Config{
-				Image: utils.ImageProxyImage,
+				Image: utils.Config.Storage.ImageTransformation.Image,
 				Env: []string{
 					"IMGPROXY_BIND=:5001",
 					"IMGPROXY_LOCAL_FILESYSTEM_ROOT=/",
@@ -868,7 +874,7 @@ EOF
 	}
 
 	// Start all functions.
-	if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.EdgeRuntimeImage, excluded) {
+	if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.Config.EdgeRuntime.Image, excluded) {
 		dbUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)
 		if err := serve.ServeFunctions(ctx, "", nil, "", dbUrl, serve.RuntimeOption{}, fsys); err != nil {
 			return err
@@ -966,12 +972,12 @@ EOF
 		portSession := uint16(5432)
 		portTransaction := uint16(6543)
 		dockerPort := portTransaction
-		if utils.Config.Db.Pooler.PoolMode == utils.SessionMode {
+		if utils.Config.Db.Pooler.PoolMode == config.SessionMode {
 			dockerPort = portSession
 		}
 		// Create pooler tenant
 		var poolerTenantBuf bytes.Buffer
-		if err := poolerTenantTemplate.Execute(&poolerTenantBuf, poolerTenant{
+		if err := poolerTenantTemplate.Option("missingkey=error").Execute(&poolerTenantBuf, poolerTenant{
 			DbHost:            dbConfig.Host,
 			DbPort:            dbConfig.Port,
 			DbDatabase:        dbConfig.Database,
@@ -1059,7 +1065,7 @@ func isContainerExcluded(imageName string, excluded map[string]bool) bool {
 
 func ExcludableContainers() []string {
 	names := []string{}
-	for _, image := range utils.ServiceImages {
+	for _, image := range config.ServiceImages {
 		names = append(names, utils.ShortContainerImageName(image))
 	}
 	return names
diff --git a/internal/status/status.go b/internal/status/status.go
index 07017661..21179ce0 100644
--- a/internal/status/status.go
+++ b/internal/status/status.go
@@ -41,7 +41,7 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
 		c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port),
 	}
 	if utils.Config.Api.Enabled && !utils.SliceContains(exclude, utils.RestId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) {
-		values[c.ApiURL] = utils.GetApiUrl("")
+		values[c.ApiURL] = utils.Config.Api.ExternalUrl
 		values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1")
 	}
 	if utils.Config.Studio.Enabled && !utils.SliceContains(exclude, utils.StudioId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) {
@@ -52,7 +52,7 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
 		values[c.AnonKey] = utils.Config.Auth.AnonKey
 		values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey
 	}
-	if utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.InbucketImage)) {
+	if utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) {
 		values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port)
 	}
 	if utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) {
@@ -174,7 +174,7 @@ var (
 
 func checkHTTPHead(ctx context.Context, path string) error {
 	healthOnce.Do(func() {
-		server := utils.GetApiUrl("")
+		server := utils.Config.Api.ExternalUrl
 		header := func(req *http.Request) {
 			req.Header.Add("apikey", utils.Config.Auth.AnonKey)
 		}
diff --git a/internal/storage/client/api.go b/internal/storage/client/api.go
index e6459bca..48e77fdd 100644
--- a/internal/storage/client/api.go
+++ b/internal/storage/client/api.go
@@ -29,7 +29,7 @@ func NewStorageAPI(ctx context.Context, projectRef string) (storage.StorageAPI,
 func newLocalClient() *fetcher.Fetcher {
 	client := status.NewKongClient()
 	return fetcher.NewFetcher(
-		utils.GetApiUrl(""),
+		utils.Config.Api.ExternalUrl,
 		fetcher.WithHTTPClient(client),
 		fetcher.WithBearerToken(utils.Config.Auth.ServiceRoleKey),
 		fetcher.WithUserAgent("SupabaseCLI/"+utils.Version),
diff --git a/internal/utils/config.go b/internal/utils/config.go
index 9f98e728..e7fb6dd5 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -1,27 +1,17 @@
 package utils
 
 import (
-	"bytes"
 	_ "embed"
 	"fmt"
 	"net"
 	"net/url"
 	"os"
-	"path/filepath"
-	"regexp"
 	"strconv"
-	"strings"
-	"text/template"
-	"time"
 
-	"github.com/BurntSushi/toml"
-	"github.com/docker/go-units"
 	"github.com/go-errors/errors"
-	"github.com/golang-jwt/jwt/v5"
-	"github.com/joho/godotenv"
 	"github.com/spf13/afero"
 	"github.com/spf13/viper"
-	"golang.org/x/mod/semver"
+	"github.com/supabase/cli/pkg/config"
 )
 
 var (
@@ -58,17 +48,10 @@ var (
 	VectorAliases      = []string{"vector"}
 	PoolerAliases      = []string{"pooler"}
 
-	InitialSchemaSql string
 	//go:embed templates/initial_schemas/13.sql
 	InitialSchemaPg13Sql string
 	//go:embed templates/initial_schemas/14.sql
 	InitialSchemaPg14Sql string
-
-	//go:embed templates/init_config.toml
-	initConfigEmbed    string
-	initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed))
-	invalidProjectId   = regexp.MustCompile("[^a-zA-Z0-9_.-]+")
-	envPattern         = regexp.MustCompile(`^env\((.*)\)$`)
 )
 
 func GetId(name string) string {
@@ -115,824 +98,24 @@ func GetDockerIds() []string {
 	}
 }
 
-// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
-type sizeInBytes int64
-
-func (s *sizeInBytes) UnmarshalText(text []byte) error {
-	size, err := units.RAMInBytes(string(text))
-	if err == nil {
-		*s = sizeInBytes(size)
-	}
-	return err
-}
-
-func (s sizeInBytes) MarshalText() (text []byte, err error) {
-	return []byte(units.BytesSize(float64(s))), nil
-}
-
-type LogflareBackend string
-
-const (
-	LogflarePostgres LogflareBackend = "postgres"
-	LogflareBigQuery LogflareBackend = "bigquery"
-)
-
-type PoolMode string
-
-const (
-	TransactionMode PoolMode = "transaction"
-	SessionMode     PoolMode = "session"
-)
-
-type AddressFamily string
-
-const (
-	AddressIPv6 AddressFamily = "IPv6"
-	AddressIPv4 AddressFamily = "IPv4"
-)
-
-func ToRealtimeEnv(addr AddressFamily) string {
-	if addr == AddressIPv6 {
-		return "-proto_dist inet6_tcp"
-	}
-	return "-proto_dist inet_tcp"
-}
-
-type CustomClaims struct {
-	// Overrides Issuer to maintain json order when marshalling
-	Issuer string `json:"iss,omitempty"`
-	Ref    string `json:"ref,omitempty"`
-	Role   string `json:"role"`
-	jwt.RegisteredClaims
-}
-
-const (
-	defaultJwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long"
-	defaultJwtExpiry = 1983812996
-)
-
-func (c CustomClaims) NewToken() *jwt.Token {
-	if c.ExpiresAt == nil {
-		c.ExpiresAt = jwt.NewNumericDate(time.Unix(defaultJwtExpiry, 0))
-	}
-	if len(c.Issuer) == 0 {
-		c.Issuer = "supabase-demo"
-	}
-	return jwt.NewWithClaims(jwt.SigningMethodHS256, c)
-}
-
-type RequestPolicy string
-
-const (
-	PolicyPerWorker RequestPolicy = "per_worker"
-	PolicyOneshot   RequestPolicy = "oneshot"
-)
-
-var Config = config{
-	Hostname: GetHostname(),
-	Api: api{
-		Image: PostgrestImage,
-	},
-	Db: db{
-		Image:    Pg15Image,
-		Password: "postgres",
-		RootKey:  "d4dc5b6d4a1d6a10b2c1e76112c994d65db7cec380572cc1839624d4be3fa275",
-		Pooler: pooler{
-			Image:         SupavisorImage,
-			TenantId:      "pooler-dev",
-			EncryptionKey: "12345678901234567890123456789032",
-			SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
-		},
-	},
-	Realtime: realtime{
-		Image:           RealtimeImage,
-		IpVersion:       AddressIPv4,
-		MaxHeaderLength: 4096,
-		TenantId:        "realtime-dev",
-		EncryptionKey:   "supabaserealtime",
-		SecretKeyBase:   "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
-	},
-	Storage: storage{
-		Image: StorageImage,
-		S3Credentials: storageS3Credentials{
-			AccessKeyId:     "625729a08b95bf1b7ff351a663f3a23c",
-			SecretAccessKey: "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907",
-			Region:          "local",
-		},
-		ImageTransformation: imageTransformation{
-			Enabled: true,
-		},
-	},
-	Auth: auth{
-		Image: GotrueImage,
-		Email: email{
-			Template: map[string]emailTemplate{
-				"invite":       {},
-				"confirmation": {},
-				"recovery":     {},
-				"magic_link":   {},
-				"email_change": {},
-			},
-			Smtp: smtp{
-				Host:       InbucketAliases[0],
-				Port:       2500,
-				AdminEmail: "admin@email.com",
-			},
-		},
-		External: map[string]provider{
-			"apple":         {},
-			"azure":         {},
-			"bitbucket":     {},
-			"discord":       {},
-			"facebook":      {},
-			"github":        {},
-			"gitlab":        {},
-			"google":        {},
-			"keycloak":      {},
-			"linkedin":      {}, // TODO: remove this field in v2
-			"linkedin_oidc": {},
-			"notion":        {},
-			"twitch":        {},
-			"twitter":       {},
-			"slack":         {},
-			"spotify":       {},
-			"workos":        {},
-			"zoom":          {},
-		},
-		JwtSecret: defaultJwtSecret,
-	},
-	Studio: studio{
-		Image:       StudioImage,
-		PgmetaImage: PgmetaImage,
-	},
-	Analytics: analytics{
-		ApiKey: "api-key",
-		// Defaults to bigquery for backwards compatibility with existing config.toml
-		Backend: LogflareBigQuery,
-	},
-}
-
-// We follow these rules when adding new config:
-//  1. Update init_config.toml (and init_config.test.toml) with the new key, default value, and comments to explain usage.
-//  2. Update config struct with new field and toml tag (spelled in snake_case).
-//  3. Add custom field validations to LoadConfigFS function for eg. integer range checks.
-//
-// If you are adding new user defined secrets, such as OAuth provider secret, the default value in
-// init_config.toml should be an env var substitution. For example,
-//
-// > secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
-//
-// If you are adding an internal config or secret that doesn't need to be overridden by the user,
-// exclude the field from toml serialization. For example,
-//
-//	type auth struct {
-//		AnonKey string `toml:"-" mapstructure:"anon_key"`
-//	}
-//
-// Use `mapstructure:"anon_key"` tag only if you want inject values from a predictable environment
-// variable, such as SUPABASE_AUTH_ANON_KEY.
-//
-// Default values for internal configs should be added to `var Config` initializer.
-type (
-	config struct {
-		ProjectId    string              `toml:"project_id"`
-		Hostname     string              `toml:"-"`
-		Api          api                 `toml:"api"`
-		Db           db                  `toml:"db" mapstructure:"db"`
-		Realtime     realtime            `toml:"realtime"`
-		Studio       studio              `toml:"studio"`
-		Inbucket     inbucket            `toml:"inbucket"`
-		Storage      storage             `toml:"storage"`
-		Auth         auth                `toml:"auth" mapstructure:"auth"`
-		EdgeRuntime  edgeRuntime         `toml:"edge_runtime"`
-		Functions    map[string]function `toml:"functions"`
-		Analytics    analytics           `toml:"analytics"`
-		Experimental experimental        `toml:"experimental" mapstructure:"-"`
-		// TODO
-		// Scripts   scripts
-	}
-
-	api struct {
-		Enabled         bool     `toml:"enabled"`
-		Image           string   `toml:"-"`
-		Port            uint16   `toml:"port"`
-		Schemas         []string `toml:"schemas"`
-		ExtraSearchPath []string `toml:"extra_search_path"`
-		MaxRows         uint     `toml:"max_rows"`
-		Tls             tlsKong  `toml:"tls"`
-	}
-
-	tlsKong struct {
-		Enabled bool `toml:"enabled"`
-	}
-
-	db struct {
-		Image        string `toml:"-"`
-		Port         uint16 `toml:"port"`
-		ShadowPort   uint16 `toml:"shadow_port"`
-		MajorVersion uint   `toml:"major_version"`
-		Password     string `toml:"-"`
-		RootKey      string `toml:"-" mapstructure:"root_key"`
-		Pooler       pooler `toml:"pooler"`
-	}
-
-	pooler struct {
-		Enabled          bool     `toml:"enabled"`
-		Image            string   `toml:"-"`
-		Port             uint16   `toml:"port"`
-		PoolMode         PoolMode `toml:"pool_mode"`
-		DefaultPoolSize  uint     `toml:"default_pool_size"`
-		MaxClientConn    uint     `toml:"max_client_conn"`
-		ConnectionString string   `toml:"-"`
-		TenantId         string   `toml:"-"`
-		EncryptionKey    string   `toml:"-"`
-		SecretKeyBase    string   `toml:"-"`
-	}
-
-	realtime struct {
-		Enabled         bool          `toml:"enabled"`
-		Image           string        `toml:"-"`
-		IpVersion       AddressFamily `toml:"ip_version"`
-		MaxHeaderLength uint          `toml:"max_header_length"`
-		TenantId        string        `toml:"-"`
-		EncryptionKey   string        `toml:"-"`
-		SecretKeyBase   string        `toml:"-"`
-	}
-
-	studio struct {
-		Enabled      bool   `toml:"enabled"`
-		Image        string `toml:"-"`
-		Port         uint16 `toml:"port"`
-		ApiUrl       string `toml:"api_url"`
-		OpenaiApiKey string `toml:"openai_api_key"`
-		PgmetaImage  string `toml:"-"`
-	}
-
-	inbucket struct {
-		Enabled  bool   `toml:"enabled"`
-		Port     uint16 `toml:"port"`
-		SmtpPort uint16 `toml:"smtp_port"`
-		Pop3Port uint16 `toml:"pop3_port"`
-	}
-
-	storage struct {
-		Enabled             bool                 `toml:"enabled"`
-		Image               string               `toml:"-"`
-		FileSizeLimit       sizeInBytes          `toml:"file_size_limit"`
-		S3Credentials       storageS3Credentials `toml:"-"`
-		ImageTransformation imageTransformation  `toml:"image_transformation"`
-		Buckets             map[string]bucket    `toml:"buckets"`
-	}
-
-	imageTransformation struct {
-		Enabled bool `toml:"enabled"`
-	}
-
-	bucket struct {
-		Public           bool        `toml:"public"`
-		FileSizeLimit    sizeInBytes `toml:"file_size_limit"`
-		AllowedMimeTypes []string    `toml:"allowed_mime_types"`
-	}
-
-	storageS3Credentials struct {
-		AccessKeyId     string `toml:"-"`
-		SecretAccessKey string `toml:"-"`
-		Region          string `toml:"-"`
-	}
-
-	auth struct {
-		Enabled                bool     `toml:"enabled"`
-		Image                  string   `toml:"-"`
-		SiteUrl                string   `toml:"site_url"`
-		AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
-
-		JwtExpiry                  uint `toml:"jwt_expiry"`
-		EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
-		RefreshTokenReuseInterval  uint `toml:"refresh_token_reuse_interval"`
-		EnableManualLinking        bool `toml:"enable_manual_linking"`
-
-		Hook     hook     `toml:"hook"`
-		Sessions sessions `toml:"sessions"`
-
-		EnableSignup           bool  `toml:"enable_signup"`
-		EnableAnonymousSignIns bool  `toml:"enable_anonymous_sign_ins"`
-		Email                  email `toml:"email"`
-		Sms                    sms   `toml:"sms"`
-		External               map[string]provider
-
-		// Custom secrets can be injected from .env file
-		JwtSecret      string `toml:"-" mapstructure:"jwt_secret"`
-		AnonKey        string `toml:"-" mapstructure:"anon_key"`
-		ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
-	}
-
-	email struct {
-		EnableSignup         bool                     `toml:"enable_signup"`
-		DoubleConfirmChanges bool                     `toml:"double_confirm_changes"`
-		EnableConfirmations  bool                     `toml:"enable_confirmations"`
-		Template             map[string]emailTemplate `toml:"template"`
-		Smtp                 smtp                     `toml:"smtp"`
-		MaxFrequency         time.Duration            `toml:"max_frequency"`
-	}
-
-	smtp struct {
-		Host       string `toml:"host"`
-		Port       uint16 `toml:"port"`
-		User       string `toml:"user"`
-		Pass       string `toml:"pass"`
-		AdminEmail string `toml:"admin_email"`
-		SenderName string `toml:"sender_name"`
-	}
-
-	emailTemplate struct {
-		Subject     string `toml:"subject"`
-		ContentPath string `toml:"content_path"`
-	}
-
-	sms struct {
-		EnableSignup        bool              `toml:"enable_signup"`
-		EnableConfirmations bool              `toml:"enable_confirmations"`
-		Template            string            `toml:"template"`
-		Twilio              twilioConfig      `toml:"twilio" mapstructure:"twilio"`
-		TwilioVerify        twilioConfig      `toml:"twilio_verify" mapstructure:"twilio_verify"`
-		Messagebird         messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"`
-		Textlocal           textlocalConfig   `toml:"textlocal" mapstructure:"textlocal"`
-		Vonage              vonageConfig      `toml:"vonage" mapstructure:"vonage"`
-		TestOTP             map[string]string `toml:"test_otp"`
-		MaxFrequency        time.Duration     `toml:"max_frequency"`
-	}
-
-	hook struct {
-		MFAVerificationAttempt      hookConfig `toml:"mfa_verification_attempt"`
-		PasswordVerificationAttempt hookConfig `toml:"password_verification_attempt"`
-		CustomAccessToken           hookConfig `toml:"custom_access_token"`
-		SendSMS                     hookConfig `toml:"send_sms"`
-		SendEmail                   hookConfig `toml:"send_email"`
-	}
-
-	hookConfig struct {
-		Enabled bool   `toml:"enabled"`
-		URI     string `toml:"uri"`
-		Secrets string `toml:"secrets"`
-	}
-
-	sessions struct {
-		Timebox           time.Duration `toml:"timebox"`
-		InactivityTimeout time.Duration `toml:"inactivity_timeout"`
-	}
-
-	twilioConfig struct {
-		Enabled           bool   `toml:"enabled"`
-		AccountSid        string `toml:"account_sid"`
-		MessageServiceSid string `toml:"message_service_sid"`
-		AuthToken         string `toml:"auth_token" mapstructure:"auth_token"`
-	}
-
-	messagebirdConfig struct {
-		Enabled    bool   `toml:"enabled"`
-		Originator string `toml:"originator"`
-		AccessKey  string `toml:"access_key" mapstructure:"access_key"`
-	}
-
-	textlocalConfig struct {
-		Enabled bool   `toml:"enabled"`
-		Sender  string `toml:"sender"`
-		ApiKey  string `toml:"api_key" mapstructure:"api_key"`
-	}
-
-	vonageConfig struct {
-		Enabled   bool   `toml:"enabled"`
-		From      string `toml:"from"`
-		ApiKey    string `toml:"api_key" mapstructure:"api_key"`
-		ApiSecret string `toml:"api_secret" mapstructure:"api_secret"`
-	}
-
-	provider struct {
-		Enabled        bool   `toml:"enabled"`
-		ClientId       string `toml:"client_id"`
-		Secret         string `toml:"secret"`
-		Url            string `toml:"url"`
-		RedirectUri    string `toml:"redirect_uri"`
-		SkipNonceCheck bool   `toml:"skip_nonce_check"`
-	}
-
-	edgeRuntime struct {
-		Enabled       bool          `toml:"enabled"`
-		Policy        RequestPolicy `toml:"policy"`
-		InspectorPort uint16        `toml:"inspector_port"`
-	}
-
-	function struct {
-		VerifyJWT *bool  `toml:"verify_jwt" json:"verifyJWT"`
-		ImportMap string `toml:"import_map" json:"importMapPath,omitempty"`
-	}
-
-	analytics struct {
-		Enabled          bool            `toml:"enabled"`
-		Port             uint16          `toml:"port"`
-		Backend          LogflareBackend `toml:"backend"`
-		GcpProjectId     string          `toml:"gcp_project_id"`
-		GcpProjectNumber string          `toml:"gcp_project_number"`
-		GcpJwtPath       string          `toml:"gcp_jwt_path"`
-		ApiKey           string          `toml:"-" mapstructure:"api_key"`
-		// Deprecated together with syslog
-		VectorPort uint16 `toml:"vector_port"`
-	}
-
-	experimental struct {
-		OrioleDBVersion string `toml:"orioledb_version"`
-		S3Host          string `toml:"s3_host"`
-		S3Region        string `toml:"s3_region"`
-		S3AccessKey     string `toml:"s3_access_key"`
-		S3SecretKey     string `toml:"s3_secret_key"`
-	}
-
-	// TODO
-	// scripts struct {
-	// 	BeforeMigrations string `toml:"before_migrations"`
-	// 	AfterMigrations  string `toml:"after_migrations"`
-	// }
-)
-
-func (h *hookConfig) HandleHook(hookType string) error {
-	// If not enabled do nothing
-	if !h.Enabled {
-		return nil
-	}
-	if h.URI == "" {
-		return errors.Errorf("missing required field in config: auth.hook.%s.uri", hookType)
-	}
-	if err := validateHookURI(h.URI, hookType); err != nil {
-		return err
-	}
-	var err error
-	if h.Secrets, err = maybeLoadEnv(h.Secrets); err != nil {
-		return errors.Errorf("missing required field in config: auth.hook.%s.secrets", hookType)
-	}
-	return nil
-}
+var Config = config.NewConfig(config.WithHostname(GetHostname()))
 
 func LoadConfigFS(fsys afero.Fs) error {
-	// Load default values
-	var buf bytes.Buffer
-	if err := initConfigTemplate.Execute(&buf, nil); err != nil {
-		return errors.Errorf("failed to initialise config template: %w", err)
-	}
-	dec := toml.NewDecoder(&buf)
-	if _, err := dec.Decode(&Config); err != nil {
-		return errors.Errorf("failed to decode config template: %w", err)
-	}
-	// Load user defined config
-	if metadata, err := toml.DecodeFS(afero.NewIOFS(fsys), ConfigPath, &Config); err != nil {
-		CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
-		cwd, osErr := os.Getwd()
-		if osErr != nil {
-			cwd = "current directory"
+	if err := Config.Load("", afero.NewIOFS(fsys)); err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
 		}
-		return errors.Errorf("cannot read config in %s: %w", Bold(cwd), err)
-	} else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
-		fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
-	}
-	// Load secrets from .env file
-	if err := loadDefaultEnv(); err != nil {
 		return err
 	}
-	if err := viper.Unmarshal(&Config); err != nil {
-		return errors.Errorf("failed to parse env to config: %w", err)
-	}
-
-	// Generate JWT tokens
-	if len(Config.Auth.AnonKey) == 0 {
-		anonToken := CustomClaims{Role: "anon"}.NewToken()
-		if signed, err := anonToken.SignedString([]byte(Config.Auth.JwtSecret)); err != nil {
-			return errors.Errorf("failed to generate anon key: %w", err)
-		} else {
-			Config.Auth.AnonKey = signed
-		}
-	}
-	if len(Config.Auth.ServiceRoleKey) == 0 {
-		anonToken := CustomClaims{Role: "service_role"}.NewToken()
-		if signed, err := anonToken.SignedString([]byte(Config.Auth.JwtSecret)); err != nil {
-			return errors.Errorf("failed to generate service_role key: %w", err)
-		} else {
-			Config.Auth.ServiceRoleKey = signed
-		}
-	}
-
-	// Process decoded TOML.
-	{
-		if Config.ProjectId == "" {
-			return errors.New("Missing required field in config: project_id")
-		} else if sanitized := sanitizeProjectId(Config.ProjectId); sanitized != Config.ProjectId {
-			fmt.Fprintln(os.Stderr, Yellow("WARNING:"), "project_id field in config is invalid. Auto-fixing to", Aqua(sanitized))
-			Config.ProjectId = sanitized
-		}
-		UpdateDockerIds()
-		// Validate api config
-		if Config.Api.Port == 0 {
-			return errors.New("Missing required field in config: api.port")
-		}
-		if Config.Api.Enabled {
-			if version, err := afero.ReadFile(fsys, RestVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
-				Config.Api.Image = replaceImageTag(PostgrestImage, string(version))
-			}
-		}
-		// Append required schemas if they are missing
-		Config.Api.Schemas = RemoveDuplicates(append([]string{"public", "storage"}, Config.Api.Schemas...))
-		Config.Api.ExtraSearchPath = RemoveDuplicates(append([]string{"public"}, Config.Api.ExtraSearchPath...))
-		// Validate db config
-		if Config.Db.Port == 0 {
-			return errors.New("Missing required field in config: db.port")
-		}
-		switch Config.Db.MajorVersion {
-		case 0:
-			return errors.New("Missing required field in config: db.major_version")
-		case 12:
-			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.")
-		case 13:
-			Config.Db.Image = Pg13Image
-			InitialSchemaSql = InitialSchemaPg13Sql
-		case 14:
-			Config.Db.Image = Pg14Image
-			InitialSchemaSql = InitialSchemaPg14Sql
-		case 15:
-			if len(Config.Experimental.OrioleDBVersion) > 0 {
-				Config.Db.Image = "supabase/postgres:orioledb-" + Config.Experimental.OrioleDBVersion
-				var err error
-				if Config.Experimental.S3Host, err = maybeLoadEnv(Config.Experimental.S3Host); err != nil {
-					return err
-				}
-				if Config.Experimental.S3Region, err = maybeLoadEnv(Config.Experimental.S3Region); err != nil {
-					return err
-				}
-				if Config.Experimental.S3AccessKey, err = maybeLoadEnv(Config.Experimental.S3AccessKey); err != nil {
-					return err
-				}
-				if Config.Experimental.S3SecretKey, err = maybeLoadEnv(Config.Experimental.S3SecretKey); err != nil {
-					return err
-				}
-			} else if version, err := afero.ReadFile(fsys, PostgresVersionPath); err == nil {
-				if strings.HasPrefix(string(version), "15.") && semver.Compare(string(version[3:]), "1.0.55") >= 0 {
-					Config.Db.Image = replaceImageTag(Pg15Image, string(version))
-				}
-			}
-		default:
-			return errors.Errorf("Failed reading config: Invalid %s: %v.", Aqua("db.major_version"), Config.Db.MajorVersion)
-		}
-		// Validate pooler config
-		if Config.Db.Pooler.Enabled {
-			allowed := []PoolMode{TransactionMode, SessionMode}
-			if !SliceContains(allowed, Config.Db.Pooler.PoolMode) {
-				return errors.Errorf("Invalid config for db.pooler.pool_mode. Must be one of: %v", allowed)
-			}
-			if version, err := afero.ReadFile(fsys, PoolerVersionPath); err == nil && len(version) > 0 {
-				Config.Db.Pooler.Image = replaceImageTag(SupavisorImage, string(version))
-			}
-		}
-		if connString, err := afero.ReadFile(fsys, PoolerUrlPath); err == nil && len(connString) > 0 {
-			Config.Db.Pooler.ConnectionString = string(connString)
-		}
-		// Validate realtime config
-		if Config.Realtime.Enabled {
-			allowed := []AddressFamily{AddressIPv6, AddressIPv4}
-			if !SliceContains(allowed, Config.Realtime.IpVersion) {
-				return errors.Errorf("Invalid config for realtime.ip_version. Must be one of: %v", allowed)
-			}
-			if version, err := afero.ReadFile(fsys, RealtimeVersionPath); err == nil && len(version) > 0 {
-				Config.Realtime.Image = replaceImageTag(RealtimeImage, string(version))
-			}
-		}
-		// Validate storage config
-		if Config.Storage.Enabled {
-			if version, err := afero.ReadFile(fsys, StorageVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
-				Config.Storage.Image = replaceImageTag(StorageImage, string(version))
-			}
-		}
-		// Validate studio config
-		if Config.Studio.Enabled {
-			if Config.Studio.Port == 0 {
-				return errors.New("Missing required field in config: studio.port")
-			}
-			if parsed, err := url.Parse(Config.Studio.ApiUrl); err != nil {
-				return errors.Errorf("Invalid config for studio.api_url: %w", err)
-			} else if parsed.Host == "" || parsed.Host == Config.Hostname {
-				Config.Studio.ApiUrl = GetApiUrl("")
-			}
-			if version, err := afero.ReadFile(fsys, StudioVersionPath); err == nil && len(version) > 0 {
-				Config.Studio.Image = replaceImageTag(StudioImage, string(version))
-			}
-			if version, err := afero.ReadFile(fsys, PgmetaVersionPath); err == nil && len(version) > 0 {
-				Config.Studio.PgmetaImage = replaceImageTag(PgmetaImage, string(version))
-			}
-			Config.Studio.OpenaiApiKey, _ = maybeLoadEnv(Config.Studio.OpenaiApiKey)
-		}
-		// Validate email config
-		if Config.Inbucket.Enabled {
-			if Config.Inbucket.Port == 0 {
-				return errors.New("Missing required field in config: inbucket.port")
-			}
-		}
-
-		// Validate auth config
-		if Config.Auth.Enabled {
-			if Config.Auth.SiteUrl == "" {
-				return errors.New("Missing required field in config: auth.site_url")
-			}
-			var err error
-			if Config.Auth.SiteUrl, err = maybeLoadEnv(Config.Auth.SiteUrl); err != nil {
-				return err
-			}
-			if version, err := afero.ReadFile(fsys, GotrueVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
-				Config.Auth.Image = replaceImageTag(GotrueImage, string(version))
-			}
-			// Validate email template
-			for _, tmpl := range Config.Auth.Email.Template {
-				if len(tmpl.ContentPath) > 0 {
-					if _, err := fsys.Stat(tmpl.ContentPath); err != nil {
-						return errors.Errorf("failed to read file info: %w", err)
-					}
-				}
-			}
-			if Config.Auth.Email.Smtp.Pass, err = maybeLoadEnv(Config.Auth.Email.Smtp.Pass); err != nil {
-				return err
-			}
-			// Validate sms config
-			if Config.Auth.Sms.Twilio.Enabled {
-				if len(Config.Auth.Sms.Twilio.AccountSid) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio.account_sid")
-				}
-				if len(Config.Auth.Sms.Twilio.MessageServiceSid) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio.message_service_sid")
-				}
-				if len(Config.Auth.Sms.Twilio.AuthToken) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio.auth_token")
-				}
-				if Config.Auth.Sms.Twilio.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.Twilio.AuthToken); err != nil {
-					return err
-				}
-			}
-			if Config.Auth.Sms.TwilioVerify.Enabled {
-				if len(Config.Auth.Sms.TwilioVerify.AccountSid) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio_verify.account_sid")
-				}
-				if len(Config.Auth.Sms.TwilioVerify.MessageServiceSid) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio_verify.message_service_sid")
-				}
-				if len(Config.Auth.Sms.TwilioVerify.AuthToken) == 0 {
-					return errors.New("Missing required field in config: auth.sms.twilio_verify.auth_token")
-				}
-				if Config.Auth.Sms.TwilioVerify.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.TwilioVerify.AuthToken); err != nil {
-					return err
-				}
-			}
-			if Config.Auth.Sms.Messagebird.Enabled {
-				if len(Config.Auth.Sms.Messagebird.Originator) == 0 {
-					return errors.New("Missing required field in config: auth.sms.messagebird.originator")
-				}
-				if len(Config.Auth.Sms.Messagebird.AccessKey) == 0 {
-					return errors.New("Missing required field in config: auth.sms.messagebird.access_key")
-				}
-				if Config.Auth.Sms.Messagebird.AccessKey, err = maybeLoadEnv(Config.Auth.Sms.Messagebird.AccessKey); err != nil {
-					return err
-				}
-			}
-			if Config.Auth.Sms.Textlocal.Enabled {
-				if len(Config.Auth.Sms.Textlocal.Sender) == 0 {
-					return errors.New("Missing required field in config: auth.sms.textlocal.sender")
-				}
-				if len(Config.Auth.Sms.Textlocal.ApiKey) == 0 {
-					return errors.New("Missing required field in config: auth.sms.textlocal.api_key")
-				}
-				if Config.Auth.Sms.Textlocal.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Textlocal.ApiKey); err != nil {
-					return err
-				}
-			}
-			if Config.Auth.Sms.Vonage.Enabled {
-				if len(Config.Auth.Sms.Vonage.From) == 0 {
-					return errors.New("Missing required field in config: auth.sms.vonage.from")
-				}
-				if len(Config.Auth.Sms.Vonage.ApiKey) == 0 {
-					return errors.New("Missing required field in config: auth.sms.vonage.api_key")
-				}
-				if len(Config.Auth.Sms.Vonage.ApiSecret) == 0 {
-					return errors.New("Missing required field in config: auth.sms.vonage.api_secret")
-				}
-				if Config.Auth.Sms.Vonage.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiKey); err != nil {
-					return err
-				}
-				if Config.Auth.Sms.Vonage.ApiSecret, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiSecret); err != nil {
-					return err
-				}
-			}
-			if err := Config.Auth.Hook.MFAVerificationAttempt.HandleHook("mfa_verification_attempt"); err != nil {
-				return err
-			}
-			if err := Config.Auth.Hook.PasswordVerificationAttempt.HandleHook("password_verification_attempt"); err != nil {
-				return err
-			}
-			if err := Config.Auth.Hook.CustomAccessToken.HandleHook("custom_access_token"); err != nil {
-				return err
-			}
-			if err := Config.Auth.Hook.SendSMS.HandleHook("send_sms"); err != nil {
-				return err
-			}
-			if err := Config.Auth.Hook.SendEmail.HandleHook("send_email"); err != nil {
-				return err
-			}
-			// Validate oauth config
-			for ext, provider := range Config.Auth.External {
-				if !provider.Enabled {
-					continue
-				}
-				if provider.ClientId == "" {
-					return errors.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
-				}
-				if !SliceContains([]string{"apple", "google"}, ext) && provider.Secret == "" {
-					return errors.Errorf("Missing required field in config: auth.external.%s.secret", ext)
-				}
-				if provider.ClientId, err = maybeLoadEnv(provider.ClientId); err != nil {
-					return err
-				}
-				if provider.Secret, err = maybeLoadEnv(provider.Secret); err != nil {
-					return err
-				}
-				if provider.RedirectUri, err = maybeLoadEnv(provider.RedirectUri); err != nil {
-					return err
-				}
-				if provider.Url, err = maybeLoadEnv(provider.Url); err != nil {
-					return err
-				}
-				Config.Auth.External[ext] = provider
-			}
-		}
-	}
-	// Validate functions config
-	if Config.EdgeRuntime.Enabled {
-		allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot}
-		if !SliceContains(allowed, Config.EdgeRuntime.Policy) {
-			return errors.Errorf("Invalid config for edge_runtime.policy. Must be one of: %v", allowed)
-		}
-	}
-	for name, functionConfig := range Config.Functions {
-		if functionConfig.VerifyJWT == nil {
-			functionConfig.VerifyJWT = Ptr(true)
-			Config.Functions[name] = functionConfig
-		}
-	}
-	// Validate logflare config
-	if Config.Analytics.Enabled {
-		switch Config.Analytics.Backend {
-		case LogflareBigQuery:
-			if len(Config.Analytics.GcpProjectId) == 0 {
-				return errors.New("Missing required field in config: analytics.gcp_project_id")
-			}
-			if len(Config.Analytics.GcpProjectNumber) == 0 {
-				return errors.New("Missing required field in config: analytics.gcp_project_number")
-			}
-			if len(Config.Analytics.GcpJwtPath) == 0 {
-				return errors.New("Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path")
-			}
-		case LogflarePostgres:
-			break
-		default:
-			allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery}
-			return errors.Errorf("Invalid config for analytics.backend. Must be one of: %v", allowed)
-		}
-	}
+	UpdateDockerIds()
 	return nil
 }
 
-func maybeLoadEnv(s string) (string, error) {
-	matches := envPattern.FindStringSubmatch(s)
-	if len(matches) == 0 {
-		return s, nil
-	}
-
-	envName := matches[1]
-	if value := os.Getenv(envName); value != "" {
-		return value, nil
-	}
-
-	return "", errors.Errorf(`Error evaluating "%s": environment variable %s is unset.`, s, envName)
-}
-
-func truncateText(text string, maxLen int) string {
-	if len(text) > maxLen {
-		return text[:maxLen]
+func ToRealtimeEnv(addr config.AddressFamily) string {
+	if addr == config.AddressIPv6 {
+		return "-proto_dist inet6_tcp"
 	}
-	return text
-}
-
-const maxProjectIdLength = 40
-
-func sanitizeProjectId(src string) string {
-	// A valid project ID must only contain alphanumeric and special characters _.-
-	sanitized := invalidProjectId.ReplaceAllString(src, "_")
-	// It must also start with an alphanumeric character
-	sanitized = strings.TrimLeft(sanitized, "_.-")
-	// Truncate sanitized ID to 40 characters since docker hostnames cannot exceed
-	// 63 characters, and we need to save space for padding supabase_*_edge_runtime.
-	return truncateText(sanitized, maxProjectIdLength)
+	return "-proto_dist inet_tcp"
 }
 
 type InitParams struct {
@@ -942,17 +125,13 @@ type InitParams struct {
 }
 
 func InitConfig(params InitParams, fsys afero.Fs) error {
-	// Defaults to current directory name as project id
-	if len(params.ProjectId) == 0 {
-		cwd, err := os.Getwd()
-		if err != nil {
-			return errors.Errorf("failed to get working directory: %w", err)
-		}
-		params.ProjectId = filepath.Base(cwd)
+	c := config.NewConfig()
+	c.ProjectId = params.ProjectId
+	if params.UseOrioleDB {
+		c.Experimental.OrioleDBVersion = "15.1.0.150"
 	}
-	params.ProjectId = sanitizeProjectId(params.ProjectId)
 	// Create config file
-	if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
+	if err := MkdirIfNotExistFS(fsys, SupabaseDirPath); err != nil {
 		return err
 	}
 	flag := os.O_WRONLY | os.O_CREATE
@@ -966,73 +145,24 @@ func InitConfig(params InitParams, fsys afero.Fs) error {
 		return errors.Errorf("failed to create config file: %w", err)
 	}
 	defer f.Close()
-	// Update from template
-	if err := initConfigTemplate.Execute(f, params); err != nil {
-		return errors.Errorf("failed to initialise config: %w", err)
-	}
-	return nil
+	return c.Eject(f)
 }
 
 func WriteConfig(fsys afero.Fs, _test bool) error {
 	return InitConfig(InitParams{}, fsys)
 }
 
-func RemoveDuplicates(slice []string) (result []string) {
-	set := make(map[string]struct{})
-	for _, item := range slice {
-		if _, exists := set[item]; !exists {
-			set[item] = struct{}{}
-			result = append(result, item)
-		}
-	}
-	return result
-}
-
-func loadDefaultEnv() error {
-	env := viper.GetString("ENV")
-	if env == "" {
-		env = "development"
-	}
-	filenames := []string{".env." + env + ".local"}
-	if env != "test" {
-		filenames = append(filenames, ".env.local")
-	}
-	filenames = append(filenames, ".env."+env, ".env")
-	for _, path := range filenames {
-		if err := loadEnvIfExists(path); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func loadEnvIfExists(path string) error {
-	if err := godotenv.Load(path); err != nil && !errors.Is(err, os.ErrNotExist) {
-		return errors.Errorf("failed to load %s: %w", Bold(".env"), err)
-	}
-	return nil
-}
-
-func validateHookURI(uri, hookName string) error {
-	parsed, err := url.Parse(uri)
-	if err != nil {
-		return errors.Errorf("failed to parse template url: %w", err)
-	}
-	if !(parsed.Scheme == "http" || parsed.Scheme == "https" || parsed.Scheme == "pg-functions") {
-		return errors.Errorf("Invalid HTTP hook config: auth.hook.%v should be a Postgres function URI, or a HTTP or HTTPS URL", hookName)
-	}
-	return nil
-}
-
 func GetApiUrl(path string) string {
-	host := net.JoinHostPort(Config.Hostname,
+	if len(Config.Api.ExternalUrl) > 0 {
+		return Config.Api.ExternalUrl + path
+	}
+	hostPort := net.JoinHostPort(Config.Hostname,
 		strconv.FormatUint(uint64(Config.Api.Port), 10),
 	)
-	apiUrl := url.URL{Host: host, Path: path}
-	if Config.Api.Tls.Enabled {
-		apiUrl.Scheme = "https"
-	} else {
-		apiUrl.Scheme = "http"
+	apiUrl := url.URL{
+		Scheme: "http",
+		Host:   hostPort,
+		Path:   path,
 	}
 	return apiUrl.String()
 }
diff --git a/internal/utils/deno.go b/internal/utils/deno.go
index bceddf62..2748785b 100644
--- a/internal/utils/deno.go
+++ b/internal/utils/deno.go
@@ -31,6 +31,8 @@ var (
 const (
 	DockerDenoDir  = "/home/deno"
 	DockerEszipDir = "/root/eszips"
+	// Legacy bundle option
+	DenoVersion = "1.30.3"
 )
 
 func GetDenoPath() (string, error) {
@@ -283,8 +285,17 @@ func (m *ImportMap) BindHostModules() []string {
 	return binds
 }
 
-func GetFunctionConfig(slug, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) function {
-	fc := Config.Functions[slug]
+type FunctionConfig struct {
+	VerifyJWT *bool  `json:"verifyJWT"`
+	ImportMap string `json:"importMapPath,omitempty"`
+}
+
+func GetFunctionConfig(slug, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) FunctionConfig {
+	fc := FunctionConfig{}
+	if c, ok := Config.Functions[slug]; ok {
+		fc.VerifyJWT = c.VerifyJWT
+		fc.ImportMap = c.ImportMap
+	}
 	// Precedence order: CLI flags > config.toml > fallback value
 	if noVerifyJWT != nil {
 		value := !*noVerifyJWT
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index b74ed21c..093c459c 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -477,8 +477,3 @@ func suggestDockerStop(ctx context.Context, hostPort string) string {
 	}
 	return ""
 }
-
-func replaceImageTag(image string, tag string) string {
-	index := strings.IndexByte(image, ':')
-	return image[:index+1] + strings.TrimSpace(tag)
-}
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index cafb546d..db6854b9 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -23,54 +23,6 @@ var (
 	SentryDsn string
 )
 
-const (
-	Pg13Image = "supabase/postgres:13.3.0"
-	Pg14Image = "supabase/postgres:14.1.0.89"
-	Pg15Image = "supabase/postgres:15.1.1.61"
-	// Append to ServiceImages when adding new dependencies below
-	// TODO: try https://github.com/axllent/mailpit
-	KongImage        = "library/kong:2.8.1"
-	InbucketImage    = "inbucket/inbucket:3.0.3"
-	PostgrestImage   = "postgrest/postgrest:v12.2.0"
-	DifferImage      = "supabase/pgadmin-schema-diff:cli-0.0.5"
-	MigraImage       = "supabase/migra:3.0.1663481299"
-	PgmetaImage      = "supabase/postgres-meta:v0.83.2"
-	StudioImage      = "supabase/studio:20240701-05dfbec"
-	ImageProxyImage  = "darthsim/imgproxy:v3.8.0"
-	EdgeRuntimeImage = "supabase/edge-runtime:v1.54.10"
-	VectorImage      = "timberio/vector:0.28.1-alpine"
-	SupavisorImage   = "supabase/supavisor:1.1.56"
-	PgProveImage     = "supabase/pg_prove:3.36"
-	GotrueImage      = "supabase/gotrue:v2.151.0"
-	RealtimeImage    = "supabase/realtime:v2.29.15"
-	StorageImage     = "supabase/storage-api:v1.0.6"
-	LogflareImage    = "supabase/logflare:1.4.0"
-	// Should be kept in-sync with EdgeRuntimeImage
-	DenoVersion = "1.30.3"
-)
-
-var ServiceImages = []string{
-	GotrueImage,
-	RealtimeImage,
-	StorageImage,
-	ImageProxyImage,
-	KongImage,
-	InbucketImage,
-	PostgrestImage,
-	PgmetaImage,
-	StudioImage,
-	EdgeRuntimeImage,
-	LogflareImage,
-	VectorImage,
-	SupavisorImage,
-}
-
-var JobImages = []string{
-	DifferImage,
-	MigraImage,
-	PgProveImage,
-}
-
 func ShortContainerImageName(imageName string) string {
 	matches := ImageNamePattern.FindStringSubmatch(imageName)
 	if len(matches) < 2 {
diff --git a/internal/utils/slice.go b/internal/utils/slice.go
index e5e3cbb5..3d036c19 100644
--- a/internal/utils/slice.go
+++ b/internal/utils/slice.go
@@ -20,3 +20,14 @@ func SliceEqual[T comparable](a, b []T) bool {
 	}
 	return true
 }
+
+func RemoveDuplicates[T comparable](slice []T) (result []T) {
+	set := make(map[T]struct{})
+	for _, item := range slice {
+		if _, exists := set[item]; !exists {
+			set[item] = struct{}{}
+			result = append(result, item)
+		}
+	}
+	return result
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 00000000..97367bd1
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,931 @@
+package config
+
+import (
+	"bytes"
+	_ "embed"
+	"fmt"
+	"io"
+	"io/fs"
+	"net"
+	"net/url"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"text/template"
+	"time"
+
+	"github.com/BurntSushi/toml"
+	"github.com/docker/go-units"
+	"github.com/go-errors/errors"
+	"github.com/golang-jwt/jwt/v5"
+	"github.com/joho/godotenv"
+	"github.com/spf13/viper"
+	"golang.org/x/mod/semver"
+)
+
+// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
+type sizeInBytes int64
+
+func (s *sizeInBytes) UnmarshalText(text []byte) error {
+	size, err := units.RAMInBytes(string(text))
+	if err == nil {
+		*s = sizeInBytes(size)
+	}
+	return err
+}
+
+func (s sizeInBytes) MarshalText() (text []byte, err error) {
+	return []byte(units.BytesSize(float64(s))), nil
+}
+
+type LogflareBackend string
+
+const (
+	LogflarePostgres LogflareBackend = "postgres"
+	LogflareBigQuery LogflareBackend = "bigquery"
+)
+
+type PoolMode string
+
+const (
+	TransactionMode PoolMode = "transaction"
+	SessionMode     PoolMode = "session"
+)
+
+type AddressFamily string
+
+const (
+	AddressIPv6 AddressFamily = "IPv6"
+	AddressIPv4 AddressFamily = "IPv4"
+)
+
+type RequestPolicy string
+
+const (
+	PolicyPerWorker RequestPolicy = "per_worker"
+	PolicyOneshot   RequestPolicy = "oneshot"
+)
+
+type CustomClaims struct {
+	// Overrides Issuer to maintain json order when marshalling
+	Issuer string `json:"iss,omitempty"`
+	Ref    string `json:"ref,omitempty"`
+	Role   string `json:"role"`
+	jwt.RegisteredClaims
+}
+
+const (
+	defaultJwtSecret = "super-secret-jwt-token-with-at-least-32-characters-long"
+	defaultJwtExpiry = 1983812996
+)
+
+func (c CustomClaims) NewToken() *jwt.Token {
+	if c.ExpiresAt == nil {
+		c.ExpiresAt = jwt.NewNumericDate(time.Unix(defaultJwtExpiry, 0))
+	}
+	if len(c.Issuer) == 0 {
+		c.Issuer = "supabase-demo"
+	}
+	return jwt.NewWithClaims(jwt.SigningMethodHS256, c)
+}
+
+// We follow these rules when adding new config:
+//  1. Update init_config.toml (and init_config.test.toml) with the new key, default value, and comments to explain usage.
+//  2. Update config struct with new field and toml tag (spelled in snake_case).
+//  3. Add custom field validations to LoadConfigFS function for eg. integer range checks.
+//
+// If you are adding new user defined secrets, such as OAuth provider secret, the default value in
+// init_config.toml should be an env var substitution. For example,
+//
+// > secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
+//
+// If you are adding an internal config or secret that doesn't need to be overridden by the user,
+// exclude the field from toml serialization. For example,
+//
+//	type auth struct {
+//		AnonKey string `toml:"-" mapstructure:"anon_key"`
+//	}
+//
+// Use `mapstructure:"anon_key"` tag only if you want inject values from a predictable environment
+// variable, such as SUPABASE_AUTH_ANON_KEY.
+//
+// Default values for internal configs should be added to `var Config` initializer.
+type (
+	config struct {
+		ProjectId    string              `toml:"project_id"`
+		Hostname     string              `toml:"-"`
+		Api          api                 `toml:"api"`
+		Db           db                  `toml:"db" mapstructure:"db"`
+		Realtime     realtime            `toml:"realtime"`
+		Studio       studio              `toml:"studio"`
+		Inbucket     inbucket            `toml:"inbucket"`
+		Storage      storage             `toml:"storage"`
+		Auth         auth                `toml:"auth" mapstructure:"auth"`
+		EdgeRuntime  edgeRuntime         `toml:"edge_runtime"`
+		Functions    map[string]function `toml:"functions"`
+		Analytics    analytics           `toml:"analytics"`
+		Experimental experimental        `toml:"experimental" mapstructure:"-"`
+	}
+
+	api struct {
+		Enabled         bool     `toml:"enabled"`
+		Image           string   `toml:"-"`
+		KongImage       string   `toml:"-"`
+		Port            uint16   `toml:"port"`
+		Schemas         []string `toml:"schemas"`
+		ExtraSearchPath []string `toml:"extra_search_path"`
+		MaxRows         uint     `toml:"max_rows"`
+		Tls             tlsKong  `toml:"tls"`
+		// TODO: replace [auth|studio].api_url
+		ExternalUrl string `toml:"-"`
+	}
+
+	tlsKong struct {
+		Enabled bool `toml:"enabled"`
+	}
+
+	db struct {
+		Image        string `toml:"-"`
+		Port         uint16 `toml:"port"`
+		ShadowPort   uint16 `toml:"shadow_port"`
+		MajorVersion uint   `toml:"major_version"`
+		Password     string `toml:"-"`
+		RootKey      string `toml:"-" mapstructure:"root_key"`
+		Pooler       pooler `toml:"pooler"`
+	}
+
+	pooler struct {
+		Enabled          bool     `toml:"enabled"`
+		Image            string   `toml:"-"`
+		Port             uint16   `toml:"port"`
+		PoolMode         PoolMode `toml:"pool_mode"`
+		DefaultPoolSize  uint     `toml:"default_pool_size"`
+		MaxClientConn    uint     `toml:"max_client_conn"`
+		ConnectionString string   `toml:"-"`
+		TenantId         string   `toml:"-"`
+		EncryptionKey    string   `toml:"-"`
+		SecretKeyBase    string   `toml:"-"`
+	}
+
+	realtime struct {
+		Enabled         bool          `toml:"enabled"`
+		Image           string        `toml:"-"`
+		IpVersion       AddressFamily `toml:"ip_version"`
+		MaxHeaderLength uint          `toml:"max_header_length"`
+		TenantId        string        `toml:"-"`
+		EncryptionKey   string        `toml:"-"`
+		SecretKeyBase   string        `toml:"-"`
+	}
+
+	studio struct {
+		Enabled      bool   `toml:"enabled"`
+		Image        string `toml:"-"`
+		Port         uint16 `toml:"port"`
+		ApiUrl       string `toml:"api_url"`
+		OpenaiApiKey string `toml:"openai_api_key"`
+		PgmetaImage  string `toml:"-"`
+	}
+
+	inbucket struct {
+		Enabled  bool   `toml:"enabled"`
+		Image    string `toml:"-"`
+		Port     uint16 `toml:"port"`
+		SmtpPort uint16 `toml:"smtp_port"`
+		Pop3Port uint16 `toml:"pop3_port"`
+	}
+
+	storage struct {
+		Enabled             bool                 `toml:"enabled"`
+		Image               string               `toml:"-"`
+		FileSizeLimit       sizeInBytes          `toml:"file_size_limit"`
+		S3Credentials       storageS3Credentials `toml:"-"`
+		ImageTransformation imageTransformation  `toml:"image_transformation"`
+		Buckets             map[string]bucket    `toml:"buckets"`
+	}
+
+	imageTransformation struct {
+		Enabled bool   `toml:"enabled"`
+		Image   string `toml:"-"`
+	}
+
+	bucket struct {
+		Public           bool        `toml:"public"`
+		FileSizeLimit    sizeInBytes `toml:"file_size_limit"`
+		AllowedMimeTypes []string    `toml:"allowed_mime_types"`
+	}
+
+	storageS3Credentials struct {
+		AccessKeyId     string `toml:"-"`
+		SecretAccessKey string `toml:"-"`
+		Region          string `toml:"-"`
+	}
+
+	auth struct {
+		Enabled                bool     `toml:"enabled"`
+		Image                  string   `toml:"-"`
+		SiteUrl                string   `toml:"site_url"`
+		AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
+
+		JwtExpiry                  uint `toml:"jwt_expiry"`
+		EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
+		RefreshTokenReuseInterval  uint `toml:"refresh_token_reuse_interval"`
+		EnableManualLinking        bool `toml:"enable_manual_linking"`
+
+		Hook     hook     `toml:"hook"`
+		Sessions sessions `toml:"sessions"`
+
+		EnableSignup           bool  `toml:"enable_signup"`
+		EnableAnonymousSignIns bool  `toml:"enable_anonymous_sign_ins"`
+		Email                  email `toml:"email"`
+		Sms                    sms   `toml:"sms"`
+		External               map[string]provider
+
+		// Custom secrets can be injected from .env file
+		JwtSecret      string `toml:"-" mapstructure:"jwt_secret"`
+		AnonKey        string `toml:"-" mapstructure:"anon_key"`
+		ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
+	}
+
+	email struct {
+		EnableSignup         bool                     `toml:"enable_signup"`
+		DoubleConfirmChanges bool                     `toml:"double_confirm_changes"`
+		EnableConfirmations  bool                     `toml:"enable_confirmations"`
+		Template             map[string]emailTemplate `toml:"template"`
+		Smtp                 smtp                     `toml:"smtp"`
+		MaxFrequency         time.Duration            `toml:"max_frequency"`
+	}
+
+	smtp struct {
+		Host       string `toml:"host"`
+		Port       uint16 `toml:"port"`
+		User       string `toml:"user"`
+		Pass       string `toml:"pass"`
+		AdminEmail string `toml:"admin_email"`
+		SenderName string `toml:"sender_name"`
+	}
+
+	emailTemplate struct {
+		Subject     string `toml:"subject"`
+		ContentPath string `toml:"content_path"`
+	}
+
+	sms struct {
+		EnableSignup        bool              `toml:"enable_signup"`
+		EnableConfirmations bool              `toml:"enable_confirmations"`
+		Template            string            `toml:"template"`
+		Twilio              twilioConfig      `toml:"twilio" mapstructure:"twilio"`
+		TwilioVerify        twilioConfig      `toml:"twilio_verify" mapstructure:"twilio_verify"`
+		Messagebird         messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"`
+		Textlocal           textlocalConfig   `toml:"textlocal" mapstructure:"textlocal"`
+		Vonage              vonageConfig      `toml:"vonage" mapstructure:"vonage"`
+		TestOTP             map[string]string `toml:"test_otp"`
+		MaxFrequency        time.Duration     `toml:"max_frequency"`
+	}
+
+	hook struct {
+		MFAVerificationAttempt      hookConfig `toml:"mfa_verification_attempt"`
+		PasswordVerificationAttempt hookConfig `toml:"password_verification_attempt"`
+		CustomAccessToken           hookConfig `toml:"custom_access_token"`
+		SendSMS                     hookConfig `toml:"send_sms"`
+		SendEmail                   hookConfig `toml:"send_email"`
+	}
+
+	hookConfig struct {
+		Enabled bool   `toml:"enabled"`
+		URI     string `toml:"uri"`
+		Secrets string `toml:"secrets"`
+	}
+
+	sessions struct {
+		Timebox           time.Duration `toml:"timebox"`
+		InactivityTimeout time.Duration `toml:"inactivity_timeout"`
+	}
+
+	twilioConfig struct {
+		Enabled           bool   `toml:"enabled"`
+		AccountSid        string `toml:"account_sid"`
+		MessageServiceSid string `toml:"message_service_sid"`
+		AuthToken         string `toml:"auth_token" mapstructure:"auth_token"`
+	}
+
+	messagebirdConfig struct {
+		Enabled    bool   `toml:"enabled"`
+		Originator string `toml:"originator"`
+		AccessKey  string `toml:"access_key" mapstructure:"access_key"`
+	}
+
+	textlocalConfig struct {
+		Enabled bool   `toml:"enabled"`
+		Sender  string `toml:"sender"`
+		ApiKey  string `toml:"api_key" mapstructure:"api_key"`
+	}
+
+	vonageConfig struct {
+		Enabled   bool   `toml:"enabled"`
+		From      string `toml:"from"`
+		ApiKey    string `toml:"api_key" mapstructure:"api_key"`
+		ApiSecret string `toml:"api_secret" mapstructure:"api_secret"`
+	}
+
+	provider struct {
+		Enabled        bool   `toml:"enabled"`
+		ClientId       string `toml:"client_id"`
+		Secret         string `toml:"secret"`
+		Url            string `toml:"url"`
+		RedirectUri    string `toml:"redirect_uri"`
+		SkipNonceCheck bool   `toml:"skip_nonce_check"`
+	}
+
+	edgeRuntime struct {
+		Enabled       bool          `toml:"enabled"`
+		Image         string        `toml:"-"`
+		Policy        RequestPolicy `toml:"policy"`
+		InspectorPort uint16        `toml:"inspector_port"`
+	}
+
+	function struct {
+		VerifyJWT *bool  `toml:"verify_jwt"`
+		ImportMap string `toml:"import_map"`
+	}
+
+	analytics struct {
+		Enabled          bool            `toml:"enabled"`
+		Image            string          `toml:"-"`
+		VectorImage      string          `toml:"-"`
+		Port             uint16          `toml:"port"`
+		Backend          LogflareBackend `toml:"backend"`
+		GcpProjectId     string          `toml:"gcp_project_id"`
+		GcpProjectNumber string          `toml:"gcp_project_number"`
+		GcpJwtPath       string          `toml:"gcp_jwt_path"`
+		ApiKey           string          `toml:"-" mapstructure:"api_key"`
+		// Deprecated together with syslog
+		VectorPort uint16 `toml:"vector_port"`
+	}
+
+	experimental struct {
+		OrioleDBVersion string `toml:"orioledb_version"`
+		S3Host          string `toml:"s3_host"`
+		S3Region        string `toml:"s3_region"`
+		S3AccessKey     string `toml:"s3_access_key"`
+		S3SecretKey     string `toml:"s3_secret_key"`
+	}
+)
+
+func (h *hookConfig) HandleHook(hookType string) error {
+	// If not enabled do nothing
+	if !h.Enabled {
+		return nil
+	}
+	if h.URI == "" {
+		return errors.Errorf("missing required field in config: auth.hook.%s.uri", hookType)
+	}
+	if err := validateHookURI(h.URI, hookType); err != nil {
+		return err
+	}
+	var err error
+	if h.Secrets, err = maybeLoadEnv(h.Secrets); err != nil {
+		return errors.Errorf("missing required field in config: auth.hook.%s.secrets", hookType)
+	}
+	return nil
+}
+
+func validateHookURI(uri, hookName string) error {
+	parsed, err := url.Parse(uri)
+	if err != nil {
+		return errors.Errorf("failed to parse template url: %w", err)
+	}
+	if !(parsed.Scheme == "http" || parsed.Scheme == "https" || parsed.Scheme == "pg-functions") {
+		return errors.Errorf("Invalid HTTP hook config: auth.hook.%v should be a Postgres function URI, or a HTTP or HTTPS URL", hookName)
+	}
+	return nil
+}
+
+type ConfigEditor func(*config)
+
+func WithHostname(hostname string) ConfigEditor {
+	return func(c *config) {
+		c.Hostname = hostname
+	}
+}
+
+func NewConfig(editors ...ConfigEditor) config {
+	initial := config{
+		Hostname: "127.0.0.1",
+		Api: api{
+			Image:     postgrestImage,
+			KongImage: kongImage,
+		},
+		Db: db{
+			Image:    Pg15Image,
+			Password: "postgres",
+			RootKey:  "d4dc5b6d4a1d6a10b2c1e76112c994d65db7cec380572cc1839624d4be3fa275",
+			Pooler: pooler{
+				Image:         supavisorImage,
+				TenantId:      "pooler-dev",
+				EncryptionKey: "12345678901234567890123456789032",
+				SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
+			},
+		},
+		Realtime: realtime{
+			Image:           realtimeImage,
+			IpVersion:       AddressIPv4,
+			MaxHeaderLength: 4096,
+			TenantId:        "realtime-dev",
+			EncryptionKey:   "supabaserealtime",
+			SecretKeyBase:   "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
+		},
+		Storage: storage{
+			Image: storageImage,
+			S3Credentials: storageS3Credentials{
+				AccessKeyId:     "625729a08b95bf1b7ff351a663f3a23c",
+				SecretAccessKey: "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907",
+				Region:          "local",
+			},
+			ImageTransformation: imageTransformation{
+				Enabled: true,
+				Image:   imageProxyImage,
+			},
+		},
+		Auth: auth{
+			Image: gotrueImage,
+			Email: email{
+				Template: map[string]emailTemplate{
+					"invite":       {},
+					"confirmation": {},
+					"recovery":     {},
+					"magic_link":   {},
+					"email_change": {},
+				},
+				Smtp: smtp{
+					Host:       "inbucket",
+					Port:       2500,
+					AdminEmail: "admin@email.com",
+				},
+			},
+			External: map[string]provider{
+				"apple":         {},
+				"azure":         {},
+				"bitbucket":     {},
+				"discord":       {},
+				"facebook":      {},
+				"github":        {},
+				"gitlab":        {},
+				"google":        {},
+				"keycloak":      {},
+				"linkedin":      {}, // TODO: remove this field in v2
+				"linkedin_oidc": {},
+				"notion":        {},
+				"twitch":        {},
+				"twitter":       {},
+				"slack":         {},
+				"spotify":       {},
+				"workos":        {},
+				"zoom":          {},
+			},
+			JwtSecret: defaultJwtSecret,
+		},
+		Inbucket: inbucket{
+			Image: inbucketImage,
+		},
+		Studio: studio{
+			Image:       studioImage,
+			PgmetaImage: pgmetaImage,
+		},
+		Analytics: analytics{
+			Image:       logflareImage,
+			VectorImage: vectorImage,
+			ApiKey:      "api-key",
+			// Defaults to bigquery for backwards compatibility with existing config.toml
+			Backend: LogflareBigQuery,
+		},
+		EdgeRuntime: edgeRuntime{
+			Image: edgeRuntimeImage,
+		},
+	}
+	for _, apply := range editors {
+		apply(&initial)
+	}
+	return initial
+}
+
+var (
+	//go:embed templates/config.toml
+	initConfigEmbed    string
+	initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed))
+
+	invalidProjectId = regexp.MustCompile("[^a-zA-Z0-9_.-]+")
+	envPattern       = regexp.MustCompile(`^env\((.*)\)$`)
+)
+
+func (c *config) Eject(w io.Writer) error {
+	// Defaults to current directory name as project id
+	if len(c.ProjectId) == 0 {
+		cwd, err := os.Getwd()
+		if err != nil {
+			return errors.Errorf("failed to get working directory: %w", err)
+		}
+		c.ProjectId = filepath.Base(cwd)
+	}
+	c.ProjectId = sanitizeProjectId(c.ProjectId)
+	// TODO: templatize all fields eventually
+	if err := initConfigTemplate.Option("missingkey=error").Execute(w, c); err != nil {
+		return errors.Errorf("failed to initialise config: %w", err)
+	}
+	return nil
+}
+
+func (c *config) Load(path string, fsys fs.FS) error {
+	builder := NewPathBuilder(path)
+	// Load default values
+	var buf bytes.Buffer
+	if err := initConfigTemplate.Option("missingkey=zero").Execute(&buf, nil); err != nil {
+		return errors.Errorf("failed to initialise config template: %w", err)
+	}
+	dec := toml.NewDecoder(&buf)
+	if _, err := dec.Decode(c); err != nil {
+		return errors.Errorf("failed to decode config template: %w", err)
+	}
+	// Load user defined config
+	if metadata, err := toml.DecodeFS(fsys, builder.ConfigPath, c); err != nil {
+		cwd, osErr := os.Getwd()
+		if osErr != nil {
+			cwd = "current directory"
+		}
+		return errors.Errorf("cannot read config in %s: %w", cwd, err)
+	} else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
+		fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
+	}
+	// Load secrets from .env file
+	if err := loadDefaultEnv(); err != nil {
+		return err
+	}
+	if err := viper.Unmarshal(c); err != nil {
+		return errors.Errorf("failed to parse env to config: %w", err)
+	}
+	// Generate JWT tokens
+	if len(c.Auth.AnonKey) == 0 {
+		anonToken := CustomClaims{Role: "anon"}.NewToken()
+		if signed, err := anonToken.SignedString([]byte(c.Auth.JwtSecret)); err != nil {
+			return errors.Errorf("failed to generate anon key: %w", err)
+		} else {
+			c.Auth.AnonKey = signed
+		}
+	}
+	if len(c.Auth.ServiceRoleKey) == 0 {
+		anonToken := CustomClaims{Role: "service_role"}.NewToken()
+		if signed, err := anonToken.SignedString([]byte(c.Auth.JwtSecret)); err != nil {
+			return errors.Errorf("failed to generate service_role key: %w", err)
+		} else {
+			c.Auth.ServiceRoleKey = signed
+		}
+	}
+	// TODO: move linked pooler connection string elsewhere
+	if connString, err := fs.ReadFile(fsys, builder.PoolerUrlPath); err == nil && len(connString) > 0 {
+		c.Db.Pooler.ConnectionString = string(connString)
+	}
+	// Update external api url
+	apiUrl := url.URL{Host: net.JoinHostPort(c.Hostname,
+		strconv.FormatUint(uint64(c.Api.Port), 10),
+	)}
+	if c.Api.Tls.Enabled {
+		apiUrl.Scheme = "https"
+	} else {
+		apiUrl.Scheme = "http"
+	}
+	c.Api.ExternalUrl = apiUrl.String()
+	// Update image versions
+	if version, err := fs.ReadFile(fsys, builder.PostgresVersionPath); err == nil {
+		if strings.HasPrefix(string(version), "15.") && semver.Compare(string(version[3:]), "1.0.55") >= 0 {
+			c.Db.Image = replaceImageTag(Pg15Image, string(version))
+		}
+	}
+	if c.Db.MajorVersion > 14 {
+		if version, err := fs.ReadFile(fsys, builder.RestVersionPath); err == nil && len(version) > 0 {
+			c.Api.Image = replaceImageTag(postgrestImage, string(version))
+		}
+		if version, err := fs.ReadFile(fsys, builder.StorageVersionPath); err == nil && len(version) > 0 {
+			c.Storage.Image = replaceImageTag(storageImage, string(version))
+		}
+		if version, err := fs.ReadFile(fsys, builder.GotrueVersionPath); err == nil && len(version) > 0 {
+			c.Auth.Image = replaceImageTag(gotrueImage, string(version))
+		}
+	}
+	if version, err := fs.ReadFile(fsys, builder.PoolerVersionPath); err == nil && len(version) > 0 {
+		c.Db.Pooler.Image = replaceImageTag(supavisorImage, string(version))
+	}
+	if version, err := fs.ReadFile(fsys, builder.RealtimeVersionPath); err == nil && len(version) > 0 {
+		c.Realtime.Image = replaceImageTag(realtimeImage, string(version))
+	}
+	if version, err := fs.ReadFile(fsys, builder.StudioVersionPath); err == nil && len(version) > 0 {
+		c.Studio.Image = replaceImageTag(studioImage, string(version))
+	}
+	if version, err := fs.ReadFile(fsys, builder.PgmetaVersionPath); err == nil && len(version) > 0 {
+		c.Studio.PgmetaImage = replaceImageTag(pgmetaImage, string(version))
+	}
+	return c.Validate()
+}
+
+func (c *config) Validate() error {
+	if c.ProjectId == "" {
+		return errors.New("Missing required field in config: project_id")
+	} else if sanitized := sanitizeProjectId(c.ProjectId); sanitized != c.ProjectId {
+		fmt.Fprintln(os.Stderr, "WARNING:", "project_id field in config is invalid. Auto-fixing to", sanitized)
+		c.ProjectId = sanitized
+	}
+	// Validate api config
+	if c.Api.Enabled {
+		if c.Api.Port == 0 {
+			return errors.New("Missing required field in config: api.port")
+		}
+		// Append required schemas if they are missing
+		c.Api.Schemas = removeDuplicates(append([]string{"public", "storage"}, c.Api.Schemas...))
+		c.Api.ExtraSearchPath = removeDuplicates(append([]string{"public"}, c.Api.ExtraSearchPath...))
+	}
+	// Validate db config
+	if c.Db.Port == 0 {
+		return errors.New("Missing required field in config: db.port")
+	}
+	switch c.Db.MajorVersion {
+	case 0:
+		return errors.New("Missing required field in config: db.major_version")
+	case 12:
+		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.")
+	case 13:
+		c.Db.Image = pg13Image
+	case 14:
+		c.Db.Image = pg14Image
+	case 15:
+		if len(c.Experimental.OrioleDBVersion) > 0 {
+			c.Db.Image = "supabase/postgres:orioledb-" + c.Experimental.OrioleDBVersion
+			var err error
+			if c.Experimental.S3Host, err = maybeLoadEnv(c.Experimental.S3Host); err != nil {
+				return err
+			}
+			if c.Experimental.S3Region, err = maybeLoadEnv(c.Experimental.S3Region); err != nil {
+				return err
+			}
+			if c.Experimental.S3AccessKey, err = maybeLoadEnv(c.Experimental.S3AccessKey); err != nil {
+				return err
+			}
+			if c.Experimental.S3SecretKey, err = maybeLoadEnv(c.Experimental.S3SecretKey); err != nil {
+				return err
+			}
+		}
+	default:
+		return errors.Errorf("Failed reading config: Invalid %s: %v.", "db.major_version", c.Db.MajorVersion)
+	}
+	// Validate pooler config
+	if c.Db.Pooler.Enabled {
+		allowed := []PoolMode{TransactionMode, SessionMode}
+		if !sliceContains(allowed, c.Db.Pooler.PoolMode) {
+			return errors.Errorf("Invalid config for db.pooler.pool_mode. Must be one of: %v", allowed)
+		}
+	}
+	// Validate realtime config
+	if c.Realtime.Enabled {
+		allowed := []AddressFamily{AddressIPv6, AddressIPv4}
+		if !sliceContains(allowed, c.Realtime.IpVersion) {
+			return errors.Errorf("Invalid config for realtime.ip_version. Must be one of: %v", allowed)
+		}
+	}
+	// Validate storage config
+	// Validate studio config
+	if c.Studio.Enabled {
+		if c.Studio.Port == 0 {
+			return errors.New("Missing required field in config: studio.port")
+		}
+		if parsed, err := url.Parse(c.Studio.ApiUrl); err != nil {
+			return errors.Errorf("Invalid config for studio.api_url: %w", err)
+		} else if parsed.Host == "" || parsed.Host == c.Hostname {
+			c.Studio.ApiUrl = c.Api.ExternalUrl
+		}
+		c.Studio.OpenaiApiKey, _ = maybeLoadEnv(c.Studio.OpenaiApiKey)
+	}
+	// Validate smtp config
+	if c.Inbucket.Enabled {
+		if c.Inbucket.Port == 0 {
+			return errors.New("Missing required field in config: inbucket.port")
+		}
+	}
+	// Validate auth config
+	if c.Auth.Enabled {
+		if c.Auth.SiteUrl == "" {
+			return errors.New("Missing required field in config: auth.site_url")
+		}
+		var err error
+		if c.Auth.SiteUrl, err = maybeLoadEnv(c.Auth.SiteUrl); err != nil {
+			return err
+		}
+		// Validate email config
+		for name, tmpl := range c.Auth.Email.Template {
+			if len(tmpl.ContentPath) > 0 && !fs.ValidPath(tmpl.ContentPath) {
+				return errors.Errorf("Invalid config for auth.email.%s.content_path: %s", name, tmpl.ContentPath)
+			}
+		}
+		if c.Auth.Email.Smtp.Pass, err = maybeLoadEnv(c.Auth.Email.Smtp.Pass); err != nil {
+			return err
+		}
+		// Validate sms config
+		if c.Auth.Sms.Twilio.Enabled {
+			if len(c.Auth.Sms.Twilio.AccountSid) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio.account_sid")
+			}
+			if len(c.Auth.Sms.Twilio.MessageServiceSid) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio.message_service_sid")
+			}
+			if len(c.Auth.Sms.Twilio.AuthToken) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio.auth_token")
+			}
+			if c.Auth.Sms.Twilio.AuthToken, err = maybeLoadEnv(c.Auth.Sms.Twilio.AuthToken); err != nil {
+				return err
+			}
+		}
+		if c.Auth.Sms.TwilioVerify.Enabled {
+			if len(c.Auth.Sms.TwilioVerify.AccountSid) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio_verify.account_sid")
+			}
+			if len(c.Auth.Sms.TwilioVerify.MessageServiceSid) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio_verify.message_service_sid")
+			}
+			if len(c.Auth.Sms.TwilioVerify.AuthToken) == 0 {
+				return errors.New("Missing required field in config: auth.sms.twilio_verify.auth_token")
+			}
+			if c.Auth.Sms.TwilioVerify.AuthToken, err = maybeLoadEnv(c.Auth.Sms.TwilioVerify.AuthToken); err != nil {
+				return err
+			}
+		}
+		if c.Auth.Sms.Messagebird.Enabled {
+			if len(c.Auth.Sms.Messagebird.Originator) == 0 {
+				return errors.New("Missing required field in config: auth.sms.messagebird.originator")
+			}
+			if len(c.Auth.Sms.Messagebird.AccessKey) == 0 {
+				return errors.New("Missing required field in config: auth.sms.messagebird.access_key")
+			}
+			if c.Auth.Sms.Messagebird.AccessKey, err = maybeLoadEnv(c.Auth.Sms.Messagebird.AccessKey); err != nil {
+				return err
+			}
+		}
+		if c.Auth.Sms.Textlocal.Enabled {
+			if len(c.Auth.Sms.Textlocal.Sender) == 0 {
+				return errors.New("Missing required field in config: auth.sms.textlocal.sender")
+			}
+			if len(c.Auth.Sms.Textlocal.ApiKey) == 0 {
+				return errors.New("Missing required field in config: auth.sms.textlocal.api_key")
+			}
+			if c.Auth.Sms.Textlocal.ApiKey, err = maybeLoadEnv(c.Auth.Sms.Textlocal.ApiKey); err != nil {
+				return err
+			}
+		}
+		if c.Auth.Sms.Vonage.Enabled {
+			if len(c.Auth.Sms.Vonage.From) == 0 {
+				return errors.New("Missing required field in config: auth.sms.vonage.from")
+			}
+			if len(c.Auth.Sms.Vonage.ApiKey) == 0 {
+				return errors.New("Missing required field in config: auth.sms.vonage.api_key")
+			}
+			if len(c.Auth.Sms.Vonage.ApiSecret) == 0 {
+				return errors.New("Missing required field in config: auth.sms.vonage.api_secret")
+			}
+			if c.Auth.Sms.Vonage.ApiKey, err = maybeLoadEnv(c.Auth.Sms.Vonage.ApiKey); err != nil {
+				return err
+			}
+			if c.Auth.Sms.Vonage.ApiSecret, err = maybeLoadEnv(c.Auth.Sms.Vonage.ApiSecret); err != nil {
+				return err
+			}
+		}
+		if err := c.Auth.Hook.MFAVerificationAttempt.HandleHook("mfa_verification_attempt"); err != nil {
+			return err
+		}
+		if err := c.Auth.Hook.PasswordVerificationAttempt.HandleHook("password_verification_attempt"); err != nil {
+			return err
+		}
+		if err := c.Auth.Hook.CustomAccessToken.HandleHook("custom_access_token"); err != nil {
+			return err
+		}
+		if err := c.Auth.Hook.SendSMS.HandleHook("send_sms"); err != nil {
+			return err
+		}
+		if err := c.Auth.Hook.SendEmail.HandleHook("send_email"); err != nil {
+			return err
+		}
+		// Validate oauth config
+		for ext, provider := range c.Auth.External {
+			if !provider.Enabled {
+				continue
+			}
+			if provider.ClientId == "" {
+				return errors.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
+			}
+			if !sliceContains([]string{"apple", "google"}, ext) && provider.Secret == "" {
+				return errors.Errorf("Missing required field in config: auth.external.%s.secret", ext)
+			}
+			if provider.ClientId, err = maybeLoadEnv(provider.ClientId); err != nil {
+				return err
+			}
+			if provider.Secret, err = maybeLoadEnv(provider.Secret); err != nil {
+				return err
+			}
+			if provider.RedirectUri, err = maybeLoadEnv(provider.RedirectUri); err != nil {
+				return err
+			}
+			if provider.Url, err = maybeLoadEnv(provider.Url); err != nil {
+				return err
+			}
+			c.Auth.External[ext] = provider
+		}
+	}
+	// Validate functions config
+	if c.EdgeRuntime.Enabled {
+		allowed := []RequestPolicy{PolicyPerWorker, PolicyOneshot}
+		if !sliceContains(allowed, c.EdgeRuntime.Policy) {
+			return errors.Errorf("Invalid config for edge_runtime.policy. Must be one of: %v", allowed)
+		}
+	}
+	for name, functionConfig := range c.Functions {
+		if functionConfig.VerifyJWT == nil {
+			functionConfig.VerifyJWT = ptr(true)
+			c.Functions[name] = functionConfig
+		}
+	}
+	// Validate logflare config
+	if c.Analytics.Enabled {
+		switch c.Analytics.Backend {
+		case LogflareBigQuery:
+			if len(c.Analytics.GcpProjectId) == 0 {
+				return errors.New("Missing required field in config: analytics.gcp_project_id")
+			}
+			if len(c.Analytics.GcpProjectNumber) == 0 {
+				return errors.New("Missing required field in config: analytics.gcp_project_number")
+			}
+			if len(c.Analytics.GcpJwtPath) == 0 {
+				return errors.New("Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path")
+			}
+		case LogflarePostgres:
+			break
+		default:
+			allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery}
+			return errors.Errorf("Invalid config for analytics.backend. Must be one of: %v", allowed)
+		}
+	}
+	return nil
+}
+
+func maybeLoadEnv(s string) (string, error) {
+	matches := envPattern.FindStringSubmatch(s)
+	if len(matches) == 0 {
+		return s, nil
+	}
+
+	envName := matches[1]
+	if value := os.Getenv(envName); value != "" {
+		return value, nil
+	}
+
+	return "", errors.Errorf(`Error evaluating "%s": environment variable %s is unset.`, s, envName)
+}
+
+func truncateText(text string, maxLen int) string {
+	if len(text) > maxLen {
+		return text[:maxLen]
+	}
+	return text
+}
+
+const maxProjectIdLength = 40
+
+func sanitizeProjectId(src string) string {
+	// A valid project ID must only contain alphanumeric and special characters _.-
+	sanitized := invalidProjectId.ReplaceAllString(src, "_")
+	// It must also start with an alphanumeric character
+	sanitized = strings.TrimLeft(sanitized, "_.-")
+	// Truncate sanitized ID to 40 characters since docker hostnames cannot exceed
+	// 63 characters, and we need to save space for padding supabase_*_edge_runtime.
+	return truncateText(sanitized, maxProjectIdLength)
+}
+
+func loadDefaultEnv() error {
+	env := viper.GetString("ENV")
+	if env == "" {
+		env = "development"
+	}
+	filenames := []string{".env." + env + ".local"}
+	if env != "test" {
+		filenames = append(filenames, ".env.local")
+	}
+	filenames = append(filenames, ".env."+env, ".env")
+	for _, path := range filenames {
+		if err := loadEnvIfExists(path); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func loadEnvIfExists(path string) error {
+	if err := godotenv.Load(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+		return errors.Errorf("failed to load %s: %w", ".env", err)
+	}
+	return nil
+}
diff --git a/pkg/config/constants.go b/pkg/config/constants.go
new file mode 100644
index 00000000..e32be4aa
--- /dev/null
+++ b/pkg/config/constants.go
@@ -0,0 +1,48 @@
+package config
+
+const (
+	pg13Image = "supabase/postgres:13.3.0"
+	pg14Image = "supabase/postgres:14.1.0.89"
+	Pg15Image = "supabase/postgres:15.1.1.61"
+	// Append to ServiceImages when adding new dependencies below
+	// TODO: try https://github.com/axllent/mailpit
+	kongImage        = "library/kong:2.8.1"
+	inbucketImage    = "inbucket/inbucket:3.0.3"
+	postgrestImage   = "postgrest/postgrest:v12.2.0"
+	pgmetaImage      = "supabase/postgres-meta:v0.83.2"
+	studioImage      = "supabase/studio:20240701-05dfbec"
+	imageProxyImage  = "darthsim/imgproxy:v3.8.0"
+	edgeRuntimeImage = "supabase/edge-runtime:v1.54.10"
+	vectorImage      = "timberio/vector:0.28.1-alpine"
+	supavisorImage   = "supabase/supavisor:1.1.56"
+	gotrueImage      = "supabase/gotrue:v2.151.0"
+	realtimeImage    = "supabase/realtime:v2.29.15"
+	storageImage     = "supabase/storage-api:v1.0.6"
+	logflareImage    = "supabase/logflare:1.4.0"
+	// Append to JobImages when adding new dependencies below
+	DifferImage  = "supabase/pgadmin-schema-diff:cli-0.0.5"
+	MigraImage   = "supabase/migra:3.0.1663481299"
+	PgProveImage = "supabase/pg_prove:3.36"
+)
+
+var ServiceImages = []string{
+	gotrueImage,
+	realtimeImage,
+	storageImage,
+	imageProxyImage,
+	kongImage,
+	inbucketImage,
+	postgrestImage,
+	pgmetaImage,
+	studioImage,
+	edgeRuntimeImage,
+	logflareImage,
+	vectorImage,
+	supavisorImage,
+}
+
+var JobImages = []string{
+	DifferImage,
+	MigraImage,
+	PgProveImage,
+}
diff --git a/internal/utils/templates/init_config.toml b/pkg/config/templates/config.toml
similarity index 99%
rename from internal/utils/templates/init_config.toml
rename to pkg/config/templates/config.toml
index 761637d6..e9545a6d 100644
--- a/internal/utils/templates/init_config.toml
+++ b/pkg/config/templates/config.toml
@@ -191,7 +191,7 @@ backend = "postgres"
 # Experimental features may be deprecated any time
 [experimental]
 # Configures Postgres storage engine to use OrioleDB (S3)
-orioledb_version = "{{if .UseOrioleDB}}15.1.0.150{{end}}"
+orioledb_version = "{{ .Experimental.OrioleDBVersion }}"
 # Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
 s3_host = "env(S3_HOST)"
 # Configures S3 bucket region, eg. us-east-1
diff --git a/pkg/config/utils.go b/pkg/config/utils.go
new file mode 100644
index 00000000..755f4867
--- /dev/null
+++ b/pkg/config/utils.go
@@ -0,0 +1,98 @@
+package config
+
+import (
+	"path/filepath"
+	"strings"
+)
+
+type pathBuilder struct {
+	SupabaseDirPath       string
+	ConfigPath            string
+	GitIgnorePath         string
+	TempDir               string
+	ImportMapsDir         string
+	ProjectRefPath        string
+	PoolerUrlPath         string
+	PostgresVersionPath   string
+	GotrueVersionPath     string
+	RestVersionPath       string
+	StorageVersionPath    string
+	StudioVersionPath     string
+	PgmetaVersionPath     string
+	PoolerVersionPath     string
+	RealtimeVersionPath   string
+	CliVersionPath        string
+	CurrBranchPath        string
+	SchemasDir            string
+	MigrationsDir         string
+	FunctionsDir          string
+	FallbackImportMapPath string
+	FallbackEnvFilePath   string
+	DbTestsDir            string
+	SeedDataPath          string
+	CustomRolesPath       string
+}
+
+func NewPathBuilder(configPath string) pathBuilder {
+	if filepath.Base(configPath) == "." {
+		configPath = filepath.Join("supabase", "config.toml")
+	}
+	// TODO: make base path configurable from toml
+	base := filepath.Dir(configPath)
+	return pathBuilder{
+		SupabaseDirPath:       base,
+		ConfigPath:            configPath,
+		GitIgnorePath:         filepath.Join(base, ".gitignore"),
+		TempDir:               filepath.Join(base, ".temp"),
+		ImportMapsDir:         filepath.Join(base, ".temp", "import_maps"),
+		ProjectRefPath:        filepath.Join(base, ".temp", "project-ref"),
+		PoolerUrlPath:         filepath.Join(base, ".temp", "pooler-url"),
+		PostgresVersionPath:   filepath.Join(base, ".temp", "postgres-version"),
+		GotrueVersionPath:     filepath.Join(base, ".temp", "gotrue-version"),
+		RestVersionPath:       filepath.Join(base, ".temp", "rest-version"),
+		StorageVersionPath:    filepath.Join(base, ".temp", "storage-version"),
+		StudioVersionPath:     filepath.Join(base, ".temp", "studio-version"),
+		PgmetaVersionPath:     filepath.Join(base, ".temp", "pgmeta-version"),
+		PoolerVersionPath:     filepath.Join(base, ".temp", "pooler-version"),
+		RealtimeVersionPath:   filepath.Join(base, ".temp", "realtime-version"),
+		CliVersionPath:        filepath.Join(base, ".temp", "cli-latest"),
+		CurrBranchPath:        filepath.Join(base, ".branches", "_current_branch"),
+		SchemasDir:            filepath.Join(base, "schemas"),
+		MigrationsDir:         filepath.Join(base, "migrations"),
+		FunctionsDir:          filepath.Join(base, "functions"),
+		FallbackImportMapPath: filepath.Join(base, "functions", "import_map.json"),
+		FallbackEnvFilePath:   filepath.Join(base, "functions", ".env"),
+		DbTestsDir:            filepath.Join(base, "tests"),
+		SeedDataPath:          filepath.Join(base, "seed.sql"),
+		CustomRolesPath:       filepath.Join(base, "roles.sql"),
+	}
+}
+
+func ptr[T any](v T) *T {
+	return &v
+}
+
+func sliceContains[T comparable](s []T, e T) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
+
+func removeDuplicates(slice []string) (result []string) {
+	set := make(map[string]struct{})
+	for _, item := range slice {
+		if _, exists := set[item]; !exists {
+			set[item] = struct{}{}
+			result = append(result, item)
+		}
+	}
+	return result
+}
+
+func replaceImageTag(image string, tag string) string {
+	index := strings.IndexByte(image, ':')
+	return image[:index+1] + strings.TrimSpace(tag)
+}
diff --git a/internal/utils/parser/state.go b/pkg/parser/state.go
similarity index 100%
rename from internal/utils/parser/state.go
rename to pkg/parser/state.go
diff --git a/internal/utils/parser/state_test.go b/pkg/parser/state_test.go
similarity index 100%
rename from internal/utils/parser/state_test.go
rename to pkg/parser/state_test.go
diff --git a/internal/utils/parser/testdata/all.sql b/pkg/parser/testdata/all.sql
similarity index 100%
rename from internal/utils/parser/testdata/all.sql
rename to pkg/parser/testdata/all.sql
diff --git a/internal/utils/parser/testdata/split_00.sql b/pkg/parser/testdata/split_00.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_00.sql
rename to pkg/parser/testdata/split_00.sql
diff --git a/internal/utils/parser/testdata/split_01.sql b/pkg/parser/testdata/split_01.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_01.sql
rename to pkg/parser/testdata/split_01.sql
diff --git a/internal/utils/parser/testdata/split_02.sql b/pkg/parser/testdata/split_02.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_02.sql
rename to pkg/parser/testdata/split_02.sql
diff --git a/internal/utils/parser/testdata/split_03.sql b/pkg/parser/testdata/split_03.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_03.sql
rename to pkg/parser/testdata/split_03.sql
diff --git a/internal/utils/parser/testdata/split_04.sql b/pkg/parser/testdata/split_04.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_04.sql
rename to pkg/parser/testdata/split_04.sql
diff --git a/internal/utils/parser/testdata/split_05.sql b/pkg/parser/testdata/split_05.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_05.sql
rename to pkg/parser/testdata/split_05.sql
diff --git a/internal/utils/parser/testdata/split_06.sql b/pkg/parser/testdata/split_06.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_06.sql
rename to pkg/parser/testdata/split_06.sql
diff --git a/internal/utils/parser/testdata/split_07.sql b/pkg/parser/testdata/split_07.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_07.sql
rename to pkg/parser/testdata/split_07.sql
diff --git a/internal/utils/parser/testdata/split_08.sql b/pkg/parser/testdata/split_08.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_08.sql
rename to pkg/parser/testdata/split_08.sql
diff --git a/internal/utils/parser/testdata/split_09.sql b/pkg/parser/testdata/split_09.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_09.sql
rename to pkg/parser/testdata/split_09.sql
diff --git a/internal/utils/parser/testdata/split_10.sql b/pkg/parser/testdata/split_10.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_10.sql
rename to pkg/parser/testdata/split_10.sql
diff --git a/internal/utils/parser/testdata/split_11.sql b/pkg/parser/testdata/split_11.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_11.sql
rename to pkg/parser/testdata/split_11.sql
diff --git a/internal/utils/parser/testdata/split_12.sql b/pkg/parser/testdata/split_12.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_12.sql
rename to pkg/parser/testdata/split_12.sql
diff --git a/internal/utils/parser/testdata/split_13.sql b/pkg/parser/testdata/split_13.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_13.sql
rename to pkg/parser/testdata/split_13.sql
diff --git a/internal/utils/parser/testdata/split_14.sql b/pkg/parser/testdata/split_14.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_14.sql
rename to pkg/parser/testdata/split_14.sql
diff --git a/internal/utils/parser/testdata/split_15.sql b/pkg/parser/testdata/split_15.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_15.sql
rename to pkg/parser/testdata/split_15.sql
diff --git a/internal/utils/parser/testdata/split_16.sql b/pkg/parser/testdata/split_16.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_16.sql
rename to pkg/parser/testdata/split_16.sql
diff --git a/internal/utils/parser/testdata/split_17.sql b/pkg/parser/testdata/split_17.sql
similarity index 100%
rename from internal/utils/parser/testdata/split_17.sql
rename to pkg/parser/testdata/split_17.sql
diff --git a/internal/utils/parser/token.go b/pkg/parser/token.go
similarity index 100%
rename from internal/utils/parser/token.go
rename to pkg/parser/token.go
diff --git a/internal/utils/parser/token_test.go b/pkg/parser/token_test.go
similarity index 100%
rename from internal/utils/parser/token_test.go
rename to pkg/parser/token_test.go
diff --git a/internal/utils/pgxv5/rows.go b/pkg/pgxv5/rows.go
similarity index 100%
rename from internal/utils/pgxv5/rows.go
rename to pkg/pgxv5/rows.go
diff --git a/tools/listdep/main.go b/tools/listdep/main.go
index 1efca3c5..bf3c8898 100644
--- a/tools/listdep/main.go
+++ b/tools/listdep/main.go
@@ -6,12 +6,12 @@ import (
 	"os"
 	"strings"
 
-	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
 func main() {
 	external := make([]string, 0)
-	for _, img := range utils.ServiceImages {
+	for _, img := range config.ServiceImages {
 		if !strings.HasPrefix(img, "supabase/") ||
 			strings.HasPrefix(img, "supabase/logflare") {
 			external = append(external, img)
diff --git a/tools/publish/main.go b/tools/publish/main.go
index 685ca7f2..7ff943ff 100644
--- a/tools/publish/main.go
+++ b/tools/publish/main.go
@@ -112,7 +112,7 @@ func updatePackage(ctx context.Context, client *github.Client, repo, path string
 	fmt.Fprintf(os.Stderr, "Updating %s: %s\n", repo, path)
 	// Render formula from template
 	var buf bytes.Buffer
-	if err := tmpl.Execute(&buf, config); err != nil {
+	if err := tmpl.Option("missingkey=error").Execute(&buf, config); err != nil {
 		return err
 	}
 	branch := "release/cli"
diff --git a/tools/selfhost/main.go b/tools/selfhost/main.go
index a58e9617..10b0f2e1 100644
--- a/tools/selfhost/main.go
+++ b/tools/selfhost/main.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/google/go-github/v62/github"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 	"github.com/supabase/cli/tools/shared"
 	"gopkg.in/yaml.v3"
 )
@@ -60,7 +61,7 @@ func updateSelfHosted(ctx context.Context, branch string) error {
 }
 
 func getStableVersions() map[string]string {
-	images := append([]string{utils.Pg15Image}, utils.ServiceImages...)
+	images := append([]string{config.Pg15Image}, config.ServiceImages...)
 	result := make(map[string]string, len(images))
 	for _, img := range images {
 		parts := strings.Split(img, ":")


Test patch:
diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go
index edae9ca0..4d36a8b1 100644
--- a/internal/db/diff/diff_test.go
+++ b/internal/db/diff/diff_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/supabase/cli/internal/testing/fstest"
 	"github.com/supabase/cli/internal/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
 var dbConfig = pgconn.Config{
@@ -57,7 +58,7 @@ func TestRun(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
 			Reply(http.StatusOK)
@@ -70,13 +71,13 @@ func TestRun(t *testing.T) {
 					Health:  &types.Health{Status: "healthy"},
 				},
 			}})
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-shadow-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-shadow-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-realtime", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-shadow-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-shadow-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-storage", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-shadow-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-shadow-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-auth", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.MigraImage), "test-migra")
 		diff := "create table test();"
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff))
 		// Setup mock postgres
@@ -133,7 +134,7 @@ func TestRun(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json").
 			ReplyError(errors.New("network error"))
 		// Run test
 		err := Run(context.Background(), []string{"public"}, "file", dbConfig, DiffSchemaMigra, fsys)
@@ -149,7 +150,7 @@ func TestMigrateShadow(t *testing.T) {
 	t.Run("migrates shadow database", func(t *testing.T) {
 		utils.Config.Db.ShadowPort = 54320
 		utils.GlobalsSql = "create schema public"
-		utils.InitialSchemaSql = "create schema private"
+		utils.InitialSchemaPg14Sql = "create schema private"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
@@ -160,7 +161,7 @@ func TestMigrateShadow(t *testing.T) {
 		defer conn.Close(t)
 		conn.Query(utils.GlobalsSql).
 			Reply("CREATE SCHEMA").
-			Query(utils.InitialSchemaSql).
+			Query(utils.InitialSchemaPg14Sql).
 			Reply("CREATE SCHEMA")
 		pgtest.MockMigrationHistory(conn)
 		conn.Query(sql).
@@ -214,10 +215,9 @@ func TestMigrateShadow(t *testing.T) {
 
 func TestDiffDatabase(t *testing.T) {
 	utils.Config.Db.MajorVersion = 14
-	utils.Config.Db.Image = utils.Pg14Image
 	utils.Config.Db.ShadowPort = 54320
 	utils.GlobalsSql = "create schema public"
-	utils.InitialSchemaSql = "create schema private"
+	utils.InitialSchemaPg14Sql = "create schema private"
 
 	t.Run("throws error on failure to create shadow", func(t *testing.T) {
 		// Setup in-memory fs
@@ -226,7 +226,7 @@ func TestDiffDatabase(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg14Image) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json").
 			ReplyError(errors.New("network error"))
 		// Run test
 		diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra)
@@ -243,7 +243,7 @@ func TestDiffDatabase(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
 			Reply(http.StatusOK).
@@ -273,7 +273,7 @@ func TestDiffDatabase(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
 			Reply(http.StatusOK).
@@ -309,7 +309,7 @@ At statement 0: create schema public`)
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
 			Reply(http.StatusOK).
@@ -322,7 +322,7 @@ At statement 0: create schema public`)
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
 			Reply(http.StatusOK)
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.MigraImage), "test-migra")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.MigraImage), "test-migra")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs").
 			ReplyError(errors.New("network error"))
@@ -334,7 +334,7 @@ At statement 0: create schema public`)
 		defer conn.Close(t)
 		conn.Query(utils.GlobalsSql).
 			Reply("CREATE SCHEMA").
-			Query(utils.InitialSchemaSql).
+			Query(utils.InitialSchemaPg14Sql).
 			Reply("CREATE SCHEMA")
 		pgtest.MockMigrationHistory(conn)
 		conn.Query(sql).
diff --git a/internal/db/dump/dump_test.go b/internal/db/dump/dump_test.go
index 952af162..0d5491ea 100644
--- a/internal/db/dump/dump_test.go
+++ b/internal/db/dump/dump_test.go
@@ -23,7 +23,7 @@ var dbConfig = pgconn.Config{
 }
 
 func TestPullCommand(t *testing.T) {
-	imageUrl := utils.GetRegistryImageUrl(utils.Pg15Image)
+	imageUrl := utils.GetRegistryImageUrl(utils.Config.Db.Image)
 	const containerId = "test-container"
 
 	t.Run("pulls from remote", func(t *testing.T) {
diff --git a/internal/db/pull/pull_test.go b/internal/db/pull/pull_test.go
index 37c1ee8f..76d61607 100644
--- a/internal/db/pull/pull_test.go
+++ b/internal/db/pull/pull_test.go
@@ -88,7 +88,7 @@ func TestPullSchema(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", "test"))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go
index 3edc2e36..6715feea 100644
--- a/internal/db/reset/reset_test.go
+++ b/internal/db/reset/reset_test.go
@@ -96,11 +96,11 @@ func TestResetCommand(t *testing.T) {
 func TestInitDatabase(t *testing.T) {
 	t.Run("initializes postgres database", func(t *testing.T) {
 		utils.Config.Db.Port = 54322
-		utils.InitialSchemaSql = "CREATE SCHEMA public"
+		utils.InitialSchemaPg14Sql = "create schema private"
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(utils.InitialSchemaSql).
+		conn.Query(utils.InitialSchemaPg14Sql).
 			Reply("CREATE SCHEMA")
 		// Run test
 		assert.NoError(t, initDatabase(context.Background(), conn.Intercept))
@@ -116,11 +116,11 @@ func TestInitDatabase(t *testing.T) {
 
 	t.Run("throws error on duplicate schema", func(t *testing.T) {
 		utils.Config.Db.Port = 54322
-		utils.InitialSchemaSql = "CREATE SCHEMA public"
+		utils.InitialSchemaPg14Sql = "create schema private"
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(utils.InitialSchemaSql).
+		conn.Query(utils.InitialSchemaPg14Sql).
 			ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`)
 		// Run test
 		err := initDatabase(context.Background(), conn.Intercept)
diff --git a/internal/db/start/start_test.go b/internal/db/start/start_test.go
index 40c9b034..d3972332 100644
--- a/internal/db/start/start_test.go
+++ b/internal/db/start/start_test.go
@@ -52,7 +52,6 @@ func TestInitBranch(t *testing.T) {
 func TestStartDatabase(t *testing.T) {
 	t.Run("initialize main branch", func(t *testing.T) {
 		utils.Config.Db.MajorVersion = 15
-		utils.Config.Db.Image = utils.Pg15Image
 		utils.DbId = "supabase_db_test"
 		utils.ConfigId = "supabase_config_test"
 		utils.Config.Db.Port = 5432
@@ -79,11 +78,11 @@ func TestStartDatabase(t *testing.T) {
 					Health:  &types.Health{Status: "healthy"},
 				},
 			}})
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
@@ -105,7 +104,6 @@ func TestStartDatabase(t *testing.T) {
 
 	t.Run("recover from backup volume", func(t *testing.T) {
 		utils.Config.Db.MajorVersion = 14
-		utils.Config.Db.Image = utils.Pg15Image
 		utils.DbId = "supabase_db_test"
 		utils.ConfigId = "supabase_config_test"
 		utils.Config.Db.Port = 5432
@@ -141,7 +139,6 @@ func TestStartDatabase(t *testing.T) {
 
 	t.Run("throws error on start failure", func(t *testing.T) {
 		utils.Config.Db.MajorVersion = 15
-		utils.Config.Db.Image = utils.Pg15Image
 		utils.DbId = "supabase_db_test"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
@@ -222,7 +219,7 @@ func TestStartCommand(t *testing.T) {
 			Get("/v" + utils.Docker.ClientVersion() + "/volumes/").
 			ReplyError(errors.New("network error"))
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Pg15Image) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json").
 			ReplyError(errors.New("network error"))
 		// Cleanup resources
 		apitest.MockDockerStop(utils.Docker)
@@ -244,7 +241,7 @@ func TestSetupDatabase(t *testing.T) {
 		}()
 		utils.Config.Db.Port = 5432
 		utils.GlobalsSql = "create schema public"
-		utils.InitialSchemaSql = "create schema private"
+		utils.InitialSchemaPg14Sql = "create schema private"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		roles := "create role postgres"
@@ -254,7 +251,7 @@ func TestSetupDatabase(t *testing.T) {
 		defer conn.Close(t)
 		conn.Query(utils.GlobalsSql).
 			Reply("CREATE SCHEMA").
-			Query(utils.InitialSchemaSql).
+			Query(utils.InitialSchemaPg14Sql).
 			Reply("CREATE SCHEMA").
 			Query(roles).
 			Reply("CREATE ROLE")
@@ -279,7 +276,7 @@ func TestSetupDatabase(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.RealtimeImage) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Realtime.Image) + "/json").
 			ReplyError(errors.New("network error"))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
@@ -298,11 +295,11 @@ func TestSetupDatabase(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
diff --git a/internal/db/test/test.go b/internal/db/test/test.go
index 8ab6989e..03aa3a54 100644
--- a/internal/db/test/test.go
+++ b/internal/db/test/test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 	"github.com/supabase/cli/internal/utils"
+	cliConfig "github.com/supabase/cli/pkg/config"
 )
 
 const (
@@ -77,7 +78,7 @@ func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afe
 	return utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
-			Image: utils.PgProveImage,
+			Image: cliConfig.PgProveImage,
 			Env: []string{
 				"PGHOST=" + config.Host,
 				fmt.Sprintf("PGPORT=%d", config.Port),
diff --git a/internal/db/test/test_test.go b/internal/db/test/test_test.go
index 0fb6003e..16d00dbe 100644
--- a/internal/db/test/test_test.go
+++ b/internal/db/test/test_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 )
 
 var dbConfig = pgconn.Config{
@@ -40,7 +41,7 @@ func TestRunCommand(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		containerId := "test-pg-prove"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.PgProveImage), containerId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.PgProveImage), containerId)
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "Result: SUCCESS"))
 		// Run test
 		err := Run(context.Background(), []string{"nested"}, dbConfig, fsys, conn.Intercept)
@@ -89,7 +90,7 @@ func TestRunCommand(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.PgProveImage) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(config.PgProveImage) + "/json").
 			ReplyError(errNetwork)
 		// Run test
 		err := Run(context.Background(), nil, dbConfig, fsys, conn.Intercept)
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 8d5d5cd2..2fde3f64 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -23,7 +23,7 @@ import (
 
 func TestDeployOne(t *testing.T) {
 	const slug = "test-func"
-	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
+	imageUrl := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image)
 	utils.EdgeRuntimeId = "test-edge-runtime"
 	const containerId = "test-container"
 
@@ -151,7 +151,7 @@ func TestDeployOne(t *testing.T) {
 
 func TestDeployAll(t *testing.T) {
 	const slug = "test-func"
-	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
+	imageUrl := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image)
 	utils.EdgeRuntimeId = "test-edge-runtime"
 	const containerId = "test-container"
 
@@ -225,7 +225,7 @@ func TestDeployAll(t *testing.T) {
 
 func TestDeployCommand(t *testing.T) {
 	const slug = "test-func"
-	imageUrl := utils.GetRegistryImageUrl(utils.EdgeRuntimeImage)
+	imageUrl := utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image)
 	utils.EdgeRuntimeId = "test-edge-runtime"
 	const containerId = "test-container"
 
diff --git a/internal/functions/serve/serve_test.go b/internal/functions/serve/serve_test.go
index ab840590..5b98ece8 100644
--- a/internal/functions/serve/serve_test.go
+++ b/internal/functions/serve/serve_test.go
@@ -34,7 +34,7 @@ func TestServeCommand(t *testing.T) {
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + containerId).
 			Reply(http.StatusOK)
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.EdgeRuntimeImage), containerId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), containerId)
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "success"))
 		// Run test
 		err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys)
diff --git a/internal/gen/types/types_test.go b/internal/gen/types/types_test.go
index 21b14eb6..125bc569 100644
--- a/internal/gen/types/types_test.go
+++ b/internal/gen/types/types_test.go
@@ -32,7 +32,7 @@ func TestGenLocalCommand(t *testing.T) {
 
 	t.Run("generates typescript types", func(t *testing.T) {
 		const containerId = "test-pgmeta"
-		imageUrl := utils.GetRegistryImageUrl(utils.PgmetaImage)
+		imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage)
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup mock docker
@@ -90,7 +90,7 @@ func TestGenLocalCommand(t *testing.T) {
 
 	t.Run("generates swift types", func(t *testing.T) {
 		const containerId = "test-pgmeta"
-		imageUrl := utils.GetRegistryImageUrl(utils.PgmetaImage)
+		imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage)
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup mock docker
@@ -174,7 +174,7 @@ func TestGenRemoteCommand(t *testing.T) {
 
 	t.Run("generates type from remote db", func(t *testing.T) {
 		const containerId = "test-pgmeta"
-		imageUrl := utils.GetRegistryImageUrl(utils.PgmetaImage)
+		imageUrl := utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage)
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
diff --git a/internal/migration/repair/repair_test.go b/internal/migration/repair/repair_test.go
index 16586ed3..d5a18fc0 100644
--- a/internal/migration/repair/repair_test.go
+++ b/internal/migration/repair/repair_test.go
@@ -20,7 +20,7 @@ import (
 	"github.com/supabase/cli/internal/testing/fstest"
 	"github.com/supabase/cli/internal/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/parser"
+	"github.com/supabase/cli/pkg/parser"
 )
 
 var dbConfig = pgconn.Config{
diff --git a/internal/migration/squash/squash_test.go b/internal/migration/squash/squash_test.go
index a91c0651..fd810683 100644
--- a/internal/migration/squash/squash_test.go
+++ b/internal/migration/squash/squash_test.go
@@ -53,7 +53,7 @@ func TestSquashCommand(t *testing.T) {
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-shadow-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
 			Reply(http.StatusOK).
@@ -66,17 +66,17 @@ func TestSquashCommand(t *testing.T) {
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
 			Reply(http.StatusOK)
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
@@ -189,7 +189,6 @@ func TestSquashVersion(t *testing.T) {
 
 func TestSquashMigrations(t *testing.T) {
 	utils.Config.Db.MajorVersion = 15
-	utils.Config.Db.Image = utils.Pg15Image
 	utils.Config.Db.ShadowPort = 54320
 
 	t.Run("throws error on shadow create failure", func(t *testing.T) {
@@ -258,7 +257,7 @@ func TestSquashMigrations(t *testing.T) {
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
 			Reply(http.StatusOK)
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.RealtimeImage) + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Realtime.Image) + "/json").
 			ReplyError(errors.New("network error"))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
@@ -292,15 +291,15 @@ func TestSquashMigrations(t *testing.T) {
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
 			Reply(http.StatusOK)
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql))
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg15Image), "test-db")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-db")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-db", sql))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
diff --git a/internal/start/start_test.go b/internal/start/start_test.go
index 16d8a54e..a7404091 100644
--- a/internal/start/start_test.go
+++ b/internal/start/start_test.go
@@ -19,6 +19,7 @@ import (
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/testing/pgtest"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/config"
 	"github.com/supabase/cli/pkg/storage"
 )
 
@@ -86,13 +87,12 @@ func TestDatabaseStart(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(types.NetworkCreateResponse{})
 		// Caches all dependencies
-		utils.Config.Db.Image = utils.Pg15Image
 		imageUrl := utils.GetRegistryImageUrl(utils.Config.Db.Image)
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/images/" + imageUrl + "/json").
 			Reply(http.StatusOK).
 			JSON(types.ImageInspect{})
-		for _, image := range utils.ServiceImages {
+		for _, image := range config.ServiceImages {
 			service := utils.GetRegistryImageUrl(image)
 			gock.New(utils.Docker.DaemonHost()).
 				Get("/v" + utils.Docker.ClientVersion() + "/images/" + service + "/json").
@@ -108,45 +108,43 @@ func TestDatabaseStart(t *testing.T) {
 			Get("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
 			Reply(http.StatusNotFound)
 		apitest.MockDockerStart(utils.Docker, imageUrl, utils.DbId)
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), "test-realtime")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-realtime", ""))
-		utils.Config.Storage.Image = utils.StorageImage
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), "test-storage")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), "test-storage")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-storage", ""))
-		utils.Config.Auth.Image = utils.GotrueImage
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), "test-auth")
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-auth")
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-auth", ""))
 		// Start services
 		utils.KongId = "test-kong"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.KongImage), utils.KongId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Api.KongImage), utils.KongId)
 		utils.GotrueId = "test-gotrue"
 		utils.Config.Auth.EnableSignup = true
 		utils.Config.Auth.Email.EnableSignup = true
 		utils.Config.Auth.Email.DoubleConfirmChanges = true
 		utils.Config.Auth.Email.EnableConfirmations = true
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.GotrueImage), utils.GotrueId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), utils.GotrueId)
 		utils.InbucketId = "test-inbucket"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.InbucketImage), utils.InbucketId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Inbucket.Image), utils.InbucketId)
 		utils.RealtimeId = "test-realtime"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.RealtimeImage), utils.RealtimeId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), utils.RealtimeId)
 		utils.RestId = "test-rest"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.PostgrestImage), utils.RestId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Api.Image), utils.RestId)
 		utils.StorageId = "test-storage"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StorageImage), utils.StorageId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), utils.StorageId)
 		utils.ImgProxyId = "test-imgproxy"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.ImageProxyImage), utils.ImgProxyId)
-		utils.DifferId = "test-differ"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.DifferImage), utils.DifferId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.ImageTransformation.Image), utils.ImgProxyId)
+		// utils.DifferId = "test-differ"
+		// apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.DifferImage), utils.DifferId)
 		utils.EdgeRuntimeId = "test-edge-runtime"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.EdgeRuntimeImage), utils.EdgeRuntimeId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), utils.EdgeRuntimeId)
 		utils.PgmetaId = "test-pgmeta"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.PgmetaImage), utils.PgmetaId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Studio.PgmetaImage), utils.PgmetaId)
 		utils.StudioId = "test-studio"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.StudioImage), utils.StudioId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Studio.Image), utils.StudioId)
 		utils.LogflareId = "test-logflare"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.LogflareImage), utils.LogflareId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Analytics.Image), utils.LogflareId)
 		utils.VectorId = "test-vector"
-		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.VectorImage), utils.VectorId)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Analytics.VectorImage), utils.VectorId)
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
@@ -167,10 +165,10 @@ func TestDatabaseStart(t *testing.T) {
 					},
 				}})
 		}
-		gock.New(utils.GetApiUrl("")).
+		gock.New(utils.Config.Api.ExternalUrl).
 			Head("/rest-admin/v1/ready").
 			Reply(http.StatusOK)
-		gock.New(utils.GetApiUrl("")).
+		gock.New(utils.Config.Api.ExternalUrl).
 			Head("/functions/v1/_internal/health").
 			Reply(http.StatusOK)
 		// Seed tenant services
@@ -183,7 +181,7 @@ func TestDatabaseStart(t *testing.T) {
 					Health:  &types.Health{Status: "healthy"},
 				},
 			}})
-		gock.New(utils.GetApiUrl("")).
+		gock.New(utils.Config.Api.ExternalUrl).
 			Get("/storage/v1/bucket").
 			Reply(http.StatusOK).
 			JSON([]storage.BucketResponse{})
@@ -207,7 +205,6 @@ func TestDatabaseStart(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(types.NetworkCreateResponse{})
 		// Caches all dependencies
-		utils.Config.Db.Image = utils.Pg15Image
 		imageUrl := utils.GetRegistryImageUrl(utils.Config.Db.Image)
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/images/" + imageUrl + "/json").
diff --git a/internal/utils/deno_test.go b/internal/utils/deno_test.go
index 72fbe71e..fa12c13a 100644
--- a/internal/utils/deno_test.go
+++ b/internal/utils/deno_test.go
@@ -1,10 +1,12 @@
 package utils
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 	"testing"
 
+	"github.com/BurntSushi/toml"
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -110,9 +112,9 @@ func TestImportMapPath(t *testing.T) {
 
 	t.Run("per function config takes precedence", func(t *testing.T) {
 		slug := "hello"
-		Config.Functions = map[string]function{
-			slug: {ImportMap: "import_map.json"},
-		}
+		config := fmt.Sprintf("[%s]\n", slug)
+		config += `import_map = "import_map.json"`
+		require.NoError(t, toml.Unmarshal([]byte(config), &Config.Functions))
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, []byte("{}"), 0644))
@@ -124,9 +126,9 @@ func TestImportMapPath(t *testing.T) {
 
 	t.Run("overrides with cli flag", func(t *testing.T) {
 		slug := "hello"
-		Config.Functions = map[string]function{
-			slug: {ImportMap: "import_map.json"},
-		}
+		config := fmt.Sprintf("[%s]\n", slug)
+		config += `import_map = "import_map.json"`
+		require.NoError(t, toml.Unmarshal([]byte(config), &Config.Functions))
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, []byte("{}"), 0644))
diff --git a/internal/utils/config_test.go b/pkg/config/config_test.go
similarity index 83%
rename from internal/utils/config_test.go
rename to pkg/config/config_test.go
index 353b7cc1..85b7a572 100644
--- a/internal/utils/config_test.go
+++ b/pkg/config/config_test.go
@@ -1,63 +1,59 @@
-package utils
+package config
 
 import (
+	"bytes"
 	_ "embed"
 	"strings"
 	"testing"
-	"text/template"
+	fs "testing/fstest"
 
 	"github.com/BurntSushi/toml"
-	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-var (
-	//go:embed templates/init_config.test.toml
-	testInitConfigEmbed    string
-	testInitConfigTemplate = template.Must(template.New("initConfig.test").Parse(testInitConfigEmbed))
-)
+//go:embed testdata/config.toml
+var testInitConfigEmbed []byte
 
 func TestConfigParsing(t *testing.T) {
-	// Reset global variable
-	copy := initConfigTemplate
-	teardown := func() {
-		initConfigTemplate = copy
-	}
-
 	t.Run("classic config file", func(t *testing.T) {
-		defer teardown()
-		fsys := afero.NewMemMapFs()
-		assert.NoError(t, WriteConfig(fsys, false))
-		assert.NoError(t, LoadConfigFS(fsys))
+		config := NewConfig()
+		// Run test
+		var buf bytes.Buffer
+		require.NoError(t, config.Eject(&buf))
+		file := fs.MapFile{Data: buf.Bytes()}
+		fsys := fs.MapFS{"config.toml": &file}
+		// Check error
+		assert.NoError(t, config.Load("config.toml", fsys))
 	})
 
 	t.Run("config file with environment variables", func(t *testing.T) {
-		defer teardown()
-		initConfigTemplate = testInitConfigTemplate
+		config := NewConfig()
 		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		assert.NoError(t, afero.WriteFile(fsys, "supabase/templates/invite.html", nil, 0644))
-		assert.NoError(t, WriteConfig(fsys, true))
+		fsys := fs.MapFS{
+			"supabase/config.toml":           &fs.MapFile{Data: testInitConfigEmbed},
+			"supabase/templates/invite.html": &fs.MapFile{},
+		}
 		// Run test
 		t.Setenv("TWILIO_AUTH_TOKEN", "token")
 		t.Setenv("AZURE_CLIENT_ID", "hello")
 		t.Setenv("AZURE_SECRET", "this is cool")
 		t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==")
 		t.Setenv("SENDGRID_API_KEY", "sendgrid")
-		assert.NoError(t, LoadConfigFS(fsys))
+		assert.NoError(t, config.Load("", fsys))
 		// Check error
-		assert.Equal(t, "hello", Config.Auth.External["azure"].ClientId)
-		assert.Equal(t, "this is cool", Config.Auth.External["azure"].Secret)
+		assert.Equal(t, "hello", config.Auth.External["azure"].ClientId)
+		assert.Equal(t, "this is cool", config.Auth.External["azure"].Secret)
 	})
 
 	t.Run("config file with environment variables fails when unset", func(t *testing.T) {
-		defer teardown()
-		initConfigTemplate = testInitConfigTemplate
+		config := NewConfig()
 		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		assert.NoError(t, WriteConfig(fsys, true))
+		fsys := fs.MapFS{
+			"supabase/config.toml": &fs.MapFile{Data: testInitConfigEmbed},
+		}
 		// Run test
-		assert.Error(t, LoadConfigFS(fsys))
+		assert.Error(t, config.Load("", fsys))
 	})
 }
 
diff --git a/internal/utils/templates/init_config.test.toml b/pkg/config/testdata/config.toml
similarity index 98%
rename from internal/utils/templates/init_config.test.toml
rename to pkg/config/testdata/config.toml
index 575c24d1..68d53d7e 100644
--- a/internal/utils/templates/init_config.test.toml
+++ b/pkg/config/testdata/config.toml
@@ -1,6 +1,6 @@
 # A string used to distinguish different Supabase projects on the same host. Defaults to the
 # working directory name when running `supabase init`.
-project_id = "{{ .ProjectId }}"
+project_id = "test"
 
 [api]
 enabled = true
@@ -121,7 +121,7 @@ sender_name = "Admin"
 # Uncomment to customize email template
 [auth.email.template.invite]
 subject = "You have been invited"
-content_path = "./supabase/templates/invite.html"
+content_path = "supabase/templates/invite.html"
 
 [auth.sms]
 # Allow/disallow new user signups via SMS to your project.