| # Database deployment |
|
|
| MedOS supports **two database drivers** chosen at runtime by a single env var. |
|
|
| | `DATABASE_URL` set? | Driver | Use case | |
| |---|---|---| |
| | Yes, starts with `postgres://` or `postgresql://` | **Postgres** (production) | Vercel, Hugging Face Spaces, CI smoke tests | |
| | Unset or invalid | **SQLite** (fallback) | Local development, ephemeral previews, offline self-hosting | |
|
|
| The schema is the same on both drivers. Migrations apply driver-specific SQL automatically. |
|
|
| > **Refusing the fallback in production.** When `NODE_ENV=production` and `DATABASE_URL` is missing or invalid, the server refuses to start instead of silently coming up on SQLite. There is no separate flag — `NODE_ENV` already tells us this is production. Set `NODE_ENV=development` locally to opt into the SQLite fallback. |
|
|
| ## Choosing a Postgres host |
|
|
| The project is tested against **Neon** but works with any Postgres ≥ 13 — Supabase, Railway, RDS, Cloud SQL, plain `postgres:16`. Neon's pooler endpoint (`...-pooler....neon.tech`) is what the code expects for serverless deployments. |
|
|
| ## The one new env var |
|
|
| For the database migration itself you only have to add **one** variable: |
|
|
| | Var | Type | Example | |
| |---|---|---| |
| | `DATABASE_URL` | secret | `postgresql://USER:PASS@ep-xxx-pooler.region.aws.neon.tech/neondb?sslmode=require` | |
|
|
| Everything else (SSL mode, pool size, statement timeout, fallback path) has a sensible default baked into the code. Defaults are listed at the bottom of this doc — you only override them if a deployment proves you need to. |
|
|
| The pre-existing MedOS env vars stay the same; they were already required before this migration: |
|
|
| - `ADMIN_EMAIL`, `ADMIN_PASSWORD` — first-run admin seed. |
| - `ENCRYPTION_KEY` — at-rest encryption for BYO HF tokens. |
| - `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `FROM_EMAIL` — email transport. |
| - `APP_URL` — used in outbound email links. |
| - `NODE_ENV` — standard Node.js env tag. Set to `production` on Vercel + HF. |
|
|
| See `.env.example` for the consolidated list. |
|
|
| ## Setting env vars per platform |
|
|
| ### GitHub Actions |
|
|
| `Settings → Secrets and variables → Actions`. |
|
|
| - **Repository secrets** for everything tagged "**secret**" above. |
| - **Repository variables** for everything tagged "variable". |
|
|
| Per-environment scoping: if you have a `production` and a `staging` Action environment, set them separately so previews can target Neon's preview branches. |
|
|
| Reference in a workflow: |
|
|
| ```yaml |
| jobs: |
| smoke: |
| runs-on: ubuntu-latest |
| env: |
| NODE_ENV: production |
| DATABASE_URL: ${{ secrets.DATABASE_URL }} |
| ADMIN_EMAIL: ${{ vars.ADMIN_EMAIL }} |
| ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} |
| ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} |
| steps: |
| - uses: actions/checkout@v4 |
| - uses: actions/setup-node@v4 |
| with: { node-version: '20' } |
| - run: cd 9-HuggingFace-Global && npm ci && npm run db:migrate |
| ``` |
|
|
| ### Hugging Face Spaces |
|
|
| `Space → Settings → Variables and secrets`. |
|
|
| - **Secrets** for the sensitive set. |
| - **Variables** for the non-sensitive set. |
|
|
| Hugging Face Spaces persists `/data` between restarts on most Space hardware tiers, so the SQLite fallback works there if you choose not to use Postgres. With `DATABASE_URL` set, the Space writes to Postgres and the on-disk SQLite at `/data/medos.db` is unused. |
|
|
| Sample `space.yml` extract: |
|
|
| ```yaml |
| emoji: 🩺 |
| sdk: docker |
| app_port: 7860 |
| secrets: |
| - DATABASE_URL |
| - ADMIN_PASSWORD |
| - ENCRYPTION_KEY |
| - SMTP_PASS |
| variables: |
| NODE_ENV: "production" |
| ADMIN_EMAIL: "admin@medos.health" |
| SMTP_HOST: "smtp.sendgrid.net" |
| SMTP_PORT: "587" |
| SMTP_USER: "apikey" |
| FROM_EMAIL: "MedOS <noreply@your-domain>" |
| APP_URL: "https://your-space.hf.space" |
| ``` |
|
|
| ### Vercel |
|
|
| `Project → Settings → Environment Variables`. |
|
|
| - Tag every var for **Production** and **Preview** (you'll typically want a separate Neon database branch for Preview). |
| - The Neon connection string already includes `sslmode=require`; do not modify it. |
|
|
| You can also use Vercel's Neon integration to inject `DATABASE_URL` automatically per branch. |
|
|
| ## Migration command |
|
|
| After setting env vars, run migrations once before the first request: |
|
|
| ```bash |
| # Production (Postgres) |
| DATABASE_URL=postgresql://... npm run db:migrate |
| |
| # Development (SQLite) |
| npm run db:migrate |
| ``` |
|
|
| The same `npm run db:migrate` works for both drivers. Migrations are version-gated and idempotent — running twice is safe. |
|
|
| ## Connection-failure behavior |
|
|
| | Driver | Boot | First request | Mid-request | |
| |---|---|---|---| |
| | Postgres + `NODE_ENV=production` | Process exits 1 if connect fails (no silent fallback) | Same as boot | 503 response, structured log line | |
| | Postgres + `NODE_ENV=development` | Logs warning, falls back to SQLite | Reads from SQLite at `$DB_PATH` | Same | |
| | SQLite (dev only) | Always succeeds locally; fails only if path is unwritable | Reads from local file | n/a | |
|
|
| Production is always Postgres-required: when `NODE_ENV=production` and `DATABASE_URL` is missing or invalid, the server refuses to start. Silent fallback is convenient in development; it is a foot-gun in production, so we never enable it there. |
|
|
| ## Rotating the database password |
|
|
| If a credential is ever exposed (e.g., pasted into a chat, a screenshot, or a logged URL), rotate immediately: |
|
|
| 1. Neon → `Branches → production → Roles → Reset password` on the user. |
| 2. Update `DATABASE_URL` in GitHub Actions secrets, HF Spaces secrets, and Vercel env vars. |
| 3. Redeploy each platform so the new connection string takes effect. |
| 4. Audit recent logins for the rotated role (Neon → `Operations`). |
|
|
| Plan for a quarterly rotation regardless of incident. |
|
|
| ## Local development |
|
|
| ```bash |
| cd 9-HuggingFace-Global |
| npm install |
| npm run dev |
| ``` |
|
|
| Without `DATABASE_URL` set, the app uses SQLite at `./medos.db`. The first request triggers the migration runner; verification codes show up on stdout because SMTP is unconfigured. |
|
|
| If you want to develop against Postgres locally, run a container: |
|
|
| ```bash |
| docker run --name medos-pg -e POSTGRES_PASSWORD=devpass -p 5432:5432 -d postgres:16 |
| export DATABASE_URL="postgresql://postgres:devpass@localhost:5432/postgres" |
| npm run db:migrate |
| npm run dev |
| ``` |
|
|
| ## Cross-references |
|
|
| - `9-HuggingFace-Global/lib/db-adapter/` — implementation. |
| - `9-HuggingFace-Global/lib/db-adapter/migrations/` — schema definitions. |
| - `SECURITY.md` (repo root) — credential reporting and rotation policy. |
| - `THREAT_MODEL.md` (repo root) — assets, including DB credentials. |
| - `PRIVACY.md` (repo root) — what data lives in the DB and how it is retained / deleted. |
|
|