DisplaySync

Roles & permissions

DisplaySync has three role hierarchies that compose:

  1. System roles — assigned globally to every user (admin or user)
  2. Organization roles — assigned per-org (owner, admin, member)
  3. 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:

RoleUse case
adminPlatform administrators (DisplaySync staff, support, devops)
userEveryone 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
RoleDescriptionKey capabilities
ownerOrg creatorTransfer ownership, delete org, all admin capabilities
adminManage the orgAdd/remove members, manage settings, create events, manage billing
memberUse the orgView 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)
RoleDescriptionAssignment
managerOwns the eventExplicit; granted by another manager or org owner/admin
technicianHands-on operatorExplicit; granted by a manager
viewerRead-only accessImplicit — 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 roleImplicit event role on every event in the org
ownermanager
adminmanager
memberviewer

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

ActionAdminUser
Create / delete organizations
Manage all users globally
Bypass org/event access checks
Set special tiers (Pilot, Managed, billing exempt)

Organization actions

ActionOwnerAdminMember
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)

ActionOrg owner / admin (implicit manager)ManagerTechnicianMember 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):

ActionViewer (implicit for org members)TechnicianManager (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

ActionOwner / AdminMemberNotes
View content library
Add content to libraryDefensive placeholder; full content-role design pending
Update contentSame
Archive / delete contentSame
Use content (assign to a sign)Per event rolePer event roleSign 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

ActionAdminOrg owner/adminOrg memberEvent managerEvent technician
Org-wide audit log
Per-event audit logView onlyView only
Per-sign audit logView onlyView only
Cross-org analytics

Feature gates by tier

Tier-level access uses two independent enforcement layers:

  1. Usage limitsenforceTierLimit(resource) middleware caps total signs / events / members / storage. See Pricing tiers → Limits and how they're enforced.
  2. Feature flagsrequireFeature(featureName) middleware checks a per-tier flag. Returns HTTP 403 with feature: <name> and currentTier in the body when the flag is off for the caller's tier.

The seven declared feature flags:

FlagFreeStarterProAgencyEnterpriseIntended scope
apiAccessPublic REST API (when v2 ships)
auditLogs/api/audit/* routes + Audit feed UI
whiteLabelCustom branding on dashboard / portals
clientViewerPortalPer-customer view-only portal
crossOrgDashboardAgency cross-managed-org overview
ssoSAML / OIDC enterprise sign-on
webhooksOutbound 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:

MiddlewareChecks
requireAuthValid 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

DateChange
2026-04-24Track X — route authorization hardened. New requireMinEventRole and requireAnyOrgMembership middleware.
2026-01-23R.3 — Event creation restricted to org owners/admins. Org members get implicit viewer access. Explicit viewer role removed from event teams.
EarlierR.2 — hierarchical event-role checking, implicit viewer access for org members.
EarlierR.1 — system roles simplified from 4 (admin, event_manager, technician, viewer) to 2 (admin, user).

See also