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 rootpackage.json— no dependencies, purely operational — frontsdeploy/scripts/manual-deploy.shwithnpm run deploy:preflight,deploy:status,deploy:backend,deploy:website,deploy:admin,deploy:migrate,deploy:allanddeploy: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 forlinux/amd64and ship the image withdocker save | gzip | ssh docker load, or--transport serverto 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:migratederived the image tag fromHEAD, but arming is itself a commit, soHEADis always one ahead of the built image — every manual migration would have refused with "build it first".deploy/scripts/manual-deploy.shnow reads the tag from the manifest'sapproved_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 (
6ad6ee8→b46b470).DynamicProductPricing1786200000000. A product now prices eitherFIXEDorDYNAMIC; a dynamic row derives from the live 24K rate × karat with making charge and discount applied, and the response carriespriceBasis: LIVE|FIXEDso no client assumes a stored snapshot is live. Armed asAUTH-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 sendexpectedTotal, which the server compares before the insert — so a409 PRICE_CHANGEDdoes 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 inaim-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_eventshas nocreated_at(bdac174). Its timestamp column isat. A query ordering bycreated_atfails 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_ENABLEDmust spell the wordtrue, 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).applySellwas 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). Aproduction-sell-gate-verifyworkflow 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 tomainand 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.orgwent live: the portal owns/, the accounting API owns/api/v1/accounting/*. Because they share an origin the browser makes no cross-origin request, soACCOUNTING_PORTAL_ORIGINstays unset and the API's CORS stays shut.accounts-api.aimgold.orgis 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: theaccountingcompose profile keeps both services out of a defaultdocker compose up, andACCOUNTING_PORTAL_MODEdefaults toreadonly. - Migration 12 — exactly-once checkout (
c68de73,a86233a,497f717,27992cb).ProductOrderIdempotency1786100000000added 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=disabledindeploy/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_MODEadopted (db1829e,e544806,646fb05,ebd7a30).deploy/production-migration-authorization.jsonbecame the arming control for altering the production database, enforced byaim-digigold-real/scripts/verify-migration-authorization.js. Four bindings must all hold at dispatch: commit-bound (only the manifest itself may differ fromHEAD), 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). Plusrefs/heads/main, an unexpiredexpires_at, a typedconfirm=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-backendnow 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,-002and-003(c59b3ca,a8b7184,0584194,3b6e31f) — with theSellEventFactscolumns 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 insideBEGIN 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 usesetGlobalPrefix('v1'), and mounting it insideadmin-apiwould have inherited a stateless, unrevocable 8-hour JWT on the surface that reads every customer's financial position. It usesx-portal-tokeninstead — 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 bothNEXT_PUBLIC_ACCOUNTING_API_MODE=apiandNEXT_PUBLIC_API_BASE_URLare 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-OTPPOST /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-freeversionCodeand an on-demand production track (42ae055). - SafeGold: send
pin_codewhen 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.
/metricsreturns 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 withflock; SSH scripts fail fast (set -e). - Admin portal recovered: the
digigold-adminNext.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-apidevopsmodule) and a View Monitoring link, and deployed atadmin.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_SYNCHRONIZEturned off in production. The three never-executed migrations were squashed into a generatedInitialSchemamigration; prod was baselined withmigration:run --fakeafter a schema-diff proved equivalence. The backend deploy workflow now takes apg_dumpbackup 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/wwwwebsite,api,admin-api,admin,monitor). All references to the retiredaimstoreorg.comdomain and the old server (187.127.130.205) were purged, including the legacyaim-digigold-real/infra/deployment tooling. - Cleanup: removed a stray 8.5 MB compiled binary (
aim-backend/), removed the website's unusedpackage-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 internalaim-codemonorepo (source snapshot only; no history carried over).4a9bf54— website styling pass across shop, trade, and transactions pages.da0e461— first version of thedeploy/stack for the new VPS.