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

Canonical source: docs/claude/firebase-backend.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.

Firebase Functions (Cloud Backend)

Performance: Always Use Warm Instances

Cloud Functions should use min_instances=1 for user-facing functions to avoid cold start delays (3-10+ seconds).

# GOOD - 1GB memory + warm instance
@https_fn.on_call(
memory=options.MemoryOption.GB_1, # ALWAYS use GB_1, not MB_512
min_instances=1,
cors=CorsOptions(...),
)

# BAD - Will OOM and have cold starts
@https_fn.on_call(memory=options.MemoryOption.MB_512)

Only skip min_instances for rarely used admin/maintenance functions.

Secret Scoping (CRITICAL — #1 cause of silent 500 errors)

Each Firebase Function only receives secrets explicitly listed in its secrets=[] parameter. Module-level SecretParam declarations don't auto-inject.

# Module level — does NOT make the secret available to all functions
STRIPE_SIGNER_SECRET_KEY: SecretParam = SecretParam('STRIPE_SIGNER_SECRET_KEY')

# This function CAN'T access STRIPE_SIGNER_SECRET_KEY
@https_fn.on_call(secrets=[STRIPE_API_KEY])
def my_function(req): ...

# This function CAN access it
@https_fn.on_call(secrets=[STRIPE_API_KEY, STRIPE_SIGNER_SECRET_KEY])
def my_function(req): ...

Before adding code that uses a SecretParam:

  1. Find the function's decorator
  2. Check if the secret is in its secrets=[] list
  3. If not, ADD it — otherwise you get a runtime 500

Dispatcher functions (like payment) route to many handlers. If you add a new handler that uses a secret the dispatcher doesn't declare, ALL calls to that handler will 500.

@init functions run at module load time. If @init accesses a secret, that secret must be in the secrets list of EVERY function in the module.

CORS Origins (REQUIRED)

Always include the FULL CORS origins list:

cors=CorsOptions(cors_origins=[
"http://localhost:3000", # Local dev - NEVER FORGET THIS
"http://localhost:3001", # Alt local dev
"http://localhost:5555", # Emulator
"http://127.0.0.1:5555", # Emulator alt
"https://app.basishealth.io", # Legacy Flutter web
"https://platform.basishealth.io", # Staff platform
"https://basis-hybrid.web.app", # Firebase hosting
])

Function Codebases

CodebasePathPurpose
defaultfunctions/Main backend (auth, payments, clinic, AI, etc.)
labsfunctions-labs/Lab-specific processing
transcoderfunctions-video/Video transcoding
functions-vertexfunctions-vertex/Vertex AI/LangChain operations

Key Callable Functions

FunctionFilePurpose
clinic_servicefunctions_clinic.pyMain clinic operations dispatcher
paymentfunctions_payment.pyStripe payment operations
permissionfunctions_permissions.pyRBAC permission management
clinical_assistantfunctions_ai.pyAI-powered clinical Q&A
clinical_quickfunctions_ai.pyFast clinical quick answers
clinical_streamfunctions_ai.pyStreaming AI responses

Key HTTP Functions (Webhooks)

FunctionPurpose
stripe_webhook / stripe_webhook_testStripe payment webhooks
stripe_general_webhookGeneral Stripe events
zoom_oauth_callbackZoom OAuth flow
google_oauth_callbackGoogle Meet OAuth flow
fullscript_oauth_callbackFullscript OAuth flow
fullscript_webhookFullscript order events
junction_webhookJunction Health lab results

New Firebase Function Checklist

  1. Memory: Use GB_1 (REQUIRED) — MB_512 causes OOM
  2. CORS: Include full origins list — missing localhost:3000 is #1 CORS error cause
  3. Test from localhost BEFORE marking complete
  4. Firestore Rules: If accessing new paths, add rules and deploy
  5. Assets/Images: If referencing images, verify file exists in public/
  6. API Endpoints: Verify exact endpoint path in official docs — don't guess

Deployment Commands

NEVER deploy all functions at once — always deploy specific functions only

cd /Users/G/basis/basis-functions

# Deploy specific function(s)
firebase deploy --only functions:clinic_service --force
firebase deploy --only functions:payment --force

# Deploy specific codebase
firebase deploy --only functions:labs

# Deploy hosting
firebase deploy --only hosting:platform

Third-Party Integrations

IntegrationFilePurpose
Terra APIfunctions_terra.pyUnified health device data (Fitbit, Apple Health, Garmin, etc.)
Stripefunctions_payment.pyPayment processing, subscriptions (see stripe-payments.md)
Zoomfunctions_zoom.pyVideo appointment meetings
Google Meetfunctions_google_meet.pyCalendar events with Meet links
Fullscriptfunctions_fullscript.pySupplement ordering and treatment plans
Junction Healthfunctions_junction.pyLab test ordering and results
Mailersendfunctions_appointment_emails.pyTransactional emails, branded domains
OpenAI / Vertex AIfunctions_ai.pyClinical assistant, copilot, document analysis

Data Flows

Appointment Booking

Client -> book_appointment (clinic_service)
-> Validate membership limits
-> Check slot availability + booking windows
-> Create scheduled event + transaction (audit)
-> Update usage counters
-> Process payment (if applicable) via Stripe
-> Send confirmation email
-> Return result

Health Data Sync

Wearable Device -> Terra API -> Webhook (Cloud Function)
-> Verify HMAC signature
-> Store raw data in GCS
-> Process in DuckDB
-> Mirror to Firestore healthSummaries
-> Mirror to clinic_users/{uid}/healthSummaries

Document Upload

Staff uploads -> Cloud Storage
-> Process document (Cloud Function)
-> AI text extraction/analysis (Vertex AI)
-> Store metadata in Firestore
-> Create documentLink in clinic_users
-> Return result

Creating Webhook Handlers

Webhooks receive HTTP POST requests from third-party services. They are fundamentally different from on_call functions.

Checklist for a New Webhook

  1. Use @https_fn.on_request — never on_call. Third-party services don't speak Firebase's callable protocol.
  2. Add POST method guard — return 405 for non-POST requests.
  3. Read raw bytes BEFORE parsing JSONrequest.get_data() then json.loads(body). If you call request.get_json() first, Flask may drain the stream, making signature verification impossible.
  4. Verify signatures with hmac.compare_digest() — never == (timing attack). Degrade gracefully during initial setup: accept if secret not yet configured.
  5. No CORS — webhooks are server-to-server. Don't add cors= parameter.
  6. Register in main.py — both the minimal-load FUNCTION_TARGET block AND the wildcard import * block.
  7. Use GB_1 memory unless the handler is trivially simple.
  8. Declare secrets explicitly in secrets=[]. Reference secrets from other modules by string name to avoid duplicate SecretParam errors.
  9. Always return 200 for acknowledged events, even unrecognized ones — returning 4xx/5xx causes third-party retries.
  10. Write an audit trail — log events to Firestore for debugging. Use an orphan collection for events that can't be matched to a clinic.

Common Gotchas

GotchaDetails
Raw bytes before JSONget_data() MUST come before get_json() when doing HMAC verification. Flask drains the stream on first read.
Duplicate SecretParamIf a secret is declared in another file, use secrets=['SECRET_NAME'] (string), not a new SecretParam('SECRET_NAME').
firebase.json rewrite optionalOnly needed if you want a branded URL (e.g., app.basishealth.io/my_webhook). Direct CF URL works without it.
Challenge-first orderingSome services (Slack) send a URL verification challenge before signing requests. Handle challenge BEFORE signature verification.
Retry deduplicationMost services retry on non-2xx. Either process idempotently (keyed on event ID) or reject retries (Slack: X-Slack-Retry-Num header).
No user contextWebhooks have no req.auth. Any Firestore operations need system-level access patterns, not permission-checked flows.
Event field name varianceThird-party payloads use inconsistent casing (EventType vs event_type vs type). Always check multiple field names.
Background threads are riskyOnly MailerSend uses threading.Thread for async processing. The function instance may be killed before the thread finishes. Process synchronously unless the sender has a very short timeout.

Template

WEBHOOK_SECRET = SecretParam('MY_SERVICE_WEBHOOK_SECRET')

@https_fn.on_request(
memory=options.MemoryOption.GB_1,
secrets=[WEBHOOK_SECRET],
)
def my_service_webhook(request) -> https_fn.Response:
if request.method != 'POST':
return https_fn.Response('Method not allowed', status=405)

# 1. Raw bytes first (for signature verification)
raw_body = request.get_data()

# 2. Verify signature
secret = WEBHOOK_SECRET.value
signature = request.headers.get('X-My-Service-Signature', '')
if secret and signature:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return https_fn.Response('Invalid signature', status=401)

# 3. Parse JSON
payload = json.loads(raw_body) if raw_body else {}

# 4. Dispatch on event type
event_type = payload.get('EventType') or payload.get('event_type') or 'unknown'

# 5. Write audit trail
fire.db.collection('myServiceEvents').document().set({
'eventType': event_type,
'payload': payload,
'receivedAt': fire.SERVER_TIMESTAMP,
})

# 6. Process event
# ...

return https_fn.Response(json.dumps({'status': 'ok'}), status=200)

Existing Webhooks Reference

HandlerFileSig VerificationRouting
fullscript_webhookfunctions_fullscript.pyNonefirebase.json rewrite
junction_webhookfunctions_junction.pyNonefirebase.json rewrite
sentry_webhookfunctions_sentry_webhook.pyHMAC-SHA256Direct CF URL
github_webhookfunctions_github_integration.pyHMAC-SHA256 (sha256= prefix)Direct CF URL
slack_eventsfunctions_slack_bot.pyHMAC-SHA256 (Slack v0: scheme)Direct CF URL
scribe_webhookfunctions_scribe.pyNoneDirect CF URL
mailersend_webhookfunctions_email_webhooks.pyHMAC-SHA256 (optional)Direct CF URL
dosespot_webhookfunctions_dosespot.pyHMAC-SHA256 (pending DoseSpot docs)Direct CF URL