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 P0–P3 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 Router — board.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: #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:
- 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 ids →enums.py(EventType,LabType). - A new activity/protocol type → add it to
enums.py EventTypeFIRST (it must exist backend-side before any surface can use it).
- Analytes / synonyms / ranges →
- 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/ theprotocol-data.tsbasisType catalog /MetricsTabmetricDefs. - Commit the source edit + ALL regenerated outputs in ONE commit. CI (
lab-registry-drift.yml) runsregen_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. - 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.pyetc.) 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. - One editor per registry per integration cycle — do not hand-edit
lab_analytes.json/enums.pyin 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. - 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/.dartfile 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; elseneeds-human) → Build Agent opens a PR into Human Review → George merges → still needs a deploy (merged ≠ live). NEVER self-applyagent: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-humanis 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 structuredblockerReason(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(% reachingagent:buildwithout 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 → decoratorsecrets=[]..github/scripts/lint_registrations.pychecks 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.pygates it (predeploy + CI). - Agent-fleet CI auth (#344): the 6 pipeline workflows use a GitHub App installation token (
actions/create-github-app-token, secretsAPP_ID/APP_PRIVATE_KEY), NOTAGENT_PAT(retired). The build-agent job also refusesagent:buildonneeds-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.invokerflips, SOC2 CC6 — needsroles/run.vieweron the functions SA). Full map + remaining blind spots (PHI-access, $ spend, OAuth-token health) indocs/claude/agent-fleet.md+ epics #365/#366. - Models: import
ATLAS_FAST/BALANCED/SMARTfromatlas_models.py— never hardcode model IDs.functions_model_watch.pyauto-detects new/deprecated models. - Kill switch:
tools/atlas_kill.py --pausesetssystem/atlas_autonomy_master; every autonomous loop checksautonomy_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 legacyclinicscollection - Never
updateDoconclinicsv2/{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 paths —
firebase deploy --only firestore:rulesis 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
onSnapshotlisteners cache permission denials and never recover - Encrypted Stripe data — use
StripeConfig.decrypt(doc.to_dict(), live), never readoauth_tokendirectly (it's stored asoauth_token_encrypted) - Stripe SDK metadata is NOT a dict —
StripeObject.metadatadoes not support.get(),dict(), orto_dict_recursive(). Usegetattr(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 runtime —
doc.data()cast as a TypeScript interface gives zero runtime safety. Fields can be missing (undefined), wrong type (e.g.genderstored as number1/2from 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 withString(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_1memory —MB_512causes OOM crashes - Always include
http://localhost:3000in 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-levelSecretParamdeclarations 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.loggerhaswarn(), NOTwarning()—from firebase_functions import logger; logger.warning(...)throwsAttributeError: module 'firebase_functions.logger' has no attribute 'warning'. These calls live inexceptblocks, so a handled warning becomes a CRASH (this silently took downheartbeat_dispatcher/the observation engine, 2026-07-21). Uselogger.warn(...). (Python's stdliblogging.LoggerDOES have.warning, sologging.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.errorand 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 usesafeOnSnapshotfrom@/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 do | Instead |
|---|---|
git stash | Use branches or cherry-picks |
git commit | Only when explicitly requested |
firebase deploy | Always confirm with user first |
flutter clean | Avoid — makes compile times too long |
Modify hybrid/basisflow/ | Deprecated legacy app |
| Modify Stripe files | Payment integration is working |
Modify ImportLabModal / UploadDocumentModal | Document upload was working; changes broke it |
Platform Overview
| Internal Name | Public Name | Path | Stack | Hosting | Status |
|---|---|---|---|---|---|
| Basis Flow Web | Basis Platform | hybrid/basisflow-web/ | Next.js | Firebase (platform.basishealth.io) | ACTIVE |
| Basis Hybrid | Basis App | hybrid/basishybrid/ | Flutter | App Stores | ACTIVE |
| Basis Web | Client Portal | hybrid/basisweb/ | Next.js | Netlify (web.basishealth.io) | ACTIVE |
| Basis Flow | (deprecated) | hybrid/basisflow/ | Flutter | — | DEPRECATED |
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
transactionscollection
Permission Model
| Role | Key Permissions |
|---|---|
| Admin | all — everything |
| Manager | Schedule, clinic, members, documents, billing |
| Medical | Schedule, members, health data, notes |
| Customer Service | Schedule, members, admins, billing (NO health data) |
| Coach | View 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 explicitlyoverride_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.py→get_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 useset_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 viaset_custom_billing.
Base plan — $999
| Variable | Value |
|---|---|
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 renewal | setup is one-time; renews at regular $495/mo app-maintenance (not the discounted $399) |
active_client_cap | 1,000 (then usage-based / custom) |
staff_seats | Unlimited |
eprescribe_seats_included | 1 |
wearables | Included |
| 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
| Item | Included | Overage |
|---|---|---|
| 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-on | Price |
|---|---|
| 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)
| Fee | Rate |
|---|---|
| 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_capand 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
@initfunctions access secrets not available to all functions
3. Merge/Update Semantics
-
set(data, merge=True)preserves fields not indata— 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_typevalues — always look up the exact key inREQUEST_MODEL_MAPin the backend code. The error message "Request type must be one of ..." means the frontend sent arequest_typestring that doesn't exist. Common mistakes:create_subscription(wrong) vsbook_subscription(correct),delete_user(wrong) vsremove_clinic_user(correct). Alwaysgrepfor the handler name inREQUEST_MODEL_MAPbefore 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.,
MembershipTypeinmodel_clinic.py) - Request model(s) — add to BOTH
Add*RequestANDUpdate*Requestclasses infunctions_clinic.py - Add handler — map
request.field_name→model.fieldNamein theadd_*function - Update handler — add
if request.field != Sentinel.NOT_PROVIDED: model.field = request.fieldin theupdate_*function - Frontend save — send
field_name(snake_case) in theclinicService()call for BOTH create and update paths - Frontend load — map the field when reading from Firestore snapshot (e.g., in the
onSnapshotcallback) - 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, waitlist | docs/claude/scheduling.md |
| Lab ordering, uploads, analysis, results | docs/claude/labs.md |
| Chat, messaging, push notifications, email | docs/claude/chat-notifications.md |
| Client onboarding, intake forms, invitations | docs/claude/onboarding.md |
| Atlas AI, copilot, knowledge base, agent tools | docs/claude/copilot.md |
| Atlas agent fleet, build/triage pipeline, operator/COO, intake router, kill switch | docs/claude/agent-fleet.md |
| Protocols, programs, biomarkers, activities | docs/claude/programs-protocols.md |
| Stripe, payments, subscriptions, multi-account | docs/claude/stripe-payments.md |
| Insurance billing, claims, RCM, Stedi, eligibility, ERA, superbills | docs/claude/insurance-rcm.md |
| Cloud Functions, CORS, secrets, dispatchers | docs/claude/firebase-backend.md |
| Firestore collections, rules, schemas, types | docs/claude/firestore-schema.md |
| Deploying, hosting, builds, Firebase targets | docs/claude/deployment.md |
| Database debugging scripts, test tools | docs/claude/debugging-tools.md |
| Atlas REST API, webhooks, MCP server | docs/claude/atlas-api-webhooks.md |
| Dates, timezones, date-only vs datetime | docs/claude/date-time-handling.md |
| CI/CD, agents, monitoring, deploys, operations | docs/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 payment | docs/claude/client-booking-access-payment-contract.md |
| Plan tab — running fixes / open issues list | docs/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, Stripe | docs/claude/billing-pricing.md |
| Business strategy — pricing rationale, margins, ARR/scale models | docs/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.
| Service | Doc | Must-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
| What | Where |
|---|---|
| Firestore rules | basis-functions/firestore.rules |
| Storage rules | basis-functions/storage.rules |
| Firestore indexes | basis-functions/firestore.indexes.json |
| Firebase config | basis-functions/firebase.json |
| Dart models | hybrid/basiscore/lib/src/models/ |
| Python models | basis-functions/functions/src/model_*.py |
| Debugging tools | basis-functions/tools/ |
| Build scripts | sh/ and hybrid/sh/ |