This guide covers the full end-to-end steps to deploy ACBU to production. For the org-limits backfill procedure run after deployment, see DEPLOYMENT_ORG_LIMITS_BACKFILL.md. For testnet bootstrap, see TESTNET_CUSTODIAL_BOOTSTRAP.md.
- Node.js 18+,
pnpm/npm - Rust +
wasm32-unknown-unknowntarget (for Soroban contracts) - Soroban CLI (
stellar contractcommands) - Stellar account with XLM for contract deployment fees
- Prisma Accelerate project (managed PostgreSQL)
- MongoDB Atlas cluster
- RabbitMQ instance (or managed broker)
- Access to fintech provider dashboards (Flutterwave, Paystack, MTN MoMo)
ACBU uses Prisma Accelerate as the primary database. Create an Accelerate project at prisma.io and obtain the connection string:
prisma+postgres://accelerate.prisma-data.net/?api_key=<YOUR_KEY>
Set this as both DATABASE_URL and PRISMA_ACCELERATE_URL in backend/.env.
Important: Prisma CLI reads only
backend/.env. If you usebackend/.env.localplaceDATABASE_URLandPRISMA_ACCELERATE_URLthere too, or copy.env.localinto.envbefore running migrations.
Create a MongoDB Atlas cluster for cache/session storage and obtain:
mongodb+srv://<user>:<password>@<cluster>.mongodb.net/acbu?retryWrites=true&w=majority
Set as MONGODB_URI.
Provision a RabbitMQ instance (e.g. CloudAMQP) and set RABBITMQ_URL:
amqp://<user>:<password>@<host>:5672
Copy all required variables into backend/.env. The full reference is in ENV_VARS.md. The minimum required set for production:
| Variable | Notes |
|---|---|
DATABASE_URL |
Prisma Accelerate URL |
PRISMA_ACCELERATE_URL |
Same value as DATABASE_URL |
MONGODB_URI |
MongoDB Atlas connection string |
RABBITMQ_URL |
RabbitMQ connection string |
JWT_SECRET |
Min 32 characters, randomly generated |
API_KEY_SALT |
Salt for API key hashing |
NODE_ENV |
Set to production |
STELLAR_NETWORK |
Set to mainnet |
STELLAR_HORIZON_URL |
Mainnet Horizon URL |
STELLAR_SECRET_KEY |
Backend signing key for contract transactions |
STELLAR_USE_DYNAMIC_FEES |
Recommended: true for mainnet |
Set fintech keys for each enabled payment provider (FLUTTERWAVE_*, PAYSTACK_*, MTN_MOMO_*).
Set oracle and reserve variables (ORACLE_UPDATE_INTERVAL_HOURS, RESERVE_MIN_RATIO, etc.) — defaults are safe for production but review against your risk parameters.
Set monitoring variables (SENTRY_DSN, SENTRY_ENVIRONMENT).
Apply all pending Prisma migrations:
cd backend
npx prisma migrate deployApply the API key lookup key migration specifically if upgrading an existing deployment:
npx prisma db execute --file prisma/sql/20260323_add_api_key_lookup_key.sqlVerify the schema is up to date:
npx prisma migrate statuscd acbu-smart-contract
cargo build --release --target wasm32-unknown-unknownOr use the Soroban CLI build shorthand:
stellar contract buildDeploy each contract and record the resulting contract ID:
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/acbu_oracle.wasm \
--source <DEPLOYER_SECRET_KEY> \
--network mainnet
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/acbu_reserve_tracker.wasm \
--source <DEPLOYER_SECRET_KEY> \
--network mainnet
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/acbu_minting.wasm \
--source <DEPLOYER_SECRET_KEY> \
--network mainnet
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/acbu_burning.wasm \
--source <DEPLOYER_SECRET_KEY> \
--network mainnetSet the returned contract IDs in backend/.env:
CONTRACT_ORACLE=<oracle_contract_id>
CONTRACT_RESERVE_TRACKER=<reserve_tracker_contract_id>
CONTRACT_MINTING=<minting_contract_id>
CONTRACT_BURNING=<burning_contract_id>
Or use per-network variants (CONTRACT_ORACLE_MAINNET, etc.) if supporting both testnet and mainnet from the same environment file.
Run the initialization script to seed oracle basket weights, rates, and reserve values:
cd backend
npx ts-node scripts/initContracts.tsEnsure STELLAR_SECRET_KEY is the admin key for all contracts before running this script.
Before the first mint can succeed, the oracle and reserve tracker must have valid state.
For each basket currency (e.g. NGN, KES, GHS, RWF), submit an initial rate:
stellar contract invoke \
--id $CONTRACT_ORACLE \
--source <ADMIN_SECRET_KEY> \
--network mainnet \
-- submit_rate \
--currency '["NGN"]' \
--rate <rate_value_7_decimal_fixed_point>Also call set_s_token_address for each currency with its deployed Stellar Asset Contract (SAC) ID.
For each basket currency, seed a non-zero value_usd so is_reserve_sufficient passes:
stellar contract invoke \
--id $CONTRACT_RESERVE_TRACKER \
--source <ADMIN_SECRET_KEY> \
--network mainnet \
-- update_reserve \
--currency '["NGN"]' \
--value_usd <value_in_7_decimal_fixed_point>value_usd must satisfy: total_reserves_usd ≥ minted_acbu_supply × RESERVE_MIN_RATIO.
cd backend
npm ci --production
npx prisma generate
npm run build
npm run start:prodVerify the service is healthy:
curl https://api.acbu.io/v1/healthExpected response: { "status": "ok" }.
If this is an upgrade of an existing deployment (not a fresh launch), run the org-limits backfill to ensure existing transactions carry correct rateSnapshot.organizationId. Full procedure in DEPLOYMENT_ORG_LIMITS_BACKFILL.md:
# Dry-run first
npx ts-node prisma/backfillOrgTransactionContext.ts
# Apply if candidate/derivable counts look correct
npx ts-node prisma/backfillOrgTransactionContext.ts --applyRegister the ACBU backend webhook URL with each active payment provider so on-ramp and off-ramp events are delivered:
| Provider | Webhook URL |
|---|---|
| Flutterwave | https://api.acbu.io/v1/webhooks/flutterwave |
| Paystack | https://api.acbu.io/v1/webhooks/paystack |
| MTN MoMo | https://api.acbu.io/v1/webhooks/mtn |
Set WEBHOOK_SECRET in backend/.env to the shared HMAC-SHA256 secret used to verify incoming payloads.
Run through the critical paths to confirm the deployment is functional:
- Oracle:
GET /v1/ratesreturns current basket rates. - Reserve check:
GET /v1/reservesreturns non-zero reserves with ratio ≥RESERVE_MIN_RATIO. - Mint (retail):
POST /v1/retail/mint/depositwith valid payload completes and returns aTransactionrow withstatus=completedand a non-nullblockchain_tx_hash. - Burn (retail):
POST /v1/retail/burnwith valid payload initiates a redemption. - API key scopes: Attempt a segment route (e.g.
POST /v1/p2p/transfer) with an API key missingp2p:write— expect a403. - Rate limiting: Exceed
RATE_LIMIT_MAX_REQUESTSin one window — expect429.
- Sentry: Confirm errors are appearing in the Sentry project by triggering a test error.
- Reserve ratio: Set up an alert if
reserve_ratiodrops belowRESERVE_ALERT_THRESHOLD(default 1.02). - Oracle staleness: Alert if oracle rates have not been updated within
ORACLE_UPDATE_INTERVAL_HOURS × 2. - RabbitMQ queue depth: Alert on unusually deep queues (pending mint/burn jobs).
If a deployment needs to be rolled back:
- Revert the backend service to the previous build artifact.
- Run
npx prisma migrate resolve --rolled-back <migration_name>for any migration that should be reverted. - Smart contract deployments are not reversible — to roll back contract logic, deploy a new contract version and update
CONTRACT_*env vars, then redeploy the backend. - If the org-limits backfill (
--apply) ran against data that should be reverted, restore from a database snapshot taken before deployment.
- ENV_VARS.md — full environment variable reference
- TESTNET_CUSTODIAL_BOOTSTRAP.md — testnet-specific bootstrap steps
- DEPLOYMENT_ORG_LIMITS_BACKFILL.md — post-deploy org limits backfill
- TECHNICAL/ARCHITECTURE.MD — system architecture overview
- TECHNICAL/DATABASE_SCHEMA.MD — database schema reference