This guide covers end-to-end deployment of all Nova Rewards components — Soroban smart contracts, Node.js backend, and Next.js frontend — for both testnet and mainnet environments.
- Prerequisites
- Environment Setup
- Smart Contract Deployment
- Backend Deployment
- Frontend Deployment
- Post-Deployment Verification
- Rollback Procedures
- Troubleshooting
| Tool | Minimum Version | Install |
|---|---|---|
| Node.js | 20.x | https://nodejs.org |
| Rust (stable) | see rust-toolchain.toml |
rustup toolchain install stable |
| Stellar CLI | latest | cargo install --locked stellar-cli --features opt |
| Docker | 24.x | https://docs.docker.com/get-docker |
| Docker Compose | 2.x | Bundled with Docker Desktop |
wasm-opt (binaryen) |
any | brew install binaryen / apt install binaryen |
| Git | 2.x | https://git-scm.com |
Verify the Rust WASM target is installed:
rustup target add wasm32v1-none- Stellar keypairs: issuer account and distribution account (funded)
- Deployer keypair for contract deployment (funded with XLM)
- Vercel account with project access
- PostgreSQL instance (managed or self-hosted)
- Redis instance (managed or self-hosted)
- SMTP credentials or SendGrid API key
git clone https://github.com/barry01-hash/Nova-Rewards.git
cd Nova-RewardsCopy the example and fill in values for your target environment:
cp novaRewards/.env.example novaRewards/.envKey variables to configure:
| Variable | Testnet Value | Mainnet Value | Notes |
|---|---|---|---|
NODE_ENV |
development |
production |
|
PORT |
3001 |
3001 |
|
DATABASE_URL |
postgresql://nova:changeme@localhost:5432/nova_rewards |
Managed DB connection string | Never commit |
REDIS_URL |
redis://localhost:6379 |
ElastiCache / managed Redis URL | |
JWT_SECRET |
Any random string | Long random secret (≥ 64 chars) | Never commit |
JWT_EXPIRES_IN |
15m |
15m |
|
JWT_REFRESH_EXPIRES_IN |
7d |
7d |
|
STELLAR_NETWORK |
testnet |
mainnet |
|
HORIZON_URL |
https://horizon-testnet.stellar.org |
https://horizon.stellar.org |
|
ISSUER_PUBLIC |
Testnet G... key |
Mainnet G... key |
|
ISSUER_SECRET |
Testnet S... key |
Mainnet S... key |
Never commit |
DISTRIBUTION_PUBLIC |
Testnet G... key |
Mainnet G... key |
|
DISTRIBUTION_SECRET |
Testnet S... key |
Mainnet S... key |
Never commit |
ALLOWED_ORIGIN |
http://localhost:3000 |
https://nova-rewards.xyz |
|
NOVA_TOKEN_CONTRACT_ID |
Output from contract deploy | Output from contract deploy | |
REWARD_POOL_CONTRACT_ID |
Output from contract deploy | Output from contract deploy |
See novaRewards/.env.example for the full variable reference including email, rate limiting, and backup settings.
Frontend build-time variables are set in Vercel's project settings (not in a committed file). For local development, create novaRewards/frontend/.env.local:
# novaRewards/frontend/.env.local — not committed to git
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org
NEXT_PUBLIC_ISSUER_PUBLIC=G... # testnet issuer public key
NEXT_PUBLIC_STELLAR_NETWORK=testnet
NEXT_PUBLIC_MULTISIG_CONTRACT_ID=C... # testnet contract IDFor Vercel deployments, set these under Project → Settings → Environment Variables, scoped to the correct environment (Preview or Production). See .env.vercel.example for the full reference.
Export these shell variables before running scripts/deploy-contracts.sh:
export DEPLOYER_SECRET=S... # Stellar secret key for the deployer account
export ADMIN_ADDRESS=G... # Stellar public key for contract admin
export NETWORK=testnet # or mainnet
# Optional overrides:
export ADMIN_SIGNERS="$ADMIN_ADDRESS"
export ADMIN_THRESHOLD=1The script scripts/deploy-contracts.sh builds, optimizes, uploads, deploys, and initializes all five workspace contracts in order:
| Order | Contract | Env Key Written |
|---|---|---|
| 1 | nova_token |
NOVA_TOKEN_CONTRACT_ID |
| 2 | reward_pool |
REWARD_POOL_CONTRACT_ID |
| 3 | vesting |
CLAIM_DISTRIBUTION_CONTRACT_ID |
| 4 | referral |
STAKING_CONTRACT_ID |
| 5 | admin_roles |
ADMIN_ROLES_CONTRACT_ID |
Contract IDs are written to .env.testnet or .env.mainnet automatically.
Step 1 — Fund the deployer account via Friendbot:
curl "https://friendbot.stellar.org?addr=$ADMIN_ADDRESS"Step 2 — (Optional) Start a local standalone network for development:
# POSIX
./scripts/start-local-testnet.sh
# PowerShell
./scripts/start-local-testnet.ps1The local network runs at http://localhost:8000/rpc with passphrase Standalone Network ; February 2017.
Step 3 — Build and test contracts before deploying:
# POSIX
./scripts/build-contracts.sh
./scripts/test-contracts.sh
# PowerShell
./scripts/build-contracts.ps1
./scripts/test-contracts.ps1Step 4 — Preview the deployment (dry run):
export DEPLOYER_SECRET=S...
export ADMIN_ADDRESS=G...
export NETWORK=testnet
bash scripts/deploy-contracts.sh --dry-runReview the printed commands. No transactions are broadcast in dry-run mode.
Step 5 — Deploy to testnet:
bash scripts/deploy-contracts.shStep 6 — Verify contract IDs were written:
cat .env.testnet
# Expected output:
# NOVA_TOKEN_CONTRACT_ID=C...
# REWARD_POOL_CONTRACT_ID=C...
# CLAIM_DISTRIBUTION_CONTRACT_ID=C...
# STAKING_CONTRACT_ID=C...
# ADMIN_ROLES_CONTRACT_ID=C...Step 7 — Verify initialization on-chain:
# Replace C... with the actual contract IDs from .env.testnet
stellar contract invoke \
--id $NOVA_TOKEN_CONTRACT_ID \
--network-passphrase "Test SDF Network ; September 2015" \
--rpc-url https://soroban-testnet.stellar.org \
--source $DEPLOYER_SECRET \
-- get_admin
stellar contract invoke \
--id $REWARD_POOL_CONTRACT_ID \
--network-passphrase "Test SDF Network ; September 2015" \
--rpc-url https://soroban-testnet.stellar.org \
--source $DEPLOYER_SECRET \
-- get_balance
⚠️ Contract deployments are irreversible on-chain. Always complete testnet deployment and verification before proceeding to mainnet.
Pre-deployment checklist:
- All contracts pass tests on testnet
- Security audit completed and findings resolved (see
docs/audits/) - Deployer account funded with sufficient XLM for all five deployments
-
ADMIN_SIGNERSandADMIN_THRESHOLDreviewed for multi-sig requirements - WASM hashes recorded for future upgrade reference
Step 1 — Tag the current state:
git tag -a "contract-v$(date +%Y%m%d)" -m "Pre-mainnet contract deploy"
git push origin --tagsStep 2 — Deploy to mainnet:
export DEPLOYER_SECRET=S... # mainnet deployer secret
export ADMIN_ADDRESS=G... # mainnet admin address
export NETWORK=mainnet
export ADMIN_SIGNERS="G... G..." # space-separated if multi-sig
export ADMIN_THRESHOLD=2 # adjust for your multi-sig policy
bash scripts/deploy-contracts.shStep 3 — Verify contract IDs:
cat .env.mainnetStep 4 — Verify initialization on mainnet:
stellar contract invoke \
--id $NOVA_TOKEN_CONTRACT_ID \
--network-passphrase "Public Global Stellar Network ; September 2015" \
--rpc-url https://soroban-rpc.stellar.org \
--source $DEPLOYER_SECRET \
-- get_adminStep 5 — Copy contract IDs to backend environment:
Update NOVA_TOKEN_CONTRACT_ID and REWARD_POOL_CONTRACT_ID in the production backend .env (or secrets manager) with the values from .env.mainnet.
The backend is a Node.js/Express application. It requires PostgreSQL and Redis to be running and reachable before starting.
Step 1 — Configure environment:
cp novaRewards/.env.example novaRewards/.env
# Edit .env with testnet/staging valuesStep 2 — Start all services with Docker Compose:
cd novaRewards
docker compose up -d --buildThis starts PostgreSQL, Redis, runs database migrations automatically via the migrate service, then starts the backend and frontend.
Step 3 — Verify migrations ran:
docker compose logs migrate
# Should end with: "All migrations applied successfully"Step 4 — Check backend health:
curl http://localhost:3001/health
# Expected: HTTP 200 with JSON statusStep 5 — Run smoke tests:
GATEWAY_URL=http://localhost:8080 bash scripts/gateway-smoke-test.shProduction deployments are triggered automatically by the CI/CD pipeline when commits land on main. For manual deployments or hotfixes:
Pre-deployment checklist:
- All CI checks pass on the branch
- Database migrations are backward-compatible (additive only)
- New contract IDs are set in the production secrets/environment
- Database backup taken (see step 2)
Step 1 — SSH into the production server:
ssh deploy@nova-rewards.xyz
cd /opt/nova-rewardsStep 2 — Take a database backup before deploying:
docker compose exec postgres pg_dump -U nova nova_rewards \
> /opt/backups/pre-deploy-$(date +%Y%m%d-%H%M).sqlStep 3 — Tag the current release:
git tag -a "release-$(date +%Y%m%d-%H%M)" -m "Pre-deploy snapshot"
git push origin --tagsStep 4 — Pull latest main:
git pull origin mainStep 5 — Rebuild and restart the backend container only:
cd novaRewards
docker compose up -d --build --no-deps backendStep 6 — Run any new migrations:
docker compose exec backend npm run migrateStep 7 — Verify health:
curl -f https://api.nova-rewards.xyz/health
bash scripts/gateway-smoke-test.shStep 8 — Monitor logs for errors (first 5 minutes):
docker compose logs -f backend --since 5mThe frontend is a Next.js application deployed to Vercel. Deployments are driven by Git events.
Preview deployments are created automatically for every pull request targeting main. The workflow .github/workflows/vercel-preview.yml handles this.
Required GitHub Secrets (set under Settings → Secrets and variables → Actions):
| Secret | Where to find it |
|---|---|
VERCEL_TOKEN |
Vercel → Account Settings → Tokens |
VERCEL_ORG_ID |
Vercel → Project Settings → General |
VERCEL_PROJECT_ID |
Vercel → Project Settings → General |
Required Vercel Environment Variables (scoped to Preview):
| Variable | Value |
|---|---|
NEXT_PUBLIC_API_URL |
https://api-preview.nova-rewards.xyz |
NEXT_PUBLIC_HORIZON_URL |
https://horizon-testnet.stellar.org |
NEXT_PUBLIC_ISSUER_PUBLIC |
Testnet issuer public key |
NEXT_PUBLIC_STELLAR_NETWORK |
testnet |
NEXT_PUBLIC_MULTISIG_CONTRACT_ID |
Testnet contract ID |
Once a PR is opened, the preview URL is posted as a comment on the PR. Verify the deployment in the Vercel dashboard under Project → Deployments.
Manual preview build (local verification):
cd novaRewards/frontend
cp .env.local.example .env.local # or create manually
npm install
npm run build
npm run start
# Visit http://localhost:3000Production deployment is triggered automatically when a PR is merged to main via .github/workflows/vercel-production.yml.
Required Vercel Environment Variables (scoped to Production):
| Variable | Value |
|---|---|
NEXT_PUBLIC_API_URL |
https://api.nova-rewards.xyz |
NEXT_PUBLIC_HORIZON_URL |
https://horizon.stellar.org |
NEXT_PUBLIC_ISSUER_PUBLIC |
Mainnet issuer public key |
NEXT_PUBLIC_STELLAR_NETWORK |
public |
NEXT_PUBLIC_MULTISIG_CONTRACT_ID |
Mainnet contract ID |
Pre-merge checklist:
- Preview deployment verified by a team member not involved in the change
- Backend production deployment completed (frontend depends on the API being live)
- All CI checks pass
Monitor the production deployment in the Vercel dashboard. The deployment is live when status shows Ready.
Run these checks after every deployment, in order.
# Replace with your environment's URL
curl -f https://api.nova-rewards.xyz/healthExpected response:
{ "status": "ok", "db": "connected", "redis": "connected" }# Testnet / staging
GATEWAY_URL=http://localhost:8080 bash scripts/gateway-smoke-test.sh
# Production
GATEWAY_URL=https://api.nova-rewards.xyz bash scripts/gateway-smoke-test.shThe script checks /auth/, /users/, /rewards/, and /leaderboard/ and exits non-zero on any gateway error (502/503/504).
node novaRewards/scripts/check-balances.jsVerifies the issuer and distribution accounts are reachable and funded.
# Verify nova_token admin is set correctly
stellar contract invoke \
--id $NOVA_TOKEN_CONTRACT_ID \
--network-passphrase "Public Global Stellar Network ; September 2015" \
--rpc-url https://soroban-rpc.stellar.org \
--source $DEPLOYER_SECRET \
-- get_admin
# Verify reward_pool balance
stellar contract invoke \
--id $REWARD_POOL_CONTRACT_ID \
--network-passphrase "Public Global Stellar Network ; September 2015" \
--rpc-url https://soroban-rpc.stellar.org \
--source $DEPLOYER_SECRET \
-- get_balance- Open the production URL in a browser.
- Confirm the network badge shows the correct network (testnet/mainnet).
- Log in with a test account and verify the dashboard loads.
- Connect a Freighter wallet and confirm the wallet address is displayed.
/healthreturns HTTP 200 withdb: connectedandredis: connected- Smoke test exits with code 0
- Vercel deployment status is Ready
- No error-level log entries in
docker compose logs backendin the 5 minutes after deploy
Vercel retains all previous deployments indefinitely.
- Open Vercel → Project → Deployments.
- Find the last known-good deployment.
- Click ⋯ → Promote to Production.
The previous build is instantly re-promoted with no rebuild. This takes effect in under 30 seconds.
ssh deploy@nova-rewards.xyz
cd /opt/nova-rewards
# List recent release tags
git tag --sort=-creatordate | head -10
# Check out the previous release
git checkout tags/release-<YYYYMMDD-HHMM>
# Rebuild and restart
cd novaRewards
docker compose up -d --build --no-deps backend
# Verify
curl -f https://api.nova-rewards.xyz/health
⚠️ Database rollbacks risk data loss. Only proceed if the migration introduced a defect and no user data was written after the migration ran.
Option A — Roll back the most recent migration:
docker compose exec backend npm run migrate:rollbackOption B — Restore from pre-deploy backup:
# Stop the backend to prevent writes during restore
docker compose stop backend
# Restore from the backup taken in step 2 of production deployment
docker compose exec -T postgres psql -U nova nova_rewards \
< /opt/backups/pre-deploy-<YYYYMMDD-HHMM>.sql
# Restart
docker compose start backend
curl -f https://api.nova-rewards.xyz/healthSoroban contracts cannot be rolled back once deployed. Mitigation options:
- Upgrade the contract to a fixed version using
stellar contract upload+stellar contract invoke -- upgrade. - Pause the contract if an admin pause function is available in
admin_roles. - Disable the affected feature in the backend by setting the relevant feature flag via the admin API.
For upgrade procedures, see docs/upgrade-guide.md.
| Tag Format | When to Create |
|---|---|
release-YYYYMMDD-HHMM |
Before every production backend deploy |
contract-vYYYYMMDD |
Before every Soroban contract deploy |
hotfix-<issue-number> |
After an emergency fix is applied |
The script requires both cargo and stellar to be on PATH.
which cargo # should print a path
which stellar # should print a path
cargo install --locked stellar-cli --features optThe deploy script enforces required variables with set -u. Export them before running:
export DEPLOYER_SECRET=S...
export ADMIN_ADDRESS=G...The deployer account needs XLM to pay transaction fees. On testnet, use Friendbot:
curl "https://friendbot.stellar.org?addr=$ADMIN_ADDRESS"On mainnet, fund the deployer account from your treasury before deploying.
Another process is using port 5432, 6379, 3001, or 3000.
# Find and stop the conflicting process
lsof -i :5432
kill -9 <PID>Or change the host port mapping in novaRewards/docker-compose.yml.
- Confirm PostgreSQL is running:
docker compose ps postgres - Check the
DATABASE_URLin.envmatches the running container. - Check migration logs:
docker compose logs migrate - Inspect backend logs:
docker compose logs backend
- Confirm Redis is running:
docker compose ps redis - Check
REDIS_URLin.env. - Test connectivity:
docker compose exec redis redis-cli ping(should returnPONG).
Build-time variables must be present at next build time. For local builds, ensure novaRewards/frontend/.env.local exists. For Vercel, confirm the variable is set in Project → Settings → Environment Variables for the correct scope (Preview or Production).
- Check the build logs in the Vercel dashboard for the specific error.
- Common causes: missing environment variable, TypeScript error, or out-of-memory during build.
- If the build OOMs, increase the Vercel build memory limit in
vercel.jsonor optimize the build.
The gateway (nginx) cannot reach the backend upstream.
# Check backend is running
docker compose ps backend
# Check nginx config
docker compose logs gateway
# Restart gateway after backend is healthy
docker compose restart gatewayThe migration runner tracks applied migrations. If the state is out of sync:
# Check which migrations have been applied
docker compose exec backend node -e "
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.query('SELECT filename FROM migrations ORDER BY applied_at').then(r => {
r.rows.forEach(row => console.log(row.filename));
pool.end();
});
"If a migration was partially applied, restore from backup (see section 7.3) and re-run.
For contract upgrade procedures, see docs/upgrade-guide.md.
For infrastructure provisioning (Terraform, Kubernetes), see docs/infra/terraform.md.
For monitoring and alerting setup, see monitoring/README.md.