Cross-cutting infrastructure Serenade owns. Products register domain services and bundles on top.
| Component | Responsibility |
|---|---|
| Kernel / lifecycle | Boot, environment, bundle registration order, shutdown |
| Dependency injection | Service container, autowiring-style resolution, scoped services |
| Event dispatcher | Sync domain and infrastructure events; subscriber tags |
| HTTP foundation | Request/response types, attributes, lifecycle (framework-agnostic core) |
| HTTP client | Outbound HttpClient (serenade-http-client); see HTTP_CLIENT.md |
| Lock | Named locks (serenade-lock); see LOCK.md |
| RateLimiter | Policies + storage (serenade-rate-limiter); see RATE_LIMITER.md |
| Scheduler | Cron / interval runner (serenade-scheduler); see SCHEDULER.md |
| HTTP kernel | Middleware pipeline, controller/action resolution |
| Routing | Route collection, requirements, method constraints |
| Configuration | Layered config (defaults, env, TOML package files; YAML still accepted) |
| Console | CLI application (bin/console analogue), commands, optional rich TUI |
| Cache | PSR-like pools (memory / filesystem / Redis, tags) plus HTTP response cache headers in serenade-http |
| Session | Session bag, flash, SessionStore, cookie session, HTTP middleware (serenade-session) |
| Security | AuthN/Z hooks, firewall, voters, CSRF token manager |
| Form | HTML forms: bind, CSRF by default, XSS-safe render (FORMS.md) |
| Translation | Translator, catalogues, locale negotiation, ICU format helpers (I18N.md) |
| Messenger | Command/query/event bus; async transport adapters |
| Serializer | DTO ↔ JSON (and other formats); normalizers |
| Validator | Constraint validation on DTOs and commands |
| Contracts | Stable traits for persistence, clock, id generation, etc. |
| Observability | Structured logging conventions (Monolog-like channels / var/log layout); tracing/metrics hooks |
| OpenAPI | Document helpers + optional explorer UIs (serenade-openapi; apps own path schemas) |
| Testing | Kernel test harness, fake container, event assertion helpers |
Created → register bundles (any order)
→ compile (sort by dependencies, then `BundleInterface::build`)
→ boot (`BundleInterface::boot`)
→ shutdown (`BundleInterface::shutdown`, reverse dependency order)
serenade-kernel exposes Kernel, App, BundleInterface / Bundle, BundleRegistry, and Environment (dev / test / prod, plus Custom for names like staging or recette). Debug defaults on for dev and test only. An application with zero bundles is a valid boot.
Kernel::boot compiles first when the kernel is still in Created. After Shutdown the kernel is terminal.
serenade-di provides ContainerBuilder → compile passes → Container.
| Concept | Role |
|---|---|
ServiceDefinition |
Id, Scope (singleton / prototype), declared Reference dependencies |
ParameterBag |
String parameters available to factories |
CompilePass |
Extensible pipeline run before the container freezes |
Container::get / get_as |
Resolve by id or alias; detects circular dependency at compile and resolve |
Factories return Box<dyn Any + Send + Sync>; callers downcast with get_as.
serenade-event provides a synchronous EventDispatcher.
| Concept | Role |
|---|---|
Event |
Named payload (cart.updated, order.placed, …) |
EventSubscriber |
Handles one event name; higher priority runs first |
RegisterEventSubscribersPass |
Collects services tagged event.subscriber into event_dispatcher |
RecordingSubscriber / assert_dispatched |
Test harness for dispatch order |
Dispatch stays in-process. Async transports belong on the messenger component.
serenade-http owns framework-agnostic types. Thin server adapters copy bytes in and out.
HTTP adapter (Actix / Axum / …)
→ Request (method, path, headers, body, attributes)
→ UrlMatcher (RouteCollection; writes `_route` + path params)
→ Middleware stack (first registered layer is outermost)
→ Controller (`RequestHandler`)
→ Response
HttpKernel::handle always returns a Response. Handler and middleware errors go through ExceptionHandler (DefaultExceptionHandler maps HttpError status and message to text/plain).
AsyncHttpKernel is the async counterpart: controllers return a future (database I/O). Wrap sync handlers with AsyncHttpKernel::from_sync. Actix listen / app take AsyncHttpKernel; sync dispatch remains for HttpKernel.
Routing lives in serenade-http: Route / RouteCollection, UrlMatcher (404 / 405), and RouteLoader for bundles. Path segments {name} become request attributes.
Server crates stay thin:
- Map the server request (method, path, headers, body) into
serenade_http::Request. - Optionally run
UrlMatcher::applyto set_routeand path parameters. - Call
HttpKernel::handle. - Map
serenade_http::Responseback to the server response type.
serenade-http-actix implements that bridge for Actix Web (from_actix, to_actix, dispatch / dispatch_async). For skeletons that only need to bind and serve a kernel, call serenade_http_actix::listen(addr, async_kernel) (or app(data) when composing Actix yourself).
serenade-http-axum is the same bridge for Axum (from_axum, to_axum, dispatch / dispatch_async, listen / router). Pick Actix or Axum at the app edge; the foundation crate stays unchanged.
Built-ins in serenade-http (see HTTP_OPS.md):
| Helper | Role |
|---|---|
RequestIdMiddleware / AsyncRequestIdMiddleware |
Propagate or generate x-request-id |
HealthMiddleware / AsyncHealthMiddleware |
/healthz + /readyz short-circuit |
Readiness |
Shared ready bit; mark_not_ready before drain |
Stop order: mark not ready → stop Actix/Axum listener → Kernel::shutdown() on the app kernel.
| Pattern | Use |
|---|---|
| Command bus | Mutations with one handler |
| Query bus | Read models (optional CQRS) |
| Event bus | Side effects after commit |
| Async transport | Queue/worker integration (product chooses backend) |
Sync in-process dispatch lives in serenade-messenger: implement Message with a stable NAME, mark as Command or Event, register handlers on MessageBus, then dispatch_command / dispatch_event. Commands require exactly one handler; events fan out (missing handlers are a no-op). Register Middleware layers with add_middleware (first registered is outermost); built-ins include LoggingMiddleware + LogSink and ValidationMiddleware + ValidateHook. DispatchContext exposes payload (&dyn Any) so validation hooks can inspect the message. Async backends implement Transport (send returns a boxed future); InMemoryTransport is the in-process adapter for tests and local workers. Durable Redis queues use WireEnvelope (message name + opaque bytes) and RedisTransport (feature redis: RPUSH/LPOP on a list key). Apps serialize payloads themselves; in-process Envelope stays Any and does not cross the wire (#11, Redis wire #209). Products wire long-running work through messenger transports rather than ad-hoc task spawning.
DTO ↔ wire formats live in serenade-serializer (#12, TOON #125). See SERIALIZER.md.
| Piece | Role |
|---|---|
Encoder / Decoder |
Intermediate serde_json::Value ↔ bytes; JsonEncoder / JsonDecoder for json |
ToonEncoder / ToonDecoder / encode_toon_string |
TOON v4.1 for LLM / agent context (Cargo feature toon) |
Normalizer / Denormalizer |
Object ↔ Value for types that need a custom graph walk |
NormalizerRegistry |
First matching normalizer/denormalizer wins |
Serializer |
Normalize then encode; decode then denormalize (SerializerInterface analogue) |
| Serde bridge | serialize_value / deserialize_value for typed Serialize / DeserializeOwned JSON |
Custom types register normalizers on the registry. Redis/XML formats remain out of scope. HTTP and MCP stay JSON; TOON is opt-in export for prompts.
Constraint checks live in serenade-validator (#13).
| Piece | Role |
|---|---|
Constraint |
Rule that may append a Violation (NotBlank, Length, Range) |
Validator / RecursiveValidator |
Runs constraints into a ConstraintViolationList |
Validatable |
Object validates itself through a Validator |
MessengerValidateHook |
Implements messenger ValidateHook; rejects with MessengerError::Rejected |
Register message types on the hook, then wrap the bus with ValidationMiddleware.
AuthN/Z and CSRF live in serenade-security (#10, CSRF in #110). See SECURITY.md.
| Piece | Role |
|---|---|
UserInterface / TokenInterface |
Principal and credentials carrier |
Voter / AccessDecisionManager |
Affirmative access checks |
FirewallMiddleware |
HTTP middleware: header → Authenticator → _security_token attribute |
CsrfTokenManager / HmacCsrfTokenManager |
Stateless HMAC CSRF tokens (_token) |
PasswordHasher / Argon2idPasswordHasher |
Argon2id hash/verify (PHC string) |
login / SessionTokenMiddleware |
Session identity bridge for HTML logins |
OAuth/OIDC is out of scope. Apps plug bearer or API-key authenticators. HTML forms use CSRF via serenade-form.
Serenade does not ship a Symfony 1-style admin generator (YAML → full CRUD UI) in the kernel or FrameworkBundle. That matches modern Symfony: core has no EasyAdmin; ecosystem and apps own the BO.
| Concern | Owner |
|---|---|
| Hand-written or SPA admin UIs | App / product (e.g. MyFeed /admin, product admin hosts) |
| Form + CSRF + escape on admin pages | Serenade (FORMS.md, SECURITY.md) |
| Firewall / session login for admin routes | Serenade primitives; apps wire authenticators |
| Optional EasyAdmin-shaped Admin CRUD bundle | serenade-admin (opt-in; not FrameworkBundle) - ADMIN.md, #177 |
Stance (not in kernel): #122. Implementation: #177.
HTML form helpers live in serenade-form (#110). See FORMS.md.
| Piece | Role |
|---|---|
Form / FormBuilder |
Fields, bind POST, validate, render |
| CSRF default on | _token + CsrfTokenManager |
escape_html / escape_attr |
XSS-safe output |
Apps stay thin: declare fields and constraints; framework owns CSRF and escape.
Optional HTML helpers live in serenade-view (#111). See VIEW.md.
| Piece | Role |
|---|---|
path / RouteCollection::generate |
Named reverse routing |
asset / AssetConfig |
Asset URL under a base prefix |
partial |
Include-style fragment nesting (no engine) |
| Re-exported escape | Same XSS helpers as Forms |
UI strings and locale live in serenade-translation (#132). See I18N.md.
| Piece | Role |
|---|---|
Translator / catalogues |
TOML-first domains, fallbacks, {name} + ICU plural subset |
LocaleNegotiator |
Query / cookie / Accept-Language → _locale |
| ICU format helpers | Number, currency code, date (feature icu) |
TranslationExtension |
DI service translator |
Product multilang entity rows stay in the application database.
PSR-like pools live in serenade-cache (#9, Redis follow-up #99). See CACHE.md.
| Piece | Role |
|---|---|
CacheItem / ArrayCacheItem |
Key, hit flag, Arc value, optional TTL, optional tags |
CacheItemPool / ArrayAdapter |
In-memory get/save/delete/clear; invalidate_tags |
FilesystemAdapter |
Disk-backed pool; TTL; marshaller; persisted tags |
RedisAdapter (feature redis) |
redis-rs + r2d2; prefix keys; SET/PX; SCAN clear; persisted tags |
cache.pool tag |
DI tag; RegisterDefaultCachePoolPass seeds cache.app |
| HTTP headers | HttpCacheHeaders / maybe_not_modified in serenade-http (see CACHE.md) |
Default DI pool is in-memory. Apps register FilesystemAdapter or RedisAdapter when they need persistence beyond process memory.
Session stickiness lives in serenade-session. See SESSION.md.
| Piece | Role |
|---|---|
Session |
Attribute bag for one request |
FlashBag |
One-shot messages on the session |
SessionStore / MemorySessionStore |
Persist attributes by opaque id |
CookieSession |
Open/commit with session-id cookie |
SessionMiddleware / AsyncSessionMiddleware |
Load/save per request on HttpKernel / AsyncHttpKernel |
Push SessionMiddleware::new(CookieSession::new(store)) (first registered is outermost). Controllers use request_session / request_session_mut. CSRF stays in serenade-security (HMAC, no session required).
Basic document search lives in serenade-search (#129, #255). See SEARCH.md.
| Piece | Role |
|---|---|
SearchDocument / SearchQuery / SearchHit |
Ids, text fields, ranked hits |
DocumentIndex |
Upsert / delete / query / clear |
MemorySearchAdapter |
Zero-deps in-memory index |
HttpSearchAdapter (feature http) |
REST stub; apps own the HTTP client |
No mandatory SaaS engine in core. Apps wire adapters when they need one.
Outgoing message types live in serenade-mailer (#153, #228, #253). See MAILER.md.
| Piece | Role |
|---|---|
Address |
Mailbox (email + optional display name; parse / parse_list) |
Body |
Text and/or HTML parts |
Attachment / ContentDisposition |
Downloadable or inline (CID) parts |
MimeTree / MimePart |
multipart/alternative, related, mixed |
Email |
Builder for From/To/Cc/Bcc/Reply-To, subject, body, attach/embed |
Transport |
Sync send contract |
NullTransport / FileTransport / SmtpTransport |
Discard / file dump / SMTP (lettre) |
EspHttpTransport (feature esp) |
HTTP ESP mail API; apps own the HTTP client |
RegisterDefaultMailerPass |
DI: service mailer defaults to null |
FrameworkExtension installs the default mailer pass. See MAILER.md.
Non-email channels live in serenade-notifier (#230). See NOTIFIER.md.
| Piece | Role |
|---|---|
Channel |
Sms / Push |
Notification |
SMS or push payload |
Transport |
Sync supports + send |
NullTransport / MemoryTransport / SmsOnlyTransport |
Discard / record / SMS-only |
RegisterDefaultNotifierPass |
DI: service notifier defaults to null |
FrameworkExtension installs the default notifier pass. Email remains in the mailer crate.
Helpers live in serenade-string (#229). See STRING.md.
| Piece | Role |
|---|---|
slug |
URL-safe lowercase slug |
snake_case / kebab_case / camel_case / pascal_case / title_case |
Case transforms |
pluralize / singularize |
English Inflector (limited irregulars) |
Pure functions; no DI pass. ICU / catalogue plurals stay in I18N.md.
Safe rule formulas live in serenade-expression (#251). See EXPRESSION.md.
| Piece | Role |
|---|---|
evaluate / evaluate_bool |
Parse and run against ExpressionContext |
ExpressionGuard |
In serenade-workflow: block transitions from an expression string |
Boolean / arithmetic / property paths only. No function calls or scripting VM.
State machines live in serenade-workflow (#239). See WORKFLOW.md.
| Piece | Role |
|---|---|
Definition / DefinitionBuilder |
Places, transitions, initial marking |
Workflow |
can / apply / enabled_transitions |
Marking / MarkingStore / MemoryMarkingStore |
Subject places + persist trait |
Guard / TransitionListener |
Block before apply; hook after apply |
Construct in app code; no FrameworkExtension pass yet.
Path helpers live in serenade-filesystem (#241). See FILESYSTEM.md.
| Piece | Role |
|---|---|
exists / is_file / is_dir |
Presence checks |
mkdir / remove / remove_tree / rename |
Directory and path ops |
dump_file / append_to_file / read / touch |
File contents |
copy / mirror |
File and tree copy |
temp_dir / temp_file |
Temporary paths |
Child processes live in serenade-process (#243). See PROCESS.md.
| Piece | Role |
|---|---|
Process |
Builder: args, cwd, env, timeout |
run / must_run |
Capture stdout/stderr; optional fail-on-nonzero |
CompletedProcess |
Exit status + output helpers |
Directory walking lives in serenade-finder (#245). See FINDER.md.
| Piece | Role |
|---|---|
Finder |
Builder: roots, files/directories, name glob, depth |
collect |
Sorted matching paths |
name_matches |
Basename * / ? helper |
Outbound HTTP lives in serenade-http-client (#184). See HTTP_CLIENT.md.
| Piece | Role |
|---|---|
HttpClient / DynHttpClient |
Async send contract (+ object-safe DI form) |
ClientRequest / ClientResponse |
Outbound request and response |
MockHttpClient / ReqwestHttpClient |
Tests / production (reqwest + rustls) |
RegisterDefaultHttpClientPass |
DI: service http_client defaults to reqwest |
FrameworkExtension installs the default HTTP client pass. See HTTP_CLIENT.md.
Named locks live in serenade-lock (#185). See LOCK.md.
| Piece | Role |
|---|---|
LockStore / LockFactory / Lock |
Persist + acquire / release / refresh |
InMemoryLockStore |
Process-local (DI default) |
FilesystemLockStore / RedisLockStore |
Disk / Redis (feature redis) |
RegisterDefaultLockPass |
DI: service lock.store defaults to in-memory |
FrameworkExtension installs the default lock pass. See LOCK.md.
Rate limiting lives in serenade-rate-limiter (#186). See RATE_LIMITER.md.
| Piece | Role |
|---|---|
Policy / RateLimiterFactory / RateLimiter |
Token bucket or fixed window; consume / reset |
InMemoryRateLimiterStorage |
Process-local (DI default) |
RedisRateLimiterStorage (feature redis) |
Multi-node Redis windows |
consume_or_exceed / too_many_requests |
App helper + HTTP 429 bridge |
RegisterDefaultRateLimiterPass |
DI: service rate_limiter.storage defaults to in-memory |
FrameworkExtension installs the default rate-limiter pass. See RATE_LIMITER.md.
Scheduling lives in serenade-scheduler (#187). See SCHEDULER.md.
| Piece | Role |
|---|---|
Trigger / Schedule / Scheduler |
Interval or cron; tick without sleeping |
ScheduleHandler / HandlerMap |
Sync dispatch for due jobs |
CommandOnDue |
Optional Messenger bridge (feature messenger) |
RegisterDefaultSchedulerPass |
DI: service scheduler defaults to empty |
serenade:scheduler:run |
Console tick loop (--once for a single tick) |
FrameworkExtension installs the default scheduler pass and console command. See SCHEDULER.md.
serenade-observability documents Monolog-like conventions on tracing: named channels (serenade::app, serenade::request, …), an app-owned var/log/{env}.log layout, and LoggingConfig / init for stderr + daily rolling files. Optional feature otel adds an OpenTelemetry tracing bridge and OTLP/HTTP export (OtelConfig, init_with_otel). Apps call init (or init_with_otel) from main; crates do not require a global logger. See OBSERVABILITY.md.
serenade-openapi provides Info/servers helpers and, behind feature actix, mounts Swagger UI, Redoc, RapiDoc, and Scalar beside Actix listen. Applications still own #[derive(OpenApi)] path and schema lists and the /openapi.json route. See OPENAPI.md.
The browser Web Debug Toolbar / Profiler lives in serenade-profiler (PROFILER.md): per-request collectors, HTML toolbar injection, and /_profiler/{token}. It is not the console. AsyncHttpKernel supports an async middleware pipeline (AsyncMiddleware) so Actix/listen apps can push the profiler the same way sync HttpKernel pushes Middleware.
serenade-config loads TOML and YAML mappings, deep-merges them, interpolates ${VAR} / ${VAR:-default}, and flattens dotted keys into the DI ParameterBag.
Preference: new apps and Serenade examples use config/packages/*.toml. YAML remains supported for Symfony familiarity. There is no PHP/XML config track.
load_dotenv(project_root, APP_ENV)
.env → .env.local (not in prod) → .env.{env} → .env.{env}.local
(later files override earlier; process env never overwritten)
defaults (TOML or YAML)
+ config/packages/*.{toml,yaml,yml} (sorted by file name; files only)
+ config/packages/{env}/*.{toml,yaml,yml} (env overlay when the directory exists)
+ ${VAR} interpolation from the process environment
load_packages loads the base directory only. load_packages_for_env applies the overlay. build_container(packages_dir, environment, extensions) uses the env-aware loader. Apps should call load_dotenv on the project root before building the container when they ship .env files.
Mappings merge; scalars and sequences in an overlay replace the base value. Unset variables without a default fail load.
The console component is Serenade’s bin/console analogue: discoverable commands, --env / --no-debug, kernel boot for ops (migrate, workers, debug). Plain commands use clap; interactive debug surfaces may use ratatui. See CONSOLE.md. Flex-like app scaffolding is separate (#30); Cargo remains the package manager.
- Tagged services (event subscriber, command handler, …)
- Compiler passes / container extensions (bundle
build()phase) - Middleware ordering
- Route loaders from bundles
Domain-specific hooks (pricing, shipping) belong in product bundles, not the Serenade core.