Skip to content

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 portalsdigigold-admin for operations at admin.aimgold.org, including a Build Tools page that dispatches deploy workflows; and aim-gold-account-admin for accounting, tax and audit at accounts.aimgold.org, a separate authentication boundary built on an opaque revocable x-portal-token rather 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 package com.aimgoldorg.app), which is published only by a manual dispatch of the deploy-android workflow, where the operator picks the Play Console track — internal (the default) or production. 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. MerchantReadinessService computes payments_permitted as zero blocking readiness items and DIGITAL_GOLD_PAYMENT_ENABLED, and assertPaymentsPermitted() 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. DigitalSellGate returns 503 unless DIGITAL_SELL_ENABLED is literally true (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 commit 6a9c54b pending contractual facts from SafeGold and a CA determination on withholding. Production runs with the switch unset and zero financial rows. See AIM_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), so ledger_entries is 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 remain NOT_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.