Files
summercms/docs/architecture/application-lifecycle.md
Jakub Zych 1f8f5e1b51 feat(11.1-03): add the Architecture and Plugins docs sections
- architecture: introduction, Go modules and workspaces, application
  lifecycle, request lifecycle
- plugins: registration, scheduling, extending, testing
- verified Examples for backpack services, towel request context,
  pact schedules and festival events
- TestDocsRequiredPages lists the eight new pages
2026-09-30 22:12:08 +02:00

4.8 KiB

title, description, section, order
title description section order
Application lifecycle What happens when an application binary starts: configuration, the backpack container, plugin ordering, Register and Boot, and database-dependent boot work. architecture 30

Application lifecycle

Every run of an application binary, whether it serves HTTP or runs a single console command, goes through the same start-up. summer build generates the main.go that performs it, so you never write it by hand.

Start-up sequence

The generated main does the following, in order:

  1. Loads configuration with compass.Load from the config/ directory, applying the environment directory and SUMMER_ variables.
  2. Creates the application container with backpack.New.
  3. Activates the plugins listed in summer.yaml with party.Activate.
  4. Collects the console commands: the framework's runtime commands (from lagoon.RuntimeCommands, conga.RuntimeCommands, surf.ServeCommand, surf.RouteListCommand and cabana.RuntimeCommands), then the commands of every plugin that implements pact.HasCommands.
  5. Publishes the command set as a bonfire.Catalog, so the scheduler can run commands in-process.
  6. Runs the command named on the command line.

Plugin ordering

party.Activate selects the plugins by ID and orders them so that every plugin comes after the plugins its party.Plugin.Requires lists. It fails before any plugin code runs when an ID is empty, duplicated or not compiled in, when a required plugin is missing, or when the requirements form a cycle.

Register, then Boot

Activation runs in phases, and each phase finishes for every plugin before the next begins:

  1. backpack.App.SetPlugins records the complete plugin set, so backpack.App.HasPlugin answers correctly from the first Register onwards.
  2. The embedded defaults of every plugin that implements pact.HasConfig are merged into the configuration under the plugin ID.
  3. party.Plugin.Register runs for every plugin. Publish services here; do not use other plugins' services yet.
  4. The framework publishes the translator and the mailer, and registers each plugin's translations and mail templates.
  5. party.Plugin.Boot runs for every plugin. Look up services, register event listeners and extend other plugins here.

This is the WinterCMS register and boot split: when any Boot runs, every plugin has already registered.

The container

backpack.App is the application container that Register and Boot receive. It holds the configuration in backpack.App.Config, the event bus in backpack.App.Events and a typed service registry. Nothing in it is process-global, so two applications in one test do not share state.

Services are keyed by their Go type. A plugin publishes a value with backpack.App.Publish and another plugin reads it with backpack.App.Lookup and the same type argument. Publish under an interface type when consumers should not depend on your implementation:

app := backpack.New(&compass.Config{})
app.SetPlugins([]string{"acme.greeter", "acme.blog"})

// acme.greeter, in its Register step: publish under the interface type.
var greeter Greeter = englishGreeter{}
if err := app.Publish(greeter); err != nil {
	fmt.Println(err)
	return
}

// acme.blog, in its Boot step: look the service up by the same type.
if app.HasPlugin("acme.greeter") {
	if found, ok := app.Lookup[Greeter](); ok {
		fmt.Println(found.Greet("blog"))
	}
}

// A second Publish under the same type is refused.
fmt.Println(app.Publish(greeter) != nil)
// Output:
// Hello, blog
// true

Capability interfaces

Beyond the four party.Plugin methods, a plugin declares what it contributes by implementing interfaces from pact. The framework package that owns a capability finds it with a type assertion: surf asks for pact.HasRoutes and pact.HasMiddleware, lagoon for pact.HasMigrations, cabana for pact.HasAdminControllers, conga for pact.HasJobs and pact.HasSchedule. A plugin that does not implement an interface simply does not take part in that capability.

Database-dependent boot work

Boot runs before any command opens the database: migrate and serve open it after activation, and commands such as key:generate never open it. Code that needs the database handle during boot, such as registering GORM callbacks, therefore goes through lagoon.OnDatabase. It runs the function immediately when the database is already published, and otherwise queues it until lagoon.Publish makes the shared *sql.DB and *gorm.DB handles available. An error from a queued function is returned by lagoon.Publish, so the command that opened the database fails instead of running with a half-registered plugin. Extending plugins shows where GORM callbacks fit.