Environments, branches and releases
This page is for developers and the person who releases SBDMS. It explains the environments that exist, which branch feeds each, and the exact steps of a release. The source of truth for the branch rules is docs/BRANCHING.md in the repository.
The environments
Section titled “The environments”| Environment | Who uses it | What it runs |
|---|---|---|
Production (sbdms.flywingsitsolutions.com) |
Health workers, admins, families | The main branch, released by hand: frontend, backend, Caddy and this guide as Docker containers on the production VM. |
| QC (alpha) | QA and the client’s testers | The qc branch. A separate Compose project on the same VM with its own ports and database. It carries a full translation dictionary that production does not. |
| Mobile backend | The Flutter app in sb-mobile/ |
The mobile branch on a different host. The mobile app never talks to the production API. |
| Local | You | Whatever you have checked out. See Running it locally. |
The QC database was copied from production once and has drifted since. It holds real family data; treat it as sensitive.
Branch to environment map
Section titled “Branch to environment map”| Branch | Worktree folder | Environment | Purpose |
|---|---|---|---|
main |
swarnabindu-prod |
Production (manual release) | Production work lands here and is released from here |
qc |
swarnabindu-alpha |
QC | What QA looks at |
mobile |
swarnabindu |
Mobile backend | Backend the Flutter app talks to |
All three are git worktrees of one repository. A fix written on one branch is cherry-picked to the others, not rewritten.
The branches were renamed on 12 September 2026 so each name says which environment it is: consolidate/jun became qc, and mobile-backend became mobile. The old staging branch is retired — it is the same commit as main and nothing deploys from it.
Release steps
Section titled “Release steps”-
Land the change on
main, in theswarnabindu-prodworktree. -
Port the same change to
qcandmobilewithgit cherry-pick. -
Deploy QC first and check it there. QC is what QA looks at, and it has the translation layer, so a template that calls
$store.i18n.t(...)works on QC and throws on production. Check shared components on both. -
Look at what you are about to release:
git log --onelineagainst what production is serving. A deploy ships everything committed onmain, not just your fix. -
Release production by building on the VM, from the
swarnabindu-prodworktree:Terminal window scripts/deploy-vm.sh prod both -
Verify with evidence, not with a green tick: the backend
healthzanswers, the served commit hash on the landing page has changed, and the behaviour you shipped works in a browser against production.
The CI workflows are retired
Section titled “The CI workflows are retired”Both GitHub Actions deploy workflows were retired on 12 September 2026. Nothing is built in CI any more. Everything is built on the VM by scripts/deploy-vm.sh.
They are switched off in three independent ways, so no single mistake can restart them:
- They are disabled in GitHub (
gh workflow disable), so no run starts for any reason. - Neither has an automatic trigger any more.
deploy.ymlanddeploy-alpha.ymlare bothworkflow_dispatchonly. - Every job is gated on the repository variable
CI_DEPLOY_ENABLED, which is nowfalse. While it is not the literal stringtrue, every job skips.
The files are still in the tree because they document what the CI shape was, not because anyone should use them.
Why they were retired
Section titled “Why they were retired”The hosted runners for this repository queue for long stretches or fail to start, so in practice every real deploy already went through the VM script. Keeping a second path alive meant maintaining the build arguments twice — and they drifted.
The drift was the real damage. The workflows baked an absolute http://<ip>:<port>/api API base, which the HTTPS hosts reject as mixed content, and for QC they left the offline feature flag switched off. Because CI re-ran on every push, the CI image quietly replaced whatever had just been deployed by hand. That is why the offline toggle kept vanishing from QC after being deployed correctly: the deploy worked, and then a push to the branch overwrote it.
scripts/deploy-vm.sh is now the only place build arguments are written down, and it checks the offline flag is really present in the built bundle before it restarts anything.
If CI is ever needed again
Section titled “If CI is ever needed again”gh variable set CI_DEPLOY_ENABLED --body true --repo govtprojs/swarnabindugh workflow enable deploy.yml --repo govtprojs/swarnabinduCopy the build arguments out of scripts/deploy-vm.sh first. The ones committed in the workflow files are the stale set described above.
The VM-direct deploy script
Section titled “The VM-direct deploy script”scripts/deploy-vm.sh <prod|alpha> [frontend|backend|both|docs] is how everything ships. It builds on the VM, driven from your machine:
- Refuses to run if the part of the working tree it is about to ship is dirty. Commit first.
- Tars
frontend/,backend/ordocs-site/(withoutnode_modules,bin,obj,dist) and streams it over SSH into a source folder on the VM. That folder is deleted first, so the image can only contain what you shipped. Why: tar extraction never removes files a branch has deleted, and production once served a page no branch contained. - Runs
docker buildon the VM with the right tag and, for the frontend, the rightPUBLIC_API_URL. - For QC, copies
deploy/alpha/docker-compose.ymlup to the VM first, so a change to the compose file (a new environment variable, say) ships with the code rather than needing a separate manual edit on the VM. - Runs
docker compose up -din the right compose folder and prunes old images.
For the frontend it also reads PUBLIC_OFFLINE_FEATURE_ENABLED back out of the built JavaScript and aborts if it is not what was asked for, rather than restarting a build that silently lost the flag.
Run it from the worktree whose code you want deployed: prod from swarnabindu-prod on main, alpha from swarnabindu-alpha on qc.
Per-environment frontend builds
Section titled “Per-environment frontend builds”Astro reads PUBLIC_API_URL at build time and bakes it into the JavaScript. Both environments are built with the relative value /api, so the browser calls whichever host served the page and Caddy routes it onward. That is what lets QC answer on both its HTTPS name and its bare port.
Never bake an absolute URL. A build pinned to http://<ip>:<port>/api works on the bare-port address and fails on the HTTPS one, because the browser blocks a plain-HTTP call from an HTTPS page as mixed content. It is not a visible failure — every page still renders, only the data calls die, so the site looks up while nothing loads.
PUBLIC_OFFLINE_FEATURE_ENABLED is baked the same way. It has been true on both QC and production since 2 October 2026. The flag only reveals offline mode; nothing appears for staff until a SuperAdmin or GeneralAdmin switches it on for a camp site, and the backend also needs Offline__Enabled=true in its environment. Build the frontend per environment from that environment’s branch, and never retag sb-fe across them. The backend image has no build-time coupling and is safe to retag.
Where things live on the production stack
Section titled “Where things live on the production stack”Paths are relative to the infra repository’s checkout on the VM; hosts and credentials are deliberately not written here.
| Thing | Location |
|---|---|
| Compose file and Caddyfile | Repository root and its caddy/ folder, bind-mounted into containers. |
| SQLite database | apps/sb/data/app.db, mounted into the backend as /app/data. This one file is the whole system; copy it before any risky operation. |
| Backend logs | apps/sb/logs/, mounted as /app/logs. One compact-JSON file per day, rolling at 256 MB. Grep by TraceId. Container output is also visible through Dozzle. |
| Caddy TLS state | Docker volumes caddy_data and caddy_config. |
Never sed -i the bind-mounted Caddyfile (a new inode breaks the mount). Write a temp file, cat it over the original, validate, then reload.
Backups
Section titled “Backups”A daily snapshot of the production database is taken at 20:30 Nepal time with SQLite’s online backup API (sqlite3 .backup), which is atomic and does not interrupt the app. The compressed copy goes to cloud storage and to a db_backups branch of the infra repository. That branch is kept off main so the deploy script’s git pull never drags snapshots onto the VM; never merge it. The job currently runs from a maintainer’s machine, a known gap tracked for migration to the VM. Proof that backups work is a recent dated file, not the existence of the job.
How this guide is built and published
Section titled “How this guide is built and published”This site is the docs-site/ folder of the same repository: a Starlight (Astro) project with plain Markdown pages under src/content/docs/.
scripts/deploy-vm.sh prod docsshipsdocs-site/to the VM and runs its Dockerfile there.- The Dockerfile installs dependencies, runs
npm run check:words(the build fails if a banned word appears anywhere insrc/orpublic/), runsastro build, and copies the static output into a small Caddy image taggedsb-docs:latest. - The infra repository’s compose file runs that image as the
docsservice. - The edge Caddy terminates TLS for
sbdocs.flywingsitsolutions.comand proxies to thedocscontainer.
There is no QC copy of the guide; it describes what production does today.