MyNEX Docs / Runbook
make · docker compose
Running the platform

From zero to a running stack, plus what to flip for production

Everything below is checked against the real Makefile, deploy/ compose files, cmd/ binaries and internal/core/config/config.go — including a handful of env var name mismatches between docs and code that are worth knowing about before you lose an hour to them.

Prerequisites

Go1.23+ (module declares go 1.23.0, toolchain go1.24.4) — both backend Dockerfiles currently build from golang:1.21-alpine, a real mismatch worth fixing before relying on make build
PostgreSQL15 (docker-compose provides it; for local-only runs, any 13+ works)
Redis7
Node.js18+ (admin console), npm
Flutterstable channel, iOS/Android toolchains as needed
Docker + Docker Composefor the containerized path

Quick start & make targets

local, without Docker
# backend — needs Postgres + Redis reachable per .env
make backend-run        # cd mynex-backend && go run ./cmd/server

# admin console
make admin-dev           # cd mynex-admin && npm run dev

# mobile app
cd mynex_mobile && flutter run --dart-define=API_BASE_URL=http://localhost:8181/api
# or: ./scripts/run_flutter.sh
TargetDoes
make backend-runcd mynex-backend && go run ./cmd/server — the real entrypoint, not go run main.go
make backend-testgo build ./... && go vet ./... && go test ./...
make admin-dev / admin-buildVite dev server / vue-tsc -b && vite build (type-check + production build)
make mobile-analyze / mobile-testflutter analyze / flutter test
make testAll of the above in sequence — the full local CI gate
make dev-installgo mod download + npm install + flutter pub get
make fmt / lintgo fmt + dart format / go vet
make build / up / down / restart / logsProduction docker-compose lifecycle (deploy/docker-compose.yml)
make dev / dev-up / dev-down / dev-logsDevelopment docker-compose lifecycle (deploy/docker-compose.dev.yml) — hot reload via air
make db-backup / db-restore FILE=...pg_dump / psql against the running mynex-postgres container
make clean / docker-cleanRemove build artifacts / containers+volumes+networks
mynex-admin/package.json has no test or lint script

CLAUDE.md's old generic guidance mentions npm run test / npm run lint for the admin console — neither exists in the real package.json (only dev, build, preview), and no test framework is even a dependency. Type-checking happens as part of npm run build (vue-tsc -b).

Docker Compose

mynex-nginx :80 / :443 mynex-api Go, internal :8181 mynex-admin built SPA, served by nginx image mynex-postgres postgres:15-alpine mynex-redis redis:7-alpine named volumes: postgres_data, redis_data, logs, uploads, nginx_logs — network: mynex-network
deploy/docker-compose.yml (prod). Dev compose additionally exposes the API directly on 8181 + a delve port 2345, runs admin via npm run dev rather than a built image, and adds adminer/redis-commander/mailhog for local inspection.
  • deploy/nginx/nginx.conf routes /api → the API (prefix stripped), /ws → the API with WebSocket upgrade headers + 7-day timeouts, everything else → the admin SPA with 404→index.html fallback. Rate-limit zones are pre-configured (10r/s general, 5r/m on login) but the HTTPS server block is commented out — TLS certs aren't wired by default.
  • Discrepancy: both compose files set CORS_ORIGINS on the API container, but config.go reads ALLOWED_ORIGINS — the compose-set value is silently ignored and the app falls back to its own default.

Sandbox mode

Confirmed accurate: the platform is fully runnable with zero external keys. Two switches control everything:

PAYMENTS_PROVIDER = sandbox (default)

Top-ups settle deterministically after PAYMENTS_SANDBOX_SETTLE_DELAY (5s). No Stripe/bank keys required. Setting STRIPE_SECRET_KEY registers the real Stripe provider alongside sandbox; per-kind overrides (PAYMENTS_PROVIDER_CARD etc.) let you mix real and sandbox per payment method.

CHAIN_ENV = sandbox (default)

NAQD mints/burns/transfers against an in-memory chain — deterministic, idempotent per reference. CHAIN_ENV=live hard-fails at boot (not a silent fallback) if SOLANA_WALLET_PRIVATE_KEY+NAQD_TOKEN_MINT_ADDRESS or POLYGON_WALLET_PRIVATE_KEY+NAQD_ERC20_CONTRACT_ADDRESS aren't set.

Cards (CardProcessor) and partner rails (PartnerRail) currently have only a sandbox implementation each — there's no env var to flip them to "real" yet, because no production implementation has been written.

Going-live checklist, per integration

IntegrationWhat to setStatus
StripeSTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEYReal provider implementation exists — just needs keys
Local bank rail (BML/MIB/SBI/BOC)MVR_GATEWAY_API_URL, MVR_GATEWAY_API_KEY, MVR_GATEWAY_MERCHANT_IDUntil set, returns RAIL_UNAVAILABLE cleanly (not a crash)
Apple PayAPPLE_MERCHANT_ID (not APPLE_PAY_MERCHANT_ID — see below), APPLE_MERCHANT_CERTIFICATE_PATH, APPLE_MERCHANT_KEY_PATHReal merchant-session validation endpoint exists
Google PayGOOGLE_MERCHANT_ID, GOOGLE_PAY_ENVIRONMENT=PRODUCTION
Solana (NAQD primary)CHAIN_ENV=live, SOLANA_RPC_URL, SOLANA_WALLET_PRIVATE_KEY (AES-256-GCM encrypted), NAQD_TOKEN_MINT_ADDRESSKnown live-chain dual-write gap — see Security
Polygon (NAQD secondary)POLYGON_RPC_URL, POLYGON_WALLET_PRIVATE_KEY, NAQD_ERC20_CONTRACT_ADDRESSSame known gap
Metals price oracleMETALS_API_KEYOracle already works off CoinGecko alone without it — this just adds a second real source for the median
Real card processor— not yet buildable via configNo production CardProcessor implementation exists. A real BIN sponsor integration needs to be written against the interface first
Real TnG rail— not yet buildable via configNo production PartnerRail implementation exists. Same — write one against the interface, register via Service.RegisterRail

Full environment variable reference

Consolidated from internal/core/config/config.go, the four docs/wiring/*.md files, and direct os.Getenv grep across every package. Vars marked ignored are set somewhere in this repo's own docker-compose/.env.example files but read under a different name by the actual Go code — setting the documented name does nothing.

Core / server

VarDefaultNotes
APP_ENVdevelopmentServer refuses to boot in production with default JWT secrets
APP_PORT:8181mynex-backend/.env.example says :8080 and both Dockerfiles EXPOSE 8080 — all stale; the real default, both compose files, and the README agree on 8181
ALLOWED_ORIGINShttp://localhost:3000docker-compose sets ignored CORS_ORIGINS instead — has no effect
DB_HOST/PORT/USER/PASSWORD/NAME/SSL_MODElocalhost / 5432 / mynex / password / mynex / disable
REDIS_HOST/PORT/PASSWORDlocalhost / 6379 / (empty)

JWT & security

VarDefaultNotes
JWT_SECRETchange-this-secretShared by user and admin tokens — see Security
JWT_ACCESS_TOKEN_EXPIRY / JWT_REFRESH_TOKEN_EXPIRY15m / 168h
ADMIN_JWT_SECRET / ADMIN_SESSION_TIMEOUTchange-this-admin-secret / 30mDefined but never actually read outside config.go — dead config
ENCRYPTION_KEY(empty)AES-256-GCM master key — card PANs, chain treasury keys

AWS / storage

VarDefaultNotes
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY(empty)Only needed for real S3 uploads (KYC docs)
AWS_REGIONus-east-1
AWS_S3_BUCKETmynex-storageCLAUDE.md and mynex-backend/.env.example both say ignored S3_BUCKET — wrong name

Payments

VarDefaultNotes
PAYMENTS_PROVIDERsandboxPlatform-wide default
PAYMENTS_PROVIDER_BANK_TRANSFER/_CARD/_CRYPTO/_APPLE_PAY/_GOOGLE_PAY(unset → default)Per-kind override
PAYMENTS_SANDBOX_SETTLE_DELAY / _WEBHOOK_SECRET5s / sandbox-dev-secret
PAYMENTS_POLL_INTERVAL / _MAX_SETTLEMENT_ATTEMPTS30s / 20
PAYMENTS_SWEEPER_INTERVAL / _BATCH_SIZE30s / 50
LOCAL_BANK_WEBHOOK_SECRETfalls back to MVR_GATEWAY_API_KEY
RECURRING_SWEEPER_INTERVAL / RECURRING_MAX_CONSECUTIVE_FAILURES1h / 3
STRIPE_SECRET_KEY / _WEBHOOK_SECRET / _PUBLISHABLE_KEY(empty)
APPLE_MERCHANT_IDmerchant.mv.mynex.walletCLAUDE.md / .env.example say ignored APPLE_PAY_MERCHANT_ID — wrong name
GOOGLE_MERCHANT_ID / GOOGLE_PAY_ENVIRONMENT(empty) / TEST
MVR_GATEWAY_API_URL/_API_KEY/_MERCHANT_ID(empty)local_bank provider

NAQD / oracle / blockchain

VarDefaultNotes
METALS_API_KEY / METALS_API_BASE_URL / COINGECKO_BASE_URLunset / real endpointsOracle sources
ORACLE_SOURCE_TIMEOUT / ORACLE_STALENESS_THRESHOLD5s / 15m
NAQD_FX_SPREAD_BPS50Oracle → FX rate provider
CHAIN_ENVsandboxsandbox \| live
SOLANA_RPC_URL / SOLANA_WEBSOCKET_URLdevnet URLs
SOLANA_WALLET_PRIVATE_KEY / NAQD_TOKEN_MINT_ADDRESSunsetRequired for CHAIN_ENV=live Solana; key is AES-256-GCM encrypted
POLYGON_RPC_URL / POLYGON_WALLET_PRIVATE_KEY / NAQD_ERC20_CONTRACT_ADDRESSunsetRequired for CHAIN_ENV=live Polygon
POLYGON_RECEIPT_TIMEOUT2m
NAQD_MINT_MIN_USD / NAQD_MINT_DAILY_LIMIT_USD10 / 10000
NAQD_BURN_MIN / NAQD_BURN_DAILY_LIMIT / NAQD_BURN_FEE_BPS10 / 1000 / 50
NAQD_APPROVAL_THRESHOLD_USD5000Maker-checker gate
NAQD_MIN_COLLATERAL_RATIO / NAQD_REBALANCE_THRESHOLD110 / 115
NAQD_WORKER_POLL_INTERVAL / NAQD_WORKER_STUCK_AFTER2s / 2m

Cards & partners

VarDefaultNotes
CARD_TIER_NONE/_BASIC/_STANDARD/_ENHANCED_PER_TXN/_DAILY/_MONTHLY25/50/200 · 100/100/1000 · 500/1000/10000 · 5000/10000/100000By KYC tier
CARD_REVEAL_TOKEN_TTL_SECONDS300
CARD_ENCRYPTION_KEYfalls back to ENCRYPTION_KEY, then a dev literalPAN-at-rest AES-256-GCM
CARD_WEBHOOK_SECRETdev literalHMAC for the sandbox card webhook
PARTNER_TIER_NONE/_BASIC/_STANDARD/_ENHANCED_DAILY/_MONTHLY50/200 · 100/1000 · 1000/10000 · 10000/100000Per debit-currency, not normalized
PARTNER_RECOVERY_SWEEP_AGE_SECONDS120Boot-time recovery sweep

Notifications

VarNotes
SMTP_HOST/PORT/USERNAME/PASSWORD, EMAIL_FROM(_NAME)Unset → logs a mock instead of sending
SMS_PROVIDER (twilio|nexmo|africastalking), SMS_API_KEY/_SECRET/_SENDER_IDOnly Africa's Talking has a real implementation; Twilio/Nexmo return "not implemented"; unset → mock
FCM_SERVER_KEY, APNS_*Real FCM client and mocked APNS client exist but PushService is never constructed anywhere — dead code today

Docker-compose-only (not read by the Go app)

POSTGRES_DB/USER/PASSWORD/PORT, NGINX_HTTP_PORT/HTTPS_PORT, VITE_API_BASE_URL, VITE_WS_URL (build-args for the admin image), and the ignored CORS_ORIGINS.

Two unrelated .env.example files exist

Root /my_nex/.env.example is docker-compose-shaped (POSTGRES_*, NGINX_*, VITE_*) and doesn't match config.go's actual variable names at all. mynex-backend/.env.example is the one that matches config.go — but even it has the S3_BUCKET/APPLE_PAY_MERCHANT_ID/ APP_PORT=:8080 issues noted above. When in doubt, trust internal/core/config/config.go over any .env.example file.

Seeds & tools

BinaryDoesRespects .env?
go run ./cmd/migrate migrateManually triggers the same AutoMigrate that already runs on every server boot — the migrations/ directory it might imply is empty; there's no separate up/down migration frameworkyes
go run ./cmd/tools/seed_adminCreates [email protected] / AdminPass123!, role super_admin. Skips if already presentyes
go run ./cmd/tools/seed_exchange_ratesUpserts 6 manual FX pairs (USD/MVR, EUR/USD, EUR/MVR + inverses), 24h validityno — hardcoded DSN (mynex/mynex)
go run ./cmd/tools/create_demo_user[email protected] / Demo123!, KYC pending, MVR wallet 50.00no — hardcoded DSN (azhan/Welcome@123)
go run ./cmd/tools/create_test_user[email protected] / Test123!, fully verified, MVR 1000 + USD 100no — same hardcoded DSN as above
go run ./cmd/tools/reconcileCompares every wallet's cached balance against its ledger sum; non-zero exit on any mismatch — CI/cron-friendlyyes
Three tools won't work against a docker-compose database as-is

create_demo_user, create_test_user, and seed_exchange_rates hardcode two different, mutually inconsistent Postgres credential sets directly in source, neither of which matches the docker-compose defaults (mynex/mynex_secret) or mynex-backend/.env.example's default (mynex/password). Edit the DSN in the source file directly if you need these against a non-default local database — seed_admin and reconcile are the two that behave normally.

Automatic startup seeds (no manual step needed)

  • Ledger system accounts — bootstrapped for MVR/USD/NAQD on every boot (BootstrapSystemAccounts).
  • FX defaultsUSD/MVR and NAQD/USD seeded on every boot (fx.Service.SeedDefaults); once the NAQD oracle is live, NAQD/USD reads are actually served by the oracle-chained rate provider instead, and this seed becomes a pure fallback.
  • Partner railstng-my config row seeded idempotently on every boot (SeedDefaultRails), immediately followed by a one-shot RecoverStuckOperations sweep.