# Roles & workspace access
**Product:** MetaSoul ERP
**Updated:** 2026-08-25 (Phase 3.1 + HIGH-5 policies)
## Roles (Spatie)
| Role | Purpose |
|------|---------|
| `admin` | Full access: Filament `/admin`, all workspace modules, settings |
| `sales` | CRM, Sales, Purchase, Inventory, POS, subscriptions, rental family |
| `support` | Helpdesk, Live Chat, Frontdesk |
| `hr` | Employees, Payroll, Recruitment, Time Off, Expenses, Attendance, Appraisals, Referrals, Fleet |
| `portal` | Customer portal only (`/my` personal pages) |
| `account_*` | Accounting modules (`account_invoice`, `account_basic`, `account_readonly`, `account_user`, `account_manager`) |
## Surfaces
| Surface | Guest | `portal` | Staff | `account_*` | `admin` |
|---------|-------|----------|-------|-------------|----------|
| Public site | Yes | Yes | Yes | Yes | Yes |
| `/my` personal (orders, tickets, …) | No | Yes | Yes | Yes | Yes |
| `/my` ERP modules | No | **No** | Role-gated | Accounting only | Yes |
| `/app/*` workspace | No | **No** | Role-gated | Accounting only | Yes |
| `/admin` Filament | No | No | No | No | Yes |
| System Settings | No | No | No | No | Yes |
## Module matrix (Phase 3.1)
| Module family | `sales` | `support` | `hr` | `account_*` | `admin` |
|---------------|---------|-----------|-----|-------------|---------|
| CRM / Sales / POS / Stock / Purchase | Yes | No | No | No | Yes |
| Helpdesk / Live Chat | No | Yes | No | No | Yes |
| Employees / HR suite / Fleet | No | No | Yes | No | Yes |
| Accounting | No | No | No | Yes | Yes |
| Shared (Contacts, Discuss, Calendar, Todo, Knowledge, Documents, Approvals, Projects) | Yes | Yes | Yes | No* | Yes |
| Unlisted / other apps | Yes† | Yes† | Yes† | Yes† | Yes |
\*Accounting-only users may still open workspace entry points; use accounting routes for books.
†Any workspace role may open modules not listed in the Core ACL map (server still requires `role.workspace`).
Source of truth: `App\Services\Auth\StaffModuleAccessService`.
## Middleware
| Alias | Class | Effect |
|-------|-------|--------|
| `role.admin` | `EnsureUserIsAdmin` | Must have `admin` |
| `role.workspace` | `EnsureWorkspaceAccess` | Must pass `User::canAccessWorkspace()` |
| `role.module:{key}` | `EnsureStaffModuleAccess` | Must be allowed for that module (e.g. `crm`, `hr`, `helpdesk`, `account`) |
| `accounting.permission` | `EnsureAccountingPermission` | Accounting ability checks |
## Policies (Laravel)
High-value workspace models use Laravel Policies registered in `AppServiceProvider`. Controllers call `$this->authorize()` **and** services re-check ownership via `findOwned()` / `findAccessible()` (defense in depth). Module entry remains gated by `role.module:*` middleware.
| Model | Policy | Notes |
|-------|--------|-------|
| `Order` | `OrderPolicy` | Sales module + owner (or admin) |
| `Partner` | `PartnerPolicy` | Contacts module |
| `Contact` | `ContactPolicy` | Contacts module + owner (or admin) |
| `Employee` | `EmployeePolicy` | HR module |
| `HelpdeskTicket` | `HelpdeskTicketPolicy` | Support staff or ticket owner |
| `AccountMove` | `AccountMovePolicy` | Accounting module + owner / linked invoice owner |
| `Invoice` | `InvoicePolicy` | Accounting/sales staff or portal owner |
| `Document` | `DocumentPolicy` | Documents module + access/edit rules |
| `CrmLead` | `CrmLeadPolicy` | CRM module + owner (or admin) |
| `PurchaseOrder` | `PurchaseOrderPolicy` | Purchase module + owner (or admin) |
| `Project` | `ProjectPolicy` | Projects module + access/manage rules |
| `CalendarEvent` | `CalendarEventPolicy` | Calendar module; view = owner/attendee; edit = owner |
| `KnowledgeArticle` | `KnowledgeArticlePolicy` | Knowledge module + owner (or admin) |
| `SignRequest` | `SignRequestPolicy` | Sign module + creator (or admin); public token routes stay token-based |
| `Payslip` / `PayRun` | `PayslipPolicy` / `PayRunPolicy` | HR payroll module; pay-run mutations HR/admin |
| `Product` | `ProductPolicy` | Sales or stock module |
| `Subscription` | `SubscriptionPolicy` | Subscriptions module + owner |
| `JobPosition` / `JobApplication` | `JobPositionPolicy` / `JobApplicationPolicy` | Recruitment module |
| `Course` | `CoursePolicy` | eLearning (`website_slides`) |
| `Event` | `EventPolicy` | Events module |
| `StockLevel` | `StockLevelPolicy` | Inventory module |
| `ProjectTask` | `ProjectTaskPolicy` | Projects module + assignee owner |
| `QualityCheck` | `QualityCheckPolicy` | Quality control module |
| `Survey` | `SurveyPolicy` | Surveys module |
### Authorization layers
1. **Route middleware** — `role.workspace` + `role.module:{key}` (role matrix in `StaffModuleAccessService`).
2. **Laravel Policy** — `$this->authorize()` / FormRequest `authorize()` on show/update/delete.
3. **Service ownership** — `findOwned()`, `findManaged()`, `assertOwned()`, etc. abort 403 on IDOR even if a controller forgets `authorize()`.
Remaining modules follow the same service-ownership pattern; add a Policy when introducing new high-value record surfaces.
## App launcher
`PortalAppLauncherService` filters tiles with `StaffModuleAccessService` so users only see apps their role may open. Route middleware enforces the same rules server-side.
## Registration
Self-registration assigns **`portal`** only. Staff accounts must be created/invited by an admin.
## Helpers on `User`
- `isAdmin()` / `canAccessPanel()` — Filament + admin gates
- `canAccessWorkspace()` — staff / admin / accounting roles
- `isPortalOnly()` — inverse of workspace access
## Demo seed accounts
Evaluation logins (emails + passwords) are **not** published on the public `/documentation` route. See **DEMO.md** in the zip pack, or open `/documentation/demo` while signed in as an admin.