Skip to main content

Permission

The model that controls who can see what and do what in Helm. Three layers stack: built-in screen list → role defaults → per-staff overrides.

Drafted from planning · v0.1

The screen-visibility layer is built. Action-level permissions (e.g., "can refund" within Sales) are a slice-1 follow-on.

Layer 1: screens

The built-in list of top-level navigation tabs:

screen_idnameWhat it shows
todayTodayThe daily dashboard
salesSalesThe till (formerly Ring-Up)
customersCustomersCustomer search and profile
serviceServiceKanban + ticket detail
inventoryInventorySKU + variant management
tradesTradesTrade-ins + consignments
rentalsRentalsFleet + bookings
ordersOrdersOnline orders + POs
reportsReportsFinancial + operational reports
settingsSettingsShop config + staff + integration creds

Seeded once at deploy time via seed_permissions.sql. Idempotent.

Layer 2: roles + role_permissions

CREATE TABLE roles (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE
);

CREATE TABLE role_permissions (
role_id INTEGER REFERENCES roles(id),
screen_id TEXT REFERENCES screens(id),
can_see INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (role_id, screen_id)
);

Defaults (illustrative; tune per shop):

roletodaysalescustomersserviceinventorytradesrentalsordersreportssettings
Sys Admin
Owner
Service Lead
Mechanic
Sales
Junior

Layer 3: staff_screen_permissions

CREATE TABLE staff_screen_permissions (
staff_id INTEGER REFERENCES staff(id),
screen_id TEXT REFERENCES screens(id),
can_see INTEGER NOT NULL,
PRIMARY KEY (staff_id, screen_id)
);

A row here for a specific (staff, screen) wins over the role default. NULL/missing → role default applies.

See ADR-0014: Per-staff permission overrides for the design rationale.

Resolution

The Worker computes the resolved permission on every request that needs gating:

SELECT COALESCE(
ssp.can_see,
rp.can_see,
0
) AS can_see
FROM staff s
LEFT JOIN role_permissions rp ON rp.role_id = s.role_id AND rp.screen_id = ?
LEFT JOIN staff_screen_permissions ssp ON ssp.staff_id = s.id AND ssp.screen_id = ?
WHERE s.id = ?

This is wrapped in a cheap helper called once per request, then cached for the request lifetime.

API authorization

API endpoints are gated by a map from URL pattern → required screen:

// Pseudo
const requireScreen = {
'/api/customers': 'customers',
'/api/tickets': 'service',
'/api/sales': 'sales',
'/api/inventory': 'inventory',
'/api/staff': 'settings', // staff management is in Settings
// ...
};

Middleware checks the resolved permission before invoking the handler.

Settings → Staff & Permissions specifically requires both screen:settings visibility AND role:owner OR sys_admin. Captured as additional middleware.

Action-level permissions (slice 1 follow-on)

Some actions within an accessible screen are more privileged. For example: anyone with Sales access can ring a sale; refunding requires Manager+. These will live in a screen_actions + role_action_permissions + staff_action_permissions triple.

Not built in v0.1; the screen-visibility layer is the foundation.

Manager override flow

For action-level permissions, a manager-override pattern:

  • The user attempts the privileged action
  • A modal appears: "Requires Manager approval"
  • Manager enters their PIN; their permission is checked
  • The action is permitted for this single change only
  • An audit_events row records the override (action: permission.overridden, override_by_staff_id: manager's id)

In-situ editing surface

On the Staff & Permissions card in Settings (only visible to roles with settings access):

  • Each staff row: checkboxes per screen (column header is screen name)
  • Select/Deselect All buttons per staff row
  • "Reset to role defaults" button per staff
  • Role dropdown per staff (changing it doesn't clear per-staff overrides — overrides win regardless)

Migrated from AIM

AIM has its own role/permission model that does not map cleanly. The migration seeds Helm's default roles + permissions but does NOT carry forward AIM's specific staff permissions — each staff member's effective access is recomputed from their role defaults at first sign-in. The Sys Admin (owner) then customizes per-staff overrides as needed.

See also