Skip to content

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.

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

  1. Land the change on main, in the swarnabindu-prod worktree.

  2. Port the same change to qc and mobile with git cherry-pick.

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

  4. Look at what you are about to release: git log --oneline against what production is serving. A deploy ships everything committed on main, not just your fix.

  5. Release production by building on the VM, from the swarnabindu-prod worktree:

    Terminal window
    scripts/deploy-vm.sh prod both
  6. Verify with evidence, not with a green tick: the backend healthz answers, the served commit hash on the landing page has changed, and the behaviour you shipped works in a browser against production.

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.yml and deploy-alpha.yml are both workflow_dispatch only.
  • Every job is gated on the repository variable CI_DEPLOY_ENABLED, which is now false. While it is not the literal string true, every job skips.

The files are still in the tree because they document what the CI shape was, not because anyone should use them.

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.

Terminal window
gh variable set CI_DEPLOY_ENABLED --body true --repo govtprojs/swarnabindu
gh workflow enable deploy.yml --repo govtprojs/swarnabindu

Copy the build arguments out of scripts/deploy-vm.sh first. The ones committed in the workflow files are the stale set described above.

scripts/deploy-vm.sh <prod|alpha> [frontend|backend|both|docs] is how everything ships. It builds on the VM, driven from your machine:

  1. Refuses to run if the part of the working tree it is about to ship is dirty. Commit first.
  2. Tars frontend/, backend/ or docs-site/ (without node_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.
  3. Runs docker build on the VM with the right tag and, for the frontend, the right PUBLIC_API_URL.
  4. For QC, copies deploy/alpha/docker-compose.yml up 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.
  5. Runs docker compose up -d in 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.

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.

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.

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.

This site is the docs-site/ folder of the same repository: a Starlight (Astro) project with plain Markdown pages under src/content/docs/.

  1. scripts/deploy-vm.sh prod docs ships docs-site/ to the VM and runs its Dockerfile there.
  2. The Dockerfile installs dependencies, runs npm run check:words (the build fails if a banned word appears anywhere in src/ or public/), runs astro build, and copies the static output into a small Caddy image tagged sb-docs:latest.
  3. The infra repository’s compose file runs that image as the docs service.
  4. The edge Caddy terminates TLS for sbdocs.flywingsitsolutions.com and proxies to the docs container.

There is no QC copy of the guide; it describes what production does today.