Bundle Setup
The API Components Bundle is the Symfony backend of CWA. It wires together API Platform, Doctrine ORM, LexikJWTAuthenticationBundle, Mercure, and a suite of content management abstractions so you spend your time building your app rather than infrastructure.
Prerequisites
- PHP 8.5+
- Symfony 7.4+ or 8.1+
- Doctrine ORM 3.7+ (with DBAL 4.4+)
- API Platform 4.4+ or 5.x
- A database server that supports recursive CTEs (see below)
Database servers
The bundle resolves a route's inherited go-live date with a recursive CTE (WITH RECURSIVE), so the database server must support one:
| Server | Minimum | Recommended |
|---|---|---|
| PostgreSQL | 8.4 | 12+ |
| MySQL | 8.0 | 8.0+ |
| MariaDB | 10.2 | 10.6+ |
| SQLite | 3.8.3 | 3.8.3+ |
The Recommended column is Doctrine DBAL 4's own floor: below it DBAL still connects but raises a deprecation, and DBAL 5 drops support. The CWA template runs PostgreSQL 16.
GET /_/routes, GET /_/pages or a page-data collection returns a 500. The sitemap is built from GET /_/routes, so it fails too.Installation
composer require "components-web-app/api-components-bundle:^2.0@alpha"
^2.0@alpha, a project with the usual minimum-stability: stable gets 1.1.11, the last 1.x release, which these docs don't describe. To follow unreleased changes, require 2.x-dev: main is aliased to it.Each release's changes are in the bundle's CHANGELOG.md, from 2.0.0-alpha.2 on, and the same notes are on its GitHub releases. Changes not yet released are listed under Unreleased. Read them before you upgrade, because breaking changes have their own heading.
The bundle requires Mercure and installs it with itself: symfony/mercure 0.7.1 or later, including 0.8, and symfony/mercure-bundle 0.4.3 or later, including 0.5. symfony/mercure 0.6 is no longer supported.
symfony/mercure 0.8 (symfony/mercure-bundle 0.5), HubInterface gains getProtocolVersion() and getCookieName(), and getUrl() moves to RemoteHubInterface. A class that decorates mercure.hub.default and doesn't have these methods breaks cache:clear with a fatal error. Implement RemoteHubInterface and forward the new methods to the inner hub. The template's api/src/Mercure/SkipAwareMercureHub.php shows how, and so does the bundle's own PublishableAwareHub.The bundle works with API Platform 4.4 and 5. Two differences are visible to clients. First, on API Platform 5, a request body with the wrong type for a property that has validation constraints returns a 422 with violations, where 4.4 returns a 400. Second, a unique-constraint violation from the database returns a 422 on API Platform 5 and a 500 on 4.4 (see Error Status Codes). On both versions, GET /me returns @context: /contexts/User.
mercure.hub.default service, so config/packages/mercure.yaml must define a hub named default, using the MERCURE_* variables under Environment Variables. Without it, the container fails to compile with non-existent service "mercure.hub.default".components-web-app/api-components-bundle, but the PHP namespace is Silverback\ApiComponentsBundle\ — the mismatch is historical, not a typo.use_symfony_listeners for you, because its listeners and actions depend on it. Don't set it to false in your own api_platform.yaml: your value overrides the bundle's, and requests such as GET /_api/me then fail with a 500.The bundle has no Flex recipe in symfony/recipes-contrib (earlier submissions were never merged), so Flex runs no recipe for it. Create these files yourself; the CWA template app (components-web-app) has a working copy of each under api/:
config/bundles.php— registerSilverback\ApiComponentsBundle\SilverbackApiComponentsBundle::class => ['all' => true]src/Entity/User.php— your user entity extendingAbstractUsersrc/Entity/RefreshToken.php— the refresh token entityconfig/packages/silverback_api_components.yaml— the bundle configurationconfig/routes/silverback_api_components.yaml— imports@SilverbackApiComponentsBundle/Resources/config/routing/all.php, with the same prefix as API Platform inconfig/routes/api_platform.yaml(/apion a fresh Flex install,/_apiin the template)config/packages/security.yaml— the security configuration (see Users & Security)
Generate JWT Keys
The bundle uses cookie-based JWT tokens for authentication. Generate a key pair once per environment:
mkdir -p config/jwt
openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout
Add the passphrase to .env.local — never commit this file:
JWT_PASSPHRASE=your_secure_passphrase
Add the key paths to .env:
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
Core Configuration
Open config/packages/silverback_api_components.yaml. This is the configuration the CWA template app ships with:
silverback_api_components:
website_name: My CWA App
user:
class_name: App\Entity\User
email_links:
default_origin: '%app.email_link_default_origin%' # the site's public origin
email_verification:
default_value: false
verify_on_register: true
verify_on_change: true
deny_unverified_login: true
email:
redirect_path_query: null
default_redirect_path: /verify-email/{{ username }}/{{ token }}
password_reset:
email:
redirect_path_query: null
default_redirect_path: /reset-password/{{ username }}/{{ token }}
new_email_confirmation:
email:
redirect_path_query: null
default_redirect_path: /confirm-new-email/{{ username }}/{{ new_email }}/{{ token }}
publishable:
permission: "is_granted('ROLE_ADMIN')"
refresh_token:
handler_id: silverback.api_components.refresh_token.storage.doctrine
options:
class: App\Entity\RefreshToken
cookie_name: api_component # must match Lexik's cookie name
ttl: 604800 # 1 week in seconds
database_user_provider: database
route_security:
- { route: "/user-area*", security: "is_granted('ROLE_USER')" }
routable_security: "is_granted('ROLE_ADMIN')"
mercure:
cookie:
samesite: '%env(JWT_COOKIE_SAMESITE)%'
user.email_verification is off if you leave it out: users start unverified, no verification email is sent, and unverified users can still sign in. The template turns it on. If you set 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. The redirect paths are front-end routes, so keep them in step with the pages your Nuxt app actually serves. See Configuration Reference for which keys are required.user.email_links.default_origin to the origin your front end is served from, such as https://www.example.com. Without it, password reset and verification emails are refused. The template builds %app.email_link_default_origin% in config/services.php from EMAIL_LINK_DEFAULT_ORIGIN, falling back to https://<BROWSER_SERVER_NAME>. See Links in Emails.Database Setup
The bundle adds tables for layouts, pages, routes, component groups, component positions, site config parameters, uploaded-file info (FileInfo), refresh tokens, and your user and component entities.
bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
Review the generated migration before running it — the initial migration is sizeable.
Environment Variables
Set these in .env (public) and .env.local (secrets):
# Database
DATABASE_URL="postgresql://user:pass@localhost:5432/app?serverVersion=16&charset=utf8"
# JWT auth
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=your_passphrase
# Mercure (real-time updates)
# MERCURE_URL is the internal publish URL — it must be reachable from the PHP container
MERCURE_URL=http://php.local/.well-known/mercure
# MERCURE_PUBLIC_URL is the subscribe URL the browser connects to
MERCURE_PUBLIC_URL=https://yourdomain.com/.well-known/mercure
MERCURE_JWT_SECRET=your_mercure_secret
# Email
MAILER_DSN=smtp://localhost:1025
Verifying the Install
Start your Symfony server and visit the API documentation UI — in the CWA template that is /_api/docs, since API Platform is mounted under the /_api prefix set in config/routes/api_platform.yaml. You should see every available resource listed.
The API entrypoint is the API root itself (/_api/), which returns the IRI of every resource collection. The Nuxt module fetches that entrypoint and discovers the Hydra documentation URL from its Link header.
Create your first admin user — pass --admin so the account has ROLE_ADMIN access:
bin/console silverback:api-components:user:create --admin
Follow the prompts to set username, email, and password. Without the flag the account is created with ROLE_USER only and cannot access the admin panel. Then load any fixtures you've defined:
bin/console doctrine:fixtures:load
What Gets Auto-Registered
You don't need to register these — the bundle provides them out of the box:
| Resource | Endpoint prefix | Purpose |
|---|---|---|
| Layout | /_/layouts | Outer page shell (header/footer) |
| Page | /_/pages | Individual pages with component groups |
| Route | /_/routes | URL → page/page data mapping |
| ComponentGroup | /_/component_groups | Named regions within a layout or page |
| ComponentPosition | /_/component_positions | Ordered slot assignments |
| Collection | /component/collections | Proxy to paginated resource lists |
| Form | /component/forms | Symfony form types via API |
Core bundle resources are served under the /_/ prefix, components under /component/, and page data under /page_data/. These sit below whatever routing prefix your app mounts API Platform on — /_api in the CWA template, so the full path to layouts is /_api/_/layouts.
Your custom components are registered when you create entity classes extending AbstractComponent.