BGBidGauge Documentation
Status 12 of the 13 items raised across this document's history are now resolved, verified directly against the source (not just the changelog). The one open item (self-service company provisioning) is an intentional scope decision, not an outstanding bug. Everything below reflects the app's actual current state — the app itself was not modified while producing this page.

Resolved

Open Firestore & Storage rules resolved

Both rule sets require the caller's companyId custom claim to match the company being accessed. See Auth & Permissions.

Single-document data model resolved

Company data lives in dedicated collections (projects, projectSummaries, itemLibrary, etc.) instead of one document. See Data Model.

Direct client writes for user management resolved

User creation/update/deletion goes exclusively through authorization-checked Cloud Functions.

No staging environment resolved

A dedicated staging Firebase project (bidgauge-staging) exists with its own build mode and deploy targets.

Security-rule collection names had drifted from the code resolved

Verified: firestore.rules now names the real collections correctly — companies/{'{'}companyId{'}'}/databaseBackups/{'{'}backupId{'}'} and .../emailLoginVerificationCodes/{'{'}codeId{'}'} — matching what functions/index.js actually writes to.

Admin bootstrap fallback (empty admin list = everyone admin) resolved

Verified: The "empty adminUsers list means anyone is admin" fallback is gone in all three places it previously existed — client (isCompanyAdminUser), security rules (isCompanyAdmin), and Cloud Functions (assertCompanyUserManager/assertCompanyAdmin). Admin status now always requires explicit membership in adminUsers/adminUids.

Cloud Functions didn't verify the caller's real company claim resolved

Verified: A new assertCompanyMember() helper cross-checks request.auth.token.companyId against the company the request targets on every company-scoped callable, throwing permission-denied on a mismatch — closing a gap where the functions previously trusted a client-supplied company id outright.

lookupTaxRate/lookupGsaPerDiem required only sign-in, not company membership resolved

Verified: Both now call assertCompanyMember(request, ...) instead of just checking request.auth — a signed-in user from an unrelated company (or one with no company claim) can no longer invoke either.

Vestigial SMTP secret and an unrelated script in the repo resolved

Verified: scripts/set-bidgauge-smtp-secret.ps1 and scripts/disable-security-defaults.mjs have both been removed from the repo.

Project and its summary weren't written atomically resolved

Verified: The project-sync effect now builds a single writeBatch(db)batch.set() for both the projects/{'{'}id{'}'} and projectSummaries/{'{'}id{'}'} documents — committed once via batch.commit(), replacing the old two-setDoc-under-Promise.all pattern. A project and its dashboard summary can no longer disagree from a partial write.

Minor edge case worth knowing: this batch isn't chunked, and Firestore batches cap at 500 operations. Since each changed project contributes 2 operations, saving more than ~250 projects in a single state update (e.g. a very large bulk import that touches hundreds of projects at once) would exceed the limit and fail. Ordinary usage (editing one project at a time) is unaffected — flagging only in case a future bulk-import feature writes many projects in one tick.

Restore's deleteMissing option was a dead parameter resolved

Verified: The restore call site is now just writeCollectionSnapshot(db.collection("users"), payload.users || []) — the unused third argument has been removed rather than left in place implying behavior that didn't exist.

Client-side tenant selection, not claim-derived resolved

Verified: Once a user is signed in, companyDocId is now set from getCompanyIdFromTokenResult(tokenResult) — the real companyId claim on the Firebase ID token — on every sign-in and token refresh. The old env-var/?companyId=/localStorage resolution (getFallbackCompanyDocId) is now explicitly scoped to pre-login use only (its own doc comment: "keeps setup/reset links and pre-login screens stable before Firebase returns a signed-in user's claims"). If a signed-in user has no valid company claim, the app now shows a dedicated message — "Your BidGauge login is missing company access. Ask an admin to update your user account..." — instead of a confusing permission error or an empty app.

Still open

No self-service company provisioning note

Verified still true: Still the same 17 exported functions in functions/index.js — no callable exists for creating a new company. Provisioning tooling has grown substantially (functions/scripts/ now holds 26 scripts, including new seed/wipe/migrate/inspect utilities), but onboarding an actual second tenant is still: mint an id → migrate/seed data → bulk-assign the companyId claim — three manual scripts run in order, not a product feature.

Intentionally deferred, not a blocker: this is a deliberate scope decision, not an oversight — new-company onboarding is admin-driven for now, and only matters once a second company needs to sign up without the team setting them up directly.

Longer-term / structural — out of scope for this pass

These weren't targeted by the security/multi-tenancy/backup work and remain exactly as before:

ObservationStatus
The application is one file — src/App.jsx, still ~23,830 linesUnchanged; src/lib/paymentApplications.js remains the one extracted, tested module
No TypeScript anywhere in the project sourceUnchanged (confirmed: no .ts/.tsx files outside node_modules)
Hand-rolled XLSX/ZIP/CRC32/CSV parsing, hand-rolled PDF layoutUnchanged — confirmed no xlsx/exceljs dependency added, buildBomWorkbookXlsx still hand-rolled in App.jsx
Base64 logo/cover-page assets stored inline in companyPrefsUnchanged — confirmed companyLogoDataUrl/coverPagePdfDataUrl still store data URLs directly rather than a Storage reference
Automated test coverageStill one module (lib/paymentApplications.js) — a good precedent, still far short of covering the pricing/SOV/PDF logic
Default npm run deploy only deploys HostingUnchanged, but deploy:production/deploy:staging deploy the full project in one command
Previous← Deployment