Repository navigation
03 Deployment Architecture
HealthBook AI (internal code name: BabyCare) is deployed on free tiers only: a Docker web service on Render for the API, Firebase Hosting for the static web app, Supabase for PostgreSQL and a private storage bucket, and an Android APK that talks to the same API. This page describes how the pieces are wired, which configuration files drive them, which environment variables exist (by name only), and which free-tier constraints shaped the design. Production deploys come from a separate private repository; this public repository is a showcase of the same code and is not wired to any production deploy.
- Run one API instance that owns every secret and every rule.
- Serve the web app as static files with strict security headers.
- Keep durable state only in Supabase (the Render disk is ephemeral).
- Wake the sleeping free API on a schedule so reminders are reconciled and delivered.
flowchart TD
REPO["Private production GitHub repository"]
subgraph renderbox["Render free plan"]
BUILD["Docker build<br/>render.yaml + Dockerfile"]
API["FastAPI API<br/>uvicorn on PORT"]
end
subgraph fb["Firebase Hosting, Spark plan"]
SITE["Static web app<br/>firebase.json + config.js"]
end
APK["Android APK<br/>Expo prebuild + Gradle"]
subgraph sb["Supabase free plan"]
POOL["Session pooler<br/>IPv4, TLS"]
PG[("PostgreSQL")]
BUCKET[("Private bucket<br/>RLS on, zero policies")]
CRON["pg_cron job every 10 minutes<br/>pg_net HTTP POST"]
end
BROWSER["Browser"]
PHONE["Android phone"]
REPO -->|"auto-deploy on commit"| BUILD
BUILD --> API
REPO -->|"firebase-tools deploy, run by the owner"| SITE
BROWSER -->|"HTTPS static files"| SITE
BROWSER -->|"HTTPS API calls to apiBase"| API
PHONE --> APK
APK -->|"HTTPS API calls"| API
API -->|"DATABASE_URL"| POOL
POOL --> PG
API -->|"service-role key over HTTPS"| BUCKET
CRON -->|"POST internal scheduler tick<br/>X-Scheduler-Key header"| API
| File | Role |
|---|---|
render.yaml |
Render Blueprint: one type: web, runtime: docker, plan: free service, healthCheckPath: /api/health, autoDeployTrigger: commit, and the environment variable list |
Dockerfile |
python:3.12-slim, installs tesseract-ocr, pip install -r requirements.lock, copies api/, web/, migrations/, alembic.ini; runs as non-root uid 10001; uvicorn api.main:app --port ${PORT:-8000} --no-access-log
|
firebase.json |
Hosting config: public: web, security headers (nosniff, no-referrer, DENY framing, HSTS, CSP whose connect-src names the API origin), no-cache for sw.js, config.js, index.html
|
deploy/firebase.template.json |
Same file with an __API_ORIGIN__ placeholder |
.firebaserc |
Default Firebase project id |
web/config.js |
Sets window.BABYCARE_CONFIG.apiBase only when location.hostname is one of the two Firebase Hosting domains; anywhere else the app stays same-origin /api
|
deploy/supabase/setup.sql |
Idempotent one-time SQL: enables pg_cron and pg_net, creates the private bucket, schedules the tick job |
mobile/app.config.js, mobile/eas.json
|
Environment-aware Expo config (EXPO_PUBLIC_ENV set to local, staging or production); non-local builds must use an https API URL or the config throws |
scripts/configure_deployment.py --api-origin https://<api>.onrender.com --firebase-project <project> regenerates web/config.js, firebase.json and .firebaserc from the template, and, when a scheduler key is supplied, writes deploy/supabase/setup.generated.sql (git-ignored because it contains that key). It contacts no service.
Free Render instances sleep after 15 idle minutes, so an in-process loop cannot be relied on. Instead setup.sql schedules a pg_cron job named babycare-reminder-tick on */10 * * * * that uses net.http_post to call https://<api>.onrender.com/api/internal/scheduler/tick with an X-Scheduler-Key header and a 90-second timeout. The comments also name cron-job.org as an alternative caller.
api/cloud.py implements the endpoint:
| Check | Behaviour |
|---|---|
BABYCARE_SCHEDULER_KEY unset or shorter than 24 characters |
404, feature disabled |
Header missing or not equal (constant-time hmac.compare_digest) |
403 |
| Accepted | Runs one api.worker.tick(): reconciles confirmed plans for every profile, delivers due notifications, updates worker_heartbeat; returns only a count of notifications awaiting delivery |
GET /api/internal/status (same key) reports database dialect, Alembic version, hardening state, storage backend, push and email configuration, last worker tick and notification counts by status, without clinical content. Because the tick queries the database through the pooler, it also counts as activity for the Supabase free project's inactivity pause.
-
DATABASE_URLis the Supabase session pooler URL.api/config.pyrewritespostgres://andpostgresql://topostgresql+psycopg://.api/database.pykeeps the pool small (pool_size5 +max_overflow5 by default, below the free pooler's per-project connection limit), recycles connections every 900 seconds, disables server-side prepared statements (prepare_threshold: None) and defaults tosslmode=require. - At startup
harden_database()enables RLS on every public table and revokes all privileges fromanonandauthenticated. In production a failure aborts startup. -
prepare_storage()creates the bucket (default namemedical-documents, 13 MB object limit) as private and refuses to run if it finds the bucket public.setup.sqlcreates it too and deliberately adds nostorage.objectspolicies.
| Group | Variables |
|---|---|
| Runtime mode |
BABYCARE_ENV, BABYCARE_DATA_DIR, PYTHONUNBUFFERED, PORT (injected by Render) |
| Database |
DATABASE_URL, BABYCARE_DB_SSLMODE, BABYCARE_DB_POOL_SIZE, BABYCARE_DB_MAX_OVERFLOW, BABYCARE_DB_POOL_TIMEOUT
|
| Encryption |
BABYCARE_FERNET_KEY (required in production), BABYCARE_BACKUP_KEY (backup archives only) |
| Storage |
BABYCARE_STORAGE, BABYCARE_ALLOW_LOCAL_STORAGE, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_STORAGE_BUCKET
|
| HTTP |
BABYCARE_CORS_ORIGINS, BABYCARE_RATE_LIMIT_STORE, BABYCARE_PUBLIC_WEB_ORIGIN
|
| Scheduler |
BABYCARE_SCHEDULER_KEY (Render generates it: generateValue: true) |
| Push |
BABYCARE_PUSH_ENABLED, EXPO_ACCESS_TOKEN, BABYCARE_VAPID_PUBLIC_KEY, BABYCARE_VAPID_PRIVATE_KEY, BABYCARE_VAPID_CONTACT
|
BABYCARE_EMAIL_PROVIDER, BABYCARE_EMAIL_API_KEY, BABYCARE_EMAIL_FROM
|
|
| Optional vision |
BABYCARE_VISION_URL (empty in render.yaml), BABYCARE_VISION_MODEL, BABYCARE_AI_KEY
|
| OCR |
TESSERACT_CMD (local Windows only) |
| Mobile build |
EXPO_PUBLIC_ENV, EXPO_PUBLIC_API_URL_PRODUCTION, EXPO_PUBLIC_API_URL_STAGING, EXPO_PUBLIC_API_URL_LOCAL, EXPO_PUBLIC_WEB_ORIGIN
|
Secrets in render.yaml are declared with sync: false, so their values are entered in the Render dashboard and never committed. render.yaml warns that BABYCARE_FERNET_KEY must stay identical to the key used to encrypt existing records.
| Constraint | Design response |
|---|---|
| Free instances sleep after 15 idle minutes and share 750 instance-hours per month | pg_cron tick every 10 minutes; web client error text warns that waking can take up to a minute |
| Render disk is ephemeral | Originals in the private Supabase bucket, records in Supabase PostgreSQL; /data is scratch only (locks, temp); production refuses local storage without explicit opt-in |
| One free instance |
BABYCARE_RATE_LIMIT_STORE=memory: per-process limiter, no database write per request |
| Outbound SMTP ports blocked on Render free | Email over HTTPS APIs (Brevo or Resend); SMTP only for a local PC |
| Supabase free pooler connection limit | At most 10 connections from the instance |
| Supabase free projects pause when inactive and keep no automatic backups | Tick doubles as activity; scripts/cloud_backup.py produces user-held encrypted backups |
| Expo push needs device testing first |
BABYCARE_PUSH_ENABLED=false by default |
| Paid AI vision |
BABYCARE_VISION_URL left empty |
- Only the API holds
SUPABASE_SERVICE_ROLE_KEY,BABYCARE_FERNET_KEY, VAPID private key and email API key; the web app and APK contain only public values (API origin, web origin). - Firebase Hosting and the API both send a CSP; the hosted CSP allows
connect-srcto the API origin only. - The scheduler endpoints answer 404 until a strong key is configured.
render.yaml, Dockerfile, firebase.json, deploy/firebase.template.json, deploy/supabase/setup.sql, web/config.js, scripts/configure_deployment.py, api/cloud.py, api/config.py, api/database.py, api/storage.py, mobile/app.config.js.
System Architecture · Deployment and CI/CD · Encryption and Private Storage · Notifications and Reminder Architecture · Security and Privacy Model · Limitations
Overview
Platform
Identity and access
Medical records
- 10 Document Ingestion
- 11 OCR and Human Verification
- 13 Medicines and Administration
- 14 Vaccination Model
- 15 Labs, Growth, Longitudinal
- 16 Illness and Lifetime Health
Daily care
Output and sharing
Security
Engineering
- 26 Testing and Validation
- 27 Deployment and CI/CD
- 28 Technology Stack
- 29 Design Decisions
- 30 Limitations
- 31 Roadmap