Repository: supabase/cli
Pull request #2712 (2024-09-27 04:15:56)

chore(deps): bump bin-links from 4.0.4 to 5.0.0
Bumps [bin-links](https://github.com/npm/bin-links) from 4.0.4 to 5.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/npm/bin-links/releases">bin-links's releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2><a href="https://github.com/npm/bin-links/compare/v4.0.4...v5.0.0">5.0.0</a> (2024-09-25)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>bin-links</code> now supports node <code>^18.17.0 || &gt;=20.5.0</code></li>
</ul>
<h3>Features</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/9648a7d49589be1c991ddc3c2842c461e2a017cb"><code>9648a7d</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/121">#121</a> ignore EACCES on linking (<a href="https://github.com/antongolub"><code>@​antongolub</code></a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/013be506a28d014e0db023457e084789f414b287"><code>013be50</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> align to npm 10 node engine range (<a href="https://github.com/reggi"><code>@​reggi</code></a>)</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/a08d09b7e823a8ca321bcab3f6168bdd8057e3cd"><code>a08d09b</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>write-file-atomic@6.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/7d9029869e4759dcaa5f90667c62840898c07c6a"><code>7d90298</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>read-cmd-shim@5.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/0c915a3daf8548f7b9d8576ff53cd92db575eeec"><code>0c915a3</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>proc-log@5.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/502417c398b67175d9961f954eb6e54388d4559d"><code>502417c</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>npm-normalize-package-bin@4.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/722452a3276803217ffd866cd5d65383d74c4591"><code>722452a</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>cmd-shim@7.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/af7e347bbf2346741500df4906c948f5f02e19fb"><code>af7e347</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/121">#121</a> add proc-log</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/463c32fed8b131c9cea5488fcfe9102200cbf6e9"><code>463c32f</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> run template-oss-apply (<a href="https://github.com/reggi"><code>@​reggi</code></a>)</li>
<li><a href="https://github.com/npm/bin-links/commit/ba52600cf31a9bfc9db983e6fc622291a61c985a"><code>ba52600</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/133">#133</a> bump <code>@​npmcli/eslint-config</code> from 4.0.5 to 5.0.0 (<a href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a href="https://github.com/npm/bin-links/commit/fdf22ae617daa5ad6516c14def9e291f2367542c"><code>fdf22ae</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/134">#134</a> postinstall for dependabot template-oss PR (<a href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a href="https://github.com/npm/bin-links/commit/d949f7de78aa0d58e0cf228f816215678878278c"><code>d949f7d</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/134">#134</a> bump <code>@​npmcli/template-oss</code> from 4.23.1 to 4.23.3 (<a href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/npm/bin-links/blob/main/CHANGELOG.md">bin-links's changelog</a>.</em></p>
<blockquote>
<h2><a href="https://github.com/npm/bin-links/compare/v4.0.4...v5.0.0">5.0.0</a> (2024-09-25)</h2>
<h3>⚠️ BREAKING CHANGES</h3>
<ul>
<li><code>bin-links</code> now supports node <code>^18.17.0 || &gt;=20.5.0</code></li>
</ul>
<h3>Features</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/9648a7d49589be1c991ddc3c2842c461e2a017cb"><code>9648a7d</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/121">#121</a> ignore EACCES on linking (<a href="https://github.com/antongolub"><code>@​antongolub</code></a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/013be506a28d014e0db023457e084789f414b287"><code>013be50</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> align to npm 10 node engine range (<a href="https://github.com/reggi"><code>@​reggi</code></a>)</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/a08d09b7e823a8ca321bcab3f6168bdd8057e3cd"><code>a08d09b</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>write-file-atomic@6.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/7d9029869e4759dcaa5f90667c62840898c07c6a"><code>7d90298</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>read-cmd-shim@5.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/0c915a3daf8548f7b9d8576ff53cd92db575eeec"><code>0c915a3</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>proc-log@5.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/502417c398b67175d9961f954eb6e54388d4559d"><code>502417c</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>npm-normalize-package-bin@4.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/722452a3276803217ffd866cd5d65383d74c4591"><code>722452a</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> <code>cmd-shim@7.0.0</code></li>
<li><a href="https://github.com/npm/bin-links/commit/af7e347bbf2346741500df4906c948f5f02e19fb"><code>af7e347</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/121">#121</a> add proc-log</li>
</ul>
<h3>Chores</h3>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/463c32fed8b131c9cea5488fcfe9102200cbf6e9"><code>463c32f</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/139">#139</a> run template-oss-apply (<a href="https://github.com/reggi"><code>@​reggi</code></a>)</li>
<li><a href="https://github.com/npm/bin-links/commit/ba52600cf31a9bfc9db983e6fc622291a61c985a"><code>ba52600</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/133">#133</a> bump <code>@​npmcli/eslint-config</code> from 4.0.5 to 5.0.0 (<a href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
<li><a href="https://github.com/npm/bin-links/commit/fdf22ae617daa5ad6516c14def9e291f2367542c"><code>fdf22ae</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/134">#134</a> postinstall for dependabot template-oss PR (<a href="https://github.com/hashtagchris"><code>@​hashtagchris</code></a>)</li>
<li><a href="https://github.com/npm/bin-links/commit/d949f7de78aa0d58e0cf228f816215678878278c"><code>d949f7d</code></a> <a href="https://redirect.github.com/npm/bin-links/pull/134">#134</a> bump <code>@​npmcli/template-oss</code> from 4.23.1 to 4.23.3 (<a href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot])</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/npm/bin-links/commit/21614a8d3f0479136a76648e2f9364aad3ffdd55"><code>21614a8</code></a> chore: release 5.0.0 (<a href="https://redirect.github.com/npm/bin-links/issues/122">#122</a>)</li>
<li><a href="https://github.com/npm/bin-links/commit/a08d09b7e823a8ca321bcab3f6168bdd8057e3cd"><code>a08d09b</code></a> deps: write-file-atomic@6.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/7d9029869e4759dcaa5f90667c62840898c07c6a"><code>7d90298</code></a> deps: read-cmd-shim@5.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/0c915a3daf8548f7b9d8576ff53cd92db575eeec"><code>0c915a3</code></a> deps: proc-log@5.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/502417c398b67175d9961f954eb6e54388d4559d"><code>502417c</code></a> deps: npm-normalize-package-bin@4.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/722452a3276803217ffd866cd5d65383d74c4591"><code>722452a</code></a> deps: cmd-shim@7.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/463c32fed8b131c9cea5488fcfe9102200cbf6e9"><code>463c32f</code></a> chore: run template-oss-apply</li>
<li><a href="https://github.com/npm/bin-links/commit/013be506a28d014e0db023457e084789f414b287"><code>013be50</code></a> fix!: align to npm 10 node engine range</li>
<li><a href="https://github.com/npm/bin-links/commit/ba52600cf31a9bfc9db983e6fc622291a61c985a"><code>ba52600</code></a> chore: bump <code>@​npmcli/eslint-config</code> from 4.0.5 to 5.0.0</li>
<li><a href="https://github.com/npm/bin-links/commit/fdf22ae617daa5ad6516c14def9e291f2367542c"><code>fdf22ae</code></a> chore: postinstall for dependabot template-oss PR</li>
<li>Additional commits viewable in <a href="https://github.com/npm/bin-links/compare/v4.0.4...v5.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bin-links&package-manager=npm_and_yarn&previous-version=4.0.4&new-version=5.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/go.mod b/go.mod
index 1d790e82..aff55f8e 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/charmbracelet/bubbletea v0.25.0
 	github.com/charmbracelet/glamour v0.7.0
 	github.com/charmbracelet/lipgloss v0.12.1
-	github.com/containers/common v0.60.2
+	github.com/containers/common v0.60.4
 	github.com/danieljoos/wincred v1.2.1
 	github.com/deepmap/oapi-codegen/v2 v2.2.0
 	github.com/docker/cli v27.2.1+incompatible
diff --git a/go.sum b/go.sum
index 4fdaab3f..43131167 100644
--- a/go.sum
+++ b/go.sum
@@ -200,8 +200,8 @@ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2
 github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/containers/common v0.60.2 h1:utcwp2YkO8c0mNlwRxsxfOiqfj157FRrBjxgjR6f+7o=
-github.com/containers/common v0.60.2/go.mod h1:I0upBi1qJX3QmzGbUOBN1LVP6RvkKhd3qQpZbQT+Q54=
+github.com/containers/common v0.60.4 h1:H5+LAMHPZEqX6vVNOQ+IguVsaFl8kbO/SZ/VPXjxhy0=
+github.com/containers/common v0.60.4/go.mod h1:I0upBi1qJX3QmzGbUOBN1LVP6RvkKhd3qQpZbQT+Q54=
 github.com/containers/storage v1.55.0 h1:wTWZ3YpcQf1F+dSP4KxG9iqDfpQY1otaUXjPpffuhgg=
 github.com/containers/storage v1.55.0/go.mod h1:28cB81IDk+y7ok60Of6u52RbCeBRucbFOeLunhER1RQ=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
diff --git a/internal/db/dump/dump.go b/internal/db/dump/dump.go
index a52b1dc4..94e2ba7a 100644
--- a/internal/db/dump/dump.go
+++ b/internal/db/dump/dump.go
@@ -100,6 +100,7 @@ func dumpData(ctx context.Context, config pgconn.Config, schema, excludeTable []
 		// "storage",
 		// "supabase_functions",
 		"supabase_migrations",
+		// TODO: Remove in a few version in favor of _supabase internal db
 		"_analytics",
 		"_realtime",
 		"_supavisor",
diff --git a/internal/db/push/push.go b/internal/db/push/push.go
index 0bff1516..f52d7897 100644
--- a/internal/db/push/push.go
+++ b/internal/db/push/push.go
@@ -10,7 +10,6 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/migration/up"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/migration"
@@ -29,43 +28,75 @@ func Run(ctx context.Context, dryRun, ignoreVersionMismatch bool, includeRoles,
 	if err != nil {
 		return err
 	}
-	if len(pending) == 0 {
+	var seeds []migration.SeedFile
+	if includeSeed {
+		seeds, err = migration.GetPendingSeeds(ctx, utils.Config.Db.Seed.SqlPaths, conn, afero.NewIOFS(fsys))
+		if err != nil {
+			return err
+		}
+	}
+	var globals []string
+	if includeRoles {
+		if exists, err := afero.Exists(fsys, utils.CustomRolesPath); err != nil {
+			return errors.Errorf("failed to find custom roles: %w", err)
+		} else if exists {
+			globals = append(globals, utils.CustomRolesPath)
+		}
+	}
+	if len(pending) == 0 && len(seeds) == 0 && len(globals) == 0 {
 		fmt.Println("Remote database is up to date.")
 		return nil
 	}
 	// Push pending migrations
 	if dryRun {
-		if includeRoles {
-			fmt.Fprintln(os.Stderr, "Would create custom roles "+utils.Bold(utils.CustomRolesPath)+"...")
+		if len(globals) > 0 {
+			fmt.Fprintln(os.Stderr, "Would create custom roles "+utils.Bold(globals[0])+"...")
 		}
-		fmt.Fprintln(os.Stderr, "Would push these migrations:")
-		fmt.Fprint(os.Stderr, utils.Bold(confirmPushAll(pending)))
-		if includeSeed {
-			seedPaths, err := utils.GetSeedFiles(fsys)
-			if err != nil {
-				return err
-			}
-			fmt.Fprintf(os.Stderr, "Would seed data %v...\n", seedPaths)
+		if len(pending) > 0 {
+			fmt.Fprintln(os.Stderr, "Would push these migrations:")
+			fmt.Fprint(os.Stderr, confirmPushAll(pending))
 		}
-	} else {
-		msg := fmt.Sprintf("Do you want to push these migrations to the remote database?\n%s\n", confirmPushAll(pending))
-		if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
-			return err
-		} else if !shouldPush {
-			return errors.New(context.Canceled)
+		if len(seeds) > 0 {
+			fmt.Fprintln(os.Stderr, "Would seed these files:")
+			fmt.Fprint(os.Stderr, confirmSeedAll(seeds))
 		}
-		if includeRoles {
-			if err := apply.CreateCustomRoles(ctx, conn, fsys); err != nil {
+	} else {
+		if len(globals) > 0 {
+			msg := "Do you want to create custom roles in the database cluster?"
+			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
+				return err
+			} else if !shouldPush {
+				return errors.New(context.Canceled)
+			}
+			if err := migration.SeedGlobals(ctx, globals, conn, afero.NewIOFS(fsys)); err != nil {
 				return err
 			}
 		}
-		if err := migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)); err != nil {
-			return err
+		if len(pending) > 0 {
+			msg := fmt.Sprintf("Do you want to push these migrations to the remote database?\n%s\n", confirmPushAll(pending))
+			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
+				return err
+			} else if !shouldPush {
+				return errors.New(context.Canceled)
+			}
+			if err := migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)); err != nil {
+				return err
+			}
+		} else {
+			fmt.Fprintln(os.Stderr, "Schema migrations are up to date.")
 		}
-		if includeSeed {
-			if err := apply.SeedDatabase(ctx, conn, fsys); err != nil {
+		if len(seeds) > 0 {
+			msg := fmt.Sprintf("Do you want to seed the remote database with these files?\n%s\n", confirmSeedAll(seeds))
+			if shouldPush, err := utils.NewConsole().PromptYesNo(ctx, msg, true); err != nil {
+				return err
+			} else if !shouldPush {
+				return errors.New(context.Canceled)
+			}
+			if err := migration.SeedData(ctx, seeds, conn, afero.NewIOFS(fsys)); err != nil {
 				return err
 			}
+		} else if includeSeed {
+			fmt.Fprintln(os.Stderr, "Seed files are up to date.")
 		}
 	}
 	fmt.Println("Finished " + utils.Aqua("supabase db push") + ".")
@@ -75,7 +106,18 @@ func Run(ctx context.Context, dryRun, ignoreVersionMismatch bool, includeRoles,
 func confirmPushAll(pending []string) (msg string) {
 	for _, path := range pending {
 		filename := filepath.Base(path)
-		msg += fmt.Sprintf(" • %s\n", filename)
+		msg += fmt.Sprintf(" • %s\n", utils.Bold(filename))
+	}
+	return msg
+}
+
+func confirmSeedAll(pending []migration.SeedFile) (msg string) {
+	for _, seed := range pending {
+		notice := seed.Path
+		if seed.Dirty {
+			notice += " (hash update)"
+		}
+		msg += fmt.Sprintf(" • %s\n", utils.Bold(notice))
 	}
 	return msg
 }
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index 62c3d335..a9d2f4f4 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/errdefs"
@@ -54,9 +55,11 @@ func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.F
 		return err
 	}
 	// Seed objects from supabase/buckets directory
-	if utils.Config.Storage.Enabled {
-		if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
-			return err
+	if resp, err := utils.Docker.ContainerInspect(ctx, utils.StorageId); err == nil {
+		if resp.State.Health == nil || resp.State.Health.Status != types.Healthy {
+			if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
+				return err
+			}
 		}
 		if err := buckets.Run(ctx, "", false, fsys); err != nil {
 			return err
@@ -97,11 +100,6 @@ func resetDatabase14(ctx context.Context, version string, fsys afero.Fs, options
 		return err
 	}
 	defer conn.Close(context.Background())
-	if utils.Config.Db.MajorVersion > 14 {
-		if err := start.SetupDatabase(ctx, conn, utils.DbId, os.Stderr, fsys); err != nil {
-			return err
-		}
-	}
 	return apply.MigrateAndSeed(ctx, version, conn, fsys)
 }
 
@@ -112,10 +110,6 @@ func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options
 	if err := utils.Docker.VolumeRemove(ctx, utils.DbId, true); err != nil {
 		return errors.Errorf("failed to remove volume: %w", err)
 	}
-	// Skip syslog if vector container is not started
-	if _, err := utils.Docker.ContainerInspect(ctx, utils.VectorId); err != nil {
-		utils.Config.Analytics.Enabled = false
-	}
 	config := start.NewContainerConfig()
 	hostConfig := start.NewHostConfig()
 	networkingConfig := network.NetworkingConfig{
@@ -132,15 +126,7 @@ func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options
 	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, utils.DbId); err != nil {
 		return err
 	}
-	conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
-	if err != nil {
-		return err
-	}
-	defer conn.Close(context.Background())
-	if err := start.SetupDatabase(ctx, conn, utils.DbId, os.Stderr, fsys); err != nil {
-		return err
-	}
-	if err := apply.MigrateAndSeed(ctx, version, conn, fsys); err != nil {
+	if err := start.SetupLocalDatabase(ctx, version, fsys, os.Stderr, options...); err != nil {
 		return err
 	}
 	fmt.Fprintln(os.Stderr, "Restarting containers...")
@@ -171,6 +157,8 @@ func recreateDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) err
 		Statements: []string{
 			"DROP DATABASE IF EXISTS postgres WITH (FORCE)",
 			"CREATE DATABASE postgres WITH OWNER postgres",
+			"DROP DATABASE IF EXISTS _supabase WITH (FORCE)",
+			"CREATE DATABASE _supabase WITH OWNER postgres",
 		},
 	}
 	return sql.ExecBatch(ctx, conn)
@@ -210,7 +198,7 @@ func restartServices(ctx context.Context) error {
 	services := listServicesToRestart()
 	result := utils.WaitAll(services, func(id string) error {
 		if err := utils.Docker.ContainerRestart(ctx, id, container.StopOptions{}); err != nil && !errdefs.IsNotFound(err) {
-			return errors.Errorf("Failed to restart %s: %w", id, err)
+			return errors.Errorf("failed to restart %s: %w", id, err)
 		}
 		return nil
 	})
diff --git a/internal/db/start/start.go b/internal/db/start/start.go
index 3039b04d..822e9aff 100644
--- a/internal/db/start/start.go
+++ b/internal/db/start/start.go
@@ -29,6 +29,8 @@ var (
 	HealthTimeout = 120 * time.Second
 	//go:embed templates/schema.sql
 	initialSchema string
+	//go:embed templates/_supabase.sql
+	_supabaseSchema string
 )
 
 func Run(ctx context.Context, fsys afero.Fs) error {
@@ -41,8 +43,6 @@ func Run(ctx context.Context, fsys afero.Fs) error {
 	} else if !errors.Is(err, utils.ErrNotRunning) {
 		return err
 	}
-	// Skip logflare container in db start
-	utils.Config.Analytics.Enabled = false
 	err := StartDatabase(ctx, fsys, os.Stderr)
 	if err != nil {
 		if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
@@ -83,6 +83,7 @@ func NewContainerConfig() container.Config {
 		},
 		Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/postgresql.schema.sql && cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && docker-entrypoint.sh postgres -D /etc/postgresql
 ` + initialSchema + `
+` + _supabaseSchema + `
 EOF
 ` + utils.Config.Db.RootKey + `
 EOF
@@ -122,7 +123,12 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
 		},
 	}
 	if utils.Config.Db.MajorVersion <= 14 {
-		config.Entrypoint = nil
+		config.Entrypoint = []string{"sh", "-c", `
+			cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql
+` + _supabaseSchema + `
+EOF
+			docker-entrypoint.sh postgres -D /etc/postgresql
+		`}
 		hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
 	}
 	// Creating volume will not override existing volume, so we must inspect explicitly
@@ -141,7 +147,7 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
 	}
 	// Initialize if we are on PG14 and there's no existing db volume
 	if utils.NoBackupVolume {
-		if err := setupDatabase(ctx, fsys, w, options...); err != nil {
+		if err := SetupLocalDatabase(ctx, "", fsys, w, options...); err != nil {
 			return err
 		}
 	}
@@ -302,7 +308,7 @@ func initSchema15(ctx context.Context, host string) error {
 	return nil
 }
 
-func setupDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
+func SetupLocalDatabase(ctx context.Context, version string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
 	conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
 	if err != nil {
 		return err
@@ -311,12 +317,16 @@ func setupDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
 	if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil {
 		return err
 	}
-	return apply.MigrateAndSeed(ctx, "", conn, fsys)
+	return apply.MigrateAndSeed(ctx, version, conn, fsys)
 }
 
 func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error {
 	if err := initSchema(ctx, conn, host, w); err != nil {
 		return err
 	}
-	return apply.CreateCustomRoles(ctx, conn, fsys)
+	err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys))
+	if errors.Is(err, os.ErrNotExist) {
+		return nil
+	}
+	return err
 }
diff --git a/internal/db/start/templates/_supabase.sql b/internal/db/start/templates/_supabase.sql
new file mode 100644
index 00000000..6e5d8487
--- /dev/null
+++ b/internal/db/start/templates/_supabase.sql
@@ -0,0 +1,13 @@
+CREATE DATABASE _supabase WITH OWNER postgres;
+
+-- Switch to the newly created _supabase database
+\c _supabase
+-- Create schemas in _supabase database for
+-- internals tools and reports to not overload user database
+-- with non-user activity
+CREATE SCHEMA IF NOT EXISTS _analytics;
+ALTER SCHEMA _analytics OWNER TO postgres;
+
+CREATE SCHEMA IF NOT EXISTS _supavisor;
+ALTER SCHEMA _supavisor OWNER TO postgres;
+\c postgres
diff --git a/internal/db/start/templates/schema.sql b/internal/db/start/templates/schema.sql
index 0d001023..534dd120 100644
--- a/internal/db/start/templates/schema.sql
+++ b/internal/db/start/templates/schema.sql
@@ -15,12 +15,6 @@ ALTER USER supabase_read_only_user WITH PASSWORD :'pgpass';
 create schema if not exists _realtime;
 alter schema _realtime owner to postgres;
 
-create schema if not exists _analytics;
-alter schema _analytics owner to postgres;
-
-create schema if not exists _supavisor;
-alter schema _supavisor owner to postgres;
-
 BEGIN;
 
 -- Create pg_net extension
diff --git a/internal/link/link.go b/internal/link/link.go
index 3a283107..1d3824b8 100644
--- a/internal/link/link.go
+++ b/internal/link/link.go
@@ -183,7 +183,10 @@ func linkDatabase(ctx context.Context, config pgconn.Config, options ...func(*pg
 	defer conn.Close(context.Background())
 	updatePostgresConfig(conn)
 	// If `schema_migrations` doesn't exist on the remote database, create it.
-	return migration.CreateMigrationTable(ctx, conn)
+	if err := migration.CreateMigrationTable(ctx, conn); err != nil {
+		return err
+	}
+	return migration.CreateSeedTable(ctx, conn)
 }
 
 func linkDatabaseVersion(ctx context.Context, projectRef string, fsys afero.Fs) error {
diff --git a/internal/migration/apply/apply.go b/internal/migration/apply/apply.go
index 6796930d..224b342f 100644
--- a/internal/migration/apply/apply.go
+++ b/internal/migration/apply/apply.go
@@ -2,9 +2,7 @@ package apply
 
 import (
 	"context"
-	"os"
 
-	"github.com/go-errors/errors"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/migration/list"
@@ -20,24 +18,16 @@ func MigrateAndSeed(ctx context.Context, version string, conn *pgx.Conn, fsys af
 	if err := migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)); err != nil {
 		return err
 	}
+	return applySeedFiles(ctx, conn, fsys)
+}
+
+func applySeedFiles(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
 	if !utils.Config.Db.Seed.Enabled {
 		return nil
 	}
-	return SeedDatabase(ctx, conn, fsys)
-}
-
-func SeedDatabase(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
-	seedPaths, err := utils.GetSeedFiles(fsys)
+	seeds, err := migration.GetPendingSeeds(ctx, utils.Config.Db.Seed.SqlPaths, conn, afero.NewIOFS(fsys))
 	if err != nil {
 		return err
 	}
-	return migration.SeedData(ctx, seedPaths, conn, afero.NewIOFS(fsys))
-}
-
-func CreateCustomRoles(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
-	err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys))
-	if errors.Is(err, os.ErrNotExist) {
-		return nil
-	}
-	return err
+	return migration.SeedData(ctx, seeds, conn, afero.NewIOFS(fsys))
 }
diff --git a/internal/start/start.go b/internal/start/start.go
index 2c3017ce..72f34b8e 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -195,7 +195,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 	// Start Logflare
 	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) {
 		env := []string{
-			"DB_DATABASE=" + dbConfig.Database,
+			"DB_DATABASE=_supabase",
 			"DB_HOSTNAME=" + dbConfig.Host,
 			fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
 			"DB_SCHEMA=_analytics",
@@ -228,7 +228,7 @@ func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers
 			)
 		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),
+				fmt.Sprintf("POSTGRES_BACKEND_URL=postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
 				"POSTGRES_BACKEND_SCHEMA=_analytics",
 			)
 		}
@@ -1045,7 +1045,7 @@ EOF
 					"PORT=4000",
 					fmt.Sprintf("PROXY_PORT_SESSION=%d", portSession),
 					fmt.Sprintf("PROXY_PORT_TRANSACTION=%d", portTransaction),
-					fmt.Sprintf("DATABASE_URL=ecto://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
+					fmt.Sprintf("DATABASE_URL=ecto://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
 					"CLUSTER_POSTGRES=true",
 					"SECRET_KEY_BASE=" + utils.Config.Db.Pooler.SecretKeyBase,
 					"VAULT_ENC_KEY=" + utils.Config.Db.Pooler.EncryptionKey,
diff --git a/internal/status/status.go b/internal/status/status.go
index e78af5cd..1b9d02f9 100644
--- a/internal/status/status.go
+++ b/internal/status/status.go
@@ -14,6 +14,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/go-errors/errors"
 	"github.com/spf13/afero"
@@ -114,7 +115,7 @@ func assertContainerHealthy(ctx context.Context, container string) error {
 		return errors.Errorf("failed to inspect container health: %w", err)
 	} else if !resp.State.Running {
 		return errors.Errorf("%s container is not running: %s", container, resp.State.Status)
-	} else if resp.State.Health != nil && resp.State.Health.Status != "healthy" {
+	} else if resp.State.Health != nil && resp.State.Health.Status != types.Healthy {
 		return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status)
 	}
 	return nil
diff --git a/internal/utils/flags/db_url.go b/internal/utils/flags/db_url.go
index 6fc91217..d089f2d1 100644
--- a/internal/utils/flags/db_url.go
+++ b/internal/utils/flags/db_url.go
@@ -50,6 +50,11 @@ func ParseDatabaseConfig(flagSet *pflag.FlagSet, fsys afero.Fs) error {
 	// Update connection config
 	switch connType {
 	case direct:
+		if err := utils.Config.Load("", utils.NewRootFS(fsys)); err != nil {
+			if !errors.Is(err, os.ErrNotExist) {
+				return err
+			}
+		}
 		if flag := flagSet.Lookup("db-url"); flag != nil {
 			config, err := pgconn.ParseConfig(flag.Value.String())
 			if err != nil {
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index d4e49f04..adb0efa9 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -8,7 +8,6 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
-	"sort"
 	"time"
 
 	"github.com/docker/docker/client"
@@ -157,26 +156,6 @@ var (
 	ErrNotRunning  = errors.Errorf("%s is not running.", Aqua("supabase start"))
 )
 
-// Match the glob patterns from the config to get a deduplicated
-// array of all migrations files to apply in the declared order.
-func GetSeedFiles(fsys afero.Fs) ([]string, error) {
-	seedPaths := Config.Db.Seed.SqlPaths
-	var files []string
-	for _, pattern := range seedPaths {
-		fullPattern := filepath.Join(SupabaseDirPath, pattern)
-		matches, err := afero.Glob(fsys, fullPattern)
-		if err != nil {
-			return nil, errors.Errorf("failed to apply glob pattern for %w", err)
-		}
-		if len(matches) == 0 {
-			fmt.Fprintf(os.Stderr, "%s Your pattern %s matched 0 seed files.\n", Yellow("WARNING:"), pattern)
-		}
-		sort.Strings(matches)
-		files = append(files, matches...)
-	}
-	return RemoveDuplicates(files), nil
-}
-
 func GetCurrentTimestamp() string {
 	// Magic number: https://stackoverflow.com/q/45160822.
 	return time.Now().UTC().Format("20060102150405")
diff --git a/package.json b/package.json
index 54a6ae29..dd3014d2 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
     "supabase": "bin/supabase"
   },
   "dependencies": {
-    "bin-links": "^4.0.3",
+    "bin-links": "^5.0.0",
     "https-proxy-agent": "^7.0.2",
     "node-fetch": "^3.3.2",
     "tar": "7.4.3"
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 3636e7e9..0b246822 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -16,6 +16,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 	"text/template"
@@ -172,8 +173,9 @@ type (
 	}
 
 	seed struct {
-		Enabled  bool     `toml:"enabled"`
-		SqlPaths []string `toml:"sql_paths"`
+		Enabled      bool     `toml:"enabled"`
+		GlobPatterns []string `toml:"sql_paths"`
+		SqlPaths     []string `toml:"-"`
 	}
 
 	pooler struct {
@@ -483,8 +485,8 @@ func NewConfig(editors ...ConfigEditor) config {
 				SecretKeyBase: "EAx3IQ/wRG1v47ZD4NE4/9RzBI8Jmil3x0yhcW4V2NHBP6c2iPIzwjofi2Ep4HIG",
 			},
 			Seed: seed{
-				Enabled:  true,
-				SqlPaths: []string{"./seed.sql"},
+				Enabled:      true,
+				GlobPatterns: []string{"./seed.sql"},
 			},
 		},
 		Realtime: realtime{
@@ -708,7 +710,11 @@ func (c *config) Load(path string, fsys fs.FS) error {
 		}
 		c.Functions[slug] = function
 	}
-	if err := c.baseConfig.Validate(); err != nil {
+
+	if err := c.Db.Seed.loadSeedPaths(builder.SupabaseDirPath, fsys); err != nil {
+		return err
+	}
+	if err := c.baseConfig.Validate(fsys); err != nil {
 		return err
 	}
 	c.Remotes = make(map[string]baseConfig, len(c.Overrides))
@@ -725,7 +731,7 @@ func (c *config) Load(path string, fsys fs.FS) error {
 		} else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
 			fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
 		}
-		if err := base.Validate(); err != nil {
+		if err := base.Validate(fsys); err != nil {
 			return err
 		}
 		c.Remotes[name] = base
@@ -733,7 +739,7 @@ func (c *config) Load(path string, fsys fs.FS) error {
 	return nil
 }
 
-func (c *baseConfig) Validate() error {
+func (c *baseConfig) Validate(fsys fs.FS) error {
 	if c.ProjectId == "" {
 		return errors.New("Missing required field in config: project_id")
 	} else if sanitized := sanitizeProjectId(c.ProjectId); sanitized != c.ProjectId {
@@ -828,8 +834,10 @@ func (c *baseConfig) Validate() error {
 		}
 		// Validate email config
 		for name, tmpl := range c.Auth.Email.Template {
-			if len(tmpl.ContentPath) > 0 && !fs.ValidPath(filepath.Clean(tmpl.ContentPath)) {
-				return errors.Errorf("Invalid config for auth.email.%s.content_path: %s", name, tmpl.ContentPath)
+			if len(tmpl.ContentPath) > 0 {
+				if _, err = fs.Stat(fsys, filepath.Clean(tmpl.ContentPath)); err != nil {
+					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 {
@@ -1041,6 +1049,40 @@ func loadEnvIfExists(path string) error {
 	return nil
 }
 
+// Match the glob patterns from the config to get a deduplicated
+// array of all migrations files to apply in the declared order.
+func (c *seed) loadSeedPaths(basePath string, fsys fs.FS) error {
+	if !c.Enabled {
+		return nil
+	}
+	if c.SqlPaths != nil {
+		// Reuse already allocated array
+		c.SqlPaths = c.SqlPaths[:0]
+	}
+	set := make(map[string]struct{})
+	for _, pattern := range c.GlobPatterns {
+		if !filepath.IsAbs(pattern) {
+			pattern = filepath.Join(basePath, pattern)
+		}
+		matches, err := fs.Glob(fsys, pattern)
+		if err != nil {
+			return errors.Errorf("failed to apply glob pattern: %w", err)
+		}
+		if len(matches) == 0 {
+			fmt.Fprintln(os.Stderr, "No seed files matched pattern:", pattern)
+		}
+		sort.Strings(matches)
+		// Remove duplicates
+		for _, item := range matches {
+			if _, exists := set[item]; !exists {
+				set[item] = struct{}{}
+				c.SqlPaths = append(c.SqlPaths, item)
+			}
+		}
+	}
+	return nil
+}
+
 func (h *hookConfig) HandleHook(hookType string) error {
 	// If not enabled do nothing
 	if !h.Enabled {
diff --git a/pkg/config/constants.go b/pkg/config/constants.go
index d75ce61b..c51e19fb 100644
--- a/pkg/config/constants.go
+++ b/pkg/config/constants.go
@@ -10,9 +10,9 @@ const (
 	inbucketImage    = "inbucket/inbucket:3.0.3"
 	postgrestImage   = "postgrest/postgrest:v12.2.0"
 	pgmetaImage      = "supabase/postgres-meta:v0.83.2"
-	studioImage      = "supabase/studio:20240923-2e3e90c"
+	studioImage      = "supabase/studio:20240930-16f2b8e"
 	imageProxyImage  = "darthsim/imgproxy:v3.8.0"
-	edgeRuntimeImage = "supabase/edge-runtime:v1.58.3"
+	edgeRuntimeImage = "supabase/edge-runtime:v1.58.11"
 	vectorImage      = "timberio/vector:0.28.1-alpine"
 	supavisorImage   = "supabase/supavisor:1.1.56"
 	gotrueImage      = "supabase/gotrue:v2.158.1"
diff --git a/pkg/migration/file.go b/pkg/migration/file.go
index 04a94556..79da1c86 100644
--- a/pkg/migration/file.go
+++ b/pkg/migration/file.go
@@ -2,6 +2,8 @@ package migration
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"io"
 	"io/fs"
 	"path/filepath"
@@ -24,6 +26,22 @@ type MigrationFile struct {
 var migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`)
 
 func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) {
+	lines, err := parseFile(path, fsys)
+	if err != nil {
+		return nil, err
+	}
+	file := MigrationFile{Statements: lines}
+	// Parse version from file name
+	filename := filepath.Base(path)
+	matches := migrateFilePattern.FindStringSubmatch(filename)
+	if len(matches) > 2 {
+		file.Version = matches[1]
+		file.Name = matches[2]
+	}
+	return &file, nil
+}
+
+func parseFile(path string, fsys fs.FS) ([]string, error) {
 	sql, err := fsys.Open(path)
 	if err != nil {
 		return nil, errors.Errorf("failed to open migration file: %w", err)
@@ -37,17 +55,7 @@ func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) {
 			}
 		}
 	}
-	file, err := NewMigrationFromReader(sql)
-	if err == nil {
-		// Parse version from file name
-		filename := filepath.Base(path)
-		matches := migrateFilePattern.FindStringSubmatch(filename)
-		if len(matches) > 2 {
-			file.Version = matches[1]
-			file.Name = matches[2]
-		}
-	}
-	return file, err
+	return parser.SplitAndTrim(sql)
 }
 
 func NewMigrationFromReader(sql io.Reader) (*MigrationFile, error) {
@@ -112,12 +120,40 @@ func (m *MigrationFile) insertVersionSQL(conn *pgx.Conn, batch *pgconn.Batch) er
 	return nil
 }
 
-func (m *MigrationFile) ExecBatchWithCache(ctx context.Context, conn *pgx.Conn) error {
+type SeedFile struct {
+	Path  string
+	Hash  string
+	Dirty bool `db:"-"`
+}
+
+func NewSeedFile(path string, fsys fs.FS) (*SeedFile, error) {
+	sql, err := fsys.Open(path)
+	if err != nil {
+		return nil, errors.Errorf("failed to open seed file: %w", err)
+	}
+	defer sql.Close()
+	hash := sha256.New()
+	if _, err := io.Copy(hash, sql); err != nil {
+		return nil, errors.Errorf("failed to hash file: %w", err)
+	}
+	digest := hex.EncodeToString(hash.Sum(nil))
+	return &SeedFile{Path: path, Hash: digest}, nil
+}
+
+func (m *SeedFile) ExecBatchWithCache(ctx context.Context, conn *pgx.Conn, fsys fs.FS) error {
+	// Parse each file individually to reduce memory usage
+	lines, err := parseFile(m.Path, fsys)
+	if err != nil {
+		return err
+	}
 	// Data statements don't mutate schemas, safe to use statement cache
 	batch := pgx.Batch{}
-	for _, line := range m.Statements {
-		batch.Queue(line)
+	if !m.Dirty {
+		for _, line := range lines {
+			batch.Queue(line)
+		}
 	}
+	batch.Queue(UPSERT_SEED_FILE, m.Path, m.Hash)
 	// No need to track version here because there are no schema changes
 	if err := conn.SendBatch(ctx, &batch).Close(); err != nil {
 		return errors.Errorf("failed to send batch: %w", err)
diff --git a/pkg/migration/history.go b/pkg/migration/history.go
index 6dc8b5fc..5ff77ae6 100644
--- a/pkg/migration/history.go
+++ b/pkg/migration/history.go
@@ -21,6 +21,9 @@ const (
 	TRUNCATE_VERSION_TABLE   = "TRUNCATE supabase_migrations.schema_migrations"
 	SELECT_VERSION_TABLE     = "SELECT * FROM supabase_migrations.schema_migrations"
 	LIST_MIGRATION_VERSION   = "SELECT version FROM supabase_migrations.schema_migrations ORDER BY version"
+	CREATE_SEED_TABLE        = "CREATE TABLE IF NOT EXISTS supabase_migrations.seed_files (path text NOT NULL PRIMARY KEY, hash text NOT NULL)"
+	UPSERT_SEED_FILE         = "INSERT INTO supabase_migrations.seed_files(path, hash) VALUES($1, $2) ON CONFLICT (path) DO UPDATE SET hash = EXCLUDED.hash"
+	SELECT_SEED_TABLE        = "SELECT path, hash FROM supabase_migrations.seed_files"
 )
 
 // TODO: support overriding `supabase_migrations.schema_migrations` with user defined <schema>.<table>
@@ -46,3 +49,24 @@ func ReadMigrationTable(ctx context.Context, conn *pgx.Conn) ([]MigrationFile, e
 	}
 	return pgxv5.CollectRows[MigrationFile](rows)
 }
+
+func CreateSeedTable(ctx context.Context, conn *pgx.Conn) error {
+	// This must be run without prepared statements because each statement in the batch depends on
+	// the previous schema change. The lock timeout will be reset when implicit transaction ends.
+	batch := pgconn.Batch{}
+	batch.ExecParams(SET_LOCK_TIMEOUT, nil, nil, nil, nil)
+	batch.ExecParams(CREATE_VERSION_SCHEMA, nil, nil, nil, nil)
+	batch.ExecParams(CREATE_SEED_TABLE, nil, nil, nil, nil)
+	if _, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil {
+		return errors.Errorf("failed to create migration table: %w", err)
+	}
+	return nil
+}
+
+func ReadSeedTable(ctx context.Context, conn *pgx.Conn) ([]SeedFile, error) {
+	rows, err := conn.Query(ctx, SELECT_SEED_TABLE)
+	if err != nil {
+		return nil, errors.Errorf("failed to read seed table: %w", err)
+	}
+	return pgxv5.CollectRows[SeedFile](rows)
+}
diff --git a/pkg/migration/seed.go b/pkg/migration/seed.go
index 62a2c875..988e8d4b 100644
--- a/pkg/migration/seed.go
+++ b/pkg/migration/seed.go
@@ -7,16 +7,70 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/go-errors/errors"
+	"github.com/jackc/pgconn"
+	"github.com/jackc/pgerrcode"
 	"github.com/jackc/pgx/v4"
 )
 
-func SeedData(ctx context.Context, pending []string, conn *pgx.Conn, fsys fs.FS) error {
-	for _, path := range pending {
-		fmt.Fprintf(os.Stderr, "Seeding data from %s...\n", path)
-		// Batch seed commands, safe to use statement cache
-		if seed, err := NewMigrationFromFile(path, fsys); err != nil {
+func getRemoteSeeds(ctx context.Context, conn *pgx.Conn) (map[string]string, error) {
+	remotes, err := ReadSeedTable(ctx, conn)
+	if err != nil {
+		var pgErr *pgconn.PgError
+		if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UndefinedTable {
+			// If seed table is undefined, the remote project has no migrations
+			return nil, nil
+		}
+		return nil, err
+	}
+	applied := make(map[string]string, len(remotes))
+	for _, seed := range remotes {
+		applied[seed.Path] = seed.Hash
+	}
+	return applied, nil
+}
+
+func GetPendingSeeds(ctx context.Context, locals []string, conn *pgx.Conn, fsys fs.FS) ([]SeedFile, error) {
+	if len(locals) == 0 {
+		return nil, nil
+	}
+	applied, err := getRemoteSeeds(ctx, conn)
+	if err != nil {
+		return nil, err
+	}
+	var pending []SeedFile
+	for _, path := range locals {
+		seed, err := NewSeedFile(path, fsys)
+		if err != nil {
+			return nil, err
+		}
+		if hash, exists := applied[seed.Path]; exists {
+			// Skip seed files that already exist
+			if hash == seed.Hash {
+				continue
+			}
+			// Mark seed file as dirty
+			seed.Dirty = true
+		}
+		pending = append(pending, *seed)
+	}
+	return pending, nil
+}
+
+func SeedData(ctx context.Context, pending []SeedFile, conn *pgx.Conn, fsys fs.FS) error {
+	if len(pending) > 0 {
+		if err := CreateSeedTable(ctx, conn); err != nil {
 			return err
-		} else if err := seed.ExecBatchWithCache(ctx, conn); err != nil {
+		}
+	}
+	for _, seed := range pending {
+		if seed.Dirty {
+			fmt.Fprintf(os.Stderr, "Updating seed hash to %s...\n", seed.Path)
+		} else {
+			fmt.Fprintf(os.Stderr, "Seeding data from %s...\n", seed.Path)
+		}
+		// Batch seed commands, safe to use statement cache
+		if err := seed.ExecBatchWithCache(ctx, conn, fsys); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/pgxv5/rows.go b/pkg/pgxv5/rows.go
index 299ce868..96aa16f5 100644
--- a/pkg/pgxv5/rows.go
+++ b/pkg/pgxv5/rows.go
@@ -104,18 +104,11 @@ func appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs [
 				return nil, err
 			}
 		} else {
-			dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
-			if dbTagPresent {
-				dbTag = strings.Split(dbTag, ",")[0]
-			}
-			if dbTag == "-" {
+			colName := GetColumnName(sf)
+			if len(colName) == 0 {
 				// Field is ignored, skip it.
 				continue
 			}
-			colName := dbTag
-			if !dbTagPresent {
-				colName = sf.Name
-			}
 			fpos := fieldPosByName(fldDescs, colName)
 			if fpos == -1 || fpos >= len(scanTargets) {
 				return nil, errors.Errorf("cannot find field %s in returned row", colName)
@@ -126,3 +119,14 @@ func appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs [
 
 	return scanTargets, err
 }
+
+func GetColumnName(sf reflect.StructField) string {
+	dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
+	if !dbTagPresent {
+		return sf.Name
+	}
+	if dbTag = strings.Split(dbTag, ",")[0]; dbTag != "-" {
+		return dbTag
+	}
+	return ""
+}


Test patch:
diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go
index d5e8c6a4..6ada103c 100644
--- a/internal/db/diff/diff_test.go
+++ b/internal/db/diff/diff_test.go
@@ -55,7 +55,7 @@ func TestRun(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-shadow-realtime")
@@ -267,7 +267,7 @@ func TestDiffDatabase(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Docker.DaemonHost()).
@@ -303,7 +303,7 @@ At statement 0: create schema public`)
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Docker.DaemonHost()).
diff --git a/internal/db/push/push_test.go b/internal/db/push/push_test.go
index 72df15ef..3a3ff3cd 100644
--- a/internal/db/push/push_test.go
+++ b/internal/db/push/push_test.go
@@ -2,6 +2,8 @@ package push
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"os"
 	"path/filepath"
 	"testing"
@@ -146,7 +148,7 @@ func TestPushAll(t *testing.T) {
 
 	t.Run("throws error on roles failure", func(t *testing.T) {
 		// Setup in-memory fs
-		fsys := &fstest.OpenErrorFs{DenyPath: utils.CustomRolesPath}
+		fsys := &fstest.StatErrorFs{DenyPath: utils.CustomRolesPath}
 		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
 		require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644))
 		// Setup mock postgres
@@ -161,23 +163,30 @@ func TestPushAll(t *testing.T) {
 	})
 
 	t.Run("throws error on seed failure", func(t *testing.T) {
-		// Setup in-memory fs
+		digest := hex.EncodeToString(sha256.New().Sum(nil))
 		seedPath := filepath.Join(utils.SupabaseDirPath, "seed.sql")
-		fsys := &fstest.OpenErrorFs{DenyPath: seedPath}
-		_, _ = fsys.Create(seedPath)
+		utils.Config.Db.Seed.SqlPaths = []string{seedPath}
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, afero.WriteFile(fsys, seedPath, []byte{}, 0644))
 		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
 		require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
 		conn.Query(migration.LIST_MIGRATION_VERSION).
+			Reply("SELECT 0").
+			Query(migration.SELECT_SEED_TABLE).
 			Reply("SELECT 0")
 		helper.MockMigrationHistory(conn).
 			Query(migration.INSERT_MIGRATION_VERSION, "0", "test", nil).
 			Reply("INSERT 0 1")
+		helper.MockSeedHistory(conn).
+			Query(migration.UPSERT_SEED_FILE, seedPath, digest).
+			ReplyError(pgerrcode.NotNullViolation, `null value in column "hash" of relation "seed_files"`)
 		// Run test
 		err := Run(context.Background(), false, false, false, true, dbConfig, fsys, conn.Intercept)
 		// Check error
-		assert.ErrorIs(t, err, os.ErrPermission)
+		assert.ErrorContains(t, err, `ERROR: null value in column "hash" of relation "seed_files" (SQLSTATE 23502)`)
 	})
 }
diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go
index 0be21f3b..c65aa0ef 100644
--- a/internal/db/reset/reset_test.go
+++ b/internal/db/reset/reset_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/migration"
 	"github.com/supabase/cli/pkg/pgtest"
+	"github.com/supabase/cli/pkg/storage"
 )
 
 func TestResetCommand(t *testing.T) {
@@ -38,6 +39,69 @@ func TestResetCommand(t *testing.T) {
 		Database: "postgres",
 	}
 
+	t.Run("seeds storage after reset", func(t *testing.T) {
+		utils.DbId = "test-reset"
+		utils.ConfigId = "test-config"
+		utils.Config.Db.MajorVersion = 15
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup mock docker
+		require.NoError(t, apitest.MockDocker(utils.Docker))
+		defer gock.OffAll()
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId).
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{})
+		gock.New(utils.Docker.DaemonHost()).
+			Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId).
+			Reply(http.StatusOK)
+		gock.New(utils.Docker.DaemonHost()).
+			Delete("/v" + utils.Docker.ClientVersion() + "/volumes/" + utils.DbId).
+			Reply(http.StatusOK)
+		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), utils.DbId)
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{
+					Running: true,
+					Health:  &types.Health{Status: types.Healthy},
+				},
+			}})
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		// Restarts services
+		utils.StorageId = "test-storage"
+		utils.GotrueId = "test-auth"
+		utils.RealtimeId = "test-realtime"
+		utils.PoolerId = "test-pooler"
+		for _, container := range listServicesToRestart() {
+			gock.New(utils.Docker.DaemonHost()).
+				Post("/v" + utils.Docker.ClientVersion() + "/containers/" + container + "/restart").
+				Reply(http.StatusOK)
+		}
+		// Seeds storage
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.StorageId + "/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{
+					Running: true,
+					Health:  &types.Health{Status: types.Healthy},
+				},
+			}})
+		gock.New(utils.Config.Api.ExternalUrl).
+			Get("/storage/v1/bucket").
+			Reply(http.StatusOK).
+			JSON([]storage.BucketResponse{})
+		// Run test
+		err := Run(context.Background(), "", dbConfig, fsys, conn.Intercept)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
 	t.Run("throws error on context canceled", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
@@ -145,6 +209,10 @@ func TestRecreateDatabase(t *testing.T) {
 			Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)").
 			Reply("DROP DATABASE").
 			Query("CREATE DATABASE postgres WITH OWNER postgres").
+			Reply("CREATE DATABASE").
+			Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)").
+			Reply("DROP DATABASE").
+			Query("CREATE DATABASE _supabase WITH OWNER postgres").
 			Reply("CREATE DATABASE")
 		// Run test
 		assert.NoError(t, recreateDatabase(context.Background(), conn.Intercept))
@@ -194,8 +262,11 @@ func TestRecreateDatabase(t *testing.T) {
 			Reply("DO").
 			Query("DROP DATABASE IF EXISTS postgres WITH (FORCE)").
 			ReplyError(pgerrcode.ObjectInUse, `database "postgres" is used by an active logical replication slot`).
-			Query("CREATE DATABASE postgres WITH OWNER postgres")
-		// Run test
+			Query("CREATE DATABASE postgres WITH OWNER postgres").
+			Query("DROP DATABASE IF EXISTS _supabase WITH (FORCE)").
+			Reply("DROP DATABASE").
+			Query("CREATE DATABASE _supabase WITH OWNER postgres").
+			Reply("CREATE DATABASE")
 		err := recreateDatabase(context.Background(), conn.Intercept)
 		// Check error
 		assert.ErrorContains(t, err, `ERROR: database "postgres" is used by an active logical replication slot (SQLSTATE 55006)`)
@@ -218,7 +289,7 @@ func TestRestartDatabase(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		// Restarts services
@@ -253,7 +324,7 @@ func TestRestartDatabase(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		// Restarts services
@@ -272,9 +343,9 @@ func TestRestartDatabase(t *testing.T) {
 		// Run test
 		err := RestartDatabase(context.Background(), io.Discard)
 		// Check error
-		assert.ErrorContains(t, err, "Failed to restart "+utils.StorageId)
-		assert.ErrorContains(t, err, "Failed to restart "+utils.GotrueId)
-		assert.ErrorContains(t, err, "Failed to restart "+utils.RealtimeId)
+		assert.ErrorContains(t, err, "failed to restart "+utils.StorageId)
+		assert.ErrorContains(t, err, "failed to restart "+utils.GotrueId)
+		assert.ErrorContains(t, err, "failed to restart "+utils.RealtimeId)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
diff --git a/internal/db/start/start_test.go b/internal/db/start/start_test.go
index e4072df9..96d97da8 100644
--- a/internal/db/start/start_test.go
+++ b/internal/db/start/start_test.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"net/http"
 	"os"
-	"path/filepath"
 	"testing"
 
 	"github.com/docker/docker/api/types"
@@ -60,8 +59,6 @@ func TestStartDatabase(t *testing.T) {
 		fsys := afero.NewMemMapFs()
 		roles := "create role test"
 		require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644))
-		seed := "INSERT INTO employees(name) VALUES ('Alice')"
-		require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.SupabaseDirPath, "seed.sql"), []byte(seed), 0644))
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
@@ -76,7 +73,7 @@ func TestStartDatabase(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Realtime.Image), "test-realtime")
@@ -89,9 +86,7 @@ func TestStartDatabase(t *testing.T) {
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
 		conn.Query(roles).
-			Reply("CREATE ROLE").
-			Query(seed).
-			Reply("INSERT 0 1")
+			Reply("CREATE ROLE")
 		// Run test
 		err := StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
 		// Check error
@@ -124,7 +119,7 @@ func TestStartDatabase(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		// Run test
@@ -257,7 +252,7 @@ func TestSetupDatabase(t *testing.T) {
 			Query(roles).
 			Reply("CREATE ROLE")
 		// Run test
-		err := setupDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
+		err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept)
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -266,12 +261,13 @@ func TestSetupDatabase(t *testing.T) {
 	t.Run("throws error on connect failure", func(t *testing.T) {
 		utils.Config.Db.Port = 0
 		// Run test
-		err := setupDatabase(context.Background(), nil, io.Discard)
+		err := SetupLocalDatabase(context.Background(), "", nil, io.Discard)
 		// Check error
 		assert.ErrorContains(t, err, "invalid port (outside range)")
 	})
 
 	t.Run("throws error on init failure", func(t *testing.T) {
+		utils.Config.Realtime.Enabled = true
 		utils.Config.Db.Port = 5432
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
@@ -283,7 +279,7 @@ func TestSetupDatabase(t *testing.T) {
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
 		// Run test
-		err := setupDatabase(context.Background(), nil, io.Discard, conn.Intercept)
+		err := SetupLocalDatabase(context.Background(), "", nil, io.Discard, conn.Intercept)
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -306,7 +302,7 @@ func TestSetupDatabase(t *testing.T) {
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
 		// Run test
-		err := setupDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
+		err := SetupLocalDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept)
 		// Check error
 		assert.ErrorIs(t, err, os.ErrPermission)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
diff --git a/internal/link/link_test.go b/internal/link/link_test.go
index f3396129..edc95d8f 100644
--- a/internal/link/link_test.go
+++ b/internal/link/link_test.go
@@ -46,6 +46,7 @@ func TestLinkCommand(t *testing.T) {
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
 		helper.MockMigrationHistory(conn)
+		helper.MockSeedHistory(conn)
 		// Flush pending mocks after test execution
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
@@ -279,6 +280,7 @@ func TestLinkDatabase(t *testing.T) {
 		})
 		defer conn.Close(t)
 		helper.MockMigrationHistory(conn)
+		helper.MockSeedHistory(conn)
 		// Run test
 		err := linkDatabase(context.Background(), dbConfig, conn.Intercept)
 		// Check error
@@ -294,6 +296,7 @@ func TestLinkDatabase(t *testing.T) {
 		})
 		defer conn.Close(t)
 		helper.MockMigrationHistory(conn)
+		helper.MockSeedHistory(conn)
 		// Run test
 		err := linkDatabase(context.Background(), dbConfig, conn.Intercept)
 		// Check error
diff --git a/internal/migration/apply/apply_test.go b/internal/migration/apply/apply_test.go
index 6c5b915f..28609374 100644
--- a/internal/migration/apply/apply_test.go
+++ b/internal/migration/apply/apply_test.go
@@ -6,7 +6,6 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/jackc/pgerrcode"
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -75,54 +74,3 @@ func TestMigrateDatabase(t *testing.T) {
 		assert.ErrorIs(t, err, os.ErrPermission)
 	})
 }
-
-func TestSeedDatabase(t *testing.T) {
-	t.Run("seeds from file", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup seed file
-		sql := "INSERT INTO employees(name) VALUES ('Alice')"
-		require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.SupabaseDirPath, "seed.sql"), []byte(sql), 0644))
-		// Setup mock postgres
-		conn := pgtest.NewConn()
-		defer conn.Close(t)
-		conn.Query(sql).
-			Reply("INSERT 0 1")
-		// Run test
-		err := SeedDatabase(context.Background(), conn.MockClient(t), fsys)
-		// Check error
-		assert.NoError(t, err)
-	})
-
-	t.Run("ignores missing seed", func(t *testing.T) {
-		assert.NoError(t, SeedDatabase(context.Background(), nil, afero.NewMemMapFs()))
-	})
-
-	t.Run("throws error on read failure", func(t *testing.T) {
-		// Setup in-memory fs
-		seedPath := filepath.Join(utils.SupabaseDirPath, "seed.sql")
-		fsys := &fstest.OpenErrorFs{DenyPath: seedPath}
-		_, _ = fsys.Create(seedPath)
-		// Run test
-		err := SeedDatabase(context.Background(), nil, fsys)
-		// Check error
-		assert.ErrorIs(t, err, os.ErrPermission)
-	})
-
-	t.Run("throws error on insert failure", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup seed file
-		sql := "INSERT INTO employees(name) VALUES ('Alice')"
-		require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.SupabaseDirPath, "seed.sql"), []byte(sql), 0644))
-		// Setup mock postgres
-		conn := pgtest.NewConn()
-		defer conn.Close(t)
-		conn.Query(sql).
-			ReplyError(pgerrcode.NotNullViolation, `null value in column "age" of relation "employees"`)
-		// Run test
-		err := SeedDatabase(context.Background(), conn.MockClient(t), fsys)
-		// Check error
-		assert.ErrorContains(t, err, `ERROR: null value in column "age" of relation "employees" (SQLSTATE 23502)`)
-	})
-}
diff --git a/internal/migration/squash/squash_test.go b/internal/migration/squash/squash_test.go
index c85f014c..cc0461f3 100644
--- a/internal/migration/squash/squash_test.go
+++ b/internal/migration/squash/squash_test.go
@@ -61,7 +61,7 @@ func TestSquashCommand(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Docker.DaemonHost()).
@@ -251,7 +251,7 @@ func TestSquashMigrations(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Docker.DaemonHost()).
@@ -286,7 +286,7 @@ func TestSquashMigrations(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Docker.DaemonHost()).
diff --git a/internal/start/start_test.go b/internal/start/start_test.go
index 654831cc..079eb3c7 100644
--- a/internal/start/start_test.go
+++ b/internal/start/start_test.go
@@ -160,7 +160,7 @@ func TestDatabaseStart(t *testing.T) {
 				JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 					State: &types.ContainerState{
 						Running: true,
-						Health:  &types.Health{Status: "healthy"},
+						Health:  &types.Health{Status: types.Healthy},
 					},
 				}})
 		}
@@ -177,7 +177,7 @@ func TestDatabaseStart(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		gock.New(utils.Config.Api.ExternalUrl).
@@ -225,7 +225,7 @@ func TestDatabaseStart(t *testing.T) {
 			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
 				State: &types.ContainerState{
 					Running: true,
-					Health:  &types.Health{Status: "healthy"},
+					Health:  &types.Health{Status: types.Healthy},
 				},
 			}})
 		// Run test
diff --git a/internal/testing/helper/history.go b/internal/testing/helper/history.go
index b73d267e..95c846b7 100644
--- a/internal/testing/helper/history.go
+++ b/internal/testing/helper/history.go
@@ -17,3 +17,12 @@ func MockMigrationHistory(conn *pgtest.MockConn) *pgtest.MockConn {
 		Reply("ALTER TABLE")
 	return conn
 }
+
+func MockSeedHistory(conn *pgtest.MockConn) *pgtest.MockConn {
+	conn.Query(migration.SET_LOCK_TIMEOUT).
+		Query(migration.CREATE_VERSION_SCHEMA).
+		Reply("CREATE SCHEMA").
+		Query(migration.CREATE_SEED_TABLE).
+		Reply("CREATE TABLE")
+	return conn
+}
diff --git a/internal/utils/misc_test.go b/internal/utils/misc_test.go
index c16abdf0..6472c214 100644
--- a/internal/utils/misc_test.go
+++ b/internal/utils/misc_test.go
@@ -75,76 +75,3 @@ func TestProjectRoot(t *testing.T) {
 		assert.Equal(t, cwd, path)
 	})
 }
-
-func TestGetSeedFiles(t *testing.T) {
-	t.Run("returns seed files matching patterns", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Create seed files
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed1.sql", []byte("INSERT INTO table1 VALUES (1);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed2.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed3.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/another.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/ignore.sql", []byte("INSERT INTO table3 VALUES (3);"), 0644))
-		// Mock config patterns
-		Config.Db.Seed.SqlPaths = []string{"seeds/seed[12].sql", "seeds/ano*.sql"}
-
-		// Run test
-		files, err := GetSeedFiles(fsys)
-
-		// Check error
-		assert.NoError(t, err)
-		// Validate files
-		assert.ElementsMatch(t, []string{"supabase/seeds/seed1.sql", "supabase/seeds/seed2.sql", "supabase/seeds/another.sql"}, files)
-	})
-	t.Run("returns seed files matching patterns skip duplicates", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Create seed files
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed1.sql", []byte("INSERT INTO table1 VALUES (1);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed2.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed3.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/another.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/ignore.sql", []byte("INSERT INTO table3 VALUES (3);"), 0644))
-		// Mock config patterns
-		Config.Db.Seed.SqlPaths = []string{"seeds/seed[12].sql", "seeds/ano*.sql", "seeds/seed*.sql"}
-
-		// Run test
-		files, err := GetSeedFiles(fsys)
-
-		// Check error
-		assert.NoError(t, err)
-		// Validate files
-		assert.ElementsMatch(t, []string{"supabase/seeds/seed1.sql", "supabase/seeds/seed2.sql", "supabase/seeds/another.sql", "supabase/seeds/seed3.sql"}, files)
-	})
-
-	t.Run("returns error on invalid pattern", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Mock config patterns
-		Config.Db.Seed.SqlPaths = []string{"[*!#@D#"}
-
-		// Run test
-		files, err := GetSeedFiles(fsys)
-
-		// Check error
-		assert.Nil(t, err)
-		// The resuling seed list should be empty
-		assert.ElementsMatch(t, []string{}, files)
-	})
-
-	t.Run("returns empty list if no files match", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Mock config patterns
-		Config.Db.Seed.SqlPaths = []string{"seeds/*.sql"}
-
-		// Run test
-		files, err := GetSeedFiles(fsys)
-
-		// Check error
-		assert.NoError(t, err)
-		// Validate files
-		assert.Empty(t, files)
-	})
-}
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 733b3c7b..7d31d77a 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -3,6 +3,7 @@ package config
 import (
 	"bytes"
 	_ "embed"
+	"path"
 	"strings"
 	"testing"
 	fs "testing/fstest"
@@ -247,3 +248,90 @@ func TestValidateHookURI(t *testing.T) {
 		})
 	}
 }
+
+func TestLoadSeedPaths(t *testing.T) {
+	t.Run("returns seed files matching patterns", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := fs.MapFS{
+			"supabase/seeds/seed1.sql":   &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")},
+			"supabase/seeds/seed2.sql":   &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/seed3.sql":   &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/ignore.sql":  &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")},
+		}
+		// Mock config patterns
+		config := seed{
+			Enabled: true,
+			GlobPatterns: []string{
+				"seeds/seed[12].sql",
+				"seeds/ano*.sql",
+			},
+		}
+		// Run test
+		err := config.loadSeedPaths("supabase", fsys)
+		// Check error
+		assert.NoError(t, err)
+		// Validate files
+		assert.ElementsMatch(t, []string{
+			"supabase/seeds/seed1.sql",
+			"supabase/seeds/seed2.sql",
+			"supabase/seeds/another.sql",
+		}, config.SqlPaths)
+	})
+	t.Run("returns seed files matching patterns skip duplicates", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := fs.MapFS{
+			"supabase/seeds/seed1.sql":   &fs.MapFile{Data: []byte("INSERT INTO table1 VALUES (1);")},
+			"supabase/seeds/seed2.sql":   &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/seed3.sql":   &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/another.sql": &fs.MapFile{Data: []byte("INSERT INTO table2 VALUES (2);")},
+			"supabase/seeds/ignore.sql":  &fs.MapFile{Data: []byte("INSERT INTO table3 VALUES (3);")},
+		}
+		// Mock config patterns
+		config := seed{
+			Enabled: true,
+			GlobPatterns: []string{
+				"seeds/seed[12].sql",
+				"seeds/ano*.sql",
+				"seeds/seed*.sql",
+			},
+		}
+		// Run test
+		err := config.loadSeedPaths("supabase", fsys)
+		// Check error
+		assert.NoError(t, err)
+		// Validate files
+		assert.ElementsMatch(t, []string{
+			"supabase/seeds/seed1.sql",
+			"supabase/seeds/seed2.sql",
+			"supabase/seeds/another.sql",
+			"supabase/seeds/seed3.sql",
+		}, config.SqlPaths)
+	})
+
+	t.Run("returns error on invalid pattern", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := fs.MapFS{}
+		// Mock config patterns
+		config := seed{Enabled: true, GlobPatterns: []string{"[*!#@D#"}}
+		// Run test
+		err := config.loadSeedPaths("", fsys)
+		// Check error
+		assert.ErrorIs(t, err, path.ErrBadPattern)
+		// The resuling seed list should be empty
+		assert.Empty(t, config.SqlPaths)
+	})
+
+	t.Run("returns empty list if no files match", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := fs.MapFS{}
+		// Mock config patterns
+		config := seed{Enabled: true, GlobPatterns: []string{"seeds/*.sql"}}
+		// Run test
+		err := config.loadSeedPaths("", fsys)
+		// Check error
+		assert.NoError(t, err)
+		// Validate files
+		assert.Empty(t, config.SqlPaths)
+	})
+}
diff --git a/pkg/migration/seed_test.go b/pkg/migration/seed_test.go
index e81d814a..7e9cd4ac 100644
--- a/pkg/migration/seed_test.go
+++ b/pkg/migration/seed_test.go
@@ -8,50 +8,138 @@ import (
 	fs "testing/fstest"
 
 	"github.com/jackc/pgerrcode"
+	"github.com/jackc/pgx/v4"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"github.com/supabase/cli/pkg/pgtest"
 )
 
 //go:embed testdata/seed.sql
 var testSeed string
 
-func TestSeedData(t *testing.T) {
+func TestPendingSeeds(t *testing.T) {
 	pending := []string{"testdata/seed.sql"}
 
-	t.Run("seeds from file", func(t *testing.T) {
+	t.Run("finds new seeds", func(t *testing.T) {
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(testSeed).
-			Reply("INSERT 0 1")
+		conn.Query(SELECT_SEED_TABLE).
+			Reply("SELECT 0")
+		// Run test
+		seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations)
+		// Check error
+		assert.NoError(t, err)
+		require.Len(t, seeds, 1)
+		assert.Equal(t, seeds[0].Path, pending[0])
+		assert.Equal(t, seeds[0].Hash, "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3")
+		assert.False(t, seeds[0].Dirty)
+	})
+
+	t.Run("finds dirty seeds", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(SELECT_SEED_TABLE).
+			Reply("SELECT 1", SeedFile{Path: pending[0], Hash: "outdated"})
+		// Run test
+		seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations)
+		// Check error
+		assert.NoError(t, err)
+		require.Len(t, seeds, 1)
+		assert.Equal(t, seeds[0].Path, pending[0])
+		assert.Equal(t, seeds[0].Hash, "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3")
+		assert.True(t, seeds[0].Dirty)
+	})
+
+	t.Run("skips applied seed", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(SELECT_SEED_TABLE).
+			Reply("SELECT 1", SeedFile{Path: pending[0], Hash: "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3"})
+		// Run test
+		seeds, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations)
+		// Check error
+		assert.NoError(t, err)
+		require.Empty(t, seeds)
+	})
+
+	t.Run("ignores missing seed table", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(SELECT_SEED_TABLE).
+			ReplyError(pgerrcode.UndefinedTable, `relation "seed_files" does not exist`)
 		// Run test
-		err := SeedData(context.Background(), pending, conn.MockClient(t), testMigrations)
+		_, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), testMigrations)
 		// Check error
 		assert.NoError(t, err)
 	})
 
 	t.Run("throws error on missing file", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(SELECT_SEED_TABLE).
+			Reply("SELECT 0")
 		// Setup in-memory fs
 		fsys := fs.MapFS{}
 		// Run test
-		err := SeedData(context.Background(), pending, nil, fsys)
+		_, err := GetPendingSeeds(context.Background(), pending, conn.MockClient(t), fsys)
 		// Check error
 		assert.ErrorIs(t, err, os.ErrNotExist)
 	})
+}
+
+func TestSeedData(t *testing.T) {
+	t.Run("seeds from file", func(t *testing.T) {
+		seed := SeedFile{
+			Path:  "testdata/seed.sql",
+			Hash:  "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3",
+			Dirty: true,
+		}
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		mockSeedHistory(conn).
+			Query(UPSERT_SEED_FILE, seed.Path, seed.Hash).
+			Reply("INSERT 0 1")
+		// Run test
+		err := SeedData(context.Background(), []SeedFile{seed}, conn.MockClient(t), testMigrations)
+		// Check error
+		assert.NoError(t, err)
+	})
 
-	t.Run("throws error on insert failure", func(t *testing.T) {
+	t.Run("throws error on upsert failure", func(t *testing.T) {
+		seed := SeedFile{
+			Path: "testdata/seed.sql",
+			Hash: "61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3",
+		}
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(testSeed).
+		mockSeedHistory(conn).
+			Query(testSeed+`;INSERT INTO supabase_migrations.seed_files(path, hash) VALUES( 'testdata/seed.sql' ,  '61868484fc0ddca2a2022217629a9fd9a4cf1ca479432046290797d6d40ffcc3' ) ON CONFLICT (path) DO UPDATE SET hash = EXCLUDED.hash`).
 			ReplyError(pgerrcode.NotNullViolation, `null value in column "age" of relation "employees"`)
 		// Run test
-		err := SeedData(context.Background(), pending, conn.MockClient(t), testMigrations)
+		err := SeedData(context.Background(), []SeedFile{seed}, conn.MockClient(t, func(cc *pgx.ConnConfig) {
+			cc.PreferSimpleProtocol = true
+		}), testMigrations)
 		// Check error
 		assert.ErrorContains(t, err, `ERROR: null value in column "age" of relation "employees" (SQLSTATE 23502)`)
 	})
 }
 
+func mockSeedHistory(conn *pgtest.MockConn) *pgtest.MockConn {
+	conn.Query(SET_LOCK_TIMEOUT).
+		Query(CREATE_VERSION_SCHEMA).
+		Reply("CREATE SCHEMA").
+		Query(CREATE_SEED_TABLE).
+		Reply("CREATE TABLE")
+	return conn
+}
+
 //go:embed testdata/1_globals.sql
 var testGlobals string
 
diff --git a/pkg/pgtest/mock.go b/pkg/pgtest/mock.go
index b73ad255..0f5e9bb8 100644
--- a/pkg/pgtest/mock.go
+++ b/pkg/pgtest/mock.go
@@ -153,7 +153,10 @@ func (r *MockConn) Reply(tag string, rows ...interface{}) *MockConn {
 		} else if t := reflect.TypeOf(rows[0]); t.Kind() == reflect.Struct {
 			s := reflect.ValueOf(rows[0])
 			for i := 0; i < s.NumField(); i++ {
-				name := t.Field(i).Name
+				name := pgxv5.GetColumnName(t.Field(i))
+				if len(name) == 0 {
+					continue
+				}
 				v := s.Field(i).Interface()
 				if fd := toFieldDescription(v); fd != nil {
 					fd.Name = []byte(name)
@@ -182,6 +185,9 @@ func (r *MockConn) Reply(tag string, rows ...interface{}) *MockConn {
 		} else if t := reflect.TypeOf(data); t.Kind() == reflect.Struct {
 			s := reflect.ValueOf(rows[0])
 			for i := 0; i < s.NumField(); i++ {
+				if name := pgxv5.GetColumnName(t.Field(i)); len(name) == 0 {
+					continue
+				}
 				v := s.Field(i).Interface()
 				if value, oid := r.encodeValueArg(v); oid > 0 {
 					dr.Values = append(dr.Values, value)