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

supabase / cli / 21474811322

29 Jan 2026 10:34AM UTC coverage: 49.822% (-6.4%) from 56.176%
21474811322

Pull #4770

github

web-flow
Merge 46bfe9ba6 into 230667a9f
Pull Request #4770: feat: add `supabase dev` command

12 of 1573 new or added lines in 18 files covered. (0.76%)

7 existing lines in 2 files now uncovered.

6860 of 13769 relevant lines covered (49.82%)

5.61 hits per line

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

0.0
/internal/dev/dev.go
1
package dev
2

3
import (
4
        "context"
5
        "fmt"
6
        "os"
7
        "os/exec"
8
        "os/signal"
9
        "path/filepath"
10
        "strings"
11
        "syscall"
12

13
        "github.com/go-errors/errors"
14
        "github.com/spf13/afero"
15
        "github.com/supabase/cli/internal/dev/onboarding"
16
        "github.com/supabase/cli/internal/start"
17
        "github.com/supabase/cli/internal/utils"
18
        "github.com/supabase/cli/internal/utils/flags"
19
)
20

21
// RunOptions configures the dev command behavior
22
type RunOptions struct {
23
        SkipOnboarding bool
24
        Interactive    bool
25
}
26

27
// Run starts the dev session with optional onboarding
NEW
28
func Run(ctx context.Context, fsys afero.Fs, opts RunOptions) error {
×
NEW
29
        // Step 1: Run onboarding if not skipped
×
NEW
30
        if !opts.SkipOnboarding {
×
NEW
31
                onboardingOpts := onboarding.Options{
×
NEW
32
                        Interactive: opts.Interactive,
×
NEW
33
                }
×
NEW
34
                if _, err := onboarding.Run(ctx, fsys, onboardingOpts); err != nil {
×
NEW
35
                        return err
×
NEW
36
                }
×
NEW
37
        } else {
×
NEW
38
                // Skip onboarding, just load config directly
×
NEW
39
                if err := flags.LoadConfig(fsys); err != nil {
×
NEW
40
                        return err
×
NEW
41
                }
×
42
        }
43

44
        // Step 2: Ensure local database is running
NEW
45
        if err := ensureDbRunning(ctx, fsys); err != nil {
×
NEW
46
                return err
×
NEW
47
        }
×
48

49
        // Step 3: Create and run the dev session
NEW
50
        session := NewSession(ctx, fsys)
×
NEW
51
        return session.Run()
×
52
}
53

54
// ensureDbRunning starts the local database if it's not already running
NEW
55
func ensureDbRunning(ctx context.Context, fsys afero.Fs) error {
×
NEW
56
        if err := utils.AssertSupabaseDbIsRunning(); err == nil {
×
NEW
57
                fmt.Fprintln(os.Stderr, "Using existing local database")
×
NEW
58
                return nil
×
NEW
59
        } else if !errors.Is(err, utils.ErrNotRunning) {
×
NEW
60
                return err
×
NEW
61
        }
×
62

NEW
63
        fmt.Fprintln(os.Stderr, "Starting local database...")
×
NEW
64
        return start.Run(ctx, fsys, nil, false)
×
65
}
66

67
// Session manages the dev mode lifecycle
68
type Session struct {
69
        ctx    context.Context
70
        cancel context.CancelFunc
71
        fsys   afero.Fs
72
        dirty  bool // tracks whether schema changes have been applied
73
}
74

75
// NewSession creates a new dev session
NEW
76
func NewSession(ctx context.Context, fsys afero.Fs) *Session {
×
NEW
77
        ctx, cancel := context.WithCancel(ctx)
×
NEW
78
        return &Session{
×
NEW
79
                ctx:    ctx,
×
NEW
80
                cancel: cancel,
×
NEW
81
                fsys:   fsys,
×
NEW
82
                dirty:  false,
×
NEW
83
        }
×
NEW
84
}
×
85

86
// Run starts the dev session main loop
NEW
87
func (s *Session) Run() error {
×
NEW
88
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
89

×
NEW
90
        // Check if schemas workflow is enabled
×
NEW
91
        if !schemasConfig.IsEnabled() {
×
NEW
92
                fmt.Fprintln(os.Stderr, "[dev] Schema workflow is disabled in config")
×
NEW
93
                fmt.Fprintln(os.Stderr, "[dev] Press Ctrl+C to stop")
×
NEW
94

×
NEW
95
                // Set up signal handling for graceful shutdown
×
NEW
96
                sigCh := make(chan os.Signal, 1)
×
NEW
97
                signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
×
NEW
98
                <-sigCh
×
NEW
99
                fmt.Fprintln(os.Stderr)
×
NEW
100
                fmt.Fprintln(os.Stderr, "[dev] Stopping dev session...")
×
NEW
101
                return nil
×
NEW
102
        }
×
103

104
        // Get watch globs from config
NEW
105
        watchGlobs := schemasConfig.Watch
×
NEW
106
        if len(watchGlobs) == 0 {
×
NEW
107
                // Fallback to default if not configured
×
NEW
108
                watchGlobs = []string{"schemas/**/*.sql"}
×
NEW
109
        }
×
110

111
        // Validate config on startup
NEW
112
        s.validateConfig()
×
NEW
113

×
NEW
114
        // Create schemas directory if using default pattern and it doesn't exist
×
NEW
115
        if exists, err := afero.DirExists(s.fsys, utils.SchemasDir); err != nil {
×
NEW
116
                return errors.Errorf("failed to check schemas directory: %w", err)
×
NEW
117
        } else if !exists {
×
NEW
118
                fmt.Fprintf(os.Stderr, "[dev] Creating %s directory...\n", utils.Aqua(utils.SchemasDir))
×
NEW
119
                if err := s.fsys.MkdirAll(utils.SchemasDir, 0755); err != nil {
×
NEW
120
                        return errors.Errorf("failed to create schemas directory: %w", err)
×
NEW
121
                }
×
122
        }
123

124
        // Set up signal handling
NEW
125
        sigCh := make(chan os.Signal, 1)
×
NEW
126
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
×
NEW
127

×
NEW
128
        // Get seed globs from [db.seed] config (already resolved to absolute paths)
×
NEW
129
        seedConfig := &utils.Config.Dev.Seed
×
NEW
130
        var seedGlobs []string
×
NEW
131
        if seedConfig.IsEnabled() && utils.Config.Db.Seed.Enabled {
×
NEW
132
                seedGlobs = utils.Config.Db.Seed.SqlPaths
×
NEW
133
        }
×
134

135
        // Display configuration
NEW
136
        fmt.Fprintln(os.Stderr)
×
NEW
137
        fmt.Fprintf(os.Stderr, "[dev] Watching: %v\n", watchGlobs)
×
NEW
138
        if schemasConfig.OnChange != "" {
×
NEW
139
                fmt.Fprintf(os.Stderr, "[dev] On change: %s\n", utils.Aqua(schemasConfig.OnChange))
×
NEW
140
        } else {
×
NEW
141
                fmt.Fprintf(os.Stderr, "[dev] On change: %s\n", utils.Aqua("(internal differ)"))
×
NEW
142
        }
×
NEW
143
        if schemasConfig.Types != "" {
×
NEW
144
                fmt.Fprintf(os.Stderr, "[dev] Types output: %s\n", utils.Aqua(schemasConfig.Types))
×
NEW
145
        }
×
NEW
146
        if seedConfig.IsEnabled() {
×
NEW
147
                if seedConfig.OnChange != "" {
×
NEW
148
                        fmt.Fprintf(os.Stderr, "[dev] Seed: %s\n", utils.Aqua(seedConfig.OnChange))
×
NEW
149
                } else if utils.Config.Db.Seed.Enabled && len(utils.Config.Db.Seed.SqlPaths) > 0 {
×
NEW
150
                        fmt.Fprintf(os.Stderr, "[dev] Seed: %s\n", utils.Aqua("(internal)"))
×
NEW
151
                }
×
152
        }
NEW
153
        fmt.Fprintln(os.Stderr, "[dev] Press Ctrl+C to stop")
×
NEW
154
        fmt.Fprintln(os.Stderr)
×
NEW
155

×
NEW
156
        // Create the schema watcher
×
NEW
157
        watcher, err := NewSchemaWatcher(s.fsys, watchGlobs, seedGlobs)
×
NEW
158
        if err != nil {
×
NEW
159
                return err
×
NEW
160
        }
×
NEW
161
        defer watcher.Close()
×
NEW
162

×
NEW
163
        // Start the watcher
×
NEW
164
        go watcher.Start()
×
NEW
165

×
NEW
166
        // Apply initial schema state
×
NEW
167
        fmt.Fprintln(os.Stderr, "[dev] Applying initial schema state...")
×
NEW
168
        if err := s.applySchemaChanges(); err != nil {
×
NEW
169
                fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
170
        } else {
×
NEW
171
                fmt.Fprintln(os.Stderr, "[dev] Initial sync complete")
×
NEW
172
        }
×
173

174
        // Run initial seed (after schema sync)
NEW
175
        if seedConfig.IsEnabled() {
×
NEW
176
                fmt.Fprintln(os.Stderr, "[dev] Running initial seed...")
×
NEW
177
                if err := s.runSeed(); err != nil {
×
NEW
178
                        fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
179
                } else {
×
NEW
180
                        fmt.Fprintln(os.Stderr, "[dev] Initial seed complete")
×
NEW
181
                }
×
182
        }
183

NEW
184
        fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
185

×
NEW
186
        // Main event loop
×
NEW
187
        for {
×
NEW
188
                select {
×
NEW
189
                case <-s.ctx.Done():
×
NEW
190
                        CleanupShadow(s.ctx)
×
NEW
191
                        return s.ctx.Err()
×
NEW
192
                case <-sigCh:
×
NEW
193
                        fmt.Fprintln(os.Stderr)
×
NEW
194
                        fmt.Fprintln(os.Stderr, "[dev] Stopping dev session...")
×
NEW
195
                        CleanupShadow(s.ctx)
×
NEW
196
                        s.showDirtyWarning()
×
NEW
197
                        return nil
×
NEW
198
                case <-watcher.RestartCh:
×
NEW
199
                        // Check if seeds changed - if so, reseed the database
×
NEW
200
                        if watcher.SeedsChanged() {
×
NEW
201
                                fmt.Fprintln(os.Stderr, "[dev] Reseeding database...")
×
NEW
202
                                if err := s.runSeed(); err != nil {
×
NEW
203
                                        fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Red("Error:"), err.Error())
×
NEW
204
                                } else {
×
NEW
205
                                        fmt.Fprintln(os.Stderr, "[dev] Reseed complete")
×
NEW
206
                                }
×
NEW
207
                                fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
208
                                continue
×
209
                        }
210

211
                        // Check if migrations changed - if so, just invalidate the shadow template
212
                        // We do NOT auto-apply migrations because:
213
                        // 1. If created by `db diff -f`, local DB already has those changes
214
                        // 2. If from external source (git pull), user should restart dev mode
NEW
215
                        if watcher.MigrationsChanged() {
×
NEW
216
                                fmt.Fprintln(os.Stderr, "[dev] Migration file changed - shadow template invalidated")
×
NEW
217
                                InvalidateShadowTemplate()
×
NEW
218
                                // Don't trigger schema diff - migrations need manual application
×
NEW
219
                                // The next schema file change will use the updated shadow
×
NEW
220
                                fmt.Fprintln(os.Stderr, "[dev] Note: Run 'supabase db reset' or restart dev mode to apply new migrations")
×
NEW
221
                                fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
222
                                continue
×
223
                        }
224

NEW
225
                        fmt.Fprintln(os.Stderr, "[dev] Applying schema changes...")
×
NEW
226
                        if err := s.applySchemaChanges(); err != nil {
×
NEW
227
                                fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Red("Error:"), err.Error())
×
NEW
228
                        } else {
×
NEW
229
                                fmt.Fprintln(os.Stderr, "[dev] Changes applied successfully")
×
NEW
230
                        }
×
NEW
231
                        fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
232
                case err := <-watcher.ErrCh:
×
NEW
233
                        CleanupShadow(s.ctx)
×
NEW
234
                        return errors.Errorf("watcher error: %w", err)
×
235
                }
236
        }
237
}
238

239
// validateConfig checks the configuration and warns about potential issues
NEW
240
func (s *Session) validateConfig() {
×
NEW
241
        schemasCfg := &utils.Config.Dev.Schemas
×
NEW
242
        seedCfg := &utils.Config.Dev.Seed
×
NEW
243

×
NEW
244
        // Warn if schema on_change command might not exist
×
NEW
245
        if schemasCfg.OnChange != "" {
×
NEW
246
                s.validateOnChangeCommand(schemasCfg.OnChange, "schemas")
×
NEW
247
        }
×
248

249
        // Warn if seed on_change command might not exist
NEW
250
        if seedCfg.OnChange != "" {
×
NEW
251
                s.validateOnChangeCommand(seedCfg.OnChange, "seed")
×
NEW
252
        }
×
253

254
        // Warn if types output directory doesn't exist
NEW
255
        if schemasCfg.Types != "" {
×
NEW
256
                dir := filepath.Dir(schemasCfg.Types)
×
NEW
257
                if dir != "." && dir != "" {
×
NEW
258
                        if exists, _ := afero.DirExists(s.fsys, dir); !exists {
×
NEW
259
                                fmt.Fprintf(os.Stderr, "[dev] %s types output directory '%s' does not exist\n", utils.Yellow("Warning:"), dir)
×
NEW
260
                        }
×
261
                }
262
        }
263
}
264

265
// validateOnChangeCommand checks if the on_change command exists
NEW
266
func (s *Session) validateOnChangeCommand(command, configSection string) {
×
NEW
267
        cmdParts := strings.Fields(command)
×
NEW
268
        if len(cmdParts) > 0 {
×
NEW
269
                cmdName := cmdParts[0]
×
NEW
270
                // Check if it's a known package manager command
×
NEW
271
                if cmdName != "npx" && cmdName != "npm" && cmdName != "yarn" && cmdName != "pnpm" && cmdName != "bunx" {
×
NEW
272
                        if _, err := exec.LookPath(cmdName); err != nil {
×
NEW
273
                                fmt.Fprintf(os.Stderr, "[dev] %s %s on_change command '%s' not found in PATH\n", utils.Yellow("Warning:"), configSection, cmdName)
×
NEW
274
                        }
×
275
                }
276
        }
277
}
278

279
// applySchemaChanges validates and applies schema changes to the local database
NEW
280
func (s *Session) applySchemaChanges() error {
×
NEW
281
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
282

×
NEW
283
        // Step 0: Verify DB is still running
×
NEW
284
        if err := utils.AssertSupabaseDbIsRunning(); err != nil {
×
NEW
285
                return errors.Errorf("local database stopped unexpectedly: %w", err)
×
NEW
286
        }
×
287

288
        // Check if we should use a custom on_change command
NEW
289
        if schemasConfig.OnChange != "" {
×
NEW
290
                return s.runCustomOnChange(schemasConfig.OnChange)
×
NEW
291
        }
×
292

293
        // Step 1: Load all schema files
NEW
294
        schemaFiles, err := loadSchemaFiles(s.fsys)
×
NEW
295
        if err != nil {
×
NEW
296
                return err
×
NEW
297
        }
×
298

NEW
299
        if len(schemaFiles) == 0 {
×
NEW
300
                fmt.Fprintln(os.Stderr, "No schema files found")
×
NEW
301
                return nil
×
NEW
302
        }
×
303

304
        // Step 2: Validate SQL syntax of all schema files
NEW
305
        if err := ValidateSchemaFiles(schemaFiles, s.fsys); err != nil {
×
NEW
306
                return err
×
NEW
307
        }
×
308

309
        // Step 3: Run diff and apply changes
NEW
310
        if err := s.diffAndApply(); err != nil {
×
NEW
311
                return err
×
NEW
312
        }
×
313

314
        // Step 4: Generate types if configured
NEW
315
        if schemasConfig.Types != "" {
×
NEW
316
                if err := s.generateTypes(schemasConfig.Types); err != nil {
×
NEW
317
                        fmt.Fprintf(os.Stderr, "%s Failed to generate types: %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
318
                }
×
319
        }
320

NEW
321
        return nil
×
322
}
323

324
// runCustomOnChange executes a custom command when files change
NEW
325
func (s *Session) runCustomOnChange(command string) error {
×
NEW
326
        fmt.Fprintf(os.Stderr, "Running: %s\n", utils.Aqua(command))
×
NEW
327

×
NEW
328
        cmd := exec.CommandContext(s.ctx, "sh", "-c", command)
×
NEW
329
        cmd.Stdout = os.Stdout
×
NEW
330
        cmd.Stderr = os.Stderr
×
NEW
331
        cmd.Dir = utils.CurrentDirAbs
×
NEW
332

×
NEW
333
        if err := cmd.Run(); err != nil {
×
NEW
334
                return errors.Errorf("on_change command failed: %w", err)
×
NEW
335
        }
×
336

NEW
337
        s.dirty = true
×
NEW
338

×
NEW
339
        // Generate types if configured (runs after custom command too)
×
NEW
340
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
341
        if schemasConfig.Types != "" {
×
NEW
342
                if err := s.generateTypes(schemasConfig.Types); err != nil {
×
NEW
343
                        fmt.Fprintf(os.Stderr, "%s Failed to generate types: %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
344
                }
×
345
        }
346

NEW
347
        return nil
×
348
}
349

350
// generateTypes generates TypeScript types and writes them to the configured path
NEW
351
func (s *Session) generateTypes(outputPath string) error {
×
NEW
352
        fmt.Fprintf(os.Stderr, "Generating types to %s...\n", utils.Aqua(outputPath))
×
NEW
353

×
NEW
354
        // Run supabase gen types typescript --local
×
NEW
355
        cmd := exec.CommandContext(s.ctx, "supabase", "gen", "types", "typescript", "--local")
×
NEW
356
        output, err := cmd.Output()
×
NEW
357
        if err != nil {
×
NEW
358
                if exitErr, ok := err.(*exec.ExitError); ok {
×
NEW
359
                        return errors.Errorf("type generation failed: %s", string(exitErr.Stderr))
×
NEW
360
                }
×
NEW
361
                return errors.Errorf("type generation failed: %w", err)
×
362
        }
363

364
        // Write output to file
NEW
365
        if err := afero.WriteFile(s.fsys, outputPath, output, 0644); err != nil {
×
NEW
366
                return errors.Errorf("failed to write types file: %w", err)
×
NEW
367
        }
×
368

NEW
369
        fmt.Fprintf(os.Stderr, "Types generated: %s\n", utils.Aqua(outputPath))
×
NEW
370
        return nil
×
371
}
372

373
// diffAndApply runs the schema diff and applies changes to local DB
NEW
374
func (s *Session) diffAndApply() error {
×
NEW
375
        applied, err := DiffAndApply(s.ctx, s.fsys, os.Stderr)
×
NEW
376
        if err != nil {
×
NEW
377
                return err
×
NEW
378
        }
×
NEW
379
        if applied {
×
NEW
380
                s.dirty = true
×
NEW
381
        }
×
NEW
382
        return nil
×
383
}
384

385
// showDirtyWarning warns if local DB has uncommitted schema changes
NEW
386
func (s *Session) showDirtyWarning() {
×
NEW
387
        if !s.dirty {
×
NEW
388
                return
×
NEW
389
        }
×
NEW
390
        fmt.Fprintf(os.Stderr, "%s Local database has uncommitted schema changes!\n", utils.Yellow("Warning:"))
×
NEW
391
        fmt.Fprintf(os.Stderr, "    Run '%s' to create a migration\n", utils.Aqua("supabase db diff -f migration_name"))
×
392
}
393

394
// loadSchemaFiles returns all .sql files in the schemas directory
NEW
395
func loadSchemaFiles(fsys afero.Fs) ([]string, error) {
×
NEW
396
        var files []string
×
NEW
397
        err := afero.Walk(fsys, utils.SchemasDir, func(path string, info os.FileInfo, err error) error {
×
NEW
398
                if err != nil {
×
NEW
399
                        return err
×
NEW
400
                }
×
NEW
401
                if info.Mode().IsRegular() && len(path) > 4 && path[len(path)-4:] == ".sql" {
×
NEW
402
                        files = append(files, path)
×
NEW
403
                }
×
NEW
404
                return nil
×
405
        })
NEW
406
        return files, err
×
407
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc