BGBidGauge Documentation
Resolved from the previous pass The earlier "everything in companies/default" model is gone. Projects, the item library, and derived summaries are now separate collections, each addressed under companies/{'{'}companyId{'}'}. A one-time migration path still runs for any company that hasn't been converted — flagged below and in Technical Risk Notes.

Collection map

Everything is partitioned by companyId — the segment Firestore/Storage security rules check against the signed-in user's custom claim (see Auth & Permissions).

companies/{'{'}companyId{'}'} e.g. 02r9bp5nv0mdtxmndu4m companyPrefs: object itemLibraryMode, projectStorageMode legacy opportunities[]/itemLibrary[] (migration-only, then cleared) projects/{'{'}projectId{'}'} full project record proposals[] → groups[] → lineItems[] serviceTickets, paymentApplications[] notes[], stage, stageHistory ↳ storageFolders/{'{'}folderId{'}'} (own subcollection, not embedded) detail-view load only, see below projectSummaries/{'{'}projectId{'}'} denormalized mirror of projects/* name, customer, stage, department location, dollar totals, soldDate always loaded for lists/dashboard; written alongside its project doc on every save, in one writeBatch (atomic — resolved) itemLibrary/{'{'}itemId{'}'} part number, cost, price category, manufacturer one doc per catalog item editPresence/{'{'}id{'}'} heartbeat every 30s expiresAtMs (90s TTL) "someone else is editing" databaseBackups/{'{'}type{'}'} metadata only manual/day/week/month raw data → Cloud Storage gzip passwordResetRequests/{'{'}id{'}'} server-only — 60s send cooldown, no codes stored rules: allow read, write: if false (client never touches it) emailLoginVerificationCodes/{'{'}uid{'}'} server-only — SHA-256(salt:code), never the raw code 10-minute TTL, 5 attempts, then locked out Root-level, outside any company: users/{'{'}uid{'}'} (per-user prefs, unchanged) and cache collections written only by Cloud Functions — taxRateCache, gsaPerDiemCache — none of these are captured by the backup system (see Cloud Functions Reference).

Fig. 3 — Collection map. Solid boxes are read/written directly by the client (subject to security rules); dashed gray boxes are Cloud-Functions-only.

Why the shape changed

Real queries, finally

The client now issues genuine collection()/query()/onSnapshot(query(...)) calls instead of downloading one document. List pages query projectSummaries chunked by stage (Firestore's in operator caps at 30 values, so stage filters are split into 30-value chunks and merged client-side).

Load only what a page needs

Detail pages (Project Summary, Payment Applications, Post Mortem, Storage) subscribe to a single projects/{'{'}id{'}'} document; list-style pages load the full projects collection only when actually required (e.g. Service Portal). This is a deliberate cost/bandwidth optimization gated by computed flags (shouldLoadFullProjectRecords, shouldLoadItemLibraryRecords).

A denormalized read model

projectSummaries exists purely so dashboards/lists don't have to load full project documents (with every proposal, line item, and note) just to show a name and a dollar total. It's a deliberate CQRS-style split, not an accident — both documents are written together in a single writeBatch, so they can't drift from a partial write.

storageFolders became a subcollection

Project file storage metadata moved from an embedded array on the project document to its own projects/{'{'}id{'}'}/storageFolders subcollection, loaded only while a user is on that project's Storage page.

What's still transitional

ObservationDetail
Legacy migration shim is still live codeThe companies/{'{'}companyId{'}'} snapshot handler still checks for legacy opportunities[]/itemLibrary[] arrays on the company doc and, if found (and the corresponding subcollection is empty), runs a one-time batched migration (migrateLegacyProjectsToProjectDocs, migrateLegacyItemLibraryToItemDocs) converting each array element into its own document, then clears the legacy field. This is correct behavior for a phased rollout, but it means the "old" data shape is still a code path that has to keep working.
Company doc still does whole-object writescompanies/{'{'}companyId{'}'} itself (now holding just companyPrefs and mode flags, not the bulk of the data) is still written with one whole-document setDoc(merge:true) on every settings change — reasonable now that it's small, but worth knowing if companyPrefs grows (e.g. very large per-user access maps).

Field reference: companyPrefs

FieldPurpose
adminUsers / adminUidsCompany Admin allow-list. Explicit membership is now required — the earlier "empty list means everyone is admin" bootstrap fallback has been removed.
locationAdminUsers / locationAdminUidsLocation Admin allow-list, scoped to Users management only.
userAccessMap of email → per-user capability flags, now including canManageCompanyUsers as an independently grantable flag (previously only full admins/location admins could manage users).
requireEmailLoginVerificationNew — turns on the custom email-code MFA step at login for the whole company.
emailLoginVerificationRememberDaysNew — configurable "remember this browser" window; defaults to 90, selectable up to a maximum of 120 in the Company Settings UI.
locations, itemCategories, bomStatusOptions, sovPhases, costCodes, termsAndConditionsUnchanged from the previous pass.
companyLogo, coverPagePdfStill stored as base64 data URLs inside this document rather than Storage — unchanged, still flagged in Technical Risk Notes.
Previous← Architecture