This guide covers deploying NotifyChain across three environments: local, staging, and production. It is the single reference for getting every component — smart contracts, listener service, and dashboard — running end-to-end.
Related documents
- Local development workflow →
LOCAL_DEVELOPMENT.md- Smart contract deploy steps →
DEPLOYMENT_PLAYBOOK.md- Contract upgrade procedure →
CONTRACT_UPGRADE_GUIDE.md- Troubleshooting →
TROUBLESHOOTING.md
- Prerequisites
- Repository Structure
- Local Deployment
- Staging Deployment
- Production Deployment
- Environment Variables Reference
- Health Checks and Verification
The following tools must be installed before deploying any environment.
| Tool | Minimum version | Install |
|---|---|---|
| Node.js | 18 | nodejs.org |
| npm | 9 | Bundled with Node.js |
| Git | any | git-scm.com |
| Tool | Minimum version | Install |
|---|---|---|
| Rust (stable) | 1.78 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| WebAssembly target | — | rustup target add wasm32-unknown-unknown |
| Stellar CLI | latest | cargo install --locked stellar-cli --features opt |
node --version # v18+
npm --version # 9+
rustc --version # 1.78+
stellar --versionNotify-Chain/
├── contract/ # Soroban smart contracts (Rust)
├── listener/ # Off-chain event listener and API (Node.js / TypeScript)
├── dashboard/ # React + Vite frontend dashboard
├── scripts/ # Utility shell scripts (health-check, fuzz coverage)
└── .github/workflows/ # CI/CD pipelines
Each component is deployed independently. The listener depends on a deployed contract address; the dashboard depends on a running listener.
Local deployment runs all three components on a single machine against the Stellar testnet.
git clone https://github.com/Core-Foundry/Notify-Chain.git
cd Notify-ChainGenerate and fund a test identity:
stellar keys generate dev-account --network testnet
stellar keys fund dev-account --network testnetBuild and deploy the AutoShare contract:
cd contract/contracts/hello-world
stellar contract build
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/hello_world.wasm \
--source dev-account \
--network testnet
# Copy the printed CONTRACT_IDInitialize the contract:
stellar contract invoke \
--id <CONTRACT_ID> \
--source dev-account \
--network testnet \
-- initialize_admin \
--admin <YOUR_PUBLIC_KEY>For TaskBounty contract steps, see
DEPLOYMENT_PLAYBOOK.md.
cd listener
cp .env.example .envEdit .env with the contract ID from step 2:
STELLAR_NETWORK=testnet
STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443
STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
CONTRACT_ADDRESSES=[{"address":"<CONTRACT_ID>","events":["*"]}]
EVENTS_API_PORT=8787
EVENTS_API_CORS_ORIGIN=http://localhost:5173
DATABASE_PATH=./data/notifications.dbInstall dependencies, run migrations, and start:
npm ci
mkdir -p data
npm run migrate
npm run devVerify the listener is healthy:
curl http://localhost:8787/health
# Expected: {"status":"ok",...}cd dashboard
cp .env.example .envThe default .env is ready for local use:
VITE_EVENTS_API_URL=http://localhost:8787/api/events
VITE_STELLAR_NETWORK=TESTNETInstall dependencies and start:
npm ci
npm run dev
# Dashboard available at http://localhost:5173- Open
http://localhost:5173in a browser. - Events from the deployed contract should appear as they are emitted on-chain.
- Check
http://localhost:8787/healthfor listener status. - Check
http://localhost:8787/api/indexing/healthfor indexing lag.
Staging is triggered automatically by the staging.yml CI workflow when commits are pushed to the staging branch. It mirrors production configuration but points at testnet.
push to staging branch
→ check-migrations job: npm run migrate + npm run check-migrations
→ deploy job: build listener + build dashboard + health check
See .github/workflows/staging.yml for the full pipeline.
If you need to deploy staging manually on a server:
Create listener/.env from the staging template:
NODE_ENV=production
LOG_LEVEL=info
STELLAR_NETWORK=testnet
STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443
STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
CONTRACT_ADDRESSES=[{"address":"<STAGING_CONTRACT_ID>","events":["*"]}]
EVENTS_API_PORT=8787
EVENTS_API_CORS_ORIGIN=https://staging.your-domain.com
DATABASE_PATH=/var/data/notify-chain/notifications.db
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/STAGING_ID/STAGING_TOKEN
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=60Create dashboard/.env:
VITE_EVENTS_API_URL=https://staging-api.your-domain.com/api/events
VITE_STELLAR_NETWORK=TESTNETcd listener
npm ci
mkdir -p /var/data/notify-chain
npm run migrate
npm run build
node dist/index.jscd dashboard
npm ci
npm run build
# Serve the dist/ folder with nginx, Caddy, or Cloudflare Pagesbash scripts/health-check.sh https://staging-api.your-domain.com/healthProduction deploys the listener against Stellar mainnet and serves the dashboard at your public domain.
- A Stellar mainnet account with sufficient XLM for contract deployment and ongoing fees.
- A server or container runtime (Linux recommended) for the listener.
- A static hosting service (Cloudflare Pages, Vercel, S3+CDN, nginx) for the dashboard.
- A SQLite-compatible persistent volume or managed database path for the listener.
- Secrets management: do not store private keys or webhook tokens in plain-text
.envfiles. Use your platform's secrets store (GitHub Actions secrets, AWS Secrets Manager, Vault, etc.).
Configure the Stellar CLI for mainnet:
stellar network add \
--rpc-url "https://soroban-rpc.stellar.org" \
--network-passphrase "Public Global Stellar Network ; September 2015" \
mainnetBuild and optimize the contract:
cd contract/contracts/hello-world
stellar contract build
stellar contract optimize \
--wasm target/wasm32-unknown-unknown/release/hello_world.wasmDeploy:
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/hello_world.optimized.wasm \
--source mainnet-deployer \
--network mainnet
# Save the CONTRACT_IDInitialize:
stellar contract invoke \
--id <CONTRACT_ID> \
--source mainnet-deployer \
--network mainnet \
-- initialize_admin \
--admin <ADMIN_ADDRESS>Verify deployment:
stellar contract invoke \
--id <CONTRACT_ID> \
--source mainnet-deployer \
--network mainnet \
-- version
# Expected: 1Set the following environment variables through your secrets manager or platform:
NODE_ENV=production
LOG_LEVEL=info
STELLAR_NETWORK=public
STELLAR_RPC_URL=https://soroban-rpc.stellar.org
STELLAR_NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015
CONTRACT_ADDRESSES=[{"address":"<MAINNET_CONTRACT_ID>","events":["*"]}]
EVENTS_API_PORT=8787
EVENTS_API_CORS_ORIGIN=https://your-production-domain.com
DATABASE_PATH=/var/data/notify-chain/notifications.db
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/PROD_ID/PROD_TOKEN
WEBHOOK_SECRETS=[{"id":"prod","secret":"<strong-random-secret>"}]
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=60
SCHEDULER_ENABLED=true
RETRY_SCHEDULER_ENABLED=trueMigrations must be applied before starting the listener. Run this once before each deployment:
cd listener
npm run migrate
npm run check-migrations
# Should report: No pending migrationscd listener
npm ci --omit=dev
npm run build
node dist/index.jsUse a process manager (systemd, PM2, Docker) to keep the process running and restart on failure.
Example PM2 setup:
npm install -g pm2
pm2 start dist/index.js --name notify-chain-listener
pm2 save
pm2 startupExample systemd unit (/etc/systemd/system/notify-chain.service):
[Unit]
Description=NotifyChain Listener
After=network.target
[Service]
Type=simple
User=notify
WorkingDirectory=/opt/notify-chain/listener
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=5
EnvironmentFile=/opt/notify-chain/listener/.env
[Install]
WantedBy=multi-user.targetsystemctl enable notify-chain
systemctl start notify-chainSet dashboard/.env (or pass as build-time variables):
VITE_EVENTS_API_URL=https://api.your-production-domain.com/api/events
VITE_STELLAR_NETWORK=PUBLICBuild:
cd dashboard
npm ci --omit=dev
npm run build
# Outputs static files to dashboard/dist/Deploy dashboard/dist/ to your static hosting provider. For Cloudflare Pages, the preview workflow is already configured in .github/workflows/preview.yml.
# Listener health
curl https://api.your-production-domain.com/health
# Indexing health
curl https://api.your-production-domain.com/api/indexing/health
# Events API
curl https://api.your-production-domain.com/api/eventsAll three should return HTTP 200 with JSON bodies.
| Variable | Default | Description |
|---|---|---|
STELLAR_NETWORK |
testnet |
testnet or public |
STELLAR_RPC_URL |
https://soroban-testnet.stellar.org:443 |
Stellar Soroban RPC endpoint |
STELLAR_NETWORK_PASSPHRASE |
Test SDF Network ; September 2015 |
Network passphrase |
CONTRACT_ADDRESSES |
— | JSON array of { address, events } objects |
| Variable | Default | Description |
|---|---|---|
EVENTS_API_PORT |
8787 |
HTTP port for the events API |
EVENTS_API_CORS_ORIGIN |
http://localhost:5173 |
Allowed CORS origin (must match dashboard URL exactly) |
WEBHOOK_SECRETS |
[] |
JSON array of { id, secret } pairs for webhook signature verification |
| Variable | Default | Description |
|---|---|---|
DATABASE_PATH |
./data/notifications.db |
Path to the SQLite database file |
| Variable | Default | Description |
|---|---|---|
POLL_INTERVAL_MS |
30000 |
How often to poll Stellar for new events (ms) |
MAX_RECONNECT_ATTEMPTS |
5 |
Max reconnect attempts before giving up |
RECONNECT_DELAY_MS |
5000 |
Delay between reconnect attempts (ms) |
| Variable | Default | Description |
|---|---|---|
SCHEDULER_ENABLED |
true |
Enable the scheduled notification dispatcher |
SCHEDULER_POLL_INTERVAL_MS |
10000 |
How often the scheduler checks for due notifications (ms) |
SCHEDULER_BATCH_SIZE |
10 |
Max notifications dispatched per poll cycle |
RETRY_SCHEDULER_ENABLED |
true |
Enable the DB-backed retry scheduler |
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
true |
Enable API rate limiting |
RATE_LIMIT_WINDOW_MS |
60000 |
Sliding window duration (ms) |
RATE_LIMIT_MAX_REQUESTS |
60 |
Max requests per window per client |
| Variable | Default | Description |
|---|---|---|
DISCORD_WEBHOOK_URL |
— | Discord webhook URL for event notifications |
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
Log verbosity: debug, info, warn, error |
NODE_ENV |
— | Set to production for JSON log output |
| Variable | Default | Description |
|---|---|---|
VITE_EVENTS_API_URL |
http://localhost:8787/api/events |
Full URL to the listener's events endpoint |
VITE_STELLAR_NETWORK |
TESTNET |
TESTNET or PUBLIC |
| Endpoint | Description |
|---|---|
GET /health |
Overall service health (Stellar RPC, Discord, database, event registry) |
GET /api/indexing/health |
Ledger sync status and indexing lag |
GET /api/notifications/health |
Notification pipeline health report |
GET /api/status |
Per-contract pause status |
A healthy deployment returns HTTP 200 from /health with "status": "ok".
A degraded deployment returns HTTP 200 with "status": "degraded" — the service is running but a non-critical dependency (e.g. Discord webhook) is unreachable.
An unhealthy deployment returns HTTP 503 with "status": "error" — a critical dependency (Stellar RPC or database) is down.
Run this sequence after any deployment to confirm all layers are working:
# 1. Listener health
curl -sf https://<your-api-host>/health | jq .status
# 2. Indexing lag (ledgerLag should be small, e.g. < 10)
curl -sf https://<your-api-host>/api/indexing/health | jq '{status, ledgerLag}'
# 3. Events API (returns recent on-chain events)
curl -sf https://<your-api-host>/api/events | jq '.count'
# 4. Dashboard loads (HTTP 200)
curl -sf -o /dev/null -w "%{http_code}" https://<your-dashboard-host>/All four commands should complete without error.