Skip to main content

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

  1. 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.
  2. 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.
  3. Read the rules. ../CLAUDE.md is the layer/imports contract for both the web app and functions/. The commit checklist at the bottom is non-optional.
  4. Skim the fragile zones. Fragile areas — read the entries for the area you're about to edit before you start, not after.
  5. Run the smoke checks. tsc -p tsconfig.json --noEmit | grep "src/" → 0 errors. npm test and 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

AreaWhat's coupledWhat to do
PayrollContext.tsx (3,037 lines) at ../src/routes/PayrollNew/PayrollContext.tsxOne 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.tsxZustand 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

AreaWhat's coupledWhat to do
functions/index.js (258 lines, every export) at ../functions/index.jsEvery 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 filesEvery 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.

FileWhy it conflictsMitigation
../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.tsRoot 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.jsEvery 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 aliasesNew aliases conflict; reordering breaks resolution.Add at the end of paths.
../database.rules.json / ../firestore.rulesSecurity-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 shifts
  • ManualShifts/{companyId}/{date}/{employeeId}/{positionId}/{subRoleId}/{shiftKey} — manager-added one-offs
  • ScheduleDrafts/{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 indexfunctions/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/tips
  • WeeklySchedule/{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:

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 mirrorfunctions/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 for this-and-future saves only. Cloud functions and mobile still read this; see the TODO: Remove dual-write comment at PayrollContext.tsx:1544.
  • PayrollPeriodSettings/{companyId}/{effectiveFromDate} — the current source of truth. Settings are versioned: the loader at PayrollContext.tsx:968-1014 does 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 at startOfPeriodStr and 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 legacy AttendanceSettings/ 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-301positionSettings, 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, with PayrollPeriodSettings/ taking precedence). The TODO: Remove dual-write at PayrollContext.tsx:1544 is 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 a this-period save may not reach mobile.
  • If you add a setting that affects tip-out math, add its key to TIP_OUT_INVALIDATING_KEYS or 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)

TreeWhat it actually isWriter / 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

  1. Trigger — a backend handler/listener decides to send a push and builds the payload. Examples: functions/modules/requests/, functions/db/posts/on-mention.ts.
  2. Senderfunctions/services/notification/notification-provider.registry.ts:4 wires 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.
  3. Token storeTokens/{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).
  4. Auxiliary push trees (database.rules.json:464-488) — server-only, not for client reads:
    • BadgeCount/{uid} — active, written by functions/utils.js:getAndUpdateCurrentBadgeCount. Drives the iOS/Android app badge.
    • NotifiedBefore/ — active, written by functions/schedule/cron/upcoming-shift-push.ts:19 to dedupe upcoming-shift reminders.
    • PushHistory/ — active, written by functions/utils.js.
    • NotificationQueues/, PushNotifications/ — active queueing infrastructure.
    • NotifiedShifts/, NotificationsPushSent/orphaned (defined in rules, no writers found).
  5. 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.
  • companieson-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 maintains AttendanceDates/.
  • flatten-notifications.ts — onWrite mirrors Notifications/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.tsonFinalize 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. requests is 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 to src/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() from firebase-functions/params (see ../functions/integrations/givex/secrets.ts for the reference pattern). Don't copy raw process.env from 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}/role to Employees/{id}/roleId will leave mobile reading undefined until the next mobile release.
  • DTOs are not shared. Backend uses ../common/dtos/; mobile duplicates types under PIVOT-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.

SymptomStart here
Payroll totals look wrongPayrollContext.tsx + the relevant utils/ calculation file
Export file rejected by payroll providerThe matching generator in export-formats/ and its snapshot test
Reports / dashboards lag the UIpg-sync errors in Rollbar; functions/db/pg-sync/
POS sales not arrivingProvider cron in functions/integrations/<provider>/ + integration-engine health logs
Employee mapped to wrong shiftspos-sync module cache state
Push notifications missing on mobilePIVOT-Mobile/index.js background handler + Notifee badge state + FCM token registration
Clock-in fails for some staff at one locationGPS / location-permission state on device; verify store coordinates in company settings
Wrong Firebase project being hitWeb: ../src/config.ts + emulator flag. Mobile: ios/firebase/ plist. Backend: firebase use.
Mobile users not getting realtime updatesRTDB shape change on backend? Grep mobile for the path.
Callable returns "unknown function" on mobileFunction renamed/removed without a mobile-compatible release?

See also