App
Application Startup
Trace how a GoLazy application is constructed and started.
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.