App

Application Startup

Trace how a GoLazy application is constructed and started.

By Guillermo Alvarez - Published - Updated

Start from main

The executable entry point stays small:

func main() {
    if err := appinit.App().ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

App() returns a fully constructed application. ListenAndServe starts the HTTP server.

Construct the app

init/app.go calls lazyapp.New:

func App() *lazyapp.App {
    return lazyapp.New(lazyapp.Config{
        Name:         "sample_app",
        Drawer:       Draw,
        Public:       app.Public,
        Views:        app.Views,
        Dependencies: Dependencies,
    })
}

That call initializes application dependencies, optional migrations, optional jobs, SEO defaults, the renderer, route scope, dispatcher, asset registry, helpers, and public asset fallback.

Initialize shared dependencies

Dependencies runs once during application construction:

func Dependencies(deps *lazydeps.Scope) error {
    _, err := lazydeps.Service(deps, "helloworldservice", func(ctx context.Context) (
        context.Context,
        helloworldservice.Service,
        error,
        context.CancelFunc,
    ) {
        service := helloworldservice.New()
        return helloworldservice.WithContext(ctx, service), service, nil, nil
    })
    return err
}

Controllers resolve shared dependencies from this context during request-local construction.

Initialize migrations

lazyapp.Config.Migrations runs after Dependencies with the dependency-initialized app context and before jobs. The app supplies one lazymigrate.DB per logical database, so each database can use its own backend:

func Migrations(ctx context.Context) (lazymigrate.Databases, error) {
    db, ok := pg.FromContext(ctx)
    if !ok {
        return nil, fmt.Errorf("postgres pool missing")
    }
    return lazymigrate.Databases{
        "postgres": {
            Backend: pgmigrate.New(db),
            Sources: []lazymigrate.Source{
                lazymigrate.FromFS(migrations.FS, "postgres"),
                pgjobs.Migrations(),
            },
        },
    }, nil
}

The app asset registry is also available in this context through lazyassets.FromContext. Asset upload backends such as golazy.dev/lazyassets/assetmigrate use that registry to turn the embedded app assets into an opt-in migration that runs only when LAZYAPP_MIGRATE requests migration work.

The app binary only runs migrations when requested:

LAZYAPP_MIGRATE=up ./sample-app    # migrate and exit
LAZYAPP_MIGRATE=auto ./sample-app  # migrate and continue startup

Unset, empty, or off skips automatic migration. If the variable is set before the app has configured migrations, startup succeeds as a no-op. When CONTROL_PLANE_ADDR is set to a separate listener during migration mode, GoLazy starts the real control plane first: /livez returns OK and /readyz reports that migrations are running. In auto mode, that listener remains active and later startup stages mount their handlers on the same control plane. If CONTROL_PLANE_ADDR resolves to the app listener, the app simply waits for migrations before starting the listener.

Initialize background jobs

lazyapp.Config.Jobs runs after Dependencies with the dependency-initialized app context. It creates a lazyjobs.JobRunner, registers the app's job definitions, injects the runner into the app context, and starts in-process workers:

func App() *lazyapp.App {
    return lazyapp.New(lazyapp.Config{
        Name:         "sample_app",
        Drawer:       Draw,
        Public:       app.Public,
        Views:        app.Views,
        Dependencies: Dependencies,
        Jobs: lazyapp.Jobs(lazyjobs.Config{
            Define: jobs.DefinedJobs,
        }),
    })
}

Read Background Jobs.

Initialize SEO defaults

lazyapp.Config.SEO runs after Dependencies, so application-wide metadata defaults can read dependency-backed values from the app context:

func App() *lazyapp.App {
    return lazyapp.New(lazyapp.Config{
        Name:         "sample_app",
        Drawer:       Draw,
        Public:       app.Public,
        Views:        app.Views,
        Dependencies: Dependencies,
        SEO:          SEO,
    })
}

Keep that function in init/seo.go. Read SEO And Sitemaps.

Draw routes

The app passes a framework-created route scope to Draw:

func Draw(router *lazyroutes.Scope) {
    router.Resources(homecontroller.New, func(home *lazyroutes.Resource) {
        home.Singular("home")
        home.Plural("home")
        home.Path("")
    })
}

Draw only registers routes. It does not create the mux, dispatcher, public fallback, or server.

Choose the address

ListenAndServe reads ADDR, then PORT, then falls back to 127.0.0.1:3000:

ADDR=127.0.0.1:4000 ./sample-app
PORT=4000 ./sample-app

A numeric ADDR or PORT value is treated as a port. A full address is passed to the underlying server.

CONTROL_PLANE_ADDR activates the default control plane during ListenAndServe. If it matches the app address, probes are mounted into the app server. If it differs, GoLazy starts a second server for the control plane. The separate server automatically includes /debug/pprof/ and the standard pprof subpaths. If it is unset, production builds do not mount control-plane endpoints on the public app listener.