Skip to main content
Synced from the repo — do not edit here

Canonical source: docs/claude/firestore-schema.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.

Firestore Schema, Rules, Types & Cross-Platform Data

Root Collections

CollectionPurposeAccess
usersUser profiles and preferencesOwner + delegated clinicians
users/{uid}/healthSummariesHealth data summariesOwner + clinic staff (if active member)
users/{uid}/eventsUser events (meals, workouts, etc.)Owner + clinic staff
users/{uid}/delegationsPer-clinician access delegationsSystem
chatsChat messages between users/staffOwner + clinic staff
adminSystem administratorsRead-only public, write via console
permissionsRBAC permission documentsSystem managed
clinicsv2Clinic organizations (USE THIS, not clinics)Staff + members
clinicsLEGACY - Do not useDeprecated
booking_pagesPublic booking pagesPublic read, staff write

Clinic Subcollections (clinicsv2/{clinicId}/...)

SubcollectionPurpose
clinic_users/{uid}Client/patient records
clinic_users/{uid}/healthSummariesMirrored health data
clinic_users/{uid}/labsLab results
clinic_users/{uid}/documentLinksDocument references
clinic_users/{uid}/goalsUser goals
clinic_users/{uid}/conditionsMedical conditions
clinic_users/{uid}/tasksUser tasks
clinic_users/{uid}/copilot_sessionsAI copilot chat sessions
clinicians/{uid}Staff/clinician records
clinicians/{uid}/availabilityV2Recurring availability rules
public_clinicians/{uid}Public projection of clinician info
scheduled/{eventId}Appointments and scheduled events
scheduled/{eventId}/notesAppointment notes
transactions/{id}Audit trail for clinic operations
holidays/{id}Clinic closures
benchmarks/{code}Clinic-level benchmark policies
settings/copilotAI copilot configuration
settings/onboardingStateOnboarding tracking
preferences/{id}Clinic preferences
noteTemplates/{id}Note templates
tasks/{id}Clinic-level tasks
config/zoomZoom integration config
config/googleGoogle Meet config
config/emailBranded email config
config/fullscriptFullscript integration
config/junctionJunction Health integration
settings/onboardingOnboarding steps configuration

Firestore Write Patterns for Basis Flow Web

CRITICAL: When writing to Firestore from the web app:

  1. Use subcollections for settings - Write to clinicsv2/{clinicId}/settings/{settingName}

    • Use setDoc(ref, data, { merge: true }) for create/update
  2. Use cloud functions for main clinic doc - If you MUST update clinicsv2/{clinicId} directly:

    • Use clinic_service with appropriate request_type
    • Cloud functions run with admin privileges
  3. Use dedicated subcollections for data: noteTemplates/{id}, formDefinitions/{id}, tasks/{id}, benchmarks/{code}

NEVER directly updateDoc on clinicsv2/{clinicId} from web client — it will fail with "Missing or insufficient permissions" unless the user has clinic_admin role.

Firestore Security Rules

ALWAYS add rules AND DEPLOY before writing new paths. Every new subcollection path needs an explicit rule in basis-functions/firestore.rules.

Adding the rule to the file is NOT enough — you MUST deploy:

cd basis-functions && firebase deploy --only firestore:rules

Each settings/{name} document needs its OWN rule

There is NO wildcard match /settings/{doc} catch-all. Every new settings document path must have an explicit rule.

Pre-flight checklist before writing any new Firestore path:

  1. grep the path in firestore.rules — if not found, ADD it first
  2. grep the path in storage.rules — if files are involved
  3. DEPLOY the rules — the file change alone does NOTHING until deployed
  4. Only then write the frontend/backend code
  5. Test the write in the browser before marking done

Recurring Bugs

  1. Adding a rule without deploying it — Firestore doesn't know about it until firebase deploy --only firestore:rules
  2. After deploying new rules, existing onSnapshot listeners remain broken — the SDK caches the permission denial. Always hard-refresh (Cmd+Shift+R) after deploying new rules.
  3. Changing Pydantic models without redeploying the function — Frontend sends new fields, deployed backend has old model. Redeploy the function before testing.

Firebase Storage Rules

When adding file upload functionality, verify the storage path has a corresponding rule in basis-functions/storage.rules.

hasPermission in storage.rules

Checks permissions/{clinicId}/users/{uid} for the permission. Checks BOTH arrays:

  • implicit_permissions (current — expanded permissions)
  • permissions (legacy fallback)
  • Also grants access if all is present in either array

Existing Allowed Paths (clinicsv2/{clinicId}/...)

PathReadWrite Permission
/logo/**authenticatededit_clinic
/users/{userId}/**owner, admin, view_documentsowner, admin, edit_documents
/staff/{staffId}/profile/**authenticatedowner, admin, edit_admins
/products/**authenticatedadmin, edit_clinic
/templates/.../references/**admin, view_documentsadmin, edit_documents
/knowledge/**admin, view_documentsadmin, edit_documents

Other Paths

PathReadWrite Permission
website/{clinicId}/**authenticatedadmin, edit_clinic
users/{userId}/**owner, adminowner, admin

Document Storage Path Conventions (CRITICAL)

The Flutter app and Web app use different Storage paths for the same files. This is the #1 cause of "Unable to load preview" errors.

PlatformUpload PathFirestore storagePath field
Flutter app (share/upload)users/{uid}/files/{uuid}.extusers/{uid}/files/{uuid}.ext
Flutter app (lab upload)users/{uid}/labs/{uuid}.extusers/{uid}/labs/{uuid}.ext
Web app (ImportLabModal)clinicsv2/{clinic}/users/{uid}/files/labs/{timestamp}_{name}Full clinicsv2 path
Web app (DocumentsTab upload)clinicsv2/{clinic}/users/{uid}/files/documents/{name}Full clinicsv2 path

A Storage trigger copies files from users/{uid}/files/ to clinicsv2/{clinic}/users/{uid}/files/, but NOT from users/{uid}/labs/.

Firestore collections that reference files:

  • documentLinks — written by Flutter, stores users/{uid}/... paths (need rewrite for web)
  • documentSummaries — written by both platforms, stores either format

When loading previews, always try multiple path variants:

  1. The stored storagePath as-is
  2. If it has /labs/, also try /files/ (and vice versa)
  3. If it's a clinicsv2/ path, also try the raw users/ path

Adding New Upload Paths

// In storage.rules, inside match /clinicsv2/{clinicId} { ... }
match /your_new_path/{allPaths=**} {
allow read: if request.auth != null;
allow write: if request.auth != null && (
isAdmin() || hasPermission(clinicId, 'all') || hasPermission(clinicId, 'required_permission')
);
}

Pydantic <-> Firestore Type Compatibility

Data in Firestore can be written from multiple sources with different serialization formats. When the Python backend reads with Pydantic models, type mismatches cause crashes.

Solution: Type Validators in model_clinic.py

from typing import Annotated
from pydantic import BeforeValidator

DateOfBirthField = Annotated[str | None, BeforeValidator(_parse_date_of_birth)]
ClinicianStatusField = Annotated[ClinicianStatus, BeforeValidator(_parse_clinician_status)]
FlexibleDatetimeField = Annotated[datetime | None, BeforeValidator(_parse_flexible_datetime)]
GenderField = Annotated[Gender, BeforeValidator(_parse_gender)]
ClinicUserStatusField = Annotated[ClinicUserStatus, BeforeValidator(_parse_clinic_user_status)]

Current Validators

Field TypeAcceptsReturnsFallback
DateOfBirthFieldTimestamp, datetime, string, NoneISO date string "YYYY-MM-DD"None
ClinicianStatusFieldString, int, enumClinicianStatus enumACTIVE
GenderFieldString "f"/"Female", int, enumGender enumUNKNOWN
ClinicUserStatusFieldString, int, enumClinicUserStatus enumACTIVE
FlexibleDatetimeFieldTimestamp, ISO string, Nonedatetime objectNone

Response Serialization

When returning data from clinic_service:

if isinstance(result, BaseModel):
result = model_to_firestore(result) # Uses exclude_defaults=True

Document IDs (like entryId) are metadata NOT in doc.to_dict(). For functions where document ID is important, return list[dict] instead of list[Model].

Common Gotchas

  1. Don't assume field types are consistent — Same field can be Timestamp in one doc, string in another
  2. Always have fallback values — Validators should gracefully degrade, not crash
  3. Test with real Firestore data — Unit tests with clean data won't catch these issues
  4. Check both read AND write paths

Field Naming Conventions

  • Cloud Function requests: Use snake_case (image_url, stripe_price_one_time)
  • Firestore storage: Use camelCase (imageUrl, stripePriceOneTime)
  • Frontend interfaces: Use camelCase to match Firestore
  • Pydantic models handle conversion via Field aliases
# CORRECT: Use camelCase directly
class MembershipType(BaseModel):
primaryLocationId: Optional[str] = Field(None)
accessibleLocationIds: list[str] = Field(default_factory=list)

# WRONG: snake_case with aliases causes issues
class MembershipType(BaseModel):
primary_location_id: Optional[str] = Field(None, alias='primaryLocationId') # DON'T

Firestore Auth Rules for Public Data

  • clinicsv2/{clinicId} requires authentication to read
  • For public/pre-auth data (e.g., sign-in pages with clinic branding), use Cloud Functions which have admin access
  • Example: resolve_clinic_by_subdomain fetches clinic info for branded login pages without requiring user auth

Cross-Platform Data Changes

Changes affect hundreds of clinics with different configurations and tens of thousands of patients.

The Problem

A common failure mode:

  • Added UI components in frontend
  • Added fields to TypeScript interface
  • Did NOT verify backend Pydantic model accepts the fields
  • Did NOT verify backend storage function preserves the fields
  • Did NOT verify other platforms can read the new data

The Reality

  • Pydantic v2 silently drops unknown fields
  • Storage functions explicitly list fields — New fields aren't auto-included
  • TypeScript interfaces don't validate runtime — Backend must also accept
  • Frontend-only changes are incomplete

Required Verification Checklist

  1. Backend Model — Field added to Pydantic model + included in storage/serialization
  2. Firestore Storage — Field is being written correctly + security rules allow it
  3. All Platforms Read Correctly — Basis Flow Web, Basis Web, Basis Hybrid
  4. Backwards Compatibility — Old data without the field doesn't crash

Trace the Full Data Lifecycle

Frontend (sends) -> Backend Model (accepts) -> Storage Function (persists) -> Firestore -> All Consumers (read)
StepCheck
Frontend sendsPayload includes field
Backend acceptsPydantic model has field
Storage persistsStorage function includes field
Firestore writesDocument has field
Other platforms readAll apps can parse

Common Cross-Platform Data Locations

Data TypeBackend ModelStorage FunctionFirestore Path
Protocols/HabitsHabitEventInput_habit_min()clinicsv2/{clinic}/clinic_users/{uid}/habits
AppointmentsEventbook_appointment()clinicsv2/{clinic}/scheduled/{id}
LabsLabResultvia document_processorclinicsv2/{clinic}/clinic_users/{uid}/labs
User profileUservarioususers/{uid}
Clinic settingsClinicDetailsupdate_clinic()clinicsv2/{clinic}

A data field doesn't exist until it's stored in Firestore and readable by all platforms.