Security

Role-Based Access for Client Portals with Spatie and LaraDashboard

By Lara Dashboard 5 views
Role-Based Access for Client Portals with Spatie and LaraDashboard
You are shipping a client portal next to your admin panel. Staff need full CMS and CRM tools. External clients only need their projects, invoices, tickets, and files. Someone copies the staff role model into the portal and suddenly a client user can see admin menus, or worse, mutate another tenant's data.
This article is about role-based access for client portals in Laravel: how to separate portal users from staff, how Spatie laravel-permission roles and permissions (and guards) fit that split, and where LaraDashboard helps without pretending one role table solves every product rule.
It is not a generic RBAC rewrite. For the staff-admin walkthrough (roles UI, middleware, policies, module permissions, impersonation), start with our complete guide: Role-Based Access Control in Laravel with LaraDashboard. Here we stay on the portal surface: external users, portal roles, guard mixups, and policy checks that keep client data client-scoped.
Disclosure: LaraDashboard is our open-source Laravel admin and CMS. We recommend it when you want a ready admin with Spatie-backed roles, modules, and activity logging. You still own portal routes, guards, and policies for client-facing apps. The patterns below also apply if you use Filament, Nova, or a custom Laravel admin.

The short answer

Staff admin RBAC answers: which internal users can manage content, settings, other users, and modules.
Client portal RBAC answers: which external users can view or act on their resources (orders, projects, tickets, documents), and never on staff-only surfaces.
Spatie laravel-permission gives you roles, permissions, middleware, Blade directives, and optional multiple guards. Laravel policies and gates still decide resource ownership. LaraDashboard gives staff a UI to manage roles and permissions for the admin product. Portal success still depends on you defining portal roles, scoping queries, and never treating "logged in" as "trusted like staff."
If your portal is a thin "view my invoices" page for one account type, a few permissions plus policies may be enough. If you have client admins, client viewers, partner agents, and staff impersonation, plan the portal role map before you invent another is_admin flag.

Portal users are not staff with fewer checkboxes

The common shortcut is one users table and one role list: Super Admin, Admin, Editor, Client. Then you hide admin nav items with @can and hope clients never hit /admin.
That fails in three predictable ways:
  1. Route leakage. A bookmarked or guessed admin URL still runs if middleware only checks auth, not a staff permission or guard.
  2. Permission name collisions. A permission like projects.view means "all projects" for staff and "my projects" for clients unless policies enforce ownership.
  3. Role assignment mistakes. Support grants a temporary staff role to a client user during debugging and forgets to revoke it.
Treat portal actors as a different audience, even if they share a database table. Same Eloquent model is fine. Same permission namespace for staff-only actions is not.

Mental model: two surfaces, one Laravel app

Most products end up with:
  • Staff surface: /admin (or similar), session auth, Spatie roles for CMS/CRM modules, activity log, maybe impersonation.
  • Portal surface: /portal or a separate subdomain, session or Sanctum token auth, portal roles (client-admin, client-member, client-billing), policies that filter by account_id / organization_id.
You can implement both in one codebase. You should not reuse staff permission strings as the only line of defense for portal writes.
Cheat sheet (prose, not a table LaraBuilder might drop):
  • Staff goal: operate the product and content. Portal goal: consume and update own account data.
  • Staff default deny: no module permission, no access. Portal default deny: no membership on the account, no access.
  • Staff checks: Spatie permission / role middleware + policies. Portal checks: auth + membership + permission + policy ownership.
  • Staff failure mode: over-privileged editor. Portal failure mode: IDOR (insecure direct object reference) across accounts.
For multi-tenant shape at SaaS scale, also see Building a Multi-Tenant SaaS: Laravel vs WordPress Multisite. RBAC and tenancy are related. They are not the same layer.

Spatie roles and permissions for portals (what to name)

Spatie associates users with roles and permissions in the database. Official docs show patterns like $user->assignRole('writer'), $user->givePermissionTo('edit articles'), and $user->can('edit articles') because permissions register on Laravel's gate. See the laravel-permission introduction.
For portals, prefer permission names that encode the portal audience:
  • Good: portal.projects.view, portal.projects.update, portal.invoices.pay, portal.users.invite
  • Risky: reusing bare projects.view for both staff "all projects" and client "mine"
Roles group those permissions:
  • client-admin: invite members, manage billing contacts, view all account projects
  • client-member: view and update assigned projects only
  • client-billing: invoices and payment methods, read-only on projects
  • client-viewer: read-only across the account
Keep staff roles (super-admin, editor, support) in a separate naming family. Do not invent a "client-super-admin" that maps to staff middleware.
LaraDashboard's admin role UI is built for the staff product: create roles, tick permission groups, assign users. Use that for internal roles. Seed portal roles in code (migrations/seeders) so portal permissions cannot be casually renamed in a way that breaks production checks. Then optionally expose a subset of portal role management to staff support users who need to fix client access without SQL.

Guards: when multiple guards help (and when they hurt)

Spatie supports multiple guards. In that mode, guards act like namespaces: each guard has its own set of roles and permissions. Creating edit articles for web does not create it for admin. Assignments must match the user's guard or you get GuardDoesNotMatch / missing role errors. Details: Using multiple guards.
When a separate portal guard helps:
  • Portal users are a different authenticatable model (for example ClientUser vs User)
  • You want hard isolation so staff roles cannot be assigned onto portal sessions by accident
  • Session cookies and providers are already split (auth.guards.portal vs auth.guards.web)
When one guard is enough:
  • One User model with a type/flag and account membership
  • You force a single $guard_name on the model (Spatie documents overriding getDefaultGuardName() so you do not duplicate every permission per guard)
  • You rely on route middleware + policies for isolation, not guard namespaces alone
Honest tradeoff: multiple guards add ceremony (duplicate permission names per guard unless you force one). Single guard is simpler but demands ironclad middleware on /admin and /portal. Pick one strategy and document it for the team. Mixing "sometimes check guard, sometimes check role name" is how incidents start.
Laravel's auth docs cover configuring guards and providers; pair that with Spatie's guard section before you invent a third session cookie.

Policies still own "this invoice belongs to that account"

Roles answer capability classes. Policies answer resource instances.
Example portal rules you should put in policies (or equivalent gates), not only in Blade:
  • User may view Invoice $invoice only if $user->belongsToAccount($invoice->account_id) and has portal.invoices.view
  • User may update Project $project only if membership role is client-admin or the project is assigned to them
  • User may never delete User records with staff roles, even if they somehow hold a mis-assigned permission string
Spatie's best-practice docs stress roles vs permissions and model policies as complementary. LaraDashboard's staff guide shows policy + authorize() patterns for admin models. Portal policies should be stricter on account scope. A permission without a membership check is how one client reads another's ticket by changing the ID in the URL.
Also hide UI with @can / @role, but never trust UI hiding as security. Server-side checks on every write path. FormRequest authorize() methods are a good place for portal actions.

Common failure cases (and how to catch them)

1. Guard mixups

Symptom: PermissionDoesNotExist or role assign fails after you added a portal guard. Or checks silently use the wrong guard's permission set.
Fix: when creating roles/permissions, set guard_name explicitly. When checking, pass the guard if you use multiples (hasPermissionTo('portal.projects.view', 'portal')). Align auth.defaults.guard and model $guard_name with Spatie's resolution order (documented in the multiple-guards page).

2. Super-admin leak into the portal

Symptom: a Gate::before that returns true for super-admin also runs on portal routes. Client impersonation or shared middleware stack grants god mode on portal controllers.
Fix: scope super-admin bypass to staff middleware groups only. Or require both role and guard / route prefix before bypassing. Spatie documents defining a super-admin carefully; treat "bypass all abilities" as staff-only.

3. Missing policies (IDOR)

Symptom: route uses middleware('permission:portal.projects.view') then Project::findOrFail($id) with no account scope.
Fix: always authorize the model. Prefer Project::query()->forAccount($accountId)->findOrFail($id) plus $this->authorize('view', $project). Add feature tests that swap account IDs.

4. Staff permission reuse

Symptom: client role includes users.manage because someone reused the staff matrix in a seeder.
Fix: separate permission catalogs. Code review seeders. Deny staff permission names on portal role attach in an application service if humans can edit roles in UI.

5. Session fixation across surfaces

Symptom: logging into admin then visiting portal (or the reverse) carries unexpected abilities because one session guard serves both route groups.
Fix: separate login endpoints, regenerate sessions on login, and middleware that rejects the wrong audience. Pair with auth, 2FA, and session security practices (HTTPS, session config, 2FA for staff).

6. API tokens without portal abilities

Symptom: Sanctum token issued for a client mobile app with abilities * or staff permission names.
Fix: map token abilities to portal permissions only. Revoke on offboarding. See our REST API first notes for Sanctum vs full OAuth.

How LaraDashboard fits (disclosure and boundaries)

LaraDashboard ships staff-facing RBAC on Spatie laravel-permission: role UI, permission groups (including module-scoped permissions), middleware, policies, Blade directives, activity logging, and guarded impersonation for support. That is the right foundation for the admin half of a client-portal product.
What you should still build for the portal half:
  • Portal route group and views (or a separate Inertia/Livewire app)
  • Portal role/permission seeders with portal.* names
  • Account membership model (user belongs to organization with a portal role)
  • Policies that enforce account scope
  • Optional separate guard if your authenticatable models differ
  • Tests for IDOR and role escalation
LaraDashboard does not replace your portal UX or your tenancy rules. It reduces the cost of running a secure staff admin next to that portal: one Laravel codebase, consistent permission package, audit trail when support changes access.
If you are still on WordPress client areas (membership plugins, role plugins, custom capabilities), compare maintenance load with WordPress security maintenance cost vs Laravel ownership and the broader secure self-hosted Laravel CMS checklist. Portal RBAC bugs are product security bugs, not "just CMS settings."

Suggested build order

  1. Write the role map on paper. Staff roles vs portal roles. No shared "admin" name for both.
  2. Seed permissions with portal. prefixes. Attach to portal roles only.
  3. Lock staff routes with staff permission middleware (and/or admin guard). Confirm clients get 403 on /admin.
  4. Add membership + policies before fancy portal UI.
  5. Add portal UI that only shows actions the policy allows.
  6. Log role changes (LaraDashboard activity log for staff; mirror critical portal grants if support can edit them).
  7. Test cross-account access, guard mismatch, and super-admin bypass paths.
Skip step 3 and you will debug production screenshots of clients inside settings. Skip step 4 and you will debug silent data leaks that never show in the UI review.

FAQ

Do I need a separate User model for portal clients?

Not always. One User model with account membership and portal roles is common. Use a separate model (and often a separate guard) when registration, password rules, or identity providers differ enough that sharing the staff user table becomes painful. Spatie works with the authenticatable models you configure per guard.

Should portal permissions live in the LaraDashboard role UI?

Staff permissions belong there. Portal permissions can be seeded and optionally exposed to trusted support roles. Avoid letting every staff editor invent new portal permission strings in production without deploy review.

Is Spatie enough without policies?

No. Spatie answers "may this user class perform this action type?" Policies answer "may they perform it on this record?" Portals need both.

Multiple guards or one?

Use multiple guards when authenticatable models or session isolation need it, and accept duplicated role/permission rows per guard (or force a single guard name on the model as Spatie documents). Use one guard when one user table and strict route middleware keep audiences apart. Do not half-configure both.

How does this differ from your complete RBAC guide?

The complete RBAC guide teaches staff-admin RBAC inside LaraDashboard (UI, middleware, policies, modules, impersonation). This article focuses on client portals: external users, portal role naming, guards, IDOR, and how the admin product sits beside the portal without leaking superuser power.

What about WordPress membership plugins for client areas?

They can work for simple content gating. Product portals with invoices, tickets, and multi-user accounts usually outgrow capability matrices and plugin stacks. Laravel + Spatie + explicit policies give you one place to test authorization. That is a product decision, not a brand slogan.

Ending note

Client portal security fails when you treat portal users as "staff with fewer boxes ticked." Separate the audiences in naming, middleware, and policies. Use Spatie for role and permission storage. Use Laravel policies for account-scoped resources. Use LaraDashboard to run the staff admin with auditable RBAC while you build the portal surface with portal.* permissions and membership checks.
CTA: Start from a written portal role map, seed portal.* permissions, lock /admin to staff-only middleware, then add policies before you polish portal UI. If you want a Laravel admin that already speaks Spatie roles for the staff side, try LaraDashboard and keep portal rules in your app code where they belong.
Questions about a portal edge case (partners, read-only auditors, or Sanctum mobile clients)? Leave a comment with your guard setup and we can reason through the failure mode.

Try Lara Dashboard for Free

Explore every feature live — no sign-up required.

Launch Live Demo