DÉMO
dev only

Organizations

Multi-tenant architecture with role management, invitations and context switching.

Overview

The boilerplate supports a multi-tenant architecture: a user can belong to multiple organizations with distinct roles. Each request is automatically scoped to the active tenant via TenantContext.

Feature Detail
Organization settingssrc/Controller/Settings/OrganizationController.php
Member managementsrc/Controller/Settings/MembersController.php
Public invitation flowsrc/Controller/Invitation/InvitationController.php
Context switchersrc/Controller/Account/SwitchOrganizationController.php
Organization creation & updatesrc/Service/Organization/OrganizationService.php
Invitation lifecyclesrc/Service/Organization/InvitationService.php
Tenant context (request-scoped)src/Service/Organization/TenantContext.php
Tenant resolution on kernel.requestsrc/EventSubscriber/TenantSubscriber.php
RBAC — per-organization permissionssrc/Security/Voter/OrganizationVoter.php
Invitation emailsrc/Mail/Organization/InvitationMailer.php
Twig function user_organizations()src/Twig/Runtime/OrganizationRuntime.php

Roles

Each organization member has a role that determines their rights:

RolePermissions
ownerFull access — deletion, ownership transfer
adminManage members, edit settings
memberAccess to business features only

RBAC — OrganizationVoter

Permissions are enforced via Symfony Voters. OrganizationVoter defines 6 attributes, with the organization as subject:

src/Security/Voter/OrganizationVoter.php
AttributeAllowed roles
ORG_SETTINGSOwner, Admin
ORG_INVITEOwner, Admin
ORG_MANAGE_MEMBERSOwner, Admin
ORG_REVOKE_INVITATIONOwner, Admin
ORG_DELETEOwner only
ORG_TRANSFER_OWNERSHIPOwner only

Usage

In a controller (throws AccessDeniedException if denied):

$this->denyAccessUnlessGranted(OrganizationVoter::ORG_SETTINGS, $organization);

In a Twig template (hides elements based on role):

{% if is_granted('ORG_INVITE', organization) %}

Organization settings

Owners and admins can edit the name and slug from /settings/organization. Access is protected by ORG_SETTINGS.

src/Controller/Settings/OrganizationController.php

The slug is auto-generated at creation via cocur/slugify (4-character random suffix on collision). It can be edited manually with uniqueness validation.

src/Service/Organization/OrganizationService.php

Member management

The /settings/members page allows inviting members, viewing pending invitations and acting on active members. Each POST action is protected by the corresponding voter:

src/Controller/Settings/MembersController.php
ActionRequired voter
Invite a memberORG_INVITE
Revoke an invitationORG_REVOKE_INVITATION
Suspend a memberORG_MANAGE_MEMBERS
Reactivate a memberORG_MANAGE_MEMBERS

Invitation flow

Email invitation is the only way to join an existing organization. The token is generated via bin2hex(random_bytes(32)), valid for 7 days.

  1. The owner or admin submits the email and role — InvitationService::invite() generates the token, checks for duplicates (existing member or pending invitation) and dispatches MemberInvitedEvent.
  2. SendInvitationEmailSubscriber receives the event and InvitationMailer sends the email with the /invitation/{token} link.
  3. The invitee clicks the link. If not logged in, the token is stored in session (_pending_invitation_token) and they can log in or create an account (pre-filled form, no organization creation).
  4. Acceptance calls InvitationService::accept() which creates the OrganizationMember, marks the invitation as accepted and dispatches MemberJoinedEvent. The tenant context automatically switches to the new organization.

Registration via invitation

When the invitee creates an account, the registration form detects the _pending_invitation_token session and adapts: email pre-filled and locked, organization field hidden, no organization created. After email verification, the user is redirected to the invitation page.

src/Controller/Invitation/InvitationController.php

Rate limiting

10 invitations per hour per organization (token bucket). Configurable in:

config/packages/framework.yaml

Tenant context

On every request, the active organization is resolved and stored in TenantContext. Repositories inject this service to automatically scope their queries to the current tenant.

TenantContext

Request-scoped service that holds the active organization for the current HTTP request.

src/Service/Organization/TenantContext.php
MethodDescription
getCurrentOrganization()Returns the current organization, or null if none is set.
setCurrentOrganization()Sets the active organization (called by TenantSubscriber).
requireCurrentOrganization()Returns the current organization or throws a LogicException. Use this in services/repositories where a tenant is mandatory.
hasOrganization()Returns true if an organization is set.

TenantSubscriber

Runs on kernel.request (priority 0, after the firewall). Reads the ID from the session, verifies the active membership, and injects it into TenantContext. If no organization is in session, the first active membership is auto-selected.

src/EventSubscriber/TenantSubscriber.php

Session key: _current_organization_id

Inter-organization context switch

A user can belong to multiple organizations. The navbar selector allows switching between them without logging out.

src/Controller/Account/SwitchOrganizationController.php

The POST route /account/switch-organization/{id} verifies the active membership, writes _current_organization_id to the session and redirects to the previous page (via the Referer header).

Twig function

The user_organizations() function exposes all active memberships for the current user. It is available globally via OrganizationExtension + OrganizationRuntime:

{% set memberships = user_organizations() %}
src/Twig/Runtime/OrganizationRuntime.php

Danger zone

The /settings/danger page exposes two irreversible actions restricted to owners: delete the organization and delete the account.

src/Controller/Account/DangerZoneController.php

Organization deletion

Protected by the ORG_DELETE voter (owner only). The user must type the exact organization name to confirm. OrganizationDeletionService deletes subscriptions, then the organization (cascades members + invitations).

src/Service/Organization/OrganizationDeletionService.php

Account deletion

The user must confirm their password. AccountDeletionService checks for sole-owner status before deleting memberships, the reset token, and then the account.

Sole-owner block: if the user is the only owner of at least one organization, deletion is blocked. The list of blocking organizations is shown in the UI.

src/Service/Auth/AccountDeletionService.php
Loading…
Loading the web debug toolbar…
Attempt #