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 extensions

Built-in Commands

cli.RunApp() automatically registers these commands:

CommandAliasesDescription
servestart, runStart the HTTP server
migrate upRun all pending migrations
migrate downRollback the last migration batch
migrate statusShow migration status table
infoDisplay app name, version, environment, uptime
healthRun health checks on all registered components
extensionsList all registered extensions with version info

When invoked with no arguments, RunApp defaults to the serve command:

./my-app              # Same as: ./my-app serve

Migration 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:

  1. Discovers all extensions implementing MigratableExtension
  2. Calls Migrate(ctx) on each one
  3. Logs the count of applied migrations per extension
  4. 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 :8080

Auto-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: true
cli.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 skipped

When 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 :8080

Priority 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 MyExtension

Migration 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 confirmation

migrate 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 extensions

Next Steps

How is this guide?

On this page