Permissions and access
How Carbon decides who can see and change what, from the module-action model to employee types, groups, invites, and shop-floor PIN accounts.
Carbon authorizes each user through explicit grants scoped to a company. Each grant combines a module, such as Sales or Inventory, with an action: view, create, update, or delete. For example, sales_view permits reading sales records, and inventory_update permits editing inventory. Grants do not imply other grants; a user with none can sign in but cannot access any module.
An employee type provides a grant template, with per-employee exceptions. See People for employee records, API keys for programmatic access under the same model, and two-factor authentication for sign-in security.
The module-action model
A single permission is written <module>_<action> — sales_view, inventory_update, purchasing_create. The modules are a fixed set defined in the database (the module enum, packages/database/supabase/migrations/20240909194622_jobs.sql:10, with Quality added in 20250325103806_quality-module.sql:1):
Accounting, Documents, Invoicing, Inventory, Parts, People, Production, Purchasing, Quality, Resources, Sales, Settings, and Users. A few internal modules exist in the data (Items, Messaging, Timecards) but are hidden from the permission screens (apps/erp/app/hooks/usePermissionMatrix.ts:64), so what you edit is the list above.
The actions are always the same four (apps/erp/app/hooks/usePermissionMatrix.ts:56):
Every permission screen in Carbon is the same widget: a matrix with a row per module and a checkbox per action (apps/erp/app/components/PermissionMatrix.tsx). Toggle a cell for one grant, a row to flip a whole module, or the header to select everything. The employee-type template, a single employee's overrides, the bulk editor, and even an API key's scopes all render this identical matrix, so the mental model is the same everywhere.
A grant is scoped to a company
Internally each grant stores the list of companies it applies to, so one person can hold different permissions in each company they belong to. In the UI you always edit permissions for the company you're currently in. The special value "0" means "all companies" and is how a true cross-company administrator is represented. See multi-company setup for how a user spans several companies.
Carbon enforces these grants in two places at once. The app checks them to show or hide screens and buttons, and the database enforces the same grants through row-level security, so a request that skips the UI still can't reach data the grant doesn't allow. That second layer is what makes the same model safe for API keys, which carry scopes in exactly this module_action shape.
Employee types
An employee type is a named permission template. Instead of hand-checking dozens of cells for every hire, you define a type once (say "Buyer" with full Purchasing and view-only Inventory) and assign it. Each type is edited with the same matrix under Settings → Users → Employee Types, and its grants become the starting point for anyone you assign to it.
Editing a type changes the template, not existing people
Changing a type's permissions only sets the defaults for people assigned after the change. Employees already on that type keep the permissions they have. To push a change to existing staff, use the bulk permission editor below.
Two types are seeded and can't be deleted, marked by a stable systemType value so an admin can rename their display label without breaking Carbon's internal lookups (packages/database/supabase/migrations/20260401000000_protect-console-operator-type.sql:6):
Every other type is yours to create, rename, and delete. A company can hold at most one type of each system kind, enforced by a unique index (20260401000000_protect-console-operator-type.sql:20).
Per-person overrides and bulk edits
An employee's permission matrix under Settings → Users → Accounts starts with the grants from their employee type. Checking or clearing a cell creates a per-employee exception without changing the type. When you switch types, Carbon asks whether to replace the current grants with the new template or preserve them.
When a change needs to reach many people at once, use the bulk permission editor. Select the users, pick a mode, then set the matrix:
This is the tool for propagating an employee-type change to existing staff, or for granting a new module to a whole department in one action. The two modes are the difference between "give everyone these too" and "make everyone exactly this" — reach for Update only when you mean to overwrite.
A permission change may take a moment to land
Carbon caches each user's effective permissions for speed. When you change someone's grants, Carbon clears their cache so the new permissions apply on their next request. If a change ever seems not to have taken, it's almost always a stale session — signing out and back in forces a fresh read.
Inviting people
You don't create passwords for people. You invite them by email, and they set up their own sign-in when they accept. Add an employee under Settings → Users → Accounts with their name, email, employee type, and location. Carbon creates their account, records an invite, and emails them a link to finish joining.
Because an invite is a real record, an employee has a clear status derived from it and their account (the employees view, packages/database/supabase/migrations/20260529160000_employees-view-status.sql:16):
An invite is a company-scoped record keyed by email, and it carries the permission set the person will start with, so their grants are ready the moment they accept. While it's outstanding you can resend the invite email or revoke it entirely, both from the accounts list. Deactivating an active employee removes them from the company, strips their grants, and marks them inactive; it doesn't delete the person, who may still belong to other companies. For the full picture of a person's record, see people.
Shop-floor PIN accounts
Console operators sign in to MES on a shared station with a 4-digit PIN instead of an email-based login. They are a separate account type.
Add one under Settings → Users → Operators (visible only when console mode is enabled for the company) with a name and a location. Carbon creates a lightweight account with no email login, assigns it the Console Operator employee type automatically, and generates a 4-digit PIN that is shown once, when the operator is created — Carbon stores only a hash of it, so copy it then. On the shop floor the operator taps their name and enters the PIN, which Carbon checks before starting their session. If a PIN is lost, reset it from the same screen to generate a new one; the old PIN stops working immediately. Resetting an operator's PIN takes users_update; setting a console PIN for any other employee (from the Accounts list) also takes settings_update.
An operator can graduate to a full user
When a PIN account needs real ERP access, convert it: give it an email and a full employee type, and Carbon promotes the same person to a normal invited user without losing their history (apps/erp/app/modules/users/users.server.ts:905). Their old floor activity stays attached to the one identity.
PIN accounts are scoped to the floor by design. They carry only the grants their Console Operator type allows, which is why a lost or shared tablet can't be used to reach the office side of Carbon. For the operator experience itself — clocking on, reporting quantities, issuing material — see the MES reference and the run the floor tour.