Skip to content

03 Deployment Architecture

Anubha Parashar edited this page Oct 10, 2026 · 1 revision

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.

Responsibilities

  • 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.

Production Deployment Architecture

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
Loading

Configuration files

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.

Scheduler wake-up

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 and storage wiring

  • DATABASE_URL is the Supabase session pooler URL. api/config.py rewrites postgres:// and postgresql:// to postgresql+psycopg://. api/database.py keeps the pool small (pool_size 5 + max_overflow 5 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 to sslmode=require.
  • At startup harden_database() enables RLS on every public table and revokes all privileges from anon and authenticated. In production a failure aborts startup.
  • prepare_storage() creates the bucket (default name medical-documents, 13 MB object limit) as private and refuses to run if it finds the bucket public. setup.sql creates it too and deliberately adds no storage.objects policies.

Environment variables (names only)

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
Email 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.

Free-tier constraints (from render.yaml and code comments)

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

Security boundaries

  • 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-src to the API origin only.
  • The scheduler endpoints answer 404 until a strong key is configured.

Implementation

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.

Related pages

System Architecture · Deployment and CI/CD · Encryption and Private Storage · Notifications and Reminder Architecture · Security and Privacy Model · Limitations

Clone this wiki locally