Running Apps
Use cli.RunApp() to wrap your Forge application in a production-ready CLI with built-in serve, migrate, and introspection commands
Running Apps
Forge applications can be started in two ways: calling app.Run() directly for simplicity, or wrapping them with cli.RunApp() for a production-ready CLI experience. The CLI wrapper adds built-in commands for serving, database migrations, health checks, and extension introspection — all in a single function call.
app.Run() vs cli.RunApp()
package main
import "github.com/xraph/forge"
func main() {
app := forge.New(
forge.WithAppName("my-app"),
forge.WithAppVersion("1.0.0"),
)
app.Router().GET("/", func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
})
// Starts the HTTP server and blocks until shutdown signal
if err := app.Run(); err != nil {
panic(err)
}
}Use app.Run() when you want the simplest possible setup — just a server, no CLI commands.
package main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/cli"
)
func main() {
// RunApp takes a setup closure that creates the app lazily
cli.RunApp(func(ctx cli.CommandContext) (forge.App, error) {
app := forge.New(
forge.WithAppName("my-app"),
forge.WithAppVersion("1.0.0"),
)
app.Router().GET("/", func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
})
return app, nil
})
}Use cli.RunApp() when you need migration commands, health checks from the terminal, or extension introspection. The setup closure receives a CommandContext so CLI flags can drive app configuration.
Quick Start
package main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/cli"
)
func main() {
cli.RunApp(func(ctx cli.CommandContext) (forge.App, error) {
return forge.New(
forge.WithAppName("my-app"),
forge.WithAppVersion("1.0.0"),
), nil
})
}Build and run:
go build -o my-app .
./my-app # Starts the server (defaults to "serve")
./my-app serve # Explicit serve command
./my-app info # Show app name, version, environment
./my-app health # Run health checks
./my-app extensions # List registered extensionsBuilt-in Commands
cli.RunApp() automatically registers these commands:
| Command | Aliases | Description |
|---|---|---|
serve | start, run | Start the HTTP server |
migrate up | Run all pending migrations | |
migrate down | Rollback the last migration batch | |
migrate status | Show migration status table | |
info | Display app name, version, environment, uptime | |
health | Run health checks on all registered components | |
extensions | List all registered extensions with version info |
When invoked with no arguments, RunApp defaults to the serve command:
./my-app # Same as: ./my-app serveMigration commands only appear when at least one registered extension implements MigratableExtension. Extensions that implement CLICommandProvider contribute additional commands automatically.
Auto-Migration on Serve
Enable WithAutoMigrate() to run pending database migrations before the HTTP server starts accepting requests:
cli.RunApp(appSetup, cli.WithAutoMigrate())When enabled, the serve command registers a PhaseBeforeRun lifecycle hook that:
- Discovers all extensions implementing
MigratableExtension - Calls
Migrate(ctx)on each one - Logs the count of applied migrations per extension
- Fails the startup if any migration errors
$ ./my-app serve
INFO running auto-migrations before serve
INFO migrations applied extension=grove applied=3
INFO server listening on :8080Auto-migration runs with priority 1000, so it executes before your other PhaseBeforeRun hooks (which default to priority 0). If a migration fails, the app will not start.
Disabling Migrations
You can disable auto-migrations at runtime without changing code. This is useful for production deployments where migrations are handled separately (e.g., in a CI/CD pipeline).
# .forge.yaml
database:
disable_migrations: truecli.RunApp(func(ctx cli.CommandContext) (forge.App, error) {
return forge.New(
forge.WithAppName("my-app"),
forge.WithDisableMigrations(),
), nil
},
cli.WithAutoMigrate(),
)
// Auto-migrate is enabled in code, but disabled via config — migrations are skippedWhen migrations are disabled, the auto-migrate hook logs a message and skips:
$ ./my-app serve
INFO auto-migrations disabled via configuration
INFO server listening on :8080Priority order: explicit forge.WithDisableMigrations() in Go code takes precedence. If not set in code, the .forge.yaml database.disable_migrations value is used. The migrate up / migrate down / migrate status CLI commands are unaffected — they always work regardless of this setting.
Options Reference
Configure cli.RunApp() behavior with functional options:
WithAutoMigrate
Run pending migrations before the HTTP server starts (during PhaseBeforeRun):
cli.RunApp(appSetup, cli.WithAutoMigrate())WithGlobalFlags
Add flags available to all commands and the setup closure:
cli.RunApp(func(ctx cli.CommandContext) (forge.App, error) {
return forge.New(
forge.WithAppName("my-app"),
forge.WithHTTPAddress(":"+ctx.String("port")),
), nil
},
cli.WithGlobalFlags(
cli.NewStringFlag("port", "p", "HTTP port", "8080"),
),
)WithExtraCommands
Add custom commands alongside the built-in ones:
cli.RunApp(appSetup,
cli.WithExtraCommands(
cli.NewCommand("seed", "Seed the database", handleSeed),
cli.NewCommand("reindex", "Rebuild search index", handleReindex),
),
)WithDisableMigrationCommands
Remove the migrate command group even when MigratableExtension extensions are present:
cli.RunApp(appSetup, cli.WithDisableMigrationCommands())WithDisableServeCommand
Remove the built-in serve command (useful for CLI-only tools that don't need an HTTP server):
cli.RunApp(appSetup, cli.WithDisableServeCommand())WithCLIName / WithCLIVersion / WithCLIDescription
Override CLI metadata (defaults to the Forge app's name and version):
cli.RunApp(appSetup,
cli.WithCLIName("myctl"),
cli.WithCLIVersion("2.0.0"),
cli.WithCLIDescription("MyApp management CLI"),
)Extension-Contributed Commands
Extensions that implement CLICommandProvider automatically contribute their commands to the CLI:
type MyExtension struct { /* ... */ }
func (e *MyExtension) CLICommands() []any {
return []any{
cli.NewCommand("seed", "Seed the database", e.handleSeed),
cli.NewCommand("dump", "Dump database schema", e.handleDump),
}
}Once registered with the app, the commands appear alongside the built-in ones:
$ ./my-app seed # Provided by MyExtension
$ ./my-app dump # Provided by MyExtensionMigration Commands
The migrate parent command provides three subcommands for managing database migrations from the terminal.
migrate up
Starts the app (initializes extensions without the HTTP server), discovers all MigratableExtension extensions, and runs pending migrations:
$ ./my-app migrate up
✓ Applied: core/create_users
✓ Applied: core/add_email_index
✓ Applied: billing/create_invoices
Applied 3 migration(s)migrate down
Rolls back the last batch of migrations. Prompts for confirmation unless --force is used:
$ ./my-app migrate down
? Are you sure you want to rollback? (y/N) y
Rolled back 1 migration(s) for grove
↩ core/add_email_index
$ ./my-app migrate down --force # Skip confirmationmigrate status
Displays a table of applied and pending migrations for each extension:
$ ./my-app migrate status
grove Migrations (grove v0.12.0):
Group: core
┌────────────────┬──────────────────┬─────────┬──────────────────────┐
│ Version │ Name │ Status │ Applied At │
├────────────────┼──────────────────┼─────────┼──────────────────────┤
│ 20240101000000 │ create_users │ applied │ 2024-01-01T12:00:00Z │
│ 20240201000000 │ add_email_index │ pending │ │
└────────────────┴──────────────────┴─────────┴──────────────────────┘Complete Example
package main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/cli"
groveext "github.com/xraph/grove/extension"
"myapp/migrations/core"
"myapp/migrations/billing"
)
func main() {
cli.RunApp(func(ctx cli.CommandContext) (forge.App, error) {
grove := groveext.New(
groveext.WithDSN(ctx.String("dsn")),
groveext.WithMigrations(core.Migrations, billing.Migrations),
)
app := forge.New(
forge.WithAppName("myapp"),
forge.WithAppVersion("1.0.0"),
forge.WithHTTPAddress(":"+ctx.String("port")),
forge.WithExtensions(grove),
)
app.Router().GET("/", func(ctx forge.Context) error {
return ctx.JSON(200, map[string]string{"status": "ok"})
})
return app, nil
},
cli.WithAutoMigrate(),
cli.WithGlobalFlags(
cli.NewStringFlag("port", "p", "HTTP port", "8080"),
cli.NewStringFlag("dsn", "d", "Database DSN", "postgres://localhost:5432/myapp"),
),
cli.WithExtraCommands(
cli.NewCommand("seed", "Seed test data", func(ctx cli.CommandContext) error {
ctx.Success("Database seeded")
return nil
}),
),
)
}Build and use:
go build -o myapp .
./myapp # Start server with auto-migrations
./myapp serve # Same as above
./myapp migrate up # Run migrations without starting the server
./myapp migrate status # Check migration state
./myapp seed # Run custom seed command
./myapp info # Show app metadata
./myapp health # Check component health
./myapp extensions # List registered extensionsNext Steps
CLI Commands
Learn about creating custom commands with flags, subcommands, and middleware.
Extensions System
Build extensions that implement MigratableExtension or CLICommandProvider.
Lifecycle
Understand lifecycle phases and hook registration.
.forge.yaml Reference
Full reference for project configuration including database.disable_migrations.
How is this guide?