Bundles are Serenade’s unit of reusable composition, analogous to Symfony bundles.
| Artifact | Example |
|---|---|
DependencyInjection extension |
Register services, parameters |
| Config schema | config/packages/serenade_foo.toml (YAML still accepted) |
| Routes | routes.toml / routes.yaml or programmatic collection |
| Event subscribers | Tagged listeners |
| Console commands | bin/console foo:bar (Serenade console Application) |
| Public services | Exported interfaces for other bundles |
Write a new first-party or third-party bundle in five steps:
- Struct +
BundleInterface- Implementname()(stable unique id). Optionallydependencies(),build(),boot(),shutdown(). Trait lives inserenade-kernel(re-exported fromserenade-bundle). Extension- Implementalias()(package config section) andload(&Config, &mut ContainerBuilder)to register services and parameters for that section.- Package config - Ship defaults under
config/packages/<alias>.toml(YAML still accepted). Apps may overlayconfig/packages/{env}/. - Optional contributions - Implement
RouteLoaderfor routes; tag event subscribers / console commands for the compile passes thatbuild_containeralways runs. - Register -
App::register_bundle(any order; kernel sorts by dependencies), thenbuild_container(..., &[&YourExtension, …])after dotenv / environment are ready.
Worked example: examples/demo-app (DemoBundle + DemoExtension + config/packages/demo.toml).
| Surface | Stability | Notes |
|---|---|---|
BundleInterface / Bundle |
Public | Lifecycle + dependency names |
Extension |
Public | Package alias + load into DI |
build_container |
Public | Packages dir + env + extension list → (Config, Container) |
FrameworkBundle / FrameworkExtension |
Public | Empty router (RouteCollection); framework package alias |
BundleError |
Public | Extension / DI / config failures |
RouteLoader (serenade-http) |
Public | Optional route contribution |
Service ids config, router, event_dispatcher, console application |
Public | Documented constants / conventions |
| Compile-pass internals, private modules inside crates | Private | Do not depend on them from other crates |
Undocumented pub items without rustdoc |
Treat as unstable until documented |
SemVer for published Serenade crates: breaking changes to the public rows above require a major bump. Prefer depending on traits and documented service ids, not on concrete framework types behind feature flags you do not own.
| Rule | Detail |
|---|---|
| Stable contracts only | Depend on BundleInterface, Extension, build_container, and documented service ids. Do not call private kernel hooks or scrape undocumented modules. |
| Cargo crate layout | One Cargo package per bundle (or a small workspace). Export the bundle struct + extension; document the package alias and required config keys. |
| SemVer | Follow Cargo / SemVer for your crate. Pin Serenade majors carefully; test against the dev branch of the framework you target. |
| No private DI tricks | Do not reach into another bundle’s private service ids unless that bundle documents them as public. |
| Wasm is a different lane | In-process bundles are not the sandboxed Wasm / WIT host story. Host plumbing lives in serenade-component-host / serenade-sandbox; see WASM.md. Product guests and ABIs are out of scope for this guide. |
| No marketplace requirement | Serenade does not ship a bundle registry. Discovery is Cargo / docs. |
| Class | Examples |
|---|---|
| Serenade core bundles | FrameworkBundle, and further framework bundles as they land |
| Product bundles | Application-owned feature bundles (catalog, cart, CMS, …) in the product repo |
| Third-party | Community Cargo crates that implement BundleInterface + Extension under this policy |
Symfony’s own features ship as bundles; product features should too, not as loose modules in one crate.
Admin CRUD is not a core bundle. There is no admin generator in FrameworkBundle. Apps own back-office UIs. Optional EasyAdmin-shaped helpers ship in serenade-admin (opt-in AdminBundle / AdminExtension, not auto-wired): ADMIN.md (#177).
Kernel / App
├── register_bundle (any order)
├── compile (topological sort by BundleInterface::dependencies, then build)
├── boot (`BundleInterface::boot`)
└── shutdown (`BundleInterface::shutdown`, reverse dependency order)
BundleInterface (alias Bundle) declares name, optional dependencies, then build / boot / shutdown. BundleRegistry sorts dependents after their dependencies; unknown deps and cycles fail at compile.
Extension loads one package alias into a ContainerBuilder. build_container merges config/packages/*.{toml,yaml,yml}, then config/packages/{env}/* when that directory exists, applies flattened parameters, registers the root config service, runs each extension on config.section(alias), and compiles with RegisterEventSubscribersPass and RegisterCommandsPass so event_dispatcher and console commands exist. Pass the environment name (same as APP_ENV) as the second argument.
FrameworkBundle / FrameworkExtension register the empty router (RouteCollection). Sample app: examples/demo-app (DemoBundle + config/packages/*.toml).
Bundles may also implement RouteLoader (serenade-http) to contribute routes.
| Bundle | Wasm / WIT guest | |
|---|---|---|
| Trust | Same process, signed release | Sandboxed guest |
| Use | First-party features, trusted extensions | Untrusted or polyglot guests |
| Serenade role | DI + lifecycle | Host plumbing (serenade-component-host, serenade-sandbox); product owns WIT ABI and fixtures |
Both can coexist in product apps; Serenade bundles are the in-process story. See WASM.md.
crates/serenade-bundle/ FrameworkBundle, Extension, build_container
examples/demo-app/ Canonical sample (DemoBundle)
├── config/packages/*.toml Package config for alias `demo`
├── src/lib.rs + bundle.rs BundleInterface, Extension, RouteLoader,
│ demo:hello command, demo.ready subscriber
├── src/main.rs Boot + dispatch demo.ready
└── src/bin/console.rs Console entry with DemoBundle wired