Realms
A Realm is a named access-control zone applied to one or more nodes in the content tree. When a node belongs to a realm, the API WebResponse endpoint reports which realms govern it and whether the current request is allowed to see them — so your frontend can decide whether to render restricted content, hide blocks, or redirect the user to an authentication screen.
Where realms are enforced
Realm authentication is enforced wherever WebResponseDataTransformerInterface is used: on the API WebResponse endpoint, and on any Twig-rendered controller that extends or invokes DefaultNodeSourceController (which calls the same transformer internally). Custom Twig controllers that do not go through DefaultNodeSourceController must call RealmResolver::denyUnlessGranted() themselves if they need realm enforcement.
Core concepts
Authentication types
Each realm uses one of three mechanisms to decide whether a visitor is granted access:
| Type constant | Value | How access is granted |
|---|---|---|
TYPE_PLAIN_PASSWORD | plain_password | Visitor provides the shared password in the request |
TYPE_ROLE | bearer_role | Visitor's JWT/session token includes the required Symfony role |
TYPE_USER | bearer_user | Visitor's JWT/session token matches one of the realm's allowed users |
Behaviour modes
When access is denied, a realm's behaviour controls what happens:
| Behaviour constant | Value | Effect |
|---|---|---|
BEHAVIOUR_NONE | none | Realm presence is reported but no access restriction is enforced |
BEHAVIOUR_DENY | deny | API responds with 401 Unauthorized |
BEHAVIOUR_HIDE_BLOCKS | hide_blocks | API responds normally but hidingBlocks: true is set in WebResponse; blocks are not rendered by the transformer |
Inheritance types
When you attach a realm to a node you choose how it propagates to child nodes:
| Inheritance constant | Value | Behaviour |
|---|---|---|
INHERITANCE_NONE | none | Realm applies only to the exact node it is attached to |
INHERITANCE_AUTO | auto | Realm is inherited automatically by all descendant nodes (default) |
INHERITANCE_ROOT | root | Realm marks only the root of an inheritance subtree; children inherit it via async processing |
Inheritance changes are processed asynchronously through Symfony Messenger (ApplyRealmNodeInheritanceMessage, CleanRealmNodeInheritanceMessage).
Managing realms in the back office
Required roles
| Role | What it grants |
|---|---|
ROLE_ACCESS_REALMS | Create, edit and delete realm definitions |
ROLE_ACCESS_REALM_NODES | Attach and detach nodes from existing realms |
Creating a realm
- Navigate to Realms in the back-office sidebar.
- Click Add a realm and fill in the form:
- Name — unique human-readable identifier (auto-generates a serialization group slug)
- Type — choose
plain_password,bearer_roleorbearer_user - Behaviour — choose
none,denyorhide_blocks - Password — (plain_password type only) the shared secret that clients must send
- Role — (bearer_role type only) the Symfony role string, e.g.
ROLE_PREMIUM - Users — (bearer_user type only) one or more Roadiz users
- Serialization group — optional; when set, extra API fields gated behind this group name are exposed only to granted visitors
Attaching a node to a realm
- Open any node in the node tree editor.
- Go to the Realms tab on the node settings panel.
- Select the realm and choose the inheritance type.
- Save — the back office fires
NodeJoinedRealmEventand schedules inheritance propagation if needed.
To detach, click the delete button next to the realm entry on the same tab. This fires NodeLeftRealmEvent and triggers cleanup of inherited realm nodes.
How authentication works in API requests
Plain-password realms
Clients must provide the shared password on every request. Two methods are supported:
Preferred — Authorization header (password never appears in server logs):
GET /api/web_response_by_path?path=/members-area
Authorization: PasswordQuery mysecretpasswordLegacy — query parameter (avoid; passwords appear in access logs and browser history):
GET /api/web_response_by_path?path=/members-area&password=mysecretpasswordThe Authorization header scheme name (PasswordQuery) is the value returned by Realm::getAuthenticationScheme() and is also present in the WWW-Authenticate challenge sent with 401 responses.
Security note
Passwords are stored as bcrypt hashes in the database. The plain-text value is never retrievable after saving. Existing passwords set before this hashing was introduced remain working via a timing-safe comparison fallback — re-save them through the admin UI to upgrade them to bcrypt hashes.
Bearer-role realms
The visitor must be authenticated (e.g. via JWT) and their token must carry the required role. No extra header is needed — RealmVoter consults Symfony's AccessDecisionManager.
GET /api/web_response_by_path?path=/premium-content
Authorization: Bearer <jwt-token>Bearer-user realms
Same as bearer-role: the visitor must be authenticated and their user identifier must match one of the realm's configured users.
GET /api/web_response_by_path?path=/vip-section
Authorization: Bearer <jwt-token>WebResponse integration
When a node has realms attached, the WebResponse payload includes a realms array and a hidingBlocks flag:
{
"@context": "/api/contexts/WebResponse",
"@id": "/api/web_response_by_path?path=/members-area",
"@type": "WebResponse",
"item": { ... },
"blocks": [],
"realms": [
{
"@type": "Realm",
"@id": "/api/realms/1",
"type": "plain_password",
"behaviour": "hide_blocks",
"name": "Members area",
"authenticationScheme": "PasswordQuery"
}
],
"hidingBlocks": true
}realms— the realms attached to this node that the current visitor has not been granted access to. If the visitor is granted, the realm does not appear here.hidingBlocks—truewhen at least onehide_blocksrealm denied the visitor. Your frontend should render a paywall or login prompt instead of the block content.
Enabling realms on a custom WebResponse
Your WebResponse model must implement RealmsAwareWebResponseInterface:
<?php
declare(strict_types=1);
namespace App\Api\Model;
use RZ\Roadiz\CoreBundle\Api\Model\BlocksAwareWebResponseInterface;
use RZ\Roadiz\CoreBundle\Api\Model\RealmsAwareWebResponseInterface;
use RZ\Roadiz\CoreBundle\Api\Model\WebResponseInterface;
use RZ\Roadiz\CoreBundle\Api\Model\WebResponseTrait;
final class WebResponse implements
WebResponseInterface,
BlocksAwareWebResponseInterface,
RealmsAwareWebResponseInterface
{
use WebResponseTrait;
}Your DataTransformer must then call injectRealms() from RealmsAwareWebResponseOutputDataTransformerTrait during the transform step. This method:
- Resolves all realms attached to the node.
- For each realm, calls
RealmResolver::isGranted()— which invokesRealmVoter. - Collects denied realms into
WebResponse::$realms. - Sets
hidingBlocks = trueif any denied realm hasBEHAVIOUR_HIDE_BLOCKS. - Throws
UnauthorizedHttpException(401) if any denied realm hasBEHAVIOUR_DENY.
Serialization groups
A realm can carry an optional serialization group name (auto-derived from the realm name unless set manually). When a visitor is granted access to such a realm, that group name is added to the API normalization context by RealmSerializationGroupNormalizer.
This lets you gate additional API fields behind realm access:
# config/api_resources/web_response.yaml
resources:
App\Api\Model\WebResponse:
operations:
page_get_by_path:
normalizationContext:
groups:
- nodes_sources
- web_response
# 'members_area' group fields are injected automatically
# when the visitor is granted the 'Members area' realmDefine your node-type fields under the custom serialization group using #[Groups(['members_area'])] on the relevant properties. Unauthenticated visitors will receive the response without those fields.
Events
| Event class | Fired when |
|---|---|
RZ\Roadiz\CoreBundle\Event\Realm\NodeJoinedRealmEvent | A node is attached to a realm |
RZ\Roadiz\CoreBundle\Event\Realm\NodeLeftRealmEvent | A node is detached from a realm |
Both events expose the RealmNode entity via $event->getRealmNode().
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use RZ\Roadiz\CoreBundle\Event\Realm\NodeJoinedRealmEvent;
use RZ\Roadiz\CoreBundle\Event\Realm\NodeLeftRealmEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class RealmSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
NodeJoinedRealmEvent::class => 'onNodeJoinedRealm',
NodeLeftRealmEvent::class => 'onNodeLeftRealm',
];
}
public function onNodeJoinedRealm(NodeJoinedRealmEvent $event): void
{
$realmNode = $event->getRealmNode();
// $realmNode->getNode(), $realmNode->getRealm(), $realmNode->getInheritanceType()
}
public function onNodeLeftRealm(NodeLeftRealmEvent $event): void
{
$realmNode = $event->getRealmNode();
}
}Frontend integration example
The following shows a minimal Nuxt 3 / Vue 3 pattern for handling realm-gated pages.
// composables/useWebResponse.ts
const route = useRoute()
const config = useRuntimeConfig()
const { data, error } = await useFetch('/api/web_response_by_path', {
params: { path: route.path },
headers: realmPassword
? { Authorization: `PasswordQuery ${realmPassword}` }
: {},
})
if (error.value?.statusCode === 401) {
// Prompt the visitor for the realm password
}
if (data.value?.hidingBlocks) {
// Render a paywall instead of block content
}The realms array in the response tells you which realms are blocking access and their authenticationScheme tells you how to authenticate (PasswordQuery vs Bearer):
for (const realm of data.value?.realms ?? []) {
if (realm.authenticationScheme === 'PasswordQuery') {
// Show a password input
} else {
// Redirect to login / JWT refresh
}
}