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>.
| Claim | Type | Description |
|---|---|---|
currentCompanyId | string or null | The company the user is currently connected to |
isOwner | boolean | User is a company owner/admin |
isManager | boolean | User is a manager for at least one position |
isHeadOffice | boolean | User has head office (multi-company) access |
headOfficeId | string or null | The head office company ID (for branch access) |
admin | boolean | Pivot 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.
UserRolevalue:'employee'- Claims:
currentCompanyId - How assigned: Has positions on their employee record, but no entries in
Managers/{companyId}/{employeeId} - Resolved in:
RoleService.resolveRole()infunctions/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).
UserRolevalue:'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()infunctions/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-positiondatabase 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.
UserRolevalue:'owner'- Claims:
currentCompanyId,isOwner: true - How assigned:
employee.isAdmin === trueoremployee.role === 'owner'(note:isAdminis 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.
UserRolevalue:'head-office'- Claims:
isHeadOffice: true,headOfficeId - How assigned: Two paths:
user.isHeadOffice === true(direct flag on User record)user.headOfficeAccess[companyId] === trueAND the user is an admin of a head office company
- Granted via:
HeadOfficeService.grantAccess()infunctions/systems/auth/services/head-office.service.ts, or by accepting a head office invitation - Special behavior: Bypasses
currentCompanyIdchecks onCompanies/$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.
UserRolevalue: 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.
| Path | Write |
|---|---|
VersionWeb | admin only |
VersionMobile | Nobody (deploy only) |
Support | Anyone (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.
| Path | Read | Write |
|---|---|---|
Users/$uid | Own uid (admin can read all) | Own uid |
Tokens/$uid | Own uid | Own uid |
BadgeCount/$uid | Own uid | Own uid |
PayrollTutorialSeen/$uid | Own uid | Own uid |
PresenceStatus/$uid | Any authenticated user | Own 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 Role | Paths |
|---|---|
| Owner only | VeloceSettings, CloverSettings, CloverApiKeySettings, ClusterSettings, MaitreDSettings, MyrSettings, LightspeedSettings, GivexSettings, LibroSettings, EmployerDSettings, NethrisSettings, PowerpaySettings, NonIntegratedPayrollService, NonIntegratedPosService, PendingMaitreDIntegration, Passwords |
| Owner or Manager | EmployeeRates, 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.
Related Documents
- Problem Statement — security vulnerabilities in the original rules
- Proposed Solution — implementation plan for the tightened rules
- Companies Access Analysis — detailed read/write analysis for the Companies node