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

Canonical source: CLAUDE.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.

CLAUDE.md - Basis Health Platform

PRODUCTION SCALE WARNING: This is a production healthcare platform serving hundreds of clinics and tens of thousands of patients. Every change must consider multi-tenant impact. This is NOT a solo project — changes affect real healthcare operations across multiple organizations.

Feature status: See FEATURES.md (FEATURES.md). After shipping ANY feature (on deploy), you MUST update ALL THREE: (1) the relevant FEATURES.md row (the capability list — Atlas's yes/no grounding), (2) a row in docs/claude/growth-content-backlog.md (docs/claude/growth-content-backlog.md) (the growth loop), AND (3) the feature's how-it-works page under docs/features/ (docs/features/) (the PRODUCT KB Atlas ingests to answer "how does X work" — plain-language, follow docs/features/_template.md; create it if missing, e.g. billing-insurance.md). FEATURES.md tells Atlas what exists; docs/features/ tells it how it works. Stale docs break the growth loop AND leave Atlas unable to answer — this is not optional.

Docs & Atlas freshness on issue close (standing instruction — ENFORCED ALWAYS): Closing an issue is NOT done until you've checked whether the change needs a docs + Atlas update and made it. Any work that changes what a user sees or does — a new capability, a changed flow, a renamed/removed control, a corrected behavior — requires updating the customer docs (the Set Up Your Organization journey + the feature reference, plus the feature's docs/features/ page) so Atlas — which ingests the docs as its KB — stays correct. The update may be new/changed explanatory text, a new page or section, a corrected instruction, or an added / removed / replaced screenshot (demo-clinic data only, PHI-safe). Ship it WITH the Fixes #N change, not later. All customer docs MUST follow the house style — docs/claude/docs-style-guide.md (docs/claude/docs-style-guide.md): open every page/section with what/why prose, write steps as full sentences that state the result, define every domain term inline, use typed callouts (Tip/Note/Warning/Ask Atlas) not bare bullets, and close with intent-labeled next steps. Never ship titles-plus-bullets with no explanatory copy.

Growth content loop (proactive — standing instruction): When ≥3 user-facing features sit 🆕 unannounced in the backlog (or if a week has passed with any unannounced), proactively prompt George: "N features are unannounced — want the weekly content bundle?" On yes, run the growth-content skill to produce copy-paste-ready posts (LinkedIn, email, X, blog/changelog, + YouTube/IG repurpose). Do NOT wait to be asked.

Task board (standing instruction): Every substantive task (new feature / feature update / bug fix) is tracked as a GitHub issue on the Ready Practice Board (org Basis-Health Project #7), NOT in scattered roadmap/*.md files. Use .github/scripts/board.sh: create "<title>" "<bug|enhancement|documentation>" <body-file> → creates the issue with its documentation in the body and drops it on the board in Todo; status <item-id> inprogress|done to move it. Move a card to In Progress when you start real work on it, and close the issue when done (the board's built-in workflow moves closed issues to Done). CLOSING MUST BE AUTOMATIC, not memory-dependent: the commit/PR that COMPLETES an issue MUST carry an explicit Fixes #N / Closes #N trailer — GitHub auto-closes it on the master merge → Done; and ship.sh also auto-closes any Fixes #N in a deployed commit (backstop for direct-to-master ships). Never leave finished work Open and never defer closing to George. (A bare #N or Blocked by #N does NOT close — only the keyword does.) Requires gh with the project scope. Label rules: type labels (bug/enhancement/documentation) are informational; agent:build triggers the autonomous build agent — NEVER add agent:build or agent:triage yourself; only George flips those. Use the "Task / Feature" issue template structure (what/why, acceptance criteria, area) so the body works as a build spec. SIZE every issue — mandatory, on EVERY creation path (you, worktree agents, triage, intake, watchdogs), not just triage. Add exactly one complexity: label = effort size: complexity:low (S — single file, <20 lines, clear fix) · complexity:medium (M — the default) · complexity:high (L — multi-file / non-trivial design) · complexity:xl (XL — multi-phase; decompose before building). board.sh create defaults complexity:medium if you omit one, but size it yourself (e.g. board.sh create "<title>" "bug,P1,complexity:low" <body>). SOURCE-label human reports: a client/patient report → reported:client; a clinic-staff / Ready-Practice-team report → reported:staff. Agent- and monitoring-filed issues carry no reported: label (they're already marked auto-generated/auto-error or are plainly agent-authored). Priority is a board FIELD, not a chip you read off the card. You still FILE priority as a bare P0P3 label (P0 = production down / money / PHI / data loss · P1 = core workflow broken, no workaround · P2 = bug with workaround / degraded UX, the default · P3 = minor/cosmetic) — board_sync.py mirrors that label into the sortable Priority single-select field and then strips the label so priority shows exactly once (sortable) instead of twice. The triage agent stamps priority for classified issues, the intake router defaults bugs to P2, priority-label.yml converts the bug template's Severity dropdown, watchdogs pass explicit priorities — when filing manually, include the P-label yourself (e.g. board.sh create "<title>" "bug,P1,complexity:low" <body>); it becomes the field on the next sync.

Intake dedup (standing instruction): All board writes go through the Atlas Intake Routerboard.sh create already routes through it automatically (dedups against open issues: appends "ALREADY TRACKED as #N" on a match, creates on a miss). Do NOT bypass it with raw gh issue create for bug/feature/gap tasks; use board.sh create so duplicates from many sources (you, worktree agents, Sentry, market-intel, synthetic-QA, staff reports) collapse into one. The HTTP front door is atlas_intake_endpoint (#222).

Dependencies (standing instruction): Tasks form a graph — declare it and respect it. (1) When creating an issue that can't start until another finishes, write "Blocked by: #N" in the body AND add the blocked label. (2) Epics/umbrellas use GitHub sub-issues (parent → children; e.g. #168 COO Operator is parent of the Atlas arm issues). (3) Before starting any card, check it: if it has blocked or a "Blocked by: #N" pointing at an OPEN issue, do NOT start it — pick another card. (4) When you close an issue, sweep for dependents: search issues whose body mentions "Blocked by: #", comment that the blocker is resolved, and remove their blocked label. Dependency direction is monitored in BOTH directions — declared at creation, released at completion.

Worktrees (standing instruction — multi-agent isolation): Several agents run in parallel in separate git worktrees (folders /Users/G/wt-<name>, each on branch wt/<name>). If you are running inside a wt-* folder: you own this folder and branch — do all your work here, commit to wt/<name>, and do NOT touch /Users/G/basis or other worktrees. When work is ready, ask George to merge your branch to master (he/the main session handles conflicts). /Users/G/basis on master is the shared integration point + source of truth. New worktrees are made with .github/scripts/new-worktree.sh <name> (creates the folder + branch + shares the memory brain). Committed files (CLAUDE.md, docs, code) travel with the branch automatically; pull master in periodically to stay current.

NEVER firebase deploy from a wt-* worktree. A functions deploy uploads the ENTIRE functions source dir — deploying from a branch that lags master silently REVERTS other agents' merged work in production. (basisweb/Netlify is exempt — it deploys from the pushed feat/basisweb branch on GitHub, not from a local folder.) A predeploy guard blocks non-master deploys; do not work around it.

Labs / metrics / protocol-types — ONE way to edit, no exceptions (standing instruction — the anti-divergence rule). These live on THREE platforms (basisflow-web, basisweb, basishybrid/basiscore) + the backend, and hand-edits are why they keep diverging. There is exactly ONE allowed workflow, and CI enforces it so a divergent edit cannot merge:

  1. Edit ONLY the source of truth, never a generated/derived file:
    • Analytes / synonyms / ranges → basis-functions/functions/generated/lab_analytes.json (+ lab_unit_conversions.*).
    • Wearable/metric benchmarks → metric_benchmarks.json. Metric/lab typed idsenums.py (EventType, LabType).
    • A new activity/protocol type → add it to enums.py EventType FIRST (it must exist backend-side before any surface can use it).
  2. Run basis-functions/tools/regen_registries.sh (the ONE command — regenerates every derived TS + Dart artifact for BOTH web apps + basiscore from the source). Never run the individual generators piecemeal, never hand-edit *-generated.ts / *_generated.dart / *_ids_generated.dart / lab-analyte-aliases.ts / LAB_SYNONYMS / the protocol-data.ts basisType catalog / MetricsTab metricDefs.
  3. Commit the source edit + ALL regenerated outputs in ONE commit. CI (lab-registry-drift.yml) runs regen_registries.sh --check + the parity/validator checks and FAILS the PR on any drift or on a type present in one layer but not the source.
  4. Cross-platform contract fields (a field one platform writes that others must read — e.g. circuitStepCompletions, medicationCompletions): add it to the backend Pydantic model (model_event.py etc.) in the SAME change, and the parity lint fails if a written field has no model. Never let a surface ship a field the backend doesn't model.
  5. One editor per registry per integration cycle — do not hand-edit lab_analytes.json/enums.py in two worktrees at once; coordinate via the issue, or transient 857-vs-866-style divergence appears until merge. When in doubt, the main/integration session owns registry edits.
  6. Some derived tables are still being converted from hand-maintained to generated (LAB_SYNONYMS, the protocol basisType catalog) under #635 — until that lands, treat those as generated-in-spirit: change them ONLY by their eventual source + regen, never by hand. The moment #635 ships, hand-editing them fails CI. If you find yourself typing an analyte key, a lab synonym, a metric code, or a protocol basisType into a .ts/.dart file by hand, STOP — you are creating the exact divergence this rule exists to prevent.

Operational playbooks live in ONE place — write there, don't re-derive (standing instruction). Repeatable operational how-tos + their tools live in docs/handbook/playbooks/, split by domain: onboarding · support · debugging · sales. Each playbook is steps + the exact tool/script paths + who-does-it (🤖 agent · 👤 staff-delegatable · 🧑‍💻 George/eng). When you figure out a repeatable process (onboard a clinic, white-label an app, diagnose a data issue, run a sales motion), write or update its playbook there rather than solving it fresh next time or burying it in an issue comment. Atlas serves these to Ready Practice staff to work a client, so keep them staff-legible. (Deep technical references stay in docs/claude/*.md per the Topic Index; playbooks POINT at those + the tools in basis-functions/tools/ etc.)

How ANY agent ships (no need to route through the main session): after George explicitly approves the deploy in YOUR chat, run .github/scripts/ship.sh --approved-by-george <your-wt-branch|-> <fn1,fn2,...> from anywhere. It safely does the whole pipeline: merges your branch into master at /Users/G/basis (aborts cleanly on conflict → hand conflicts to the integration session), pushes, deploys the named functions from master, scans output for silent function DELETIONS (stop + restore immediately if seen), and syncs your worktree back. One ship at a time (lock). Never run raw firebase deploy yourself; never pass the flag without George's actual approval in-conversation.

Atlas & the agent fleet (standing context): Atlas is Ready Practice's AI — copilot + clinical assistant + consumer Health Assistant + COO/operator + Slack employee — backed by an autonomous agent fleet (triage, build, orchestrator, market-intel, cost-watch, model-watch, observations, operator, learning-digest, intake router). Full map + how they connect: docs/claude/agent-fleet.md (docs/claude/agent-fleet.md). Must-knows before touching any of it:

  • Build/triage pipeline: issue + agent:triage → Triage Agent's suitability gate (deterministic + non-critical-path + checkable criteria → agent:build; design/cross-repo → needs-human-design; under-specified → needs-acceptance-criteria; else needs-human) → Build Agent opens a PR into Human Review → George merges → still needs a deploy (merged ≠ live). NEVER self-apply agent:triage/agent:build — only George flips those.
  • Autonomy-gap loop (STANDING DIRECTIVE for EVERY agent + George — build agent, triage, intake router, worktree agents, this session, and human): needs-human is NEVER a terminal verdict — it is a logged capability gap. Whenever anything (triage, a build, you, George) decides a task needs a human, it MUST record the specific structured blockerReason (under-specified · no-automated-verification · design-subjective · critical-path-money/auth/phi · cross-repo · runtime-verify-needed · ambiguous-scope) — not just the label. Those reasons roll up into a capability-gap ledger (weekly autonomy digest → auto-files the top gaps into Ideas), and each recurring reason gets a capability built that removes the WHOLE class (under-specified→triage auto-writes criteria; no-automated-verification→give the fleet real verification: parity harness, synthetic smoke tests, preview-deploy; design-subjective→visual-regression/Figma-diff; cross-repo→multi-repo worktree build). North-star metric: autonomy rate (% reaching agent:build without a human) → drive toward 100%. So the standing question on EVERY issue is "what would make this autonomously buildable + shippable?" — and we chip away at the answer. Critical-path is NOT auto-blocked-at-build: the build agent only ever drafts a PR you merge, so critical work still gets BUILT, then reviewed before merge by a QA hire and/or the top available model (Fable 5) doing code review — the human-merge gate is the safety net, not a human-build block. The orchestrator is board-driven (picks the highest-priority Todo item → pipeline), and its selection + the gate criteria keep updating from the autonomy-gap ledger. Tracked as the Autonomy-gap epic (see the Ready Practice board).
  • Adding an Atlas tool = 5 silent-fail layers: TOOLS schema (functions_ai_agent.py) → TOOL_PERMISSION_MAP (functions_permissions.py, default-DENY) → intent set (functions_orchestrator.py, missing = STRIPPED) → dispatch branch → decorator secrets=[]. .github/scripts/lint_registrations.py checks them (predeploy + CI).
  • Untrusted text → LLM prompts must be FENCED (#305): any error/user/issue-sourced string interpolated into a prompt (triage gate, dedup, build-agent, intake) must go through fence_untrusted() (functions) or an explicit ===== UNTRUSTED DATA ===== marker — else it's a prompt-injection vector. .github/scripts/lint_prompt_fencing.py gates it (predeploy + CI).
  • Agent-fleet CI auth (#344): the 6 pipeline workflows use a GitHub App installation token (actions/create-github-app-token, secrets APP_ID/APP_PRIVATE_KEY), NOT AGENT_PAT (retired). The build-agent job also refuses agent:build on needs-human*-labelled issues (#217 suitability guard).
  • Observability watchdogs (scheduled, → intake+Slack, detection-only): error/ops/cost/model + ci_watch (CI failures), uptime_watch (endpoint down, 15-min), freshness_watch (pipeline stalled/stale output), configdrift_watch (Cloud Run public-exposure run.invoker flips, SOC2 CC6 — needs roles/run.viewer on the functions SA). Full map + remaining blind spots (PHI-access, $ spend, OAuth-token health) in docs/claude/agent-fleet.md + epics #365/#366.
  • Models: import ATLAS_FAST/BALANCED/SMART from atlas_models.py — never hardcode model IDs. functions_model_watch.py auto-detects new/deprecated models.
  • Kill switch: tools/atlas_kill.py --pause sets system/atlas_autonomy_master; every autonomous loop checks autonomy_paused() (deadman: fail-closed after 3 read failures). Autonomy is per-tenant (atlas_tenancy.py), never global.

Critical Rules

Data & Storage

  • Always use clinicsv2 — never the legacy clinics collection
  • Never updateDoc on clinicsv2/{clinicId} directly from web client — use subcollections or Cloud Functions
  • camelCase for all Firestore fields — Python Pydantic models must use camelCase field names directly (not snake_case with aliases)
  • Date-only fields = strings — store birthday/due-date as "1990-01-15" not Firestore Timestamps (causes 1-day shift for UTC-offset users)
  • Add Firestore rules AND DEPLOY before writing new pathsfirebase deploy --only firestore:rules is mandatory. The rule file is just a local text file until deployed. This is the #1 recurring cause of permission errors.
  • Add Storage rules before new upload paths — verify path exists in basis-functions/storage.rules
  • Each settings/{name} doc needs its OWN rule — there is no wildcard catch-all
  • After deploying new rules, hard-refresh (Cmd+Shift+R) — existing onSnapshot listeners cache permission denials and never recover
  • Encrypted Stripe data — use StripeConfig.decrypt(doc.to_dict(), live), never read oauth_token directly (it's stored as oauth_token_encrypted)
  • Stripe SDK metadata is NOT a dictStripeObject.metadata does not support .get(), dict(), or to_dict_recursive(). Use getattr(obj.metadata, 'key', None) to read metadata keys. This applies to PaymentIntent, WebhookEndpoint, and all other Stripe objects. The deployed Stripe SDK version may differ from local — never trust local SDK tests for method availability.
  • Firestore field types are NEVER guaranteed at runtimedoc.data() cast as a TypeScript interface gives zero runtime safety. Fields can be missing (undefined), wrong type (e.g. gender stored as number 1/2 from Flutter enum, not string "male"), or inconsistent across documents. Rules: (1) Every property access needs a fallback: (field || ''), (field ?? 0). (2) Before calling string methods (.trim(), .toLowerCase(), .split()), coerce with String(value). (3) Before calling array methods, guard with (field || []). (4) Never trust TypeScript interfaces on Firestore data — they lie at runtime.

Firebase Functions

  • Always use GB_1 memoryMB_512 causes OOM crashes
  • Always include http://localhost:3000 in CORS origins — #1 cause of local dev CORS failures
  • Never deploy all functions at once — always firebase deploy --only functions:specific_function --force
  • Secret scoping — each function only receives secrets in its secrets=[] list. Module-level SecretParam declarations don't auto-inject. Missing secrets cause silent 500 errors.
  • Changing Pydantic models requires redeploying the function — frontend sends new fields, deployed backend has old model = "invalid request data" error
  • firebase_functions.logger has warn(), NOT warning()from firebase_functions import logger; logger.warning(...) throws AttributeError: module 'firebase_functions.logger' has no attribute 'warning'. These calls live in except blocks, so a handled warning becomes a CRASH (this silently took down heartbeat_dispatcher/the observation engine, 2026-07-21). Use logger.warn(...). (Python's stdlib logging.Logger DOES have .warning, so logging.getLogger(__name__).warning(...) is fine — the footgun is only the firebase logger.)

UI Rules

  • No browser alert() / browser notifications — use toasts (useToast()) or inline modals
  • Every async operation must show a toast — success AND error, always; never console.error and leave the user wondering
  • Errors must explain WHY in plain language — "Your role doesn't have permission" not "Error 403"
  • Never use raw onSnapshot — always use safeOnSnapshot from @/lib/firebase; raw listeners that receive a Firestore error corrupt the SDK's internal watch stream and crash every other listener on the page

Testing

  • Test with Admin role, NOT System Admin — System Admin bypasses all permission checks
  • Never say "fixed" without user confirmation — wait for runtime verification
  • Use the debugging tools (basis-functions/tools/) to verify end-to-end before declaring done

Forbidden Operations (ask first)

Never doInstead
git stashUse branches or cherry-picks
git commitOnly when explicitly requested
firebase deployAlways confirm with user first
flutter cleanAvoid — makes compile times too long
Modify hybrid/basisflow/Deprecated legacy app
Modify Stripe filesPayment integration is working
Modify ImportLabModal / UploadDocumentModalDocument upload was working; changes broke it

Platform Overview

Internal NamePublic NamePathStackHostingStatus
Basis Flow WebBasis Platformhybrid/basisflow-web/Next.jsFirebase (platform.basishealth.io)ACTIVE
Basis HybridBasis Apphybrid/basishybrid/FlutterApp StoresACTIVE
Basis WebClient Portalhybrid/basisweb/Next.jsNetlify (web.basishealth.io)ACTIVE
Basis Flow(deprecated)hybrid/basisflow/FlutterDEPRECATED

Shared: basiscore/ (Dart models), basiscoreui/ (UI components), basis-functions/ (Python Cloud Functions)


HIPAA Summary

All health data is PHI. Key rules:

  • PHI is never logged to console/Cloud Logging
  • Permission checks before accessing patient data
  • No PHI exposed in error messages
  • AI/LLM: User opt-in required (preferences.llmOptIn), HIPAA-aware prompts, no training on patient data
  • Audit trail created for significant operations via transactions collection

Permission Model

RoleKey Permissions
Adminall — everything
ManagerSchedule, clinic, members, documents, billing
MedicalSchedule, members, health data, notes
Customer ServiceSchedule, members, admins, billing (NO health data)
CoachView schedule, view members, view/edit admins

Hierarchy: all > clinic_admin > individual permissions. Each permission has view_* and edit_* variants.

Standalone permissions (NOT implied by any parent):

  • view_revenue — must be assigned explicitly
  • override_booking_parameters — must be assigned explicitly

Storage: clinicsv2/{clinic}/clinicians/{uid}.role + permissions/{clinic}/users/{uid}


Pricing Model

Last updated 2026-07-01. This is the refactorable source of truth for the business model. Intent: this model lives in the backend billing plans (functions_ai_billing.pyget_billing_plans) and is READ by all three surfaces — signup/onboarding, in-app Settings → Billing, and the public pricing page. Do NOT hardcode pricing per surface (that's how the old freemium page drifted). Custom deals use set_custom_billing. Strategy = premium cash-cow, charge up front, no free tier (see the strategy memo). All [PLACEHOLDER] values must be set from real vendor costs before going live.

Public offering

  • One self-serve plan ($999/mo) or Custom. No Solo/Growth/tiered public lineup for now. Custom is sales-led per client via set_custom_billing.

Base plan — $999

VariableValue
base_monthly$999/mo
base_annual_upfront (first-year signup)$8,495/yr (~29% off; paid upfront) — renews at standard annual $9,995/yr (~17% off)
White-label renewalsetup is one-time; renews at regular $495/mo app-maintenance (not the discounted $399)
active_client_cap1,000 (then usage-based / custom)
staff_seatsUnlimited
eprescribe_seats_included1
wearablesIncluded
Atlas AI (text: copilot + doc/lab analysis + client chat)Pooled monthly allowance → usage-based overage

AI allowances + overage — all [PLACEHOLDER], set from real token/vendor costs

ItemIncludedOverage
Atlas AI (pooled text)[PLACEHOLDER] (≈ 2,000 copilot Qs or ~300 doc analyses)[PLACEHOLDER] /1K tokens
AI Scribe (Recall.ai, per min)300 min [PLACEHOLDER]$0.12/min [PLACEHOLDER]
AI Call Center (ElevenLabs+Twilio, per min)60 min [PLACEHOLDER]$0.90/min [PLACEHOLDER]
SMS[PLACEHOLDER] /mo$0.02 /msg (~1.5x Twilio cost)

Add-ons

Add-onPrice
White-labeled app$4,995 setup + $495/mo · annual-upfront: $2,995 setup + $399/mo (prepaid)
Lab Ordering (LabCorp / Quest / BioReference · home kits · at-home phlebotomy)$495/mo (per-panel fees extra)
Medical Provider Network$495/mo + $60–100/consult
Additional e-prescribe seat$99/seat/mo

Usage-based platform fees (revenue on top of subscription)

FeeRate
Payment processing platform fee+0.25% of card volume (on top of Stripe 2.9% + $0.30 pass-through)
Insurance billing (RCM, % of net collections, tiered)3.0% ≤$25k · 2.75% $25–100k · 2.5% $100–250k · 2.0% $250k+ · min $300/mo · payer fees at cost

Financing

  • Third-party financing (spread monthly, Ready collects upfront) — US clients only, and only when annual contract value > $10k.

Custom / done-for-you

  • Bespoke via set_custom_billing (per-client). Done-for-you tier: from $25k setup / $100k ACV → "Book a call".

Margin target & COGS

  • Target 80% gross margin per account. COGS = Atlas inference + GCP infra + Terra (future, per connected client) + audio-AI vendor costs (Recall.ai, ElevenLabs, Twilio). Margin protectors: the active_client_cap and the AI allowances/overage. Verify before trusting: real GCP-per-active-client and per-minute vendor costs — the lines most likely to erode 80%.

Open items (blocking "real" pricing)

  • Set real per-token cost → Atlas pooled allowance + overage rate.
  • Set real Recall.ai (Scribe) + ElevenLabs/Twilio (Call Center) per-min costs → included minutes + overage.
  • Set SMS included + overage.
  • Decide whether an onboarding/setup fee applies to the $999 plan (done-for-you has $25k).

Pre-Flight Checklist for Backend Integration

Before writing code that integrates with an existing backend system, audit these five contracts:

1. Data Serialization

  • Check if fields are encrypted at rest (e.g., Stripe: oauth_token -> oauth_token_encrypted)
  • Check if fields are JSON-serialized (stored as JSON string, not dict)
  • Verify the field name actually exists in Firestore

2. Secret/Credential Scoping

  • Check the function's secrets=[] list in its decorator
  • For dispatcher functions, check the dispatcher's secrets list
  • Check if @init functions access secrets not available to all functions

3. Merge/Update Semantics

  • set(data, merge=True) preserves fields not in data — temporary flags survive
  • Completion steps MUST explicitly clear temporary flags (e.g., pending)

4. Auth/Permission Contract

  • Handler registered in REQUEST_MODEL_MAP
  • Correct role requirements
  • CORS origins include localhost
  • NEVER guess request_type values — always look up the exact key in REQUEST_MODEL_MAP in the backend code. The error message "Request type must be one of ..." means the frontend sent a request_type string that doesn't exist. Common mistakes: create_subscription (wrong) vs book_subscription (correct), delete_user (wrong) vs remove_clinic_user (correct). Always grep for the handler name in REQUEST_MODEL_MAP before writing the frontend call.

5. Deployment Contract

  • Backend Python changes require function redeploy
  • Firestore rules changes require rules deploy
  • Frontend changes against deployed backend — is the backend version current?

6. Adding a New Field (Full-Stack Checklist)

Adding a field to a Pydantic model is NOT enough. Every new field requires ALL of these steps or it silently won't persist:

  • Pydantic model — add the field to the model class (e.g., MembershipType in model_clinic.py)
  • Request model(s) — add to BOTH Add*Request AND Update*Request classes in functions_clinic.py
  • Add handler — map request.field_namemodel.fieldName in the add_* function
  • Update handler — add if request.field != Sentinel.NOT_PROVIDED: model.field = request.field in the update_* function
  • Frontend save — send field_name (snake_case) in the clinicService() call for BOTH create and update paths
  • Frontend load — map the field when reading from Firestore snapshot (e.g., in the onSnapshot callback)
  • Frontend type — add to the TypeScript interface

Common failure: Field saves once but "doesn't persist" — the field is written to Firestore but the frontend load mapping is missing, so it appears empty when reopened.


Topic Index

For detailed documentation, read the relevant topic file:

Working on...Read this file
Booking, availability, slots, memberships, waitlistdocs/claude/scheduling.md
Lab ordering, uploads, analysis, resultsdocs/claude/labs.md
Chat, messaging, push notifications, emaildocs/claude/chat-notifications.md
Client onboarding, intake forms, invitationsdocs/claude/onboarding.md
Atlas AI, copilot, knowledge base, agent toolsdocs/claude/copilot.md
Atlas agent fleet, build/triage pipeline, operator/COO, intake router, kill switchdocs/claude/agent-fleet.md
Protocols, programs, biomarkers, activitiesdocs/claude/programs-protocols.md
Stripe, payments, subscriptions, multi-accountdocs/claude/stripe-payments.md
Insurance billing, claims, RCM, Stedi, eligibility, ERA, superbillsdocs/claude/insurance-rcm.md
Cloud Functions, CORS, secrets, dispatchersdocs/claude/firebase-backend.md
Firestore collections, rules, schemas, typesdocs/claude/firestore-schema.md
Deploying, hosting, builds, Firebase targetsdocs/claude/deployment.md
Database debugging scripts, test toolsdocs/claude/debugging-tools.md
Atlas REST API, webhooks, MCP serverdocs/claude/atlas-api-webhooks.md
Dates, timezones, date-only vs datetimedocs/claude/date-time-handling.md
CI/CD, agents, monitoring, deploys, operationsdocs/claude/operations.md
Plan/Metrics tab Firestore reads (data contract)docs/claude/plan-metrics-data-contract.md
Client booking access rules, isPublic, membership allowances, over-capacity paymentdocs/claude/client-booking-access-payment-contract.md
Plan tab — running fixes / open issues listdocs/claude/plan-tab-fixes.md
Filing / triaging / scoping a bug (report + agent-triage schema)docs/claude/bug-scoping-template.md
Signup / onboarding flow redesign (screens, decisions, build tasks)docs/claude/onboarding-signup-flow.md
Billing, pricing model, plans, usage metering, Stripedocs/claude/billing-pricing.md
Business strategy — pricing rationale, margins, ARR/scale modelsdocs/claude/business-strategy.md

Key Service Integrations

Operational deep-dives on each external service we receive data from or call into. The inline rules below are the must-know production constraints — read the linked doc before changing the integration. Add a new file under docs/claude/services/ for each new third-party we integrate.

ServiceDocMust-know production rules
Terra (wearable webhooks)docs/claude/services/terra.md(1) NEVER return 5xx from the hook — Terra circuit-breaks immediately and queued retries burn the 10-retry budget. (2) Hook decorator MUST declare secrets=[TERRA_WEBHOOK_SECRET] — module-level SecretParam doesn't auto-inject and missing it silently disables sig verification. (3) Do NOT apply @error_handler to the hook — it converts every HTTPException (including abort()) into a 500. (4) HMAC compare must use hmac.compare_digest. (5) Same-user webhooks fire in parallel — Terra does not serialize, we must

Quick Reference

Common Commands

# Start emulators
cd /Users/G/basis/basis-functions
firebase emulators:start --import ./emulator-data --export-on-exit ./emulator-data

# Deploy specific function
firebase deploy --only functions:clinic_service --force

# Deploy Firestore rules
firebase deploy --only firestore:rules

# Build & deploy Basis Flow Web (chunk-load-error-proof)
# Runs a clean build, OVERLAY-copies out/ over public-platform/ (keeps old
# content-hashed chunks so tabs on a prior build don't 404 -> hard crash),
# prunes _next/static files >7d old, then deploys hosting:platform.
.github/scripts/publish-platform.sh
# NOTE: do NOT go back to `rm -rf public-platform/* && cp` — wiping old chunks is
# exactly what strands open tabs (missing chunk -> SPA rewrite serves index.html
# as JS -> ChunkLoadError). The overlay + broadened auto-reload handler in
# basisflow-web/app/layout.tsx are the chunk-load-error protection.

Key File Locations

WhatWhere
Firestore rulesbasis-functions/firestore.rules
Storage rulesbasis-functions/storage.rules
Firestore indexesbasis-functions/firestore.indexes.json
Firebase configbasis-functions/firebase.json
Dart modelshybrid/basiscore/lib/src/models/
Python modelsbasis-functions/functions/src/model_*.py
Debugging toolsbasis-functions/tools/
Build scriptssh/ and hybrid/sh/