Skip to content

Changelog

This project does not use tagged releases yet. Application code deploys continuously from main (see Architecture). Three things deliberately do not: schema migrations run only under the armed, authorization-gated migrate path — the migrate-production workflow or the equivalent npm run deploy:migrate fallback, both enforcing the same manifest — the Android app publishes on manual dispatch only, and the accounting stack is redeployed only by account-admin-enable. There is also a non-CI manual path (npm run deploy:*) for when GitHub Actions is unavailable. The history below groups the repository's actual git history by theme.

2026-08-18 — A manual deploy path, and migration 13

The deployment story gained a second driver and the migration protocol got a correctness fix.

  • Manual deployment fallback (3b51a39, 3a8e119, d627b64, 86fd07a). A root package.json — no dependencies, purely operational — fronts deploy/scripts/manual-deploy.sh with npm run deploy:preflight, deploy:status, deploy:backend, deploy:website, deploy:admin, deploy:migrate, deploy:all and deploy:dry-run. Its header is explicit that this is not a shortcut: it is the same sequence driven over SSH instead of by a runner, and it refuses in all the same places — verified backup, pending-migration refusal, Caddyfile bind-mount staleness check. Two build transports: build locally for linux/amd64 and ship the image with docker save | gzip | ssh docker load, or --transport server to rsync the source and build on the VPS. The website is forced onto the server transport because bun dies with SIGILL under QEMU's amd64 emulation on arm64. Operator-visible: this is the path that actually applied migration 13.
  • Manual-migrate image tag fixed (6bf91a8). npm run deploy:migrate derived the image tag from HEAD, but arming is itself a commit, so HEAD is always one ahead of the built image — every manual migration would have refused with "build it first". deploy/scripts/manual-deploy.sh now reads the tag from the manifest's approved_commit_sha, and dies loudly when the manifest is disarmed. The CI workflow was unaffected. The authorization was re-armed against the fix (02b7217) before being spent.
  • Migration 13 — dynamic product pricing (6ad6ee8b46b470). DynamicProductPricing1786200000000. A product now prices either FIXED or DYNAMIC; a dynamic row derives from the live 24K rate × karat with making charge and discount applied, and the response carries priceBasis: LIVE|FIXED so no client assumes a stored snapshot is live. Armed as AUTH-2026-08-18-DYNAMIC-PRODUCT-PRICING-001, spent, and disarmed the same day.

2026-08-17 — Integer paise in the cart, server-generated passwords

  • The cart computes in integer paise (d6e6ea7). Line totals, GST and delivery are integer arithmetic end to end. The website mirrors the server's arithmetic in paise so it can send expectedTotal, which the server compares before the insert — so a 409 PRICE_CHANGED does not consume the idempotency key and the client can re-confirm with the same one.
  • Account Admin temporary passwords are server-generated (e1a2d45, 26771b5). An administrator no longer chooses another user's first password; the server mints it and displays it exactly once. Operator-visible: if the one-time display is missed, the password must be regenerated, not recovered.
  • Clipboard failure surfaced (2325b64). The credential dialog's copy-to-clipboard failed silently behind a "paste doesn't work" report. This is in aim-gold-account-admin, not the operations portal.
  • Read-only VPS access diagnostics (034bdc9, 42f1d93). A dispatch-only workflow that reports reachability and the git revision baked into each running image.

2026-08-16 — Account Admin RBAC and the governance carve-out

  • RBAC hardening (f82c223). Closed the super-admin identity gap, made refusals auditable, and shipped Create User / Change Role.
  • Phase 13 — governance survives read-only (21d556b). Read-only mode refuses every mutating request, but six Super-Admin governance writes are carved out, because a deployment that cannot manage its own access is a deployment nobody can recover. The exemption is read at handler level only, never inherited from the controller class, so adding a mutating route to an exempt controller cannot silently acquire the exemption.
  • accounting_audit_events has no created_at (bdac174). Its timestamp column is at. A query ordering by created_at fails outright.

2026-08-15 — DIGITAL SELL built, gated, and frozen

The largest functional push in this period. Sell was named everywhere and enforced nowhere; it is now built to roughly 96% and deliberately frozen.

  • The gates that were named everywhere and enforced nowhere (f845f23). DIGITAL_SELL_ENABLED must spell the word true, compared case-insensitively — 1, yes, empty, __FILL_ME__ and unset are all off. The refusal is 503, not 403, because it is not about the customer. The assertion is the first statement of the sell path, so no partial work happens before it.
  • Invariants, concurrency and honesty (51cafbb, 8e4037a, a5e4d8b, d0f9247, 250ccdd). applySell was debiting one order three times under concurrency. Sell is now priced from a stored quote rather than a 10-second cache, requires a caller-owned verified bank account (a sell against an unverified one would debit the gold and then fail to pay for it), and the sell screen stopped telling the customer three things that were not true.
  • Frozen at ~96% (0e0056a). What blocks the rest is recorded rather than worked around — principally that no approved sell-accounting policy exists, so the correct debit and credit are not yet knowable.
  • Ask the deployment, not the repository (949dffa, 4a3f7b0). A production-sell-gate-verify workflow probes the running system; the first version of the probe was proving DTO validation rather than the kill switch.
  • KYC: the authorisation, the decision path, and the BUY gate (de62ba4).
  • Phase 7 — six P0 engineering blockers closed (a9d604f), followed by three closeout passes: six internal defects found by the pre-external inventory (eca6058), holdings truth, the website customer flow and payout webhook identity (406d81a), and the last seven dependency-free findings (24acc56, 08-16).
  • Website legal identity passed at build time (9528b6f). Every legal and support string is a build arg that renders as nothing unless it is both set and named in the verified-fields allowlist — so a misconfiguration produces an absence, never a false claim.
  • Android publish is manual-only, and Flutter finally has CI (2ac9094). It previously fired on every push to main and published straight to real internal testers with no PR check having run.

2026-08-13 — The Account Admin portal is published

  • One public hostname, two upstreams, no second door (ea40e89). accounts.aimgold.org went live: the portal owns /, the accounting API owns /api/v1/accounting/*. Because they share an origin the browser makes no cross-origin request, so ACCOUNTING_PORTAL_ORIGIN stays unset and the API's CORS stays shut. accounts-api.aimgold.org is deliberately not published — a second hostname would double the public surface for no gain. A private-tunnel approach was tried first (5ff531d, c8723ef, 9024d40). Operator-visible: this portal reads every customer's financial position and is now reachable from the internet. Two controls remain: the accounting compose profile keeps both services out of a default docker compose up, and ACCOUNTING_PORTAL_MODE defaults to readonly.
  • Migration 12 — exactly-once checkout (c68de73, a86233a, 497f717, 27992cb). ProductOrderIdempotency1786100000000 added a partial unique index on (user_id, idempotency_key), making the only live commerce path exactly-once at the database rather than in application logic.
  • Off-host backup: the owner's decision, recorded (1bd7a02). OFFSITE_BACKUP_MODE=disabled in deploy/backup-policy.env. Cloudflare R2 was evaluated and rejected; the residual risk — a total VPS loss takes the database and every archive with it — is explicitly accepted, not overlooked. The same commit stopped a green run that meant nothing.
  • Stop shipping encrypted PII to the lowest admin role (f52f59e).

2026-08-08 — Production migrations become an armed, gated act

The single most consequential change to how this system is operated.

  • SINGLE_OWNER_CONTROL_MODE adopted (db1829e, e544806, 646fb05, ebd7a30). deploy/production-migration-authorization.json became the arming control for altering the production database, enforced by aim-digigold-real/scripts/verify-migration-authorization.js. Four bindings must all hold at dispatch: commit-bound (only the manifest itself may differ from HEAD), list-bound (the exact pending migrations, in order), operator-bound (the arming login must be the triggering login), and single-use (a one-time id recorded in a server-side consumed ledger). Plus refs/heads/main, an unexpired expires_at, a typed confirm=MIGRATE, and a verified backup. This is not four-eyes approval — one account arms and spends, and the manifest says so in capitals. It buys deliberateness, not independent review. Operator-visible: deploy-backend now refuses to deploy while any migration is pending, so a release carrying one takes three steps — deploy (builds, then refuses) → migrate → deploy again.
  • The accounting schema reached production under three successive authorizations — AUTH-2026-08-08-ACCOUNTING-SCHEMA-001, -002 and -003 (c59b3ca, a8b7184, 0584194, 3b6e31f) — with the SellEventFacts columns verified against the live tables they altered (aa2900f). Two gate bugs were found and fixed while doing it: the pending list was parsed from "everything before" rather than a bounded region (fd56a9a), and the stdout the gate depends on was not being captured (7bad0e6).
  • Read-only mode, enforced server-side (5e5db40, 4d02973). Every write refused by read-only mode is audited. Dispatch-only enablement for the whole accounting stack (72a91a5).
  • Owner bootstrap, proven end to end (d57a4c1, 669e875, 8f1df9e, 872d8f9, 20af2b0). A reviewed script for the Owner's first login and password rotation, runnable from the VPS itself.
  • Stop deploy/** doc changes stampeding four SSH deploys (8417b9f). The deploy workflows moved to narrow, explicit path lists naming only the files actually copied to the server.
  • Read-only production probes (36ce493, a832a57). CI can now ask the live database and the VPS questions without being able to change anything — every statement runs inside BEGIN READ ONLY.

2026-08-07 — A third backend app and a second portal

4d57c42 added the AIM Gold Account Admin portal and the accounting API — the largest structural change since the repository was created.

  • apps/accounting-api (port 3003, prefix /api/v1/accounting) is a third NestJS app rather than a module, for two reasons: the portal's client hard-codes that prefix while both existing apps use setGlobalPrefix('v1'), and mounting it inside admin-api would have inherited a stateless, unrevocable 8-hour JWT on the surface that reads every customer's financial position. It uses x-portal-token instead — opaque, stored only as a SHA-256 hash, resolved from the database on every request, and revocable.
  • aim-gold-account-admin (port 3004) is a de-branded reusable accounting core with the gold business layered on as one opt-in domain module. It runs standalone against an in-browser mock, and switches to the live backend only when both NEXT_PUBLIC_ACCOUNTING_API_MODE=api and NEXT_PUBLIC_API_BASE_URL are set — either one missing keeps it in mock mode deliberately, so a half-configured build cannot look live.
  • Twelve accounting determinations gate every statutory figure. All ship NOT_CONFIGURED. A digital-gold platform cannot derive from its own data whether a customer purchase is a sale or a deposit, and guessing produces books that balance and are wrong.
  • See ADR 0014.

Also that week: a pre-merge production safety pass fixed five defects without merging (e09e461), and production migrations were first gated with verified backups (646fb05).

2026-07-25 → 07-30 — Auth module, payment readiness, Android

  • Production-ready authentication module (8cef010) and a pre-OTP POST /v1/auth/check-phone (e46f66c).
  • GET /v1/public/payment-readiness (d85af85) — a states-only contract clients consume instead of re-deriving checkout availability, so a client can never open a checkout the backend would refuse.
  • Security headers and payment-gateway/legal environment surface expanded (323de28).
  • Android: auto-publish to the Play Console on push (fe1a496), later made manual-only; collision-free versionCode and an on-demand production track (42ae055).
  • SafeGold: send pin_code when registering a customer (7b5d8f9).
  • Silver removed from the UI across all screens (e36520b).

2026-07-23 — Provider credentials required in production

PR #1 (security/externalize-provider-credentials) merged and deployed. The alots.in SMS and SafeGold API keys are no longer hardcoded anywhere in the source (libs/config/src/app.config.ts, libs/common/src/sms/sms.service.ts); they are read from the environment only. Operator-visible: the backend now fails to boot in production unless SMS_USERNAME, SMS_APIKEY, SAFEGOLD_API_KEY, and SAFEGOLD_GOLD_TOKEN are set in /opt/aim/deploy/.env.production (libs/config/src/validation.schema.ts). A fresh install fills these via deploy/gen-env.sh (__FILL_ME__ slots). The previously-committed keys must still be rotated at the providers.

2026-07-23 — Security hardening

A review pass fixed and deployed several backend/edge issues (ADR 0013):

  • Webhook HMAC checks now verify the raw request body, not a re-serialized one (Razorpay, RazorpayX, KYC).
  • Closed an IDOR on bank-account penny-drop verification (now owner-scoped).
  • Payout retry no longer bypasses the ₹1L ADMIN approval cap.
  • Rate limiting is now actually enforced (global ThrottlerGuard, 100/60 s).
  • Admin API CORS defaults closed instead of allow-all.
  • /metrics returns 404 publicly (still scraped internally).

Separately, provider credentials (alots.in SMS, SafeGold) were moved out of source into env-only, required in production — see the entry below.

2026-07-23 — CI/CD, admin portal, monitoring, migrations

The repository went from "manually deployed snapshot" to a fully automated platform in one day (commits 7a3cb8d..1addd3c):

  • CI/CD: GitHub Actions workflows build Docker images, push them to GHCR (ghcr.io/aimgoldorg/*), and deploy to the production VPS over SSH (docker compose pull + restart, health-check verified). Deploys are serialized on the server with flock; SSH scripts fail fast (set -e).
  • Admin portal recovered: the digigold-admin Next.js app (previously only on the old server) was brought into the repo, given a Build Tools page (pipeline status + deploy buttons backed by the new admin-api devops module) and a View Monitoring link, and deployed at admin.aimgold.org.
  • Monitoring committed + migrated: the Grafana/Prometheus/Loki/Tempo/Alloy stack moved into git and onto the production VPS at monitor.aimgold.org.
  • Schema now migration-managed: DB_SYNCHRONIZE turned off in production. The three never-executed migrations were squashed into a generated InitialSchema migration; prod was baselined with migration:run --fake after a schema-diff proved equivalence. The backend deploy workflow now takes a pg_dump backup and runs pending migrations before restarting. (Superseded 2026-08-07/08: the deploy workflow no longer runs migrations — it refuses to deploy while any are pending. See the entries below.)
  • Domain consolidation: everything moved to aimgold.org (aimgold.org/www website, api, admin-api, admin, monitor). All references to the retired aimstoreorg.com domain and the old server (187.127.130.205) were purged, including the legacy aim-digigold-real/infra/ deployment tooling.
  • Cleanup: removed a stray 8.5 MB compiled binary (aim-backend/), removed the website's unused package-lock.json (builds use Bun), restored the backend's lost .eslintrc.js, added .dockerignore.

2026-07-21 — Repository created

  • 9dc09ea — initial import of the AIM GOLD projects from the internal aim-code monorepo (source snapshot only; no history carried over).
  • 4a9bf54 — website styling pass across shop, trade, and transactions pages.
  • da0e461 — first version of the deploy/ stack for the new VPS.