BGBidGauge Documentation
GroupFunctionsAuth requirement
User managementcreateCompanyUser, sendCompanyUserSetupEmail, sendCompanyUserPasswordResetEmail, updateCompanyUserProfile, deleteCompanyUser, listCompanyUsersAdmin / Location Admin / canManageCompanyUsers
Self-service password resetsendPasswordResetEmailNone (public, anti-enumeration by design)
Email MFAsendEmailLoginVerificationCode, verifyEmailLoginVerificationCodeAny signed-in user
Backup & recoverycreateManualDatabaseSnapshot, createDailyDatabaseSnapshot, createWeeklyDatabaseSnapshot, createMonthlyDatabaseSnapshot, listDatabaseBackups, restoreDatabaseBackupCompany Admin only (scheduled ones run as the system, no caller)
External datalookupTaxRate, lookupGsaPerDiemAny signed-in user

User management

createCompanyUserCreates or reuses a Firebase Auth user by email, with no password set — accounts are provisioned passwordless, then completed via the setup-email flow below.
sendCompanyUserSetupEmailSends a branded "create your password" email for a newly created user, via the Gmail-based mailer (see below).
sendCompanyUserPasswordResetEmailAdmin-triggered password reset email for an existing user (distinct from the self-service flow — no cooldown, since it's already authorization-gated).
updateCompanyUserProfileUpdates a user's Auth display name.
deleteCompanyUserRequires the caller to pass confirmText === "delete". Unassigns the user from every projects/projectSummaries/itemLibrary record, and only deletes the underlying Firebase Auth account if that person isn't a member of any other company (checked via userBelongsToAnyOtherCompany) — so deleting someone from one company doesn't destroy their login if they also belong to a second company.
listCompanyUsersPages through Firebase Auth users, tags each with admin/locationAdmin/subscribed. A non-full-admin caller (location admin or canManageCompanyUsers) only sees users whose locationIds overlap their own.

Self-service password reset

sendPasswordResetEmailCallable from the login screen with just an email address. Always returns {'{'}ok:true{'}'} whether or not the email exists (anti-enumeration). Enforces a 60-second cooldown per email via a doc in passwordResetRequests keyed by a SHA-256 hash of the lowercased email — the collection stores only a timestamp, never a code.

Email MFA

sendEmailLoginVerificationCodeNo-ops unless companyPrefs.requireEmailLoginVerification is on. 60s cooldown, generates a 6-digit code, stores only SHA-256(salt:code) (never the raw code) with a 10-minute expiry, sends it via the Gmail mailer.
verifyEmailLoginVerificationCodeValidates the code against the stored hash; 5 wrong attempts deletes the doc and locks the caller out until a new code is requested. On success, clamps the caller's requested remember-window to a maximum of 120 days and writes the expiry into the user's Auth custom claims (bidgaugeEmailMfaByCompany[companyId]) via setCustomUserClaims — this is the entire "remembered device" mechanism; see Auth & Permissions.

Backup & recovery

Four snapshot types, each independently rotated (a new snapshot of a given type overwrites the previous one of that same type — there's no history beyond "the current manual/daily/weekly/monthly copy").

FunctionTriggerDetail
createManualDatabaseSnapshotonCall, admin-onlyOn-demand snapshot, type "manual"
createDailyDatabaseSnapshotonSchedule 0 3 * * * (America/New_York)"Yesterday Snapshot", type "day"
createWeeklyDatabaseSnapshotonSchedule 10 3 * * 1"Weekly Snapshot", type "week", Mondays
createMonthlyDatabaseSnapshotonSchedule 20 3 1 * *"Monthly Snapshot", type "month", 1st of month
listDatabaseBackupsonCall, admin-onlyReturns metadata for all 4 types (size, counts, timestamps); never returns raw backup content
restoreDatabaseBackuponCall, admin-onlyRequires confirmText === "RESTORE" (exact case). Validates the snapshot's companyId matches the target before touching anything.

What a snapshot captures

Exactly three subcollections per company — projects, projectSummaries, itemLibrary — plus the company doc itself and the global users collection. Stored as gzip-compressed JSON in Cloud Storage at database-backups/{'{'}companyId{'}'}/{'{'}type{'}'}.json.gz; only metadata (size, counts, timestamps, who triggered it) lives in Firestore, at companies/{'{'}companyId{'}'}/databaseBackups/{'{'}type{'}'} — note this is a different collection name than the backups path named in firestore.rules; see Auth & Permissions.

Explicitly excluded from every backup Project Storage files, Firebase Auth accounts/passwords, the tax/GSA API caches, and edit-presence records. This isn't an explicit skip check — those collections simply aren't in the list of what gets read, so they're never at risk of being captured or (during a restore) overwritten.

Restore mechanics

On restore, the target company's projects, projectSummaries, and itemLibrary collections are fully deleted, then rewritten from the snapshot (merge:false); the company doc is overwritten entirely; users docs are restored without deleting ones absent from the snapshot.

Implementation note The restore path passes a deleteMissing option intended to control whether users absent from the snapshot get removed, but the underlying writeCollectionSnapshot function doesn't actually read that parameter — it's accepted and silently ignored. In practice, restoring a backup never deletes a users doc, regardless of what's passed. Not a data-loss risk, but worth fixing or removing the dead parameter so a future reader doesn't assume it does something it doesn't.

External data

lookupTaxRateLive sales tax via ZipTax (GET https://api.zip-tax.com/request/v60, header X-API-KEY) — this replaces the earlier Avalara AvaTax integration. Cached 30 days in taxRateCache/{'{'}country_region_zip5{'}'}. Secret: ZIPTAX_API_KEY.
lookupGsaPerDiemUnchanged — live lodging/M&IE rate via the GSA Travel API, cached 30 days in gsaPerDiemCache/{'{'}fiscalYear_zip{'}'}. Secret: GSA_API_KEY (falls back to the public DEMO_KEY if unset).

Branded email delivery

Every email-sending function (invites, resets, MFA codes) routes through one shared mailer, not a third-party email service:

Dead configuration A secret BIDGAUGE_SMTP_PASSWORD is provisioned by scripts/set-bidgauge-smtp-secret.ps1 but is not referenced anywhere in functions/index.js — no SMTP/nodemailer code exists. This looks like leftover tooling from an earlier design that was superseded by the Gmail API approach; harmless but worth removing to avoid confusing a future maintainer. See Technical Risk Notes.

Secrets & config summary

Secret / env varUsed by
ZIPTAX_API_KEYlookupTaxRate
GSA_API_KEYlookupGsaPerDiem (falls back to DEMO_KEY)
GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKENAll email-sending callables
BIDGAUGE_COMPANY_ID (env var)Fallback company id for functions that default to a single company (defaults to "default" if unset)
BACKUP_BUCKET (env var)Overrides the Cloud Storage bucket used for backup files
APP_BASE_URL (env var)Base URL used when composing branded password action links
Previous← Auth & Permissions