# Roles and permissions FloCafe has five fixed staff identities: owner, manager, cashier, server, and chef. This page is the in-app **permission editor and UI capability matrix**: what each role can reach in the interface, and how an owner changes that. It is not the full authorization reference. The backend enforces authorization by resolving an effective permission set from SQLite for every protected action, across roughly 245 `requirePermission`/`requireAnyPermission` call sites plus a handful of reviewed context-policy role checks that no middleware sees; that surface is described in [the authentication and authorization page](../architecture/authentication-and-authorization.md). Read this page to understand what a role sees today and how to change it, and that page to understand the resolution model and what stays role-based on purpose. Unlike the identities themselves, **access is configurable.** An owner edits role defaults and per-user exceptions from **Staff > Role permissions**; there is no separate IAM product, but there is no fixed, uneditable permission grant either. ## Source of truth The runtime source of truth is [`shared/permissions.ts`](../../shared/permissions.ts): `PERMISSION_DEFINITIONS` is the complete, executable catalog — every permission id, its area, its shipped default roles, whether it is owner-configurable, and its risk tier. `authorization.manage` and `staff.privileged.manage` are marked `configurable: false`; they are protected and always resolve to the active owner (see [Resolution model](#resolution-model)). `orders.item.price_override` and `offline-mode.manage` are also `configurable: false`: no override can grant them, so they stay with the owner. For each permission, the effective value for a request resolves in this order: 1. The protected owner rule, for the two protected permissions above. The other non-configurable permissions accept no override and keep their owner default. 2. An explicit user override (`allow` or `deny`), if one exists for that user and permission. 3. An explicit role override (`allow` or `deny`), if one exists for that role and permission. 4. The shipped default in `PERMISSION_DEFINITIONS`. An override is sparse: choosing **Inherit** in the editor deletes the override row rather than copying the current resolved value, so a permission introduced by a later upgrade receives its reviewed shipped default rather than silently freezing at whatever it resolved to before the upgrade. [`main/services/authorization.ts`](../../main/services/authorization.ts) implements this resolution and is read from current database state on every protected action — nothing is cached in the JWT, and a change takes effect on the requesting user's next request without a sign-out. ## Resolution model `authorization.manage` (who can reach this editor) and `staff.privileged.manage` (who can modify an owner or manager account) are protected: an active owner always has them, and no override, role change, or upgrade can grant either to another role. This guarantees the product always has a recoverable administration path and that account control over owner/manager users cannot be delegated away. The owner-only `/api/authorization` API backs this page: | Method | Endpoint | Purpose | | -------- | -------------------------------------------- | ---------------------------------------------------------------------------- | | `GET` | `/api/authorization/catalog` | The stable permission catalog and the role identities. | | `GET` | `/api/authorization/roles` | Every role's effective permissions, overrides, and revision. | | `PUT` | `/api/authorization/roles/:role` | Atomically replace one role's override set. | | `GET` | `/api/authorization/users` | The staff list this editor's per-user picker uses. | | `GET` | `/api/authorization/users/:userId` | One user's effective permissions, overrides, and revision. | | `PUT` | `/api/authorization/users/:userId` | Atomically replace one user's override set. | | `DELETE` | `/api/authorization/users/:userId/overrides` | Clear every override for a user, restoring inheritance. | | `GET` | `/api/authorization/audit` | Newest-first change history: actor, target, permission, before/after effect. | Every `PUT`/`DELETE` body must carry the last-seen `revision` string; a stale revision — another owner saved first — returns `409` with the current state, rather than silently overwriting it. Override entries are `{ "permission_id": "...", "effect": "allow" | "deny" }`; an unknown, duplicate, or protected permission id rejects the whole request. Writes are one SQLite transaction and append rows to `authorization_audit_log`, which **Staff > Permission change history** renders below the editor. `role_permission_overrides`, `user_permission_overrides`, and `authorization_audit_log` were added in schema migration v93. ## Permission matrix The table below is the **shipped default** — what each role can reach with zero overrides applied, which is also exactly what every existing installation resolves to immediately after upgrading to this feature. A check means the role reaches the capability by default. A dash means it does not. An owner can flip any configurable cell for a role, or for one user, from the in-app editor. | Area | Capability | Owner | Manager | Cashier | Server | Chef | | ------------ | ---------------------------------------------------------- | :---: | :-----: | :-----: | :----: | :--: | | Orders | Use the POS terminal | ✓ | ✓ | ✓ | — | — | | Reports | View the owner dashboard | ✓ | — | — | — | — | | Orders | View and create orders | ✓ | ✓ | ✓ | ✓ | — | | Orders | Update order status | ✓ | ✓ | ✓ | ✓ | ✓ | | Orders | Change order customers and discounts | ✓ | ✓ | — | — | — | | Orders | Cancel pending order items | ✓ | ✓ | — | — | — | | Orders | Void in-progress order items (Staff Approval PIN may be required) | ✓ | ✓ | — | — | — | | Orders | Restore cancelled order items | ✓ | ✓ | — | — | — | | Orders | Edit quantity or replace order items | ✓ | — | — | — | — | | Orders | Override order item prices | ✓ | — | — | — | — | | Orders | Create and manage held orders | ✓ | ✓ | ✓ | ✓ | — | | Payments | View bills, take payments, and print receipts | ✓ | ✓ | ✓ | — | — | | Payments | Apply bill discounts | ✓ | ✓ | — | — | — | | Payments | Mark bills printed | ✓ | ✓ | ✓ | — | — | | Payments | View payment methods | ✓ | ✓ | ✓ | ✓ | ✓ | | Payments | Manage payment methods | ✓ | ✓ | — | — | — | | Payments | View expenses and balances | ✓ | ✓ | — | — | — | | Payments | Create and correct expense records | ✓ | ✓ | — | — | — | | Payments | Record expense payments | ✓ | ✓ | — | — | — | | Payments | Reverse expense payments | ✓ | ✓ | — | — | — | | Payments | Print bills and kitchen tickets | ✓ | ✓ | ✓ | — | — | | Payments | Open a cash shift | ✓ | ✓ | ✓ | — | — | | Payments | Close own cash shift (owner/manager can close any) | ✓ | ✓ | ✓ | — | — | | Customers | View, search, and create customers | ✓ | ✓ | ✓ | ✓ | — | | Customers | Edit customers | ✓ | ✓ | ✓ | — | — | | Customers | Repair customer phone records | ✓ | ✓ | — | — | — | | Customers | Clean up customer records | ✓ | — | — | — | — | | Menu | Manage products, categories, and addons | ✓ | ✓ | — | — | — | | Menu | Import and export menu data | ✓ | ✓ | — | — | — | | Menu | Manage supplies and recipes | ✓ | ✓ | — | — | — | | Orders | Manage tables | ✓ | ✓ | — | — | — | | Orders | Move orders between tables | ✓ | ✓ | ✓ | ✓ | — | | Kitchen | Use the kitchen display system | ✓ | ✓ | — | — | ✓ | | Kitchen | Pair a kitchen display | ✓ | ✓ | — | — | — | | Kitchen | Manage kitchen stations and assignments | ✓ | ✓ | — | — | — | | Reports | View sales and operations reports | ✓ | ✓ | — | — | — | | Reports | View financial figures (revenue, refunds, payment mix) | ✓ | — | — | — | — | | Staff | View and manage staff accounts | ✓ | ✓ | — | — | — | | Staff | Reset staff passwords and PINs without their current credentials | ✓ | — | — | — | — | | Staff | Manage owner and manager accounts and roles | ✓ | — | — | — | — | | Staff | Manage cashier, server, and chef accounts | ✓ | ✓ | — | — | — | | Staff | Manage role and user permissions | ✓ | — | — | — | — | | Settings | View store and operational settings | ✓ | ✓ | ✓ | ✓ | ✓ | | Settings | Change store and operational settings | ✓ | ✓ | — | — | — | | Settings | View and test tax packs | ✓ | ✓ | — | — | — | | Settings | Install, activate, and manage tax packs | ✓ | — | — | — | — | | Settings | Change tax configuration | ✓ | ✓ | — | — | — | | Settings | View print templates | ✓ | ✓ | — | — | — | | Settings | Manage print templates | ✓ | — | — | — | — | | Settings | Manage printers | ✓ | ✓ | — | — | — | | Integrations | Use WhatsApp messaging | ✓ | ✓ | ✓ | — | — | | Integrations | Configure WhatsApp | ✓ | ✓ | — | — | — | | Integrations | Manage cloud settings | ✓ | ✓ | — | — | — | | Integrations | Manage Google Drive backups | ✓ | — | — | — | — | | Integrations | Manage cloud account and data controls | ✓ | — | — | — | — | | System | Use database tools and backups | ✓ | — | — | — | — | | System | Turn Offline mode on or off | ✓ | — | — | — | — | | System | Manage mobile access (FloAdmin sync and device pairing) | ✓ | — | — | — | — | | Orders | Use the standalone Server App | ✓ | ✓ | — | ✓ | — | | Support | Contact support and view diagnostics | ✓ | ✓ | ✓ | ✓ | ✓ | `authorization.manage` and `staff.privileged.manage` (the last two Staff rows a non-owner will never see change), `orders.item.price_override` and `offline-mode.manage` are not editable in the table above; every other row is `configurable: true` in `shared/permissions.ts` and can be overridden per role or per user. ## Scope notes **Editable, not read-only.** The in-app table lets an owner choose Inherit, Allow, or Deny for every configurable permission, per role or for one staff member's exceptions. A role change retains that user's exceptions; the editor shows their source (shipped default, role override, or user override) so they can be reviewed or cleared. **Staff > Permission change history** lists every save. **Cash shifts.** Opening, viewing, and closing shifts are gated by `cash.shifts.open`, `cash.shifts.view`, and `cash.shifts.close`. Non-`owner`/`manager` roles (such as `cashier`) may only view, preview, close, attach explicit drawer movements to, read/export the shift X-report, or print the shift Z-report (`POST /api/cash-closures/:id/print`) for the shift they opened (`403` when targeting another user's shift), whereas an `owner` or `manager` can inspect, export, print, and close any staff member's shift, with the closing actor recorded on the closure row. On `POST /api/cash-sessions/:id/close` and `POST /api/cash-closures/movements`, the own-session rule is enforced inside the write transaction, where the stored session and the actor are read together. This is a context policy, not a permission - see [the authentication and authorization page](../architecture/authentication-and-authorization.md), [business time](../architecture/business-time.md#cash-closures), and per-route error codes in [Cash sessions](api.md#cash-sessions), [Cash closures](api.md#cash-closures), and [Reports](api.md#reports). **Order cancellation and completion overrides.** Staff holding `orders.status.update` can cancel a whole order without a PIN only while the order and all of its items are `pending` and no bill holds collected funds. If the order or any item has advanced to `preparing` or later, or if any bill holds recorded collected payment, an active owner or manager approval PIN (`override_pin`, with `orders.status.update` and `orders.item.cancel`) is required. Completing an order blocked by unsettled payable bills, an unbilled positive total, or undelivered kitchen items (when `require_kitchen_delivered_before_settlement` applies) likewise requires an explicit active owner or manager PIN (with `payments.take` when overriding unsettled payment), even when the logged-in user is already an owner or manager. Full safeguard, restock, and audit rules are in [the Orders API reference](api.md#orders). **Paid bill payment method correction.** Only an active `owner` or `manager` holding `payments.take` can correct the tender method on a recorded payment line of a `paid` bill (`PATCH /api/bills/:id/payments/:index/method`) while its cash shift and store business day remain open (`Yes (with Staff Approval PIN)` for Owner and Manager; `No` for Cashier, Server, and Chef). Every correction requires an active `owner` or `manager` Staff Approval PIN (`override_pin`, `approver_id`); a `cashier`, `server`, or `chef` caller or PIN never authorizes this action (`403`). Full shift, refund, split-sibling, amount-immutability, and audit rules are in [the Bills API reference](api.md#bills). **Owner visibility.** The permission editor itself is rendered only for an authenticated user holding `authorization.manage` — an active owner, always. The API enforces this independently; hiding a control in the interface is not a security boundary. **KDS scope.** Chef access is narrowed further by the user's assigned `category_ids` and by kitchen station assignment, on top of the `kitchen.use`/`kitchen.status.update` permissions. Owner and manager KDS access is unrestricted by category, subject to the KDS being enabled. Station and category narrowing is re-evaluated inside the transaction that applies a status change; see [the authentication and authorization page](../architecture/authentication-and-authorization.md). **Orders are never ownership-gated.** Anyone with the relevant order permission — the "View and create orders" row above — can view and act on every order, including ones other staff created. There is no per-order `user_id` check anywhere in the authorization model. Restriction is by permission and, for kitchen operations, by KDS stage, station, and category. Accountability comes from audit attribution, not from hiding orders between staff. Multi-counter ordering follows this invariant: counter roles (`ordering`, `payment`, `combined`) guide interface defaults, but staff permissions remain authoritative and orders are never ownership-gated across counters. **Expenses.** Expense records are store-wide: `expenses.view` reads them, and `expenses.manage` together with view writes, voids, and replaces them. There is no per-expense ownership check, and every mutation is attributed to the authenticated actor. `expenses.pay` and `expenses.reverse` are separate configurable ids for payment and reversal authority, so neither is implied by the manage grant. Cash payments and cash reversals also need `cash.movements.manage` and a real open shift, because each writes one linked drawer movement; that session requirement is deliberate and does not follow the configurable `require_open_shift` gate. `expenses.view` also gates the Expenses page and its navigation entry; the page offers payment, reversal, void, and replacement only when the matching grant is present, and it never opens a shift or grants a permission on the operator's behalf. The [Expenses API reference](api.md#expenses) lists the expense operations exposed over HTTP. **Server App.** The standalone Server App is gated by `server-app.use`, shipped by default to `server`, `manager`, and `owner`. When the feature is disabled its routes return 404 rather than 403, so the surface is not discoverable. It is separate from the dashboard navigation. **Staff management.** Managers can create and edit operational staff details by default (`staff.operational.manage`), but cannot modify or deactivate owner or manager accounts: that needs the protected `staff.privileged.manage`, which only an owner holds. Resetting an existing staff member's password or PIN without their current credential (`PUT /api/staff/:id` with a non-empty `password` or defined `pin`) additionally requires `staff.credentials.reset`, which ships to `owner` by default and can be granted to managers for operational staff targets (`cashier`, `server`, `chef`). Only an owner can change the role on an existing account. Demoting or deactivating an account is refused when no other active account would still hold all four of `authorization.manage`, `staff.privileged.manage`, `staff.operational.manage`, and `settings.manage`, so the last administrator is protected even while other active owners remain who have been denied one of those capabilities. **Staff PINs and Staff Approval PINs.** Every active user regardless of role (`owner`, `manager`, `cashier`, `server`, `chef`) can change their own password (`POST /api/auth/password/change`) and set or change their own 4-6 digit sign-in PIN (`POST /api/auth/pin/change`, verifying `current_pin` when a PIN is already set or `current_password` on first-time setup). Owners (and managers granted `staff.credentials.reset` for operational staff) can also set, reset, or clear a 4-6 digit PIN in Staff management for quick user switching on shared terminals, while only active `owner` and `manager` PINs qualify as a Staff Approval PIN. See [PIN quick user switch and terminal lock](../architecture/authentication-and-authorization.md#pin-quick-user-switch-and-terminal-lock). **Conditional surfaces.** Business type, feature settings such as KDS or WhatsApp, and account state can hide or disable a surface independently of whether the permission is granted. **Settings controls and navigation.** Staff holding `settings.view` (shipped to all roles) can view the Settings page. Tabs, cards, and individual configuration controls that require administrative or domain-specific permissions (such as `settings.manage`, `tax-configuration.manage`, `printers.manage`, `cloud.manage`, `whatsapp.manage`, `database.manage`, or the `owner` role) are rendered strictly read-only or disabled for users lacking the matching grant. Desktop update controls (such as automatic check toggling and beta channel opt-in) require `settings.manage` both in the interface and at the Electron IPC boundary. Unsaved-change navigation intercepts activate only when saveable dirty changes exist for the current user, preventing read-only users from being trapped on `/settings`. ## Presentation The in-app editor uses roles or one selected staff member as the subject, and permissions as rows, grouped by area, with a semantic HTML table, an Effective column pairing check/dash icons with explicit Allowed/Not allowed text, and an Override column offering Inherit/Allow/Deny per row. It keeps horizontal overflow with a sticky capability column so cross-permission comparison stays fast at narrow desktop widths. The pattern follows [W3C table guidance](https://www.w3.org/WAI/tutorials/tables/), [GOV.UK table guidance](https://design-system.service.gov.uk/components/table/), and [WCAG guidance on non-color state indicators](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html). ## Verification ```sh npm run test:authorization-permissions npm run test:self-service-credentials npm run test:staff-authz npm run test:orders-authz npm run test:settings-ipc-rbac npm run test:kds-integration npm run test:server-app-server-role ``` A static audit test rejects any newly introduced `requireRole(...)` runtime gate in `main/routes/`. Direct role checks remain only as reviewed context policy - see [Configurable permissions](../architecture/authentication-and-authorization.md#configurable-permissions) - none of which decide whether a request may attempt the operation at all.