Configuration Reference
Full YAML structure with every option, its type, and default value.
Leave out any required field and the container fails to compile, with an error that names the missing key. The required fields are:
website_nameuser, withuser.class_namepublishable, withpublishable.permissionrefresh_token, withhandler_id,cookie_name,ttlanddatabase_user_providerrefresh_token.options.class, but only whenhandler_idis the Doctrine storage handler (silverback.api_components.refresh_token.storage.doctrine). A custom handler doesn't need it.
Everything else has a default. That includes all of user.email_verification, which defaults to off: users start unverified, no verification email is sent, and unverified users can still sign in. Anything stricter needs a redirect target for its emails, so you have to opt in. If you turn on verify_on_register or verify_on_change, you must also set email.default_redirect_path or email.redirect_path_query, or the container won't compile.
user.email_links compiles without a value, but then every email that carries a link (password reset, verification, welcome, account enabled) is refused. Set default_origin or allowed_origins. See Links in Emails.Full Configuration
silverback_api_components:
# Required. Used in email subjects and templates.
website_name: My CWA App
# Table prefix for all bundle-managed tables. Default: '_acb_'
table_prefix: '_acb_'
# Key under which runtime metadata is nested in API responses. Default: '_metadata'
metadata_key: '_metadata'
# ─── User ────────────────────────────────────────────────────────────────
user:
# Required. Fully-qualified class name of your User entity.
class_name: App\Entity\User
email_verification:
enabled: true
# Whether new users start verified (false = must verify email)
default_value: false
# Send a verification email when a new user registers
verify_on_register: true
# Re-send verification when the user changes their email
verify_on_change: true
# Block login for unverified email addresses
deny_unverified_login: true
email:
# Query param name carrying the redirect path (optional)
redirect_path_query: ~
# Path with {{ username }} and {{ token }} placeholders
default_redirect_path: /verify-email/{{ username }}/{{ token }}
subject: Please verify your email
# Minimum seconds between verification emails (429 until then). Default: 300
repeat_ttl_seconds: 300
new_email_confirmation:
email:
redirect_path_query: ~
default_redirect_path: /confirm-new-email/{{ username }}/{{ new_email }}/{{ token }}
subject: Please confirm your new email address
# Token validity in seconds. Default: 86400 (24 hours)
request_timeout_seconds: 86400
# Minimum seconds between confirmation emails (429 until then). Default: 300
repeat_ttl_seconds: 300
password_reset:
email:
redirect_path_query: ~
default_redirect_path: /reset-password/{{ username }}/{{ token }}
subject: Your password reset request
# Minimum seconds between password reset emails only (429 until then).
# Default: 86400 (24 hours)
repeat_ttl_seconds: 86400
# Reset token validity in seconds. Default: 3600
request_timeout_seconds: 3600
# Origin that links in user emails point to. Set one of these, or every
# email carrying a link is refused. See Users & Security.
email_links:
# scheme://host[:port]. Default: null
default_origin: 'https://www.example.com'
# Regexes, anchored by the bundle, that the request's Origin/Referer
# may match to be used instead. Default: []
allowed_origins: []
emails:
welcome:
enabled: true
subject: 'Welcome to {{ website_name }}'
user_enabled:
enabled: true
subject: 'Your account has been enabled'
username_changed:
enabled: true
subject: 'Your username has been updated'
password_changed:
enabled: true
subject: 'Your password has been changed'
# ─── Publishable ─────────────────────────────────────────────────────────
publishable:
# Symfony expression: who can read/write draft resources and publish
permission: "is_granted('ROLE_ADMIN')"
# ─── HTTP Cache Safety ───────────────────────────────────────────────────
http_cache:
# Resource classes whose GET responses are marked `private, no-store`
# for authenticated users, so a shared cache never stores an admin's
# draft view. Any Publishable resource is treated this way automatically,
# in addition to this list. Default: [Route, ResourceManifest, ComponentPosition]
personalised_resource_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\Route'
- 'Silverback\ApiComponentsBundle\ApiResource\ResourceManifest'
- 'Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition'
# Resource classes whose anonymous GET responses have `s-maxage` and
# `max-age` capped at the soonest future route go-live date.
# Default: [Route, RoutableInterface, ResourceManifest]
scheduled_expiry_resource_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\Route'
- 'Silverback\ApiComponentsBundle\Entity\Core\RoutableInterface'
- 'Silverback\ApiComponentsBundle\ApiResource\ResourceManifest'
# Resource classes whose writes also purge the `cwa-html` surrogate key,
# dropping every cached page. List only leaf resources.
# Default: [SiteConfigParameter]
purge_rendered_html_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\SiteConfigParameter'
# ─── Built-in Components ─────────────────────────────────────────────────
enabled_components:
form: true # The Form component (Symfony FormType → JSON)
collection: true # The Collection component (resource list proxy)
# ─── Refresh Tokens ──────────────────────────────────────────────────────
# Every key in this block except `options` is required — there are no defaults.
refresh_token:
# Service ID of the refresh token storage handler
handler_id: silverback.api_components.refresh_token.storage.doctrine
options:
# Your RefreshToken entity class (see the template app's src/Entity/RefreshToken.php)
class: App\Entity\RefreshToken
# Must match the cookie name in lexik_jwt_authentication.set_cookies
# and token_extractors.cookie.name. The template app uses `api_component`.
cookie_name: api_components
# Token lifetime in seconds (the template uses 604800 = 1 week)
ttl: 604800
# The user provider alias used in security.yaml
database_user_provider: database
# ─── Mercure ─────────────────────────────────────────────────────────────
mercure:
# Name of the Mercure hub (if multiple hubs; null = default hub)
hub_name: ~
cookie:
# SameSite attribute for the Mercure cookie. Default: 'strict'
samesite: '%env(JWT_COOKIE_SAMESITE)%'
# Scope subscriber JWT tokens to resources the current user can access.
# Recommended in production. Default: false (all topics subscribed).
secure_subscriptions: false
# ─── Route Security ──────────────────────────────────────────────────────
# URL pattern → Symfony security expression. Evaluated per-route.
route_security:
- { route: '/user-area*', security: "is_granted('ROLE_USER')" }
- { route: '/admin*', security: "is_granted('ROLE_ADMIN')" }
# Expression for who can read pages, page data and components that no live
# Route reaches, and who can create pages and page data. Default: null (no gate)
routable_security: "is_granted('ROLE_ADMIN')"
# ─── Orphaned Resources ──────────────────────────────────────────────────
# Emails sent by `scan-orphaned` when the orphans differ from the last
# stored report. See Console Commands. Needs bundle 2.0.0-alpha.6 or later.
orphaned_resources:
notify:
# A list, or one comma-separated string so an env var can supply it.
# Default: [] (no emails)
recipients: '%env(ORPHANED_RESOURCES_NOTIFY_RECIPIENTS)%'
# Front-end path of the admin page, linked on
# user.email_links.default_origin. Default: '/_cwa/orphaned'
admin_page_path: '/_cwa/orphaned'
# Default: 'Orphaned resources changed on {{ website_name }}'
subject: 'Orphaned resources changed on {{ website_name }}'
There is no enabled flag for the orphaned resources email: no recipients means no email. Literal addresses are checked when the container compiles; addresses from an environment variable are checked when an email is sent. See scan-orphaned for when it sends.
Mercure Secure Subscriptions
By default the subscriber JWT token issued at login includes every resource topic — Mercure will push updates for any resource the front-end subscribes to, regardless of the current user's access level. This is fine for public content but leaks real-time updates for resources that have server-side security expressions.
Set mercure.secure_subscriptions: true to scope the token: the bundle evaluates each API resource's security expression at token-generation time and omits topics the current user cannot access:
silverback_api_components:
mercure:
secure_subscriptions: true
Caveats:
- Security expressions that reference
object(item-level security, e.g.object.owner == user) cannot be evaluated at token time because there is no concrete entity instance. Those resource topics are always included — you cannot scope to specific items, only to the class/role level. - The Mercure authorization cookie is written at login and re-issued whenever the JWT is refreshed. A role change mid-session therefore doesn't take effect immediately — the scope widens or narrows at the next refresh. Sign out and back in if you need it applied straight away.
Enable in production for any application with role-gated resources. Leave false (the default) for fully public sites.
HTTP Cache Safety
Some resources look different depending on who's asking. When an admin is signed in, GET /_/routes/{path}, the resource manifest, and any draft of a Publishable component return the draft view — the same URL a signed-out visitor uses to get the published view (the API decides from the auth cookie, not the URL). If a shared cache stored the admin's response, it could later serve that draft to the public.
The bundle prevents this at the source. On a GET/HEAD for an authenticated user, when the resource is in the affected set, a kernel.response listener overrides API Platform's default public caching:
Cache-Control: private, no-store
It also strips the long shared s-maxage from that response. Anonymous requests, and any resource not in the affected set (a Layout, say), are untouched and stay publicly cacheable — so the only variant a shared cache ever holds is the published one.
The affected set is the configured personalised_resource_classes (default Route, ResourceManifest, ComponentPosition) plus every Publishable resource, matched dynamically — you don't list your Publishable components. Subclasses of a listed class match too. Add your own resource classes if they vary by session:
silverback_api_components:
http_cache:
personalised_resource_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\Route'
- 'Silverback\ApiComponentsBundle\ApiResource\ResourceManifest'
- 'Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition'
- 'App\Entity\MyPersonalisedComponent'
Vary: Cookie. Varying on the cookie would collapse the cache hit-rate for anonymous visitors (cookies churn), and it isn't needed — the unsafe responses are marked non-cacheable outright rather than cached under a cookie key. The cacheable anonymous variant carries no cookie to vary on.This header is the API-side contract a service worker relies on to cache CWA responses safely — a no-store/private response is the signal to not store it. See Progressive Web App & Offline on the front-end for the full picture.
Cache Tags and Purging
With API Platform's HTTP cache invalidation turned on, every cacheable response is tagged with the IRIs of the resources it contains. With the template's Souin purger, the tags go in a Surrogate-Key header. When a resource is written, the bundle sends its IRI to the shared cache as a purge, so every cached response that used the resource is dropped.
Two IRIs are never tagged, because nothing can purge them: JSON-LD blank nodes (/.well-known/genid/…) and /_/resource_metadatas.
The HTTP cache purge after a write failed, so these tags may still be cached. Those responses can stay stale until they expire or you fix the cache and purge again, so alert on that log.Tags are IRIs generated by Symfony's router, so a route import prefix such as the template's /_api is included whether the write comes over HTTP or from the CLI (fixtures, console commands, messenger workers). A Route write, for example, purges /_api/_/routes and /_api/_/routes//my-route.
framework.router.default_uri to a URL that includes that path. A request's base path is only known over HTTP, so without it every write made from the CLI purges unprefixed tags and misses the cache. The template doesn't need this: /_api is a route prefix, not a base path.Resource manifests are tagged per page
A resource manifest (GET /_/resource_manifest/{id}) is the exception. Its body is only a list of IRIs, so it changes only when the page's structure changes: which component sits in which group, which layout a page uses, or which page a route publishes. Instead of the IRI of every resource it lists, a manifest carries one grouping key per rendering depth:
Surrogate-Key: manifest:/_/pages/8f3c…
A nested page carries one key for each ancestor depth as well as its own. A page-data manifest also carries the key of the template page it renders through. A manifest requested by route path and the same manifest requested by ID carry identical keys, so one purge drops both.
A write to any of these purges the manifests that contain it: Route, Page, any AbstractPageData, Layout, ComponentGroup and ComponentPosition. The bundle works upward from the written resource to the pages that contain it, so adding a component to a group in a shared layout purges the manifest of every page using that layout. A change inside a template page purges the manifest of every page data that uses it.
One exception: a component placed through a pageDataProperty still purges the manifests of the page data that references it, because the bundle treats that write as a change to the page data itself.
The key count no longer grows with the size of the page. Before, a 44-resource manifest produced about 2.7 KB of Surrogate-Key header, which is more than Souin's 1500-byte batch and large enough to need a bigger proxy buffer. A manifest now carries a handful of keys whatever the page size.
Scheduled Publication and Cache Lifetime
A scheduled publication changes what a response should contain at a set time, with no write to trigger a purge. The bundle handles this by shortening the cache lifetime instead, on anonymous, successful GET responses that already have an s-maxage or max-age.
Expires caps the cache lifetime. A component with a future publishedAt sends an Expires header set to that date, including on anonymous requests, so the front end can see a change coming without seeing the draft. Shared caches give s-maxage priority over Expires, so the bundle now also lowers s-maxage and max-age to the time left until Expires whenever that's sooner. This applies to any response that sets Expires, not only publishable components.
Route go-live dates cap the classes in scheduled_expiry_resource_classes. For these responses, s-maxage and max-age are capped at the soonest future liveAt across all routes, whether or not the response mentions that route. That's deliberate: a route going live changes pages that never mention it (a navigation menu, for example), and no cache tag can express that. The default list covers routes, pages and page data (RoutableInterface) and the resource manifest, which is where a page render starts:
silverback_api_components:
http_cache:
scheduled_expiry_resource_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\Route'
- 'Silverback\ApiComponentsBundle\Entity\Core\RoutableInterface'
- 'Silverback\ApiComponentsBundle\ApiResource\ResourceManifest'
Subclasses and implementations of a listed class match too.
Expires. A collection that contains a resource with a pending publication is only capped if its class is in scheduled_expiry_resource_classes.Because every scheduled change now shortens the responses it affects, the front end can take a page's lifetime from the shortest s-maxage among the API responses it used, without reading publication dates it can't see anonymously. That's what the Nuxt module does — see Page Caching. It makes a long shared_max_age safe.
Purging Every Rendered Page
The Nuxt module tags each cached page with the IRIs of the resources it rendered, so a write to any of them drops the affected pages. Some resources change every page without being one of those resources. Site config is the main case: changing siteName would leave every cached page showing the old name until it expired.
A write to a class listed in purge_rendered_html_classes therefore also purges the surrogate key cwa-html, which the module adds to every page it caches. The default list is SiteConfigParameter, and subclasses match:
silverback_api_components:
http_cache:
purge_rendered_html_classes:
- 'Silverback\ApiComponentsBundle\Entity\Core\SiteConfigParameter'
The purge is sent once per write request. Saving several site config parameters is several requests and several purges, which is harmless because the key is already gone after the first.
cwa-html is a contract shared by the bundle and the Nuxt module. Both sides must use the same name. A mismatch fails silently: the purge matches nothing and pages stay stale until they expire.POST /_/site_config_parameters now needs the same permission as updating or deleting a parameter, publishable.permission (usually ROLE_ADMIN). Before, the POST operation had no security of its own, so an application that creates site config parameters as a non-admin user will now get a 403.Flushing the Whole HTTP Cache
Tag purges only reach resources the API has seen change. After a direct database edit, a data fix or an import, flush every cached response instead:
php bin/console silverback:api-components:purge-http-cache
or, as an admin, POST /_/http_cache/purge with no body. It returns 204. Both drop every cached response, API responses and rendered page HTML together. There is no option to flush only the API responses. The Nuxt module's site settings has a Purge all cached data button that sends the same request; see Purging all cached data.
The flush is sent to Souin's flush endpoint, <invalidation url>/flush, for each URL in api_platform.http_cache.invalidation.urls.
| Situation | Command | Endpoint |
|---|---|---|
| Flushed | exits 0 | 204 |
| The purger is not Souin, or no invalidation URL is set | exits 0, flushes nothing, and says so | 501 |
| The cache refused or couldn't be reached | exits 1 with the error | 502 |
purge-rendered-html (Purging Every Rendered Page). It drops only the pages, so the API responses stay cached. A full flush sends every request back to PHP and Nuxt until the cache refills.Environment Variables
| Variable | Purpose |
|---|---|
DATABASE_URL | Doctrine connection string |
JWT_SECRET_KEY | Path to private key PEM file |
JWT_PUBLIC_KEY | Path to public key PEM file |
JWT_PASSPHRASE | Private key passphrase (.env.local only) |
JWT_COOKIE_SAMESITE | Cookie SameSite attribute (strict, lax, none) |
MERCURE_URL | Internal API → Mercure hub URL |
MERCURE_PUBLIC_URL | Browser → Mercure hub URL |
MERCURE_JWT_SECRET | Shared secret for Mercure publisher JWT |
MAILER_DSN | Symfony Mailer transport |
APP_SECRET | Symfony application secret |
Services You Can Override
All ~320 bundle services have stable string IDs in the form silverback.api_components.*. Each service's FQCN is also registered as an alias, so you can decorate using either form in config/services.yaml:
# config/services.yaml
services:
App\YourCustomFilesystemProvider:
decorates: Silverback\ApiComponentsBundle\Flysystem\FilesystemProvider
# or equivalently using the stable string ID:
App\YourCustomFilesystemProvider:
decorates: silverback.api_components.flysystem.filesystem_provider
Commonly overridden services:
| Service ID | Purpose |
|---|---|
Silverback\ApiComponentsBundle\Flysystem\FilesystemProvider | Custom storage adapter resolution |
Silverback\ApiComponentsBundle\Security\UserChecker | Custom account checks on login |
silverback.security.jwt_manager | JWT creation/parsing (decorates lexik_jwt_authentication.jwt_manager) |
The bundle does not own the login success/failure handlers — those are Lexik's (lexik_jwt_authentication.handler.authentication_success and …_failure) and are wired on the login firewall in security.yaml rather than decorated. See Users & Security.
The string IDs follow the class namespace: Silverback\ApiComponentsBundle\Foo\BarBaz → silverback.api_components.foo.bar_baz. Inspect vendor/components-web-app/api-components-bundle/src/Resources/config/services.php for the full list.
Data Fixtures
Use AbstractCwaScaffold and CwaFixtureBuilder to seed your database with layouts, pages, routes, components, and page data in a fluent API.
Console Commands
Symfony console commands provided by the API Components Bundle for managing users, scanning for orphaned data, and generating component entities.