This is the single authoritative guide for deploying SplitNaira from source to a running environment. It covers testnet, staging, and production in order. A new operator should be able to complete a full release without asking maintainers.
Related docs:
- Prerequisites
- Environment Variables Reference
- Phase 1 — Testnet Deploy
- Phase 2 — Staging Deploy
- Phase 3 — Production Deploy
- Smoke Tests
- Rollback Procedures
- Operational Notes
| Tool | Minimum version | Install reference |
|---|---|---|
| Node.js | 18 | nodejs.org |
| Rust (stable) | 1.76 | rustup default stable |
| Soroban CLI | 0.28 | cargo install soroban-cli --locked |
| Stellar CLI | latest stable | cargo install stellar-cli |
| PostgreSQL | 14 | Managed DB or self-hosted |
| Docker | any recent | Required for frontend container deploys |
See docs/SOROBAN_SETUP.md for Windows-specific Rust/MSVC setup.
- GitHub repository write access (to trigger CD workflows)
- Render account with backend service configured (or equivalent PaaS)
- PostgreSQL connection string for each environment
- Stellar deployer key funded with Lumens on the target network
- Freighter wallet (for manual smoke tests via the UI)
rustup target add wasm32v1-noneCopy .env.example to .env (or set these as secrets/config vars in your hosting platform) before deploying each service.
See docs/environments.md for the full matrix and per-environment examples.
See docs/secrets.md for authoritative secret handling, rotation procedures, and the rule to never commit .env files.
| Variable | Description | Example |
|---|---|---|
PORT |
HTTP port the API listens on | 3001 |
CORS_ORIGIN |
Comma-separated allowed origins | https://app.splitnaira.com |
LOG_LEVEL |
Winston log level | info |
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@host:5432/splitnaira |
HORIZON_URL |
Stellar Horizon endpoint | https://horizon-testnet.stellar.org |
SOROBAN_RPC_URL |
Soroban RPC endpoint | https://soroban-testnet.stellar.org |
SOROBAN_NETWORK_PASSPHRASE |
Network passphrase | Test SDF Network ; September 2015 |
CONTRACT_ID |
Deployed Soroban contract ID | C... (56-char Stellar contract address) |
SIMULATOR_ACCOUNT |
Optional simulator account address | G... |
| Variable | Description | Example |
|---|---|---|
NEXT_PUBLIC_STELLAR_NETWORK |
Network name | testnet or mainnet |
NEXT_PUBLIC_SOROBAN_RPC_URL |
Soroban RPC endpoint | https://soroban-testnet.stellar.org |
NEXT_PUBLIC_CONTRACT_ID |
Deployed Soroban contract ID | C... |
NEXT_PUBLIC_HORIZON_URL |
Stellar Horizon endpoint | https://horizon-testnet.stellar.org |
NEXT_PUBLIC_API_BASE_URL |
Backend API base URL | https://api.splitnaira.com |
| Network | Passphrase |
|---|---|
| Testnet | Test SDF Network ; September 2015 |
| Mainnet | Public Global Stellar Network ; September 2015 |
Use testnet to validate the full stack before touching staging or production. All steps are safe to repeat.
cd contracts
cargo test --locked
cargo fmt -- --check
cargo clippy --all-targets -- -D warnings
cargo build --release --target wasm32v1-none --lockedVerify the artifact exists:
contracts/target/wasm32v1-none/release/splitnaira_contract.wasm
Run from the repo root:
npm run generate:contract-interface
npm run generate:contract-typesReview diffs in contracts/interface/splitnaira.contract-interface.json and backend/src/generated/contract-types.ts / frontend/src/generated/contract-types.ts. Commit any changes before deploying.
# Add testnet network config (one-time)
stellar network add testnet \
--rpc-url https://soroban-testnet.stellar.org \
--network-passphrase "Test SDF Network ; September 2015"
# Generate and fund a deployer key (one-time per environment)
stellar keys generate deployer
stellar keys fund deployer --network testnet
# Deploy
cd contracts
stellar contract deploy \
--wasm target/wasm32v1-none/release/splitnaira_contract.wasm \
--source deployer \
--network testnetRecord the contract ID printed by the deploy command. You will need it in the next steps.
See contract-release-and-upgrade-runbook.md §5 for full contract deploy details and §7 for the upgrade path.
After deploying the contract, set the contract admin and allowlist the documented standard testnet assets in one step.
Standard testnet assets:
- USD Coin (USDC):
CBLASIRZ7CUKC7S5IS3VSNMQGKZ5FTRWLHZZXH7H4YG6ZLRFPJF5H2LR - Soroban Waved USD (wUSD):
CDLZJQG2OZZXZAU3YICESOJE73SOXREH74DRBEDAFTMPAQWX3JD3YQ
Run:
CONTRACT_ID=<contract id> ADMIN_SECRET=<admin secret> ./scripts/bootstrap-allowlist.shIf ADMIN_PUBLIC cannot be derived automatically from ADMIN_SECRET, provide it explicitly:
ADMIN_PUBLIC=<admin public key> CONTRACT_ID=<contract id> ADMIN_SECRET=<admin secret> ./scripts/bootstrap-allowlist.shThe script builds and signs set_admin and allow_token operations for the testnet tokens, and prints the transaction output for each operation.
Verify the bootstrap result:
soroban contract invoke \
--id <contract id> \
--network testnet \
--network-passphrase "Test SDF Network ; September 2015" \
--rpc-url https://soroban-testnet.stellar.org \
--fn is_token_allowed \
--args address <token contract id>Expected output for each allowlisted token is true.
cd backend
cp .env.example .envSet at minimum:
DATABASE_URL=postgresql://...
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
SOROBAN_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
HORIZON_URL=https://horizon-testnet.stellar.org
CONTRACT_ID=<contract ID from Step 3>
CORS_ORIGIN=http://localhost:3000
The backend uses TypeORM. In non-production environments synchronize: true is set, so the schema is applied automatically on first start. For production, see Phase 3 §DB migrations.
cd backend
npm ci
npm run build
npm run startConfirm the server logs Database connection established and Server started on port 3001.
cd frontend
cp .env.example .env.localSet:
NEXT_PUBLIC_STELLAR_NETWORK=testnet
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_CONTRACT_ID=<contract ID from Step 3>
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
npm ci
npm run build
npm run startSee §6 Smoke Tests.
Staging mirrors production configuration but uses testnet (or a dedicated staging network). The goal is to validate the CD pipeline and infrastructure before touching production.
- All testnet smoke tests pass (Phase 1 complete)
- Contract interface artifacts are committed and up to date
- Backend and frontend tests pass in CI (
npm run testin each directory) -
DATABASE_URLpoints to the staging database -
CONTRACT_IDis set to the testnet contract deployed in Phase 1 -
CORS_ORIGINis set to the staging frontend URL -
NEXT_PUBLIC_API_BASE_URLpoints to the staging backend URL
Push to main or trigger the workflow manually:
- Go to Actions → Backend Deploy in GitHub.
- Click Run workflow and select the
mainbranch. - Set
deploy_environmenttostagingorproduction. - The pipeline runs
verify-backend(lint + build), validates deployment configuration, then calls the Render deploy hook.
Required GitHub secrets:
RENDER_BACKEND_DEPLOY_HOOK_URLMAINNET_CONTRACT_ID(production only)
For explicit production releases, use the dedicated manual workflow:
- Actions → Mainnet Deploy
deploy_environmentdefaults toproduction- This workflow validates mainnet deploy configuration before invoking Render
- It is intended for safe, human-reviewed mainnet rollouts
See docs/backend-deploy.md for full CI/CD details.
Docker (recommended for staging/production):
cd frontend
docker build -t splitnaira-frontend .
docker run -p 3000:3000 \
-e NEXT_PUBLIC_STELLAR_NETWORK=testnet \
-e NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org \
-e NEXT_PUBLIC_CONTRACT_ID=<contract ID> \
-e NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org \
-e NEXT_PUBLIC_API_BASE_URL=https://api-staging.splitnaira.com \
splitnaira-frontendSee frontend/DOCKER.md for full Docker options.
Node (alternative):
cd frontend
npm ci
npm run build
npm run startRepeat the smoke test suite (§6) against the staging URLs. All checks must pass before proceeding to production.
Stop. Confirm staging smoke tests passed before continuing.
Production uses Stellar mainnet. The contract ID, network passphrase, and RPC URLs are different from testnet.
# Add mainnet network config (one-time)
stellar network add mainnet \
--rpc-url https://soroban-mainnet.stellar.org \
--network-passphrase "Public Global Stellar Network ; September 2015"
# Fund deployer key on mainnet with real XLM before deploying
stellar keys fund deployer --network mainnet # only works on testnet; fund manually on mainnet
cd contracts
stellar contract deploy \
--wasm target/wasm32v1-none/release/splitnaira_contract.wasm \
--source deployer \
--network mainnetRecord the mainnet contract ID. Store it in your secrets manager or CI environment variables — do not commit it to the repository.
Backend (set in Render or your hosting platform's config vars):
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://... (production DB)
SOROBAN_RPC_URL=https://soroban-mainnet.stellar.org
SOROBAN_NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015
HORIZON_URL=https://horizon.stellar.org
CONTRACT_ID=<mainnet contract ID>
CORS_ORIGIN=https://app.splitnaira.com
LOG_LEVEL=info
Frontend (set as build-time env vars or Docker env):
NEXT_PUBLIC_STELLAR_NETWORK=mainnet
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-mainnet.stellar.org
NEXT_PUBLIC_CONTRACT_ID=<mainnet contract ID>
NEXT_PUBLIC_HORIZON_URL=https://horizon.stellar.org
NEXT_PUBLIC_API_BASE_URL=https://api.splitnaira.com
In production synchronize is disabled. Apply schema changes explicitly before starting the backend:
cd backend
npm run build
# Run TypeORM migrations (add this script if not present):
node dist/node_modules/.bin/typeorm migration:run -d dist/src/services/database.jsIf no migration files exist yet, the initial schema is applied by the first
synchronize: truerun in a non-production environment. For production, generate and commit migration files from that schema before go-live.
Push to main or trigger the GitHub Actions workflow:
- Actions → Backend Deploy → Run workflow (select
main). - Monitor the
verify-backendanddeploy-backendjob logs. - Confirm the Render service shows a successful deploy and the health endpoint responds.
cd frontend
docker build -t splitnaira-frontend:prod .
# Push to your container registry and deploy via your hosting platform
docker tag splitnaira-frontend:prod <registry>/splitnaira-frontend:prod
docker push <registry>/splitnaira-frontend:prodOr trigger your frontend CD pipeline if configured.
Repeat the smoke test suite (§6) against production URLs. If any check fails, execute the rollback procedure (§7) immediately.
Run these checks after every deploy, against the target environment's URLs.
curl -s https://<backend-url>/health | jq .
# Expected: { "status": "ok" }
curl -s https://<backend-url>/ | jq .
# Expected: { "name": "SplitNaira API", "status": "ok", "version": "0.1.0" }A successful /health response confirms the backend connected to the database. If the health check fails, check DATABASE_URL and network access from the backend host.
curl -s https://<backend-url>/splits | jq .
# Expected: 200 with a projects array (may be empty on first deploy)Using the UI or Stellar CLI, execute one full flow:
- Create a project (
create_project) - Deposit funds (
deposit) - Distribute to collaborators (
distribute)
Verify the Horizon event stream includes:
project_createddeposit_receivedpayment_sentdistribution_complete
Open https://<frontend-url> in a browser. Confirm:
- Page loads without console errors
- Freighter wallet connects on testnet/mainnet as appropriate
- The splits dashboard renders (may be empty)
curl -s https://<backend-url>/api/openapi.json | jq .info
# Expected: OpenAPI info block with title and versionSoroban contracts are immutable once deployed. Rollback means pointing services back to the previous contract ID.
- Retrieve the last known-good
CONTRACT_IDfrom your secrets manager or deployment history. - Update
CONTRACT_IDin backend config vars and redeploy the backend (§5 Step 4). - Update
NEXT_PUBLIC_CONTRACT_IDin frontend config and redeploy the frontend (§5 Step 5). - Run smoke tests to confirm the previous contract is responding correctly.
Keep the last stable contract ID documented in your deployment log or secrets manager at all times.
See contract-release-and-upgrade-runbook.md §7 for the full upgrade/rollback strategy including blue/green canary approach.
Render retains previous deploys. To roll back:
- Open the Render dashboard → your backend service → Deploys.
- Select the last successful deploy and click Redeploy.
- Confirm the health endpoint responds after the rollback deploy completes.
If using another platform, redeploy the previous Docker image tag or Git SHA.
Redeploy the previous Docker image tag:
docker pull <registry>/splitnaira-frontend:<previous-tag>
# Restart your container or update your hosting platform to use the previous tagOr revert the frontend CD pipeline to the previous commit SHA.
If a migration caused data issues:
- Stop the backend to prevent further writes.
- Restore from the most recent pre-migration backup.
- Redeploy the previous backend version.
- Verify data integrity before resuming traffic.
Always take a database snapshot before running migrations in production.
Soroban contract storage has a TTL. For long-lived or quiet projects, call refresh_project_storage(project_id) on a weekly or bi-weekly cadence to prevent state eviction. See contract-release-and-upgrade-runbook.md §9 for the full TTL policy and incident response steps.
The backend applies per-route rate limits. Auth endpoints (/users/register, /users/login) have stricter limits to block credential stuffing. If you see 429 responses during smoke tests, wait for the window to reset or adjust limits in backend/src/middleware/rate-limit.ts before deploying.
CORS_ORIGIN accepts a comma-separated list of origins. In production, set it to the exact frontend URL(s) — no trailing slash, no wildcards.
The backend uses Winston. Set LOG_LEVEL=debug temporarily to diagnose startup issues, then revert to info for production.
The backend handles SIGTERM and SIGINT for graceful shutdown. The default force-exit timeout is 10 seconds (SHUTDOWN_FORCE_TIMEOUT_MS). Increase this if your hosting platform sends SIGTERM well before killing the process.
The frontend supports English (en) and French (fr) via next-intl. Locale-prefixed routes (/en, /fr) are enabled by default. To add a language, update frontend/src/i18n/routing.ts and add frontend/messages/<locale>.json.