Skip to main content
Vito uses two overlapping role systems:
  1. System roles (UserRole enum) — stored in the users.role column. Controls access to the Filament admin panel and super-admin features.
  2. Tenant roles (Role enum + Spatie Permission) — scoped to a tenant membership. Controls what actions a staff member can perform within a business.

System roles

Defined in App\Domain\Identity\Enums\UserRole:
Enum valueDB valueDescription
SUPER_ADMINsuper_adminFull platform access, bypasses all tenant checks
TENANT_OWNERsellerOwner of one or more tenants (legacy DB value)
STAFFstaffEmployee member of a tenant
CUSTOMERuserEnd consumer (legacy DB value)
The super_admin role can also be granted by listing an email in the APP_SUPER_ADMINS environment variable (comma-separated). This takes precedence over the database role column.
// User::isSuperAdmin()
$adminEmail = config('auth.super_admin_email');
if ($adminEmail && strcasecmp($this->email, $adminEmail) === 0) {
    return true;
}
return $this->role->value === UserRole::SUPER_ADMIN->value;

Tenant roles and permissions

Tenant-scoped roles are managed by Spatie Laravel Permission and defined in App\Domain\Access\Enums\Role:
RoleValueTypical use
OWNERownerFull control of the tenant, all permissions
ADMINadminManage catalog, orders, staff, analytics
MANAGERmanagerManage catalog, orders, and fulfillment
OPERATORoperatorView catalog and orders, fulfill orders
VIEWERviewerRead-only access across most resources

Permission list

Permissions are defined in App\Domain\Access\Enums\Permission and assigned to roles in config/rbac.php:
// config/rbac.php — example for the 'admin' role
Role::ADMIN->value => [
    Permission::TENANT_VIEW->value,
    Permission::TENANT_UPDATE->value,
    Permission::CATALOG_VIEW->value,
    Permission::CATALOG_CREATE->value,
    // ...
    Permission::AI_USE->value,
],
PermissionValueDescription
TENANT_VIEWtenant.viewView tenant profile
TENANT_UPDATEtenant.updateUpdate tenant profile
SETTINGS_VIEWsettings.viewView settings
SETTINGS_UPDATEsettings.updateUpdate settings
BILLING_VIEWbilling.viewView billing information
BILLING_MANAGEbilling.manageManage billing and subscriptions
CATALOG_VIEWcatalog.viewView products
CATALOG_CREATEcatalog.createCreate products
CATALOG_UPDATEcatalog.updateUpdate products
CATALOG_DELETEcatalog.deleteDelete products
CATALOG_PUBLISHcatalog.publishPublish / unpublish products
MENU_VIEWmenu.viewView menu sections
MENU_MANAGEmenu.manageManage menu sections
INVENTORY_VIEWinventory.viewView stock levels
INVENTORY_ADJUSTinventory.adjustAdjust stock quantities
INVENTORY_TRANSFERinventory.transferTransfer stock between locations
ORDERS_VIEWorders.viewView orders
ORDERS_CREATEorders.createCreate orders
ORDERS_MANAGEorders.manageUpdate order metadata
ORDERS_FULFILLorders.fulfillMark orders as fulfilled
ORDERS_CANCELorders.cancelCancel orders
ORDERS_EXPORTorders.exportExport orders to file
PAYMENTS_VIEWpayments.viewView payment records
PAYMENTS_RECORDpayments.recordRecord a payment
CUSTOMERS_VIEWcustomers.viewView customer list
CUSTOMERS_EXPORTcustomers.exportExport customer data
APPOINTMENTS_VIEWappointments.viewView appointments
APPOINTMENTS_MANAGEappointments.manageCreate and update appointments
STAFF_VIEWstaff.viewView staff list
STAFF_MANAGEstaff.manageInvite and manage staff
STAFF_ASSIGN_ROLESstaff.assign_rolesAssign roles to staff
MARKETING_VIEWmarketing.viewView campaigns and leads
MARKETING_MANAGEmarketing.manageCreate and manage campaigns
ANALYTICS_VIEWanalytics.viewView analytics dashboards
ANALYTICS_EXPORTanalytics.exportExport analytics data
INTEGRATIONS_VIEWintegrations.viewView integrations
INTEGRATIONS_MANAGEintegrations.manageConfigure integrations
WEBHOOKS_VIEWwebhooks.viewView webhook configurations
WEBHOOKS_MANAGEwebhooks.manageManage webhook configurations
AI_USEai.useAccess AI generation tools

Role presets

config/rbac.php defines presets for common staff roles tied to a plan capability:
PresetBase roleKey permissionsRequires capability
cashieroperatororders.view, orders.manage, payments.recordcheckout_basic
kitchen_staffoperatororders.view, orders.fulfillkitchen_display
service_provideroperatorappointments.view, appointments.manageappointment_booking
warehouse_clerkoperatorinventory.view, inventory.adjust, inventory.transferinventory_tracking

Checking permissions in code

In PHP (Spatie)

// Check a permission
$user->can('orders.view');

// Check a role
$user->hasRole('admin');

// Assign a role to a user
$user->assignRole('manager');

// Give a direct permission
$user->givePermissionTo('catalog.publish');

Checking system role

// Check super admin (email whitelist OR role column)
$user->isSuperAdmin();

// Check tenant ownership (membership or owner column)
$user->isTenantOwner();

In Filament resources

Filament resources respect Laravel’s Gate. Use canAccess or canViewAny methods that delegate to the registered Policy:
// ActivityLogResource — only super_admin and auditor
public static function canAccess(): bool
{
    $user = auth()->user();
    return $user->isSuperAdmin() || $rawRole === 'auditor';
}

Tenant ownership middleware

The tenant.ownership middleware (EnsureTenantOwnership) is applied to all protected tenant API routes:
Route::middleware(['auth:sanctum', 'tenant.ownership', 'throttle:api'])->group(function () {
    Route::prefix('orders')->group(function () { ... });
    Route::prefix('settings')->group(function () { ... });
    Route::prefix('reports')->group(function () { ... });
});
For every route-model-bound parameter, the middleware verifies that the model’s tenant_id belongs to one of the authenticated user’s tenants. On a mismatch:
  1. A TENANT_OWNERSHIP_VIOLATION event is logged to the security log channel.
  2. The request is aborted with 404 (not 403) to avoid disclosing resource existence.
Models covered: Product, Order, Coupon, MenuSection, Lead, Campaign.

Impersonation

Super-admins can impersonate tenant users directly from the Filament admin panel. The original admin session is preserved in impersonator_id and impersonator_name session keys.

Leaving impersonation

GET /impersonate/leave
The ImpersonationController::leave method:
  1. Verifies that an active impersonation session exists (impersonator_id in session).
  2. Logs the impersonation_ended event with the original admin ID, impersonated user ID, IP, and user agent.
  3. Clears the impersonation session keys.
  4. Logs out the impersonated user and restores the original admin session.
  5. Regenerates the session ID to prevent session fixation.
$this->logger->info('impersonation_ended', [
    'original_admin_id'   => $impersonatorId,
    'impersonated_user_id' => $impersonatedUserId,
    'ip_address'          => $request->ip(),
    'user_agent'          => $request->userAgent(),
    'timestamp'           => now()->toIso8601String(),
]);

$request->session()->forget(['impersonator_id', 'impersonator_name', 'password_hash_check']);
$this->auth->logout();
$admin = User::query()->find($impersonatorId);
$this->auth->login($admin);
$request->session()->regenerate();
If the original admin account cannot be found after impersonation ends (e.g., it was deleted), the session is fully invalidated and the browser is redirected to /login. This failure is logged at emergency severity.

Build docs developers (and LLMs) love