Roles & permissions
DisplaySync has three role hierarchies that compose:
- System roles — assigned globally to every user (
adminoruser) - Organization roles — assigned per-org (
owner,admin,member) - Event roles — assigned per-event (
manager,technician)
A user's effective permissions on a given resource are the union of their system role's permissions, their org role's permissions on that org, and their event role's permissions on that event. Implicit access rules fill in gaps so org members never need to be explicitly added to every event.
System roles
Stored in Users.role. Two values:
| Role | Use case |
|---|---|
| admin | Platform administrators (DisplaySync staff, support, devops) |
| user | Everyone else — permissions come entirely from org and event roles |
Admin bypasses all org and event checks. That's why platform admins can answer "yes I see your sign" in support without being on the customer's team.
Organization roles
Stored in OrganizationUsers.role. Three values, hierarchical:
owner > admin > member
| Role | Description | Key capabilities |
|---|---|---|
| owner | Org creator | Transfer ownership, delete org, all admin capabilities |
| admin | Manage the org | Add/remove members, manage settings, create events, manage billing |
| member | Use the org | View resources, be assigned to event teams, implicit viewer access to events |
Rules:
- An org always has exactly one owner. Transferring ownership swaps which user holds it.
- The owner can't be removed without transferring first.
- Only org owners and admins can create events (see R.3 enhancement).
Event roles
Stored in EventUsers.role. Two values are explicitly assignable; a third is implicit for org members.
manager > technician > viewer (implicit)
| Role | Description | Assignment |
|---|---|---|
| manager | Owns the event | Explicit; granted by another manager or org owner/admin |
| technician | Hands-on operator | Explicit; granted by a manager |
| viewer | Read-only access | Implicit — every org member has viewer access to every event in their org |
Implicit viewer access matters because org members never need to be individually added to events to see them. Event teams (manager + technician) are for write access only.
Org → event implicit roles
| Org role | Implicit event role on every event in the org |
|---|---|
| owner | manager |
| admin | manager |
| member | viewer |
So an org admin who hasn't been added to an event's team still sees and can edit that event as if they were a manager.
EventUser cascade gotcha
EventUser's userId foreign key references the users table directly
(with onDelete: CASCADE on User delete). It does not reference
OrganizationUser. Removing a user from an org via
OrganizationUser.destroy does NOT cascade-delete their EventUser
rows — the user remains a User, only their OrganizationUser row is
gone, so any EventUser rows still point at a now-non-org-member User.
The event-controller team-listing logic filters these orphaned rows
on read (returns explicit + virtual implicit, drops EventUser rows
whose user isn't an org member). Explicit cleanup happens in the
org-membership removal flow.
Permission matrix
The authoritative grid for what each role can do.
System actions
| Action | Admin | User |
|---|---|---|
| Create / delete organizations | ✅ | ❌ |
| Manage all users globally | ✅ | ❌ |
| Bypass org/event access checks | ✅ | ❌ |
| Set special tiers (Pilot, Managed, billing exempt) | ✅ | ❌ |
Organization actions
| Action | Owner | Admin | Member |
|---|---|---|---|
| View org details | ✅ | ✅ | ✅ |
| Update org settings | ✅ | ✅ | ❌ |
| Manage members (invite, remove, change role) | ✅ | ✅ | ❌ |
| Promote member to admin | ✅ | ✅ | ❌ |
| Transfer ownership | ✅ | ❌ | ❌ |
| Delete organization | ✅ | ❌ | ❌ |
| Manage billing (Stripe portal, tier change) | ✅ | ❌ | ❌ |
| Create events | ✅ | ✅ | ❌ |
Event actions (explicit + implicit)
| Action | Org owner / admin (implicit manager) | Manager | Technician | Member with no event role (implicit viewer) |
|---|---|---|---|---|
| View event | ✅ | ✅ | ✅ | ✅ |
| View signs in event | ✅ | ✅ | ✅ | ✅ |
| View content in event | ✅ | ✅ | ✅ | ✅ |
| View analytics | ✅ | ✅ | ✅ | ✅ |
| Update event details | ✅ | ✅ | ❌ | ❌ |
| Manage event team | ✅ | ✅ | ❌ | ❌ |
| Archive / unarchive event | ✅ | ✅ | ❌ | ❌ |
Sign actions in an event
The authoritative matrix per the route-authorization spec (Track X, 2026-04-24):
| Action | Viewer (implicit for org members) | Technician | Manager (incl. org owner/admin) |
|---|---|---|---|
| View sign list | ✅ | ✅ | ✅ |
| View sign detail + live status | ✅ | ✅ | ✅ |
| View sign analytics | ✅ | ✅ | ✅ |
| Pre-register sign for event | ❌ | ❌ | ✅ |
| Claim sign to event | ❌ | ✅ | ✅ |
| Link sign to pre-created record | ❌ | ✅ | ✅ |
| Unlink sign from event | ❌ | ✅ | ✅ |
| Update sign fields (name, location) | ❌ | ✅ | ✅ |
| Set sign content (assign URL) | ❌ | ✅ | ✅ |
| Send remote command (refresh, reboot, fetch logs) | ❌ | ✅ | ✅ |
| Delete sign entirely | ❌ | ❌ | ✅ |
Content actions
| Action | Owner / Admin | Member | Notes |
|---|---|---|---|
| View content library | ✅ | ✅ | |
| Add content to library | ✅ | ❌ | Defensive placeholder; full content-role design pending |
| Update content | ✅ | ❌ | Same |
| Archive / delete content | ✅ | ❌ | Same |
| Use content (assign to a sign) | Per event role | Per event role | Sign assignment uses event-role gates above |
The content placeholder will relax to include an explicit "content editor" role in a future RBAC update.
Audit & analytics
| Action | Admin | Org owner/admin | Org member | Event manager | Event technician |
|---|---|---|---|---|---|
| Org-wide audit log | ✅ | ✅ | ❌ | ❌ | ❌ |
| Per-event audit log | ✅ | ✅ | View only | ✅ | View only |
| Per-sign audit log | ✅ | ✅ | View only | ✅ | View only |
| Cross-org analytics | ✅ | ❌ | ❌ | ❌ | ❌ |
Feature gates by tier
Tier-level access uses two independent enforcement layers:
- Usage limits —
enforceTierLimit(resource)middleware caps total signs / events / members / storage. See Pricing tiers → Limits and how they're enforced. - Feature flags —
requireFeature(featureName)middleware checks a per-tier flag. Returns HTTP 403 withfeature: <name>andcurrentTierin the body when the flag is off for the caller's tier.
The seven declared feature flags:
| Flag | Free | Starter | Pro | Agency | Enterprise | Intended scope |
|---|---|---|---|---|---|---|
apiAccess | ❌ | ❌ | ✅ | ✅ | ✅ | Public REST API (when v2 ships) |
auditLogs | ❌ | ❌ | ✅ | ✅ | ✅ | /api/audit/* routes + Audit feed UI |
whiteLabel | ❌ | ❌ | ❌ | ✅ | ✅ | Custom branding on dashboard / portals |
clientViewerPortal | ❌ | ❌ | ❌ | ✅ | ✅ | Per-customer view-only portal |
crossOrgDashboard | ❌ | ❌ | ❌ | ✅ | ✅ | Agency cross-managed-org overview |
sso | ❌ | ❌ | ❌ | ❌ | ✅ | SAML / OIDC enterprise sign-on |
webhooks | ❌ | ❌ | ❌ | ❌ | ✅ | Outbound webhook subscriptions |
Middleware deployed; route wiring forward-looking
The requireFeature() middleware is in place and tested, but isn't
yet attached to specific route handlers — the flags exist on the
tier config in advance of the features they gate shipping. When each
feature lands (e.g., the public API in v2), the routes will gate on
the matching flag for that tier. Until then, gating happens at the
UI level via build-time feature flags in the dashboard, which is
independent of per-org tier flags.
requireAnyOrgMembership
Different from event-role gating. requireAnyOrgMembership checks "the caller is a member of SOME org" — used for endpoints that are org-context-dependent but not org-specific. Currently wired on playlist routes; refuses outsiders with 403 instead of returning an empty list. System admins bypass this check.
Notification subscriptions
Anyone with view access to an event can subscribe to its notifications. The default subscription is enabled when:
- An org owner/admin (because they have implicit manager access)
- A user explicitly added to an event's team in any role
See Notifications → Event subscriptions.
Cross-organization team membership
A user can belong to multiple orgs. Each membership has an independent role. The dashboard shows the org switcher in the top-left; switching orgs changes which org's data you're operating on but doesn't change your roles in other orgs.
This is enabled on Agency tier and above as a feature gate — see Pricing tiers → Feature gates.
Common patterns
"Conference manager + AV crew"
- Conference organizer: org admin (or owner if it's their company's account)
- Event lead: event manager
- AV technicians: event technician
- Stakeholders / view-only people: org member, no event team membership — they see everything as implicit viewers
"Multi-event AV company"
- Account owner: org owner
- Account admins: org admin
- Per-event leads: event manager on the events they own
- Floating techs who work multiple events: org member + event technician on the events they're assigned to
"Solo operator"
- Single user as org owner. Implicit manager on all events. No team to set up.
How permissions are enforced
The backend uses a stack of middleware on every protected route:
| Middleware | Checks |
|---|---|
requireAuth | Valid session |
requireOrgRole(...roles) | Caller has at least one of the listed org roles in the target org |
| `requireResourceAccess('sign' | 'schedule' |
requireMinEventRole(role) | Caller's effective event role ≥ given role (with implicit access) |
requireFeature(feature) | Caller's org tier supports the feature |
Failures return 403 Forbidden with a brief message. The dashboard's UI also hides actions you can't perform — so you shouldn't normally hit a 403 from a button click.
Change history
| Date | Change |
|---|---|
| 2026-04-24 | Track X — route authorization hardened. New requireMinEventRole and requireAnyOrgMembership middleware. |
| 2026-01-23 | R.3 — Event creation restricted to org owners/admins. Org members get implicit viewer access. Explicit viewer role removed from event teams. |
| Earlier | R.2 — hierarchical event-role checking, implicit viewer access for org members. |
| Earlier | R.1 — system roles simplified from 4 (admin, event_manager, technician, viewer) to 2 (admin, user). |
See also
- Pricing tiers — billing actions are owner-only
- Operations → Notifications — subscription model uses event roles
- Claiming signs — the "manager pre-registers, technician claims" workflow uses these roles