Working Safely On This Codebase
A focused day-one (and "before-you-touch-this") guide. Same data as fragile areas but cut by concern instead of by zone — what's coupled, what conflicts, what overlaps, what fires when you're not looking.
If something contradicts this doc, trust the code; then update this doc.
Before you start
- Confirm which surface you're touching. Web (
src/), backend (functions/), and mobile (PIVOT-Mobile/) all read the same Firebase project. A change in one can silently break the others. - Confirm which Firebase project. Three envs — dev (
pivot-dev-59310), staging (pivot-not-production-project), prod (pivot-inc). IDs appear in../src/config.ts,functions/.env.<project-id>, mobile native plists, GitHub Actions, and Fastlane lanes. Mismatches are a common debugging black hole. - Read the rules.
../CLAUDE.mdis the layer/imports contract for both the web app andfunctions/. The commit checklist at the bottom is non-optional. - Skim the fragile zones. Fragile areas — read the entries for the area you're about to edit before you start, not after.
- Run the smoke checks.
tsc -p tsconfig.json --noEmit | grep "src/"→ 0 errors.npm testand Playwright e2e for any UI-touching change.
Tightly coupled areas
Touch these and the blast radius is wide. Where possible, push new code into focused hooks/modules rather than extending the hotspot.
Web
| Area | What's coupled | What to do |
|---|---|---|
PayrollContext.tsx (3,037 lines) at ../src/routes/PayrollNew/PayrollContext.tsx | One Context owns 20+ state slices, ~16 RTDB subscriptions, and the entire hours/tips/cuts calculation pipeline. Heavily memoized (~37 useMemo/useCallback sites — render perf is fine; the risk is size). Bugs propagate to 8 export formats under export-formats/. | Don't add to it. New logic goes in a focused hook under routes/PayrollNew/hooks/. See fragile-areas.md. |
Mega-components: DailyTable (~8K), ReviewSchedule (~3.5K), ProfileModal (~2.5K), Hours ShiftPopover (~2.2K), OwnerDashboard (~2.8K), PositionViewPopover (~2.1K), TipModal (~2.0K), ConflictsList (~2.0K). Note: a second ShiftPopover (~1.3K) lives under SchedulePage/. | Each mixes 5+ unrelated subsections. Edits in one section can crash another. | Split before extending. Hook + sub-component, then add. |
Redux store at ../src/store/ | companies, employees, currentCompanyId, notifications slices read by many features. Direct useSelector scattered (~29 occurrences). | Wrap reads in a hook (useCompany, useEmployees) per CLAUDE.md. New state goes in Zustand. |
IntegrationProvider at ../src/modules/integrations/context/IntegrationProvider.tsx | Zustand store + React Context + Firestore listeners (via useIntegrationStatesListener on the IntegrationSettings collection) for 3 plugin domains (POS, payroll, reservations). Persists to sessionStorage (integrations.store.ts:100). | Don't add a fourth domain without splitting. sessionStorage drops state when the tab closes — don't store anything authoritative there. |
src/i18n.ts (10,226 lines, single object) | Every feature edits it. Every PR conflicts. | Add keys at the end of the relevant section. Coordinate with whoever else has open PRs. |
Backend
| Area | What's coupled | What to do |
|---|---|---|
functions/index.js (258 lines, every export) at ../functions/index.js | Every Cloud Function — modules, legacy on-call, http, cron — exported here. | Adding a function = editing this file. Expect conflicts. |
integration-engine/ at ../functions/integration-engine/ (~300 TS files) | Deep inheritance: BaseProvider → domain (sales/reservation) → concrete provider. Sync planner + observability. | Read core/, your domain, and one existing provider end-to-end before extending. Don't bypass the engine into functions/integrations/. See fragile-areas.md. |
requests module at ../functions/modules/requests/ | Multi-type state machine (availability, time-off, swap, exchange, emergency). Transitions interlinked. Imports services/ (legacy boundary leak). No container.ts — wires deps differently than every other module. | Read the logic/ state-transition functions before touching. Don't fix one type without checking the others. |
Module container.ts files | Every wiring change touches one. Renaming a repository class touches every consumer. | Stable contract names matter more than stable class names. |
Common sources of merge conflicts
Files that everyone edits, with mitigations.
| File | Why it conflicts | Mitigation |
|---|---|---|
../src/i18n.ts (10,226 lines) | Single translation object; every feature adds keys. | Append within the relevant section. Coordinate when you see "i18n: …" in another open PR. |
../src/store/reducers.ts | Root combineReducers — every new slice or rename touches it. Recent commits (338404562, 3eb072a69, db1c4e1e9) all edited it. | Resolve by keeping every entry; never delete a slice you don't recognize. |
../functions/index.js | Every new function/cron/listener exported here. | Expect conflicts on every backend PR; resolve by keeping all exports. |
../functions/shared/types/events.ts (currently empty) | Reserved for cross-module event payloads. Will be a hotspot once the event bus lands. | Watch for it post-event-bus. |
Module container.ts (each module) | Wiring changes when repos/services are renamed. | Don't rename without grepping; prefer additive edits. |
package.json + lockfile (web/functions/mobile) | Dep upgrades collide. | Don't upgrade speculatively; rebase before merging. |
../tsconfig.json path aliases | New aliases conflict; reordering breaks resolution. | Add at the end of paths. |
../database.rules.json / ../firestore.rules | Security-rules conflicts merge cleanly but are easy to merge wrong. | Re-test in emulator after any rules merge. |
Files / modules that tend to overlap
Same data, multiple owners. When you change one, check the others.
Employees
Written and read by HR (../src/routes/Employees/),
Payroll, Schedule, Requests, POS sync (functions/modules/pos-sync/),
mobile profile (PIVOT-Mobile/src/routes/Profile/), and the invitation
flow (onCallCompleteInvitation). RTDB path
Employees/{employeeId}.
Schedule shifts vs. attendance punches — two different RTDB trees
This catches everyone the first time. There is no single Shifts/ tree;
schedule and attendance live separately and are joined at calculation
time.
Schedule (planned) — functions/modules/schedule/repositories/shift-mutation.rtdb-repository.ts:9-18 writes to three paths depending on state:
WeeklySchedule/{companyId}/{date}/{employeeId}/{positionId}/{subRoleId}/{shiftKey}— published shiftsManualShifts/{companyId}/{date}/{employeeId}/{positionId}/{subRoleId}/{shiftKey}— manager-added one-offsScheduleDrafts/{companyId}/{scheduleId}/shifts/{employeeId}/{date}/{shiftKey}— drafts
Attendance (actual punches) — Attendance/{companyId}/{date}/{employeeId}/{shiftKey}. Written by the backend HTTP function during clock-in/out (next bullet). Read by payroll.
Mobile/web clock-in flow — Mobile (PIVOT-Mobile/src/routes/Attendance/ClockIn/index.tsx:301,312) and web both call the HTTPS function functions/http/clock-in-out.ts (/setEmployeeClockInV2, /setEmployeeClockOutV2, GET with query params). The handler runs an RTDB transaction (lines 83, 209-226) writing to Attendance/{companyId}/{todayDate}/{employeeId}/{newKey} and guards against duplicate active shifts. Clients do not write the clock-in path directly.
Manual punches do write directly, though — the manager UI flow goes through the separate attendance module at functions/modules/attendance/repositories/attendance.rtdb-repository.ts:149-153 which writes Attendance/{companyId}/{date}/{employeeId}/{shiftKey} with a manual_<timestamp>_<rand> shiftKey (line 77) versus auto push().key for clock-in. Code that joins schedule + attendance must handle both shiftKey formats.
Legacy index — functions/db/attendance-dates-sync.js triggers off Attendance/{companyId}/{date} writes and maintains AttendanceDates/{companyId}/{date} (a date-level index used by other queries). Touching Attendance/ writes implicitly updates this index.
Payroll reads (functions/modules/payroll/repositories/attendance-source.rtdb-repository.ts:34,71):
Attendance/{companyId}/{date}/{employeeId}/...— actual hours/tipsWeeklySchedule/{companyId}/{date}/...— scheduled comparison
Mobile reads both WeeklySchedule/ and Attendance/ in parallel from PIVOT-Mobile/src/routes/Attendance/index.tsx.
POS sync (functions/modules/pos-sync/) does not write to schedule, attendance, or sales trees. Its only writes are:
EmployeeIntegrationIds/{companyId}/{mappingKey}/{pivotEmployeeId}→posEmployeeId(the mapping) —employee-mapping.rtdb-repository.ts:15,38PosCachedEmployees/{companyId}/{posType}(cache of POS-side employee lists) —sync-pos-employees.handler.ts:28
The mapping is consumed by the POS provider crons under functions/integrations/<provider>/ when they attribute sales/punches to PIVOT employees; pos-sync itself only maintains the lookup.
Postgres mirror — functions/db/pg-sync/on-schedule-write.listener.ts mirrors WeeklySchedule/, ManualShifts/, and OpenShifts/ to Cloud SQL for reports. Heads-up: the listener also has a ScheduleDrafts/{companyId}/{date}/{employeeId} path (line ~336), but the live drafts tree is at ScheduleDrafts/{companyId}/{scheduleId}/shifts/{employeeId}/{date} — these don't match, so the draft listener may not fire. Failures are silent — reports lag the live UI when pg-sync errors. Check Rollbar.
A change in any of these can desync the others. The most common bug: code that joins schedule + attendance assumes consistent shiftKeys and breaks when one side gets renamed.
Attendance settings (tip-out groups, roles, rates) — versioned by effective date, scoped invalidation
Two RTDB trees:
AttendanceSettings/{companyId}— legacy, dual-written forthis-and-futuresaves only. Cloud functions and mobile still read this; see theTODO: Remove dual-writecomment atPayrollContext.tsx:1544.PayrollPeriodSettings/{companyId}/{effectiveFromDate}— the current source of truth. Settings are versioned: the loader atPayrollContext.tsx:968-1014does an "effective from" lookup (orderByKey().endAt(startOfPeriodStr).limitToLast(1)), so each period reads the most recent record on or before its start.
Type defs at src/types/attendance.ts:220-288 (PeriodAttendanceSettings, tipOutGroups: TipOutGroup[]). Read only by payroll — Schedule does not consume tipOutGroups.
Edits do not bleed into prior periods. Save scope is explicit (persistPeriodSettings, PayrollContext.tsx:1466-1577):
scope === 'this-period'uses a sandwich pattern: writes the new settings atstartOfPeriodStrand writes the previously-loaded settings at the next period's start, but only if no entry already exists there (line 1562:if (!nextSnapshot.exists())). Existing future overrides are not overwritten.scope === 'this-and-future'merges only the changed keys into every existing future override (lines 1523-1542) — other settings on future periods are preserved — and dual-writes the changed keys to the legacyAttendanceSettings/path (lines 1546-1548).
First save / seeding — when no PayrollPeriodSettings/ record exists for a period, the loader silently lazy-migrates from the legacy AttendanceSettings/ (PayrollContext.tsx:998-1014) and writes the seed back. The first save after that becomes effectively a no-op for that period.
Tip-out invalidation is scoped to current+future, not history. When any key in TIP_OUT_INVALIDATING_KEYS (line 295-301 — positionSettings, tipOutFrequency, tipOutGroups, bonusTipOutConfig, fixedTipOutConfig) changes, the persist call wipes TipsOut/{companyId}/{date} and TipsOutActivities/{companyId}/{date} for the affected range (lines 1492-1515). The wipe is date-level — nullifying a date nukes every employee/shift under it. For this-period scope that's a single period; for this-and-future it's everything from startOfPeriodStr onward. Manual tip-out overrides in the current period get wiped, so mid-period edits do change current-period totals.
Deleted tip-out groups propagate via the sandwich — the previous-settings snapshot is written verbatim to the next period, so removing a group preserves its deletion forward. Prior periods keep the group via their own effective record.
Footguns to remember:
- Cloud functions already read both stores and merge them (
payroll-settings.rtdb-repository.ts:21-40, withPayrollPeriodSettings/taking precedence). TheTODO: Remove dual-writeatPayrollContext.tsx:1544is partially obsolete — cloud functions don't strictly need the legacy write anymore. Mobile reads are still unconfirmed — assume mobile reads legacy until proven otherwise, so athis-periodsave may not reach mobile. - If you add a setting that affects tip-out math, add its key to
TIP_OUT_INVALIDATING_KEYSor stale overrides will linger. - Exercise the export-format snapshot tests when changing settings logic — a column drift can affect 10 generators.
"Notifications" is two unrelated systems — push delivery vs. the request inbox
The name is overloaded. Several RTDB trees with Notifications in the
name actually hold requests (swap, replace, emergency, day-off,
availabilities, applicant, post mentions). Real device push lives in a
different set of trees. Touch one assuming it's the other and things
break in surprising ways.
A. The request inbox (misnamed historically — these are not push messages)
| Tree | What it actually is | Writer / reader |
|---|---|---|
Notifications/{companyId}/{notificationId} | Approval-request records — still actively written: types include replace, swap, emergency, dayOff, availabilities, applicant, post, comment, direct, onHold (see database.rules.json:430 for the indexed types). Rendered as the manager/employee "notifications" list. | Many writers: legacy functions/db/ + functions/http/, the applicants module, mobile (15+ direct write call-sites), functions/db/posts/on-mention.ts. Read by web and mobile UI. |
NotificationsFlat/{companyId}/{type}/{notificationId} | Flattened/indexed mirror of Notifications/ for fast .indexOn lookups (employeeId, employeeOpponentId, etc.). | Built by the onWrite trigger functions/db/flatten-notifications.ts:4-45 — every write to Notifications/ fans out into NotificationsFlat/. |
NotificationsFlatV2/{companyId}/{employeeId}/{state}/{type} | Orphaned. Defined in database.rules.json:409-422 with an indexOn on createdAt, but no writers and (as of last check) no readers — the previously-cited mobile read at PIVOT-Mobile/src/routes/Manager/ManagerMenu/index.tsx is no longer present. Functionally dead. | None. Don't rely on it; either implement reader+writer or remove the rules entry. |
Requests/{employeeId}/{requestId} | Per-employee request tree (legacy). Mobile writes from shift-swap and emergency flows. Schedule-exchange flows now go through a createRequest() helper (see PIVOT-Mobile/src/routes/SchedulePage/ScheduleExchange/components/FinalPage.tsx and the emergency variant); Manager/ManagerReplacer/components/Finish.tsx (~line 98) still writes the Requests/ path directly. Linked to Notifications/ via notificationListIds. | Mobile writes; web/cloud functions read. |
RequestsToAnswer/{companyId}/company/{requestId} | Inbox of requests the manager still owes a response on — drives the unread badge in the manager UI. | Web-client-only: subscribed at src/index.tsx:279-289; written from web approval flows e.g. HireApplicantModal.tsx:286, Applicants/components/Direct.js:49. No backend cloud-function writer found. |
RequestsV2/{companyId}/{requestId} | The new consolidated request store — server-only, dark-launched (no live web/mobile reads yet). Rules pin client .write: false (database.rules.json:1102-1110). | Repository functions/modules/requests/repositories/request.repository.ts:11 (basePath = 'RequestsV2'). Today the only thing populating it is the one-shot migration 016-migrate-notifications-to-requests-v2.ts. |
The Redux notifications slice at src/store/reducers/notifications.ts holds applicant/group state derived from these trees — it is not a feed of push messages.
Migration state: Migration 016 was a one-shot backfill of legacy Notifications/ into RequestsV2/. It is not an ongoing dual-write. Today, the old approval system (Notifications/ + Requests/ + RequestsToAnswer/) is still the live system; RequestsV2/ exists but nothing reads it yet. When you change request-handling code, check whether new flows should also write to RequestsV2/ — they currently don't.
B. Real push delivery (FCM) — separate code path, separate trees
- Trigger — a backend handler/listener decides to send a push and builds the payload. Examples:
functions/modules/requests/,functions/db/posts/on-mention.ts. - Sender —
functions/services/notification/notification-provider.registry.ts:4wires the registered providers. Only FCM (push) is implemented today; the registry shape supports email/SMS but no provider is wired in. Direct helper:functions/utils/send-push-notification.ts:8,38. - Token store —
Tokens/{userId}/{token}(RTDB). Mobile registers on login + on token refresh, and removes the token on logout (PIVOT-Mobile/src/auth/services/auth.service.ts:92). - Auxiliary push trees (
database.rules.json:464-488) — server-only, not for client reads:BadgeCount/{uid}— active, written byfunctions/utils.js:getAndUpdateCurrentBadgeCount. Drives the iOS/Android app badge.NotifiedBefore/— active, written byfunctions/schedule/cron/upcoming-shift-push.ts:19to dedupe upcoming-shift reminders.PushHistory/— active, written byfunctions/utils.js.NotificationQueues/,PushNotifications/— active queueing infrastructure.NotifiedShifts/,NotificationsPushSent/— orphaned (defined in rules, no writers found).
- Mobile receiver — two distinct handlers in
PIVOT-Mobile/index.js:messaging().setBackgroundMessageHandler()(lines 29-100) — marks chats unread, respects mute, updates the badge via Notifee. AsyncStorage writes here can fail silently on iOS cold-app-start backgrounding.notifee.onBackgroundEvent()(lines 102-136) — maps deep links when the user taps a notification.
A backend payload-shape change tends to surface here: the receiver parses data.* fields by name. A token-tree mutation surfaces as "no push for some users."
The three Firebase projects
Project IDs duplicated across ../src/config.ts,
functions/.env.<project-id>, PIVOT-Mobile/ios/firebase/*.plist,
PIVOT-Mobile/android/app/src/{dev,qa,prod}/google-services.json,
GitHub Actions workflows, and Fastlane lanes. A drift in one of those
lets you run web against staging while functions deploy to dev. Always
verify before deploy/debug.
Hidden dependencies and side effects
Writes to RTDB/Firestore/Postgres trigger more than they look like they do. Before adding or changing a write path, check:
Postgres mirror
../functions/db/pg-sync/ holds 9 files;
the 5 active listeners are on-company-write, on-employee-write,
on-user-write, on-schedule-write, on-schedule-settings-write.
They listen on RTDB writes and mirror to Cloud SQL via Kysely.
Failures are silent; the two stores diverge until pg-sync errors
are spotted in Rollbar. Reports run off Postgres and lag the live UI.
If you add a new RTDB write path that should appear in reports, file a follow-up to add a corresponding pg-sync listener.
Module event listeners
../functions/modules/<x>/endpoints/events/ —
each module can register Firestore listeners or Pub/Sub subscribers:
schedule— 5 listeners:on-branch-location-change,on-manual-shift-change,on-draft-change,on-published-change,on-week-starting-day-change.companies—on-branch-updated.listener.ts.- Other modules don't register events yet.
Adding a write path that another module's listener watches is a hidden
dependency. Grep endpoints/events/ for the path you're touching.
Legacy listeners
../functions/db/ is pre-FSD and still active.
Inventory (not exhaustive but representative — touch only when fixing
a bug):
attendance-dates-sync.js— onCreate/onDelete maintainsAttendanceDates/.flatten-notifications.ts— onWrite mirrorsNotifications/→NotificationsFlat/.process-push-notifications.ts— push pipeline.sync-notification-settings.js.head-office-access.js.on-document-signature.ts,on-support-report.ts.post-reminder-bulk-push.ts.payroll/on-payroll-break-end.ts.posts/on-mention.ts,posts/on-post-files-update.ts,posts/on-post-media-update.ts.schedule/on-generated-roles-change.ts,on-published-roles-change.ts,schedule-dates-sync-legacy.js,duplicate-weekly-schedule-settings.ts.
These cross-import freely and predate the layer rules. Don't refactor opportunistically.
Cron jobs
Two trees — ../functions/cron/ (14 legacy jobs,
each implemented as functions.pubsub.schedule().onRun(), not
Cloud Scheduler) and functions/modules/<x>/endpoints/scheduled/ (new
modular pattern; currently only requests/expire-requests.ts).
Both wired through ../functions/index.js.
Cloud Storage trigger
../functions/storage/on-legal-document-upload.ts —
onFinalize trigger on Cloud Storage uploads. Touching the document-signing
flow can fire side effects you don't see in RTDB.
Mobile background handlers (two of them)
Both in ../../PIVOT-Mobile/index.js:
- Lines 29-100:
messaging().setBackgroundMessageHandler()— marks chats unread (UnreadChats/{companyId}/{uid}/{chatId}), respects mute (EmployeeChats/.../mutedUntil), updates badge via Notifee. AsyncStorage writes can fail silently during cold-app-start backgrounding on iOS. - Lines 102-136:
notifee.onBackgroundEvent()— deep-link routing when the user taps a notification. Separate side-effect surface.
Any backend notification payload change must be tested against both handlers (foregrounded, backgrounded, killed).
Mobile FCM registration & cleanup
On login, mobile calls messaging().getToken() and writes
Tokens/{userId}/{token}. On logout, PIVOT-Mobile/src/auth/services/auth.service.ts:92
removes the entry. Backend code that prunes or rotates this tree
de-registers the device — surfaces as "no push for some users."
Web Zustand persistence
../src/modules/integrations/store/integrations.store.ts (line 100)
persists to sessionStorage via createJSONStorage. State drops
when the tab closes — don't store anything authoritative there.
styled-components ThemeProvider scope
Components rendered outside the top-level ThemeProvider crash with
cryptic "cannot read property of undefined" errors at theme-token
access. Common during portal rendering and tests — wrap with
ThemeProvider in test setup if a snapshot crashes.
Best practices to avoid breaking things
- No cross-module imports on the backend. Communication is async events.
requestsis the only legacy exception — don't copy its pattern. - Promote on the rule of three. Don't pre-share. Code in
routes/{Feature}/until a second feature needs it; only then move tosrc/components/ui//src/hooks//src/services/. - When adding an RTDB write path: check
../functions/db/pg-sync/for a corresponding listener; if reports need it and there's no listener, file a follow-up. - When changing a callable Cloud Function (renaming, payload, return shape):
before merging, grep
PIVOT-Mobile/for the callable name. Mobile ships separately and breaks silently between releases. The full set of callables mobile invokes today:deleteUser,getDashboardLaborCostPerHour,getDashboardSales,onCallCalculateSales,onCallCompleteInvitation,onCallEmployeeBreakToggler,onCallNotifyUnseenMembers,onCallVerifyInvitationToken,reschedulePost.grep -rn "onCallEmployeeBreakToggler\|onCallCompleteInvitation\|onCallVerifyInvitationToken" <path-to>/PIVOT-Mobile/src/ - When changing a notification payload: test against the mobile
background handler in
PIVOT-Mobile/index.js(foreground / background / killed states). - Secrets: new integrations use
defineSecret()fromfirebase-functions/params(see../functions/integrations/givex/secrets.tsfor the reference pattern). Don't copy rawprocess.envfrom migrations or scripts.
Cross-repo (web ↔ mobile) coupling
The web and mobile apps share one Firebase project per environment. That means:
- RTDB shape changes silently break mobile. Mobile has no compile-time
contract with the backend — it reads RTDB paths directly. Renaming
Employees/{id}/roletoEmployees/{id}/roleIdwill leave mobile readingundefineduntil the next mobile release. - DTOs are not shared. Backend uses
../common/dtos/; mobile duplicates types underPIVOT-Mobile/src/types/. When you change a DTO, manually update the mobile type. There is no codegen. - Callable function names are a public API. Mobile invokes them by
string. Renaming = a release-cycle break.
# Before renaming a callable:
grep -rn "<callableName>" <path-to>/PIVOT-Mobile/src/ - FCM payload shape is a public API. Same logic — mobile parses
data.*fields by name. - Auth flows. Mobile wires Google, Facebook, and email/password
sign-in. Apple sign-in is not actually wired despite native iOS support
for it — don't assume it works. Changes to invitation/onboarding callables
(
onCallVerifyInvitationToken,onCallCompleteInvitation) need mobile testing. - Force-update killswitch exists. Mobile checks
dbDataPlatform.forceUpdate- a version field from RTDB and can hard-force an update. Useful for emergency rollouts when a backend change breaks an old client. There is no per-callable versioning though — a signature change still breaks every shipped mobile build until you force-update.
- No codegen / schema validation between backend and mobile. No OpenAPI, no JSON Schema, no runtime contract check. RTDB shape, callable signatures, and FCM payload shape are all "trust the docs" contracts.
Mobile build/release runbook: PIVOT-Mobile/BUILD_COMMANDS.md.
Mobile setup: PIVOT-Mobile/Readme.md.
When something is broken — first place to look
A short triage table. The full version with deeper guidance is in fragile-areas.md.
| Symptom | Start here |
|---|---|
| Payroll totals look wrong | PayrollContext.tsx + the relevant utils/ calculation file |
| Export file rejected by payroll provider | The matching generator in export-formats/ and its snapshot test |
| Reports / dashboards lag the UI | pg-sync errors in Rollbar; functions/db/pg-sync/ |
| POS sales not arriving | Provider cron in functions/integrations/<provider>/ + integration-engine health logs |
| Employee mapped to wrong shifts | pos-sync module cache state |
| Push notifications missing on mobile | PIVOT-Mobile/index.js background handler + Notifee badge state + FCM token registration |
| Clock-in fails for some staff at one location | GPS / location-permission state on device; verify store coordinates in company settings |
| Wrong Firebase project being hit | Web: ../src/config.ts + emulator flag. Mobile: ios/firebase/ plist. Backend: firebase use. |
| Mobile users not getting realtime updates | RTDB shape change on backend? Grep mobile for the path. |
| Callable returns "unknown function" on mobile | Function renamed/removed without a mobile-compatible release? |
See also
- Architecture overview — system map, bootstrap chains, module inventory
- External systems — every external system PIVOT talks to
- Decisions — why the architecture looks the way it does
- Fragile areas — per-zone "what bites" detail
- Payroll overview — payroll deep dive
CLAUDE.md— coding-rules contract