Repository navigation
27 Deployment and CI CD
This page describes how HealthBook AI (internal code name: BabyCare) is validated and packaged: the GitHub Actions workflow, the container image, the Docker Compose files for self-hosting, the deployment helper scripts and the encrypted backup tool. Continuous integration runs on GitHub's free Actions minutes and holds no medical data, cloud secrets or deploy keys. Deploys are separate from CI: Render auto-deploys the API from the private production repository on commit, and the owner publishes the web app with npx firebase-tools deploy. This public repository is a showcase and triggers no production deploy.
- Prove the backend on both supported databases (SQLite and PostgreSQL) with synthetic data only.
- Prove the web client parses and its user journeys work in a real browser.
- Prove the mobile app type-checks and bundles, and produce a test APK.
- Build a reproducible, non-root container from a lockfile.
| Input | Output |
|---|---|
Push to main, pull request, or manual workflow_dispatch
|
Workflow "Validate BabyCare" with four jobs |
requirements.lock |
Pinned Python environment in CI and in the Docker image |
package.json / mobile/package.json (with lockfiles) |
npm ci installs for Playwright and Expo |
vars.BABYCARE_API_URL (repository variable, optional) |
API URL baked into the debug APK; falls back to https://example.invalid
|
| Playwright failure |
playwright-results artifact, kept 7 days |
| APK job |
BabyCare-AI-debug.apk artifact, kept 30 days |
flowchart LR
TRIG["Push to main, pull request,<br/>or manual dispatch"]
subgraph backend["Job backend"]
B1["Python 3.12 + tesseract-ocr"]
B2["pip install requirements.lock<br/>compileall api migrations scripts"]
B3["pytest on SQLite<br/>BABYCARE_STORAGE local"]
B4["pytest on PostgreSQL 17 service<br/>synthetic data, migrations test skipped"]
B5["pip-audit, non-blocking"]
end
subgraph web["Job web"]
W1["node --check on every web JS file"]
W2["npm ci + Playwright Chromium"]
W3["browser_fixture.py<br/>synthetic PDF and photos"]
W4["playwright test<br/>uvicorn started by webServer"]
end
subgraph mobile["Job mobile"]
M1["npm ci in mobile"]
M2["tsc --noEmit"]
M3["expo export android + ios"]
M4["npm audit, non-blocking"]
end
subgraph apk["Job android-apk<br/>main or dispatch only"]
A1["Node 20 + Temurin JDK 17"]
A2["expo prebuild android"]
A3["gradlew assembleDebug"]
A4["Upload debug APK artifact"]
end
TRIG --> B1 --> B2 --> B3 --> B4 --> B5
TRIG --> W1 --> W2 --> W3 --> W4
TRIG --> M1 --> M2 --> M3 --> M4
TRIG --> A1 --> A2 --> A3 --> A4
The four jobs run in parallel on ubuntu-latest. They do not depend on each other.
| Job | Timeout | What it does |
|---|---|---|
backend — "Backend tests (SQLite + PostgreSQL staging)" |
25 min | Starts a postgres:17 service container with a pg_isready health check. Sets up Python 3.12 with pip cache, installs tesseract-ocr from apt, installs requirements.lock plus pip-audit, byte-compiles api, migrations and scripts. Runs python -m pytest -q against SQLite with BABYCARE_STORAGE=local, then runs the suite again against PostgreSQL (--ignore=tests/test_migrations.py) with BABYCARE_DB_SSLMODE=disable and the test database URLs pointing at the service. Ends with pip-audit -r requirements.lock reported but not failing the job. |
web — "Web checks + browser specs" |
25 min | Python 3.12 and Node 20. Runs node --check on every web/*.js, npm ci, npx playwright install --with-deps chromium, python scripts/browser_fixture.py (writes a clearly fictional vaccination-card PDF and synthetic stool photos with EXIF, WhatsApp-style and missing timestamps), then npx playwright test with CI=1. With CI set, playwright.config.js starts uvicorn api.main:app on 127.0.0.1:8000 with its own data directory and waits for /api/health. Uploads test-results on failure. |
mobile — "Mobile typecheck + export build check" |
25 min | Works in mobile/. npm ci, npm run typecheck (tsc --noEmit), then npx expo export --platform android --platform ios with EXPO_PUBLIC_ENV=production and a placeholder HTTPS API URL. This bundles the JavaScript for both platforms without a native build or signing. npm audit --omit=dev --audit-level=high is reported but non-blocking. |
android-apk — "Android debug APK (artifact, unsigned debug key)" |
40 min | Only on workflow_dispatch or refs/heads/main. Node 20 and Temurin JDK 17; uses the Android SDK preinstalled on the runner. npx expo prebuild --platform android --no-install, then ./gradlew assembleDebug --no-daemon in android/, and uploads app-debug.apk. The production config in mobile/app.config.js throws if the API URL is not https. |
The Playwright specs in tests/browser/ cover care, the care journal, export, the family workspace, lifetime health, password reset, review and confirm, sharing, stool intake and a "request storm" spec that guards the web client's request de-duplication.
| Step | Detail |
|---|---|
| Base | python:3.12-slim |
| System packages |
tesseract-ocr (no recommends), apt lists removed |
| Python packages | pip install --no-cache-dir -r requirements.lock |
| Contents |
api/, web/, migrations/, alembic.ini only; .dockerignore excludes .git, .env, private_data, mobile, tests, *.sqlite3, *.enc, logs |
| User |
babycare, uid 10001; /data owned by it with mode 700 and declared as a volume |
| Command |
uvicorn api.main:app --host 0.0.0.0 --port ${PORT:-8000} --no-access-log (Render injects PORT; access logs are off so URLs never reach logs) |
| File | Services |
|---|---|
docker-compose.yml |
babycare (API bound to 127.0.0.1:8000 only, volume babycare_private:/data, optional .env, health check calling /api/health) and worker (same image, python -m api.worker, starts after the API is healthy). Default database is SQLite in the volume. |
docker-compose.postgres.yml |
Overlay adding db (postgres:17, POSTGRES_PASSWORD required from .env, named volume, pg_isready health check) and pointing both babycare and worker at it through DATABASE_URL. Used as docker compose -f docker-compose.yml -f docker-compose.postgres.yml up --build. |
There is no separate migration step in any pipeline. Importing api/main.py calls api.migrate.upgrade(), which:
- takes a file lock (
migration.lockin the data directory, 60-second timeout) so the API and worker never migrate concurrently; - on SQLite, if the database has profile rows and is not at the Alembic head, writes a Fernet-encrypted snapshot
before-migration-<timestamp>.sqlite.enc; - runs
alembic upgrade head(migrations/versions/0001_legacy.pyto0010_password_reset.py).
The worker's main() runs the same upgrade() first. After migrations, harden_database() re-applies RLS and role revocation on every start.
| Script | Purpose |
|---|---|
scripts/configure_deployment.py |
Takes --api-origin (must be an https origin without a path) and --firebase-project (validated id). Writes web/config.js, firebase.json (from deploy/firebase.template.json, CSP connect-src set to the origin) and .firebaserc. With --scheduler-key or BABYCARE_SCHEDULER_KEY (at least 24 characters) it also writes the git-ignored deploy/supabase/setup.generated.sql. Contacts no service. |
scripts/build_android.ps1 |
Local Windows APK build. Runs expo prebuild for -Env local, staging or production, limits ABIs to arm64-v8a,x86_64, mirrors mobile/ to a short path (the native CMake/ninja step breaks on Windows' 260-character path limit), runs assembleRelease or assembleDebug, copies the APK to android-builds/. A staging build without an API URL is refused. Without a release keystore the APK uses the standard debug keystore; real signing reads BABYCARE_UPLOAD_STORE_FILE, BABYCARE_UPLOAD_STORE_PASSWORD, BABYCARE_UPLOAD_KEY_ALIAS, BABYCARE_UPLOAD_KEY_PASSWORD. |
scripts/cloud_backup.py |
create, verify, restore of a full encrypted backup (any database, any document store). Rows are dumped table by table (still application-encrypted), every stored object is added with its SHA-256, and the ZIP is encrypted with BABYCARE_BACKUP_KEY. The application key is excluded unless --include-app-key. verify checks row counts, hashes and that every document row has its original. restore upgrades the target schema, refuses a non-empty database and never overwrites an existing object. The output file is opened with exclusive create, so an existing backup is never overwritten. |
scripts/browser_fixture.py |
Synthetic fixtures for the Playwright job. |
scripts/import_bundle.py |
Imports a reviewed scan bundle into a local backend through the authenticated API; localhost targets only; everything created stays unconfirmed. |
- CI uses only throwaway credentials for its PostgreSQL service and placeholder API URLs; no repository secret is required by any job.
- The Docker image excludes
.env, data directories, databases and encrypted files. - The Compose API port is bound to loopback; the comment in
.env.examplestates the local server must never be exposed directly to the internet.
.github/workflows/ci.yml, playwright.config.js, Dockerfile, .dockerignore, docker-compose.yml, docker-compose.postgres.yml, api/migrate.py, api/worker.py, scripts/configure_deployment.py, scripts/build_android.ps1, scripts/cloud_backup.py, scripts/browser_fixture.py, api/datasync.py.
Deployment Architecture · Testing and Validation · Android Application Architecture · Database and Data Model · Technology Stack
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