Skip to main content

User Types and Auth Claims

Last updated: 2026-04-03

This document defines all user types in the Pivot platform, the Firebase Auth custom claims that back them, and how they map to RTDB security rule access patterns.


Custom Claims

Every authenticated user gets custom claims set on their Firebase Auth token during session initialization (session.service.ts). These claims are checked in RTDB security rules via auth.token.<claim>.

ClaimTypeDescription
currentCompanyIdstring or nullThe company the user is currently connected to
isOwnerbooleanUser is a company owner/admin
isManagerbooleanUser is a manager for at least one position
isHeadOfficebooleanUser has head office (multi-company) access
headOfficeIdstring or nullThe head office company ID (for branch access)
adminbooleanPivot platform super-admin (set manually)

Claims are set once per session by SessionService.setCustomClaims() in functions/systems/auth/services/session.service.ts.


User Types

There are 6 distinct user types, from lowest to highest privilege:

1. Unauthenticated

No Firebase Auth session. Can only access paths with .read: true (public paths).

RTDB rule check: auth === null

2. Employee

A standard company employee with no management responsibilities.

  • UserRole value: 'employee'
  • Claims: currentCompanyId
  • How assigned: Has positions on their employee record, but no entries in Managers/{companyId}/{employeeId}
  • Resolved in: RoleService.resolveRole() in functions/systems/auth/services/role.service.ts

RTDB rule check: auth !== null && auth.token.currentCompanyId === $companyId

3. Manager

An employee who manages one or more positions (departments).

  • UserRole value: 'manager'
  • Claims: currentCompanyId, isManager: true
  • How assigned: Has entries in Managers/{companyId}/{employeeId}/{positionId} (set by owners via the Roles page)
  • Resolved in: ManagerRepository.getManagerPositions() in functions/repositories/manager/manager.repository.ts
  • Auto-cleanup: When positions are removed from an employee, their manager entries are cleaned up by the on-remove-employee-position database trigger

RTDB rule check: auth !== null && auth.token.isManager === true && auth.token.currentCompanyId === $companyId

4. Owner

The company owner/admin with full control over company data.

  • UserRole value: 'owner'
  • Claims: currentCompanyId, isOwner: true
  • How assigned: employee.isAdmin === true or employee.role === 'owner' (note: isAdmin is a legacy field name that means company owner, not platform admin)
  • Resolved in: RoleService.resolveRole() — checked before manager resolution

RTDB rule check: auth !== null && auth.token.isOwner === true && auth.token.currentCompanyId === $companyId

5. Head Office

A multi-company administrator who can access data across multiple branch locations.

  • UserRole value: 'head-office'
  • Claims: isHeadOffice: true, headOfficeId
  • How assigned: Two paths:
    • user.isHeadOffice === true (direct flag on User record)
    • user.headOfficeAccess[companyId] === true AND the user is an admin of a head office company
  • Granted via: HeadOfficeService.grantAccess() in functions/systems/auth/services/head-office.service.ts, or by accepting a head office invitation
  • Special behavior: Bypasses currentCompanyId checks on Companies/$companyId — can read any company

RTDB rule check: auth !== null && auth.token.isHeadOffice === true

The headOfficeId claim is also used in Companies rules to allow reading a branch company when auth.token.headOfficeId === data.child('headOfficeId').val().

6. Pivot Admin

A platform-level super-admin (internal Pivot team only). This is separate from UserRole — it is checked independently via the admin custom claim.

  • UserRole value: n/a (checked separately from the role system)
  • Claims: admin: true
  • How assigned: Manually via node functions/scripts/set-custom-claim.js — never set automatically
  • Used for: Reading all Companies, reading all Users, writing VersionWeb, OwnerDashboard access

RTDB rule check: auth !== null && auth.token.admin === true


'user' Role

There is also a 'user' role in the UserRole type (employee.positions is empty). In practice it behaves identically to 'employee' in the RTDB rules since both only require auth !== null with currentCompanyId. It represents a user who has been linked to a company but has no assigned positions yet (e.g. during onboarding).


RTDB Rule Access Patterns

The security rules use 5 access patterns that map to the user types above:

Public

Anyone can read, including unauthenticated users. Writes are either fully locked or admin-only.

PathWrite
VersionWebadmin only
VersionMobileNobody (deploy only)
SupportAnyone (unauthenticated write allowed)

Backend-Only

Both read and write are false. Only accessible via the Firebase Admin SDK (Cloud Functions).

NotificationQueues, NotifiedBefore, NotifiedShifts, NotificationsPushSent, PushHistory, PushNotifications, Migrations, Cache, FilesToDelete, InvitationTokens, givex, migrations

User-Scoped

Access is restricted to the user's own $uid path.

PathReadWrite
Users/$uidOwn uid (admin can read all)Own uid
Tokens/$uidOwn uidOwn uid
BadgeCount/$uidOwn uidOwn uid
PayrollTutorialSeen/$uidOwn uidOwn uid
PresenceStatus/$uidAny authenticated userOwn uid only

Company-Scoped

Requires auth.token.currentCompanyId === $companyId for both read and write.

This is the most common pattern, covering the majority of paths: WeeklySchedule, Attendance, Posts, Templates, Managers, Documents, TipsOut, NotificationsFlat, NotificationsFlatV2, and many more (44 paths total — see the test file for the complete list).

Company-Scoped with Role-Gated Writes

Read access follows the standard company-scoped pattern, but writes require a specific role.

Write RolePaths
Owner onlyVeloceSettings, CloverSettings, CloverApiKeySettings, ClusterSettings, MaitreDSettings, MyrSettings, LightspeedSettings, GivexSettings, LibroSettings, EmployerDSettings, NethrisSettings, PowerpaySettings, NonIntegratedPayrollService, NonIntegratedPosService, PendingMaitreDIntegration, Passwords
Owner or ManagerEmployeeRates, Companies/$companyId/jobs, Companies/$companyId/daysStructure, Companies/$companyId/employeesOrder, Companies/$companyId/includeOnCallShifts
Client read-only (backend writes only)VeloceInvoices, CloverInvoices, CloverEmployees, ClusterInvoices, MaitreDInvoices, MaitreDInvoicesNetSales, MaitreDArchive, MyrInvoices, LightspeedDates, LibroReservations, LibroResevations, LibroWalkIns, LibroExpected, WeatherForecast, HeadOfficeShifts

Known Issues

CompanySettings Root Read Cascade

CompanySettings has a root-level .read: "auth !== null" rule alongside $companyId-level rules. Because RTDB rules cascade downward, the root rule grants read access to all company settings for any authenticated user, effectively overriding the $companyId scoping. The write rules are correctly scoped. This should be fixed by removing the root .read.

Session Claim Staleness

Custom claims are set once per session. If a user's role changes (e.g. promoted to manager) while they have an active session, the old claims remain until the next init-session call. This is a known Firebase Auth limitation.