Architecture Overview
This is the system map. After reading it you should be able to point to any feature, integration, or module and know roughly where its code lives.
Pair this page with the module boundary rules in ../../CLAUDE.md — the rules tell you what's allowed; this doc tells you what exists.
1. System Map
┌─────────────────────────┐
│ Firebase Auth │
└────────────┬────────────┘
│ ID tokens
┌────────────────────────────┼────────────────────────────┐
│ │ │
┌──────┴───────┐ ┌─────────┴────────┐ ┌────────┴────────┐
│ Web app │ │ Cloud Functions │ │ Mobile app │
│ (pivot) │ HTTPS │ backend │ HTTPS │ (PIVOT-Mobile) │
│ React 18 SPA │ ──ky──▶ │ Hono routers │ ◀──ky── │ RN 0.77 bare │
│ │ bearer │ (modular monolith)│ bearer │ │
└──────┬───────┘ └─────┬────────────┘ └────────┬────────┘
│ │ │
│ Firebase JS SDK 8 │ Firebase Admin │ @react-native-firebase v21
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ │
│ RTDB ─── Firestore ─── Cloud Storage ─── FCM ─── Cloud SQL (PG) │
│ │
│ Pub/Sub ─── Cloud Scheduler ─── Secret Manager │
│ │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ External integrations │
│ (POS, payroll, Stripe, │
│ OpenTable, SMTP, Slack, │
│ Rollbar, weather, …) │
└─────────────────────────────┘
Both clients authenticate against Firebase Auth, then call the
backend over HTTPS with the ID token attached as Authorization: Bearer <token>. The backend verifies the token via Firebase Admin
in middleware. Both clients also subscribe directly to RTDB for
real-time updates (chat, schedule changes, presence) — those reads
are gated by ../../database.rules.json.
See external systems for what PIVOT talks to.
2. Web Frontend (pivot/src/)
Bootstrap chain
src/index.tsx ← Firebase init, store, providers
└─ <SessionProvider> ← Zustand session (user, role, company)
└─ <Provider> ← Redux store (legacy)
└─ <App> ← src/App.js (462 lines)
└─ <BrowserRouter>
└─ <RouteWithLayout> + <PrivateRoute>
└─ feature pages (lazy-loaded)
Key files:
../../src/index.tsx— Firebase init (withREACT_APP_USE_EMULATORsupport), root subscriptions to employees / company / notifications / presence../../src/App.js— router + lazy-loaded routes (40+) viareact-loadable../../src/utils/routes.tsx—PrivateRoute/PublicRoute/AuthRouteguards../../src/config.ts— Firebase project config
State management — dual stack
The web app is mid-migration from Redux to Zustand:
| Store | Lives in | Owns |
|---|---|---|
| Redux (legacy) | ../../src/store/, ../../src/actions/ | employees, companies, current company, dashboard, notifications, login user, modals, i18n |
| Zustand session | ../../src/auth/store/session.store.ts | user, employments, role, current company/employee/settings, presence, head-office impersonation (persisted to sessionStorage) |
| Zustand requests | ../../src/modules/requests/store/ | request list + counts |
| Zustand integrations | ../../src/modules/integrations/store/ | integration status |
Rule of thumb: new features use Zustand. Redux stays for back-compat. See decisions.md.
Feature module pattern
Per CLAUDE.md, every feature in
../../src/routes/ follows:
routes/{Feature}/
├── index.tsx ← shell: wires sub-components, no logic
├── {Feature}Context.tsx ← shared state (only if multi-component)
├── hooks/ ← feature-specific logic + Firebase I/O
├── services/ ← feature-specific async functions
├── components/{X}/ ← dumb UI (no Firebase, no useSelector)
├── utils/ ← pure helpers
├── types.ts ← feature types
└── constants.ts ← enums, magic strings
Representative features:
| Feature | Entry | Notes |
|---|---|---|
| PayrollNew | ../../src/routes/PayrollNew/ | Tabs: Hours / Tips / Cuts. 8 export-format generators. Mega PayrollContext.tsx — see fragile-areas.md. |
| SchedulePage | ../../src/routes/SchedulePage/ | Daily / weekly / review views. Some giant components. |
| Employees | ../../src/routes/Employees/ | Table + ProfileModal (~2.5K lines). |
| Billing | ../../src/routes/Billing/ | Stripe Elements; calls backend Stripe handlers. |
| CompanySettings | ../../src/routes/CompanySettings/ | Form-driven Firebase writes. |
| Requests | ../../src/modules/requests/ | Zustand store; types: Availability, TimeOff, Exchange, … |
| Integrations | ../../src/modules/integrations/ | Connection flows for POS / reservations. |
Shared layer
| Folder | Contents |
|---|---|
../../src/components/ui/ | Select, Input, SearchInput, TimeInput, OutlineButton, Loader*, TimePicker, DayPicker, MentionInput, … |
../../src/hooks/ | useLocaleFromUrl, useNotifyAgain, usePopoverCloseOnScroll |
../../src/services/ | company.service.ts, employeeDocuments, posSync.service, realtime/, http.client.ts (ky + ID token bearer auto-injection) |
../../src/styles/ | theme.ts (palette), main.scss, feature SCSS partials |
../../src/utils/ | date/format/validation pure functions |
../../src/i18n.ts | ~10K translation keys (en + fr) |
../../common/ | DTOs shared with backend (@common/dtos, @common/entities) |
Path aliases
Defined in ../../tsconfig.json:
@components/*, @/*, @utils/*, @common/*.
3. Backend (pivot/functions/)
A modular monolith — one Firebase Cloud Functions deployment, with internal module boundaries enforced by directory structure and manual DI. See decisions.md for the why.
Module inventory (15 modules)
All under ../../functions/modules/:
| Module | Purpose |
|---|---|
ai-memory | LLM memory/rules per company/employee. Uses @anthropic-ai/sdk. |
applicants | Job applicant tracking (workflows, statuses). |
attendance | Clock-in/out punches, RTDB-backed. |
chat | Group messaging, real-time sync. |
companies | Company profiles, settings, roles, sandbox, branch sync. |
docs | Document API (minimal). |
documents | Legal docs, signatures, e-sign workflows. |
employees | Employee data, integration IDs, payroll source. |
integrations | Integration metadata / registry. |
payroll | Settings, tip distribution, attendance anomaly detection. |
pos-sync | Maps POS employees ↔ Pivot employees (cache layer). |
posts | Social-feed posts, media, mention notifications. |
requests | Shift/time-off/swap state machine. Large (~1.1K handler lines). |
sales | Sales analytics from POS (invoices, employees). |
schedule | Shifts, drafts, templates, head-office cascades. |
Per-module structure
functions/modules/<name>/
├── types/ ← pure data shapes
├── logic/ ← pure functions (no I/O)
├── contracts/ ← repository / service interfaces
├── handlers/ ← orchestration (logic + contracts)
├── repositories/ ← RtdbXxxRepository, PgXxxRepository, …
├── services/ ← external API / FCM / email implementations
├── endpoints/
│ ├── apis/ ← Hono sub-routers, one main router.ts
│ ├── events/ ← Pub/Sub / RTDB / Firestore triggers
│ └── scheduled/ ← Cloud Scheduler v2 cron
├── container.ts ← wires contracts → concrete implementations
└── index.ts ← module entry point
The strict layer-import rules and naming conventions live in CLAUDE.md.
Endpoint trace (example: list attendance punches)
- Router
../../functions/modules/attendance/endpoints/apis/attendance.router.ts— Hono route + zod validation - Handler
../../functions/modules/attendance/handlers/— orchestration, dependency-injected via closure - Contract
../../functions/modules/attendance/contracts/—AttendanceRepositoryinterface - Repository
../../functions/modules/attendance/repositories/—RtdbAttendanceRepositoryimplementation - Wiring
../../functions/modules/attendance/container.ts—new RtdbAttendanceRepository()+listPunchesHandler({ attendanceRepository })
Middleware applied on every request:
withCors → traceMiddleware → authMiddleware → loggerMiddleware → errorHandler.
Cross-cutting
| Folder | What lives there |
|---|---|
../../functions/shared/middleware/ | auth, trace, logger, error-handler, company-scope, firebase-auth, … |
../../functions/shared/infrastructure/ | logger.ts (structured JSON), pg-client.ts (Kysely + Cloud SQL Connector), hono-adapter.ts, errors.ts (AppError enum), request-context.ts (AsyncLocalStorage for trace/uid/logger) |
../../functions/shared/types/ | pagination, result types, event payloads (cross-module) |
Legacy zones (predate the FSD layout — be careful)
| Folder | What it holds |
|---|---|
../../functions/db/ | RTDB / Firestore listener bag — incl. pg-sync/ (RTDB → Postgres one-way listeners). |
../../functions/http/ | Legacy HTTP endpoints (clock-in-out, get-company-posts, …). |
../../functions/on-call/ | One-off callables (print-pdf, copy-company, labor-calc, sales-calc). |
../../functions/cron/ | 13 legacy crons (9 .ts + 4 .js), mostly v1 functions.pubsub.schedule(). New crons go in modules/<x>/endpoints/scheduled/. |
../../functions/schedule/ | Upcoming-shift push cron, separate from the schedule module. |
../../functions/services/ | Global service registry (notifications, mailer, encryption, realtime channel). |
../../functions/integration-engine/ | Provider abstraction layer for POS / reservations — see external systems. |
../../functions/integrations/ | Per-provider implementations (Lightspeed, Clover, Givex, Cluster, Libro, Myr, Nethris, …). |
../../functions/systems/stripe/ | Stripe webhook + subscription mgmt. |
Build & deploy
- Build:
../../functions/build.js— esbuild per-entry-point bundling. Eachendpoints/apis/router.ts,endpoints/events/*.ts,endpoints/scheduled/*.tsbecomes a separate Cloud Function. Post-build path-alias rewrite. - Entry:
../../functions/index.js— exports all functions (still has legacy direct exports alongside module exports). - Deploy:
npm run deploy-staging-funcfrom repo root; production via Firebase CLI.
Secrets & config
- Secrets enum:
../../functions/get-secret.ts— type-safe accessor; ~56 entries cached at cold start from GCP Secret Manager. - New code: prefer
defineSecret()fromfirebase-functions/params(seeintegrations/givex/secrets.tspattern). - Non-sensitive: per-env
functions/.env.{default,development,production}— see../../CLAUDE.md→ "Environment Variables & Secrets" for the intendedconfig()wrapper pattern (target, not yet adopted everywhere). - Never use raw
process.envin new code.
4. Mobile (PIVOT-Mobile/)
A bare React Native app (not Expo) targeting iOS & Android. Aimed at on-floor staff — clock in/out, view schedule, exchange shifts, chat, and (for managers) approve requests.
Tech stack
- RN 0.77.2, native projects in
ios/andandroid/ - Navigation:
react-router-nativev6 — chosen to mirror the web app's mental model (see decisions.md) - State: Redux (auth, employees, notifications, locale) + Zustand stores under
src/stores/for UI-local state - Firebase:
@react-native-firebase/{auth,database,functions,messaging,storage}v21 - HTTP:
src/services/httpClient.ts— ky-based, mirrors web's bearer-token pattern - i18n: Lingui v5, JSON catalogs split by feature × locale under
src/locales/{en,fr}/ - Notifications: FCM backend +
@notifee/react-nativefor local display, badge management, mute respect - Error tracking: Rollbar
Screens (17, under PIVOT-Mobile/src/routes/)
Attendance (clock-in/out + history, GPS-validated, QR via vision-camera),
SchedulePage (calendar, availability, time-off, shift exchanges),
Messages, GroupsNew (social posts), Group, Manager (manager-only views — requireManager gate),
Dashboard, Profile (CV, documents), Sales, StaffOnFloor (Mapbox),
WeatherRadar, Notifications, Libro, MediaGallery, SPLH, LaborCost,
Auth (sign in/up, password reset, invitations).
Native features
GPS / geolocation (clock-in distance check), vision-camera (QR for punch),
Mapbox, photo library + image-crop-picker, FCM push, biometrics-adjacent
permissions, haptic feedback, deep linking via peopleapp:// URL scheme.
Build & release
- Three Firebase project configs in
PIVOT-Mobile/ios/firebase/(dev / qa / prod) with separate bundle IDs. - Fastlane lanes (
PIVOT-Mobile/fastlane/Fastfile):dev→ TestFlight,beta→ TestFlight,release→ App Store. - GitHub Actions workflows under
PIVOT-Mobile/.github/workflows/:{ios,android}-{dev,qa,production}-build.yml. - Manual steps in
PIVOT-Mobile/BUILD_COMMANDS.md.
5. Three End-to-End Walkthroughs
A. Manager creates a shift on web → staff sees it on mobile
Web: SchedulePage UI
├─ src/routes/SchedulePage/services/* writes to RTDB
│ path: WeeklySchedule/{companyId}/{weekKey}/{shiftId}
│
└─ Backend: functions/db/pg-sync/on-schedule-write.listener.ts
picks up the RTDB write
└─ syncs to Cloud SQL via Kysely
(so reports / analytics can query Postgres later)
Mobile: PIVOT-Mobile/src/routes/SchedulePage
└─ @react-native-firebase/database subscription on the same path
fires; UI updates in real time
B. Staff clocks in on mobile
Mobile: src/routes/Attendance/ClockIn
├─ vision-camera scans QR
├─ @react-native-community/geolocation validates distance
└─ src/utils/api.ts → httpsCallable
Backend: functions/modules/attendance/endpoints/apis/attendance.router.ts
├─ authMiddleware verifies ID token
├─ traceMiddleware tags traceId
└─ createPunchHandler → AttendanceRepository → RTDB write
path: Attendance/{companyId}/{date}/{punchId}
Cron: functions/cron/late-for-shift-notification.ts
└─ may fire push via FCM if no punch by shift start
C. Manager runs a payroll export on web
src/routes/PayrollNew/PayrollContext.tsx
├─ subscribes to AttendanceSettings/{companyId}/... (RTDB)
├─ subscribes to PosSync/{companyId}/... (RTDB)
├─ aggregates hours / tips / cuts in-memory
│ (calculations in src/routes/PayrollNew/utils/)
└─ user picks export format → one of:
src/routes/PayrollNew/export-formats/
├─ generate-powerpay-export.ts
├─ generateAcombaExport.ts
├─ generateByShiftExport.ts
├─ generateByWeekExport.ts
├─ generatePayevolutionExport.ts
├─ generatePayrollExport.ts
├─ generateQuickBooksOnlineExport.ts
└─ generateQuickBooksTimeExport.ts
→ produces CSV / Excel client-side, no backend round-trip
6. Cheat-Sheets
Web src/ top level
src/
├── App.js ← router shell
├── index.tsx ← bootstrap
├── config.ts ← Firebase config
├── i18n.ts ← all translation keys
├── store/ ← Redux (legacy)
├── actions/ ← Redux thunks
├── auth/store/ ← Zustand session store
├── modules/ ← Zustand-first feature modules (requests, integrations)
├── routes/ ← FSD-style features (most code lives here)
├── components/ui/ ← shared UI primitives
├── hooks/ ← shared hooks
├── services/ ← shared services (incl. http.client.ts)
├── styles/ ← theme.ts, SCSS partials
├── utils/ ← pure helpers, route guards
└── types/ ← global types
Backend functions/ top level
functions/
├── index.js ← exports all functions
├── build.js ← esbuild config
├── get-secret.ts ← typed secret enum
├── tsconfig.json
├── modules/ ← FSD modular monolith (15 modules)
├── shared/ ← middleware, infrastructure, types, logic
├── integrations/ ← per-POS / payroll / reservation provider impls
├── integration-engine/ ← provider abstraction (BaseProvider/…)
├── systems/stripe/ ← Stripe webhook + subscription mgmt
├── db/ ← legacy RTDB/Firestore listeners + pg-sync
├── http/ ← legacy HTTP endpoints
├── on-call/ ← legacy callables
├── cron/ ← legacy v1 Pub/Sub crons
├── schedule/ ← legacy upcoming-shift push cron
└── services/ ← global service registry
Mobile PIVOT-Mobile/src/ top level
src/
├── index.js (root) ← Firebase messaging, deep links, fonts
├── createStore.js ← Redux store factory
├── Router.tsx ← React Router Native
├── routes/ ← 17 screens
├── components/ ← shared UI
├── services/ ← HTTP + Firebase wrappers
├── stores/ ← Zustand (UI-local)
├── reducers/ ← Redux
├── actions/ ← Redux thunks
├── auth/ ← AuthService + invitation store
├── hooks/
├── utils/ ← theme, time format, RTDB wrapper, rollbar
├── contexts/
├── locales/{en,fr}/ ← Lingui catalogs
├── config/i18n.ts ← Lingui setup
└── types/
What Next
→ External systems: external-systems.md → The "why" for the choices above: decisions.md → Land mines: fragile-areas.md