Karat — AIM GOLD¶
Karat is the monorepo for AIM GOLD, a digital-gold platform: buy, save (SIP), and hold 24K gold from a phone or the web, plus a jewellery shop and a back-office admin console.
The problem¶
Retail gold buyers in India want to save small amounts regularly, hold gold digitally without storage risk, and redeem or buy jewellery — with KYC, payments, and a custodian (SafeGold) handled for them. Running that safely needs a customer app + website, a payments/KYC/gold-custody backend, an admin console, and observability, all deployable by a small team.
The solution¶
One repository containing every part of the platform:
- a Flutter mobile app (Android only) and a React website for customers;
- a NestJS backend — three Fastify apps sharing one Docker image and one
libs/layer: the customer API (apps/api, :3000), the operations admin API (apps/admin-api, :3001) and the accounting API (apps/accounting-api, :3003, mounted at/api/v1/accounting) — integrating SafeGold (gold custody), a provider-agnostic payment-gateway layer whose only adapter today is Razorpay for payments and RazorpayX for payouts (aim-digigold-real/libs/payments/src/payments-gateway.module.ts), and the alots.in SMS gateway; - two Next.js portals —
digigold-adminfor operations atadmin.aimgold.org, including a Build Tools page that dispatches deploy workflows; andaim-gold-account-adminfor accounting, tax and audit ataccounts.aimgold.org, a separate authentication boundary built on an opaque revocablex-portal-tokenrather than the operations portal's stateless JWT, with an 8-role / 25-permission model and deployed in server-enforced read-only mode (ADR 0014); - a single-VPS Docker Compose stack behind Caddy, deployed by
GitHub Actions → GHCR → SSH pull, with
npm run deploy:*(deploy/scripts/manual-deploy.sh) as a full-fidelity fallback that builds locally and ships the image over SSH when GitHub Actions is unavailable; - a Grafana/Prometheus/Loki/Tempo monitoring stack.
A second Go/Fiber backend (aim-gold-backend/) is a from-scratch
re-implementation of the same domain. It is parked — deliberately out of
production and out of CI/CD, with no workflow, image or compose service — and
whether it is the intended future backend is an open question. See ADR
0012.
Who it's for¶
- Customers — via the website at
aimgold.org, and the Android app (Google Play packagecom.aimgoldorg.app), which is published only by a manual dispatch of thedeploy-androidworkflow, where the operator picks the Play Console track —internal(the default) orproduction. There is no iOS build. - Operations/admin staff — via the admin portal (
admin.aimgold.org). - Accounting, tax and audit staff — via the Account Admin portal
(
accounts.aimgold.org), a separate application with its own login, roles and read-only enforcement (ADR 0014). - Developers — this repository; start with Getting Started.
Current status¶
Early-stage, actively deployed. What is live end-to-end today:
- Account, auth (OTP/MPIN/biometric), and profile.
- KYC — the customer submits documents and PII, an operator reviews and approves them in the operations portal, and approval registers the customer with SafeGold. Verification is manual; see the stubbed list below.
- The jewellery shop: catalogue → cart → Cash-on-Delivery order, with
per-product pricing that is either fixed or derived from the live 24K
gold rate — purity, making charge and discount applied at read and order
time, in integer paise
(
aim-digigold-real/libs/common/src/money/product-pricing.ts). Physical orders deliberately require account standing but not digital-gold KYC. - SIP (recurring gold savings) plan creation — live on the website
and the backend. The mobile app gates SIP off with the rest of the
digital-gold transaction surface (
DIGIGOLD_TRANSACTIONS_ENABLED, default false). Installment debits ride the buy flow and are therefore gated with it. - Live gold price, transactions, the operations console, monitoring, and push-to-deploy CI/CD for application code.
Migrations are never applied by a merge
Push-to-deploy stops at the schema. A backend deploy refuses outright
while the database has pending migrations — deploy-backend.yml prints
"REFUSING TO DEPLOY" rather than let a merge alter the production schema
as a side effect of shipping code. Schema changes run only through
.github/workflows/migrate-production.yml: manual dispatch, confirm
typed as exactly MIGRATE, dry_run true by default, and an armed
manifest at deploy/production-migration-authorization.json binding the
run to an approved commit SHA, an exact ordered migration list, the
triggering GitHub login, and a one-time id recorded in a server-side
consumed ledger. This is single-owner control, not four-eyes approval;
the trade-off is written down in
AIM_GOLD_PRODUCTION_MIGRATION_GOVERNANCE.md. The manifest's resting
state is disarmed (authorization_id: null).
What is deliberately gated off — built, and refused at runtime:
- Digital-gold buy/sell settlement. The refusal is server-side and
evidence-based, not a flag.
MerchantReadinessServicecomputespayments_permittedas zero blocking readiness items andDIGITAL_GOLD_PAYMENT_ENABLED, andassertPaymentsPermitted()runs before an order is created from a quote (aim-digigold-real/libs/common/src/legal/merchant-readiness.service.ts), so a flag flipped by accident cannot open checkout while GSTIN, provider disclosure or legal content are unverified. - Digital sell.
DigitalSellGatereturns 503 unlessDIGITAL_SELL_ENABLEDis literallytrue(aim-digigold-real/libs/common/src/capability/digital-sell.gate.ts) — 503 because "not available" is what is true, where 403 would claim something about the customer. This is not unbuilt work: sell is ~96% engineered — server-stored sell quotes, holding locks, provider-outcome-unknown handling, RazorpayX payouts with a webhook event log — and frozen at commit6a9c54bpending contractual facts from SafeGold and a CA determination on withholding. Production runs with the switch unset and zero financial rows. SeeAIM_GOLD_DIGITAL_SELL_ENGINEERING_FREEZE.md. - The double-entry ledger posts nothing. Money-moving paths route through
AccountingPostingGate, which refuses when auto-posting is off (ACCOUNTING_POSTING_DISABLED) and refuses again when it is on but the event is not yet wired to a posting engine (ACCOUNTING_POSTING_BLOCKED_ENGINE_NOT_WIRED), soledger_entriesis empty by design (aim-digigold-real/libs/finance/src/accounting/posting-gate.service.ts). Twelve accounting determinations gate every statutory figure; seven were approved in production on 2026-08-11 and five remainNOT_CONFIGURED, so the posting engine is still not ready — which is why the Account Admin portal is deployed read-only. See ADR 0014.
A client flag is not a gate
The Flutter app and the website carry their own kill switches, but they
only hide surfaces — they decide nothing about money. Every client flag
is combined with the server's answer using AND, never OR: the website
computes checkout_available && features.digitalGoldPayment
(aimgold-website/src/lib/usePaymentReadiness.ts), so a client flag can
only ever be more restrictive than the API. Turning one on against a
refusing server produces an error, not a transaction. The server side
fails closed so that a half-configured build cannot look live.
What is genuinely stubbed or absent: silver (no rate source), push-notification delivery (records are written; there is no FCM, SMS or SMTP adapter behind them), and automated identity verification — the Aadhaar-OTP endpoints return 501 and the KYC webhook receiver verifies its HMAC but does not yet parse provider events, so KYC review is manual today.
See Features for the exact per-feature status, and the
Changelog for how the platform reached its current state — it
is current to 2026-08-18 and covers the accounting API and Account Admin
portal, publishing accounts.aimgold.org, dynamic product pricing and the
manual deploy path. Backups, the off-host policy and the
monthly restore drill are documented in
Backup and Recovery.
Production domains
aimgold.org + www (website) · api.aimgold.org (customer API) ·
admin-api.aimgold.org (admin API) · admin.aimgold.org (operations
portal) · accounts.aimgold.org (Account Admin portal, with the
accounting API path-split onto the same origin at
/api/v1/accounting/*) · monitor.aimgold.org (Grafana) ·
docs.aimgold.org (this documentation). All on a single VPS behind
Caddy. accounts-api.aimgold.org is deliberately not published and has
no DNS record.