think
16px
820px

Prod release plan — dms.val.id (VM1)

Written 2026-08-20. Nothing has been changed on prod. This is the plan to agree before
anything is.

The trigger is "wire dms.val.id to the licence check". That turned out not to be a config
change, so it becomes a rider on the next release instead — which is the honest framing,
because the release is much bigger than the thing that prompted it.

1. Why the check cannot be switched on by itself

Two independent blockers, both on the deployment side:

blocker evidence
prod's compose has no LICENSE_CHECK plumbing grep -c LICENSE_CHECK docker-compose.yml0 on dms1 (VM2 has it at line 618)
prod's binary has no client code runs obscura-server:v1.0.1 = commit 264b6d05; internal/licensecheck first appears in bb2a4fb4, 5 commits later

So pinning the current image — the safe instinct — is precisely what prevents it working.
Both blockers disappear on their own once prod moves to a build that contains the client:
the compose file arrives with the checkout, the client arrives with the image. No
licence-specific work is needed in the release.
Afterwards it is three minutes:

  1. Issue the token (-issue-token "dms.val.id — Cloud Enterprise (dms + sakti)" -customer "PT Virtue Digital Indonesia")
  2. Add LICENSE_CHECK_URL + LICENSE_CHECK_TOKEN to prod.env
  3. Recreate, confirm the boot line licence check enabled and the reported
    db:cfc0d84937ccfad09186e19b3ebc764e

2. What the release actually carries — this is the risk

Prod is 55 commits behind main and at schema 209. Four tenant migrations pending,
and they run against both t_dms and t_sakti:

migration shape risk
00210_letters_are_documents 218 lines; INSERT INTO documents, INSERT INTO document_versions, UPDATE documents 🔴 data migration — materialises a document per existing letter
00211_acl_grant_reason additive column, default '' trivial, reversible
00212_letter_filing_rules new table trivial
00213_letter_comments_are_document_comments moves comments, then DROP TABLE letter_comments 🔴 moves data and drops a table

No new control-plane migrations (control is at 22 and none were added), so the
Cloud-only control set is not a factor this time.

The licence check is the smallest thing in this release by a wide margin. The letters →
documents restructure is the event; treat the release as that, with the check as a footnote.

3. Preflight (read-only, do all of it)

./deploy/obscura doctor                      # image staleness; catches the stale-sidecar trap
cat /run/obscura-updater/status.json         # ABSENT on prod — no updater, no nightly channel
docker inspect deploy-obscura-1 --format '{{index .Config.Labels "com.docker.compose.project.config_files"}}'

Facts already established, so nobody has to rediscover them:

  • Three compose files: docker-compose.yml, docker-compose.prod.yml, docker-compose.vm1.yml.
    This is the OBSCURA_COMPOSE_FILES colon-separated trap that 502'd prod once already.
  • Env file is prod.env (0600, vdi). .env also exists and is nearly empty — it is
    not what prod deploys with. --env-file makes compose ignore .env entirely.
  • No updater container, no channel setting. Prod is deployed deliberately against a
    release tag, unlike VM2's nightly GHCR channel. There is no "running tag" file to read
    back, so the tag must be stated explicitly.
  • Tenant schemas: t_dms, t_sakti. Both get all four migrations.

4. Sequence

  1. Cut a release that contains the client. Prod is on a release tag, and per the
    rollout notes stable was never cut past v1.0.1 — so "a new image landing" on the
    nightly channel does not by itself give prod anything to install. This step is the
    real prerequisite.
  2. Back up first, and verify the backup exists — not just that the command exited 0.
    Two migrations move data and one drops a table; this is the only real undo.
  3. Update the checkout, then build obscura-backup, then deploy — in that order. A
    stale local tag makes update.sh exit 1 in silence.
  4. Deploy with all three compose files and --env-file prod.env. State the image tag
    explicitly; OBSCURA_SERVER_IMAGE is un-persisted and its fallback is a stale local
    image (this rolled VM2 back today).
  5. Verify migrations landed on both tenantsmax(version_id) should read 213 in
    t_dms and t_sakti, not just one.
  6. Smoke-test letters and comments on both tenants. That is where the data moved.
  7. Then, and only then, the three-minute licence wiring from §1.

5. Rollback

  • Image: redeploy the previous tag (v1.0.1) — trivial, that is what pinning buys.
  • Schema: not trivial. 00213 drops letter_comments and its down-migration
    recreates and refills it; 00210 backfills documents. Rolling back after users have
    written data means restoring from the backup, not running goose down. Decide the
    cutoff before starting: after smoke-test passes, forward-fix only.

6. Open questions

  1. Who cuts the release, and does it come off main or a release branch?
  2. Maintenance window? dms and sakti are both live; 00210 rewrites letter data for
    both. Off-hours is worth it.
  3. Has letters-are-documents been exercised anywhere with real data? The demo runs it,
    but with demo-sized data. Prod has real correspondence, and two tenants.
  4. Backup restore drill — has one been done recently enough to trust step 2?

Not blocking any of this

The portal is live and does not depend on the release. VM2 and the x056 demo are already
checking in. Prod's licence runs to 2035-12-31, so nothing expires while this waits,
and the check is advisory — an unwired prod behaves exactly as it does today.