Common operational procedures for the Stellar Remittance Platform backend.
Prerequisites: SSH access to the server, a copy of the production .env file, and DATABASE_URL available in your shell.
The backend is a Node.js/Express process started from the backend/ directory.
# Find and stop the running process
kill $(lsof -ti tcp:3001)
# Start in the background (production)
cd backend
node src/server.js &
# Or with PM2 (if configured)
pm2 restart future-backendVerify recovery:
curl -f http://localhost:3001/healthExpected response: { "status": "ok" } (or equivalent).
Note:
PORTdefaults to3001. If overridden via env, adjust thelsofcommand accordingly.
The project uses Prisma. There is no automatic migrate down command; rollback is a two-step process: mark the migration as rolled back, then apply a corrective migration.
Step 1 — Identify the latest applied migration:
cd backend
DATABASE_URL="<value>" npx prisma migrate statusNote the name of the last applied migration (e.g. 20240601120000_add_payment_streams).
Step 2 — Mark it as rolled back:
DATABASE_URL="<value>" npx prisma migrate resolve \
--rolled-back 20240601120000_add_payment_streamsStep 3 — Apply the corrective schema change:
Edit prisma/schema.prisma to revert the unwanted change, then generate and deploy a new migration:
DATABASE_URL="<value>" npx prisma migrate deployWarning:
prisma migrate resolve --rolled-backonly updates Prisma's migration history table. You must also manually reverse any DDL changes (e.g.DROP COLUMN) in the database before deploying the corrective migration.
Use the API to cancel a stream so that the event sourcing layer records the cancellation correctly. Cancelling directly in the database skips the StreamCancelled event published by the service layer.
Cancel a single stream by ID:
curl -X POST http://localhost:3001/api/v1/streaming/<stream-id>/cancelList all active streams (to find IDs that need cancellation):
curl "http://localhost:3001/api/v1/streaming?senderPublicKey=<GXXX...>"Bulk-cancel all active streams (emergency only):
If the API is unavailable, use the Prisma CLI to cancel directly. Be aware this bypasses event publishing.
cd backend
node --input-type=module <<'EOF'
import prisma from './src/db/client.js';
const { count } = await prisma.paymentStream.updateMany({
where: { status: 'ACTIVE' },
data: { status: 'CANCELLED' },
});
console.log(`Cancelled ${count} streams`);
await prisma.$disconnect();
EOFThe rate-limiter whitelist is an in-memory Set seeded at startup from the RATE_LIMIT_WHITELIST environment variable. There is no persistent store — changes require an env update and a process restart.
Step 1 — Add the IP to the whitelist in backend/.env:
# Open .env and append the IP to RATE_LIMIT_WHITELIST (comma-separated)
# Example — before:
RATE_LIMIT_WHITELIST=10.0.0.1
# After:
RATE_LIMIT_WHITELIST=10.0.0.1,203.0.113.42Step 2 — Restart the backend (see Section 1) to reload the env.
Step 3 — Verify the IP is no longer rate-limited:
curl -I -H "X-Forwarded-For: 203.0.113.42" http://localhost:3001/health
# Expect HTTP 200, not 429If
CONFIG_WATCH=trueis set, config file changes reload automatically without a restart — but the whitelist Set is populated at process boot only, so a restart is still required.
- Identify the incident type:
UNAUTHORIZED_ACCESS,DATA_BREACH,MALWARE_DETECTED,DDoS_ATTACK. - Assess severity:
CRITICAL,HIGH,MEDIUM,LOW. - Determine affected systems (e.g.
api,database,stellar-node).
curl -X POST http://localhost:3001/api/v1/security/incidents/create \
-H "Content-Type: application/json" \
-d '{
"type": "UNAUTHORIZED_ACCESS",
"severity": "CRITICAL",
"description": "Suspicious login attempts from IP 203.0.113.99",
"affectedSystems": ["api", "auth"]
}'Save the returned id (e.g. INC-1719140400000) for all subsequent updates.
- For unauthorized access: revoke active JWT sessions by rotating
JWT_SECRETin.envand restarting the backend. - For DDoS: add attacking IPs to
RATE_LIMIT_WHITELISTnegation — i.e. do not whitelist them; lowerRATE_LIMIT_MAXandRATE_LIMIT_WINDOW_MSas needed, then restart. - For a data breach: take the backend offline, preserve logs, and do not alter any files before forensics.
# Call for each completed playbook action
curl -X POST http://localhost:3001/api/v1/security/incidents/<INC-ID>/action \
-H "Content-Type: application/json" \
-d '{ "action": "Block user account" }'- Review
GET /api/v1/security/audit-logfor the affected timeframe. - Rotate any compromised secrets (see
backend/CONFIGURATION.md— Secret rotation). - Update
RATE_LIMIT_WHITELISTor other env vars as needed and redeploy. - File a post-mortem within 48 hours.
The backup system is implemented in backend/src/backup/manager.js. Backups are compressed (pg_dump custom format + gzip) and optionally AES-256-GCM encrypted.
BACKUP_DIR— directory containing the backup files (default./backups)DATABASE_URL— connection string for the target databaseBACKUP_ENC_KEY— 32-byte hex key (required only if the backup is encrypted, i.e. file ends in.enc)- PostgreSQL client tools (
pg_restore) installed and on$PATH
cd backend
node --input-type=module <<'EOF'
import { listBackups } from './src/backup/manager.js';
const backups = await listBackups();
backups.slice(0, 5).forEach(b => console.log(b.createdAt, b.file, `${(b.size/1e6).toFixed(1)} MB`));
EOFcd backend
BACKUP_FILE=/path/to/backup.dump.gz node --input-type=module <<'EOF'
import { verifyBackup } from './src/backup/manager.js';
const result = await verifyBackup(process.env.BACKUP_FILE);
console.log('Valid:', result.valid);
if (!result.valid) {
console.error('Checksum mismatch — do not restore this file');
process.exit(1);
}
EOFcd backend
DATABASE_URL="postgresql://user:password@host:5432/dbname" \
BACKUP_ENC_KEY="<32-byte-hex-key-if-encrypted>" \
BACKUP_FILE=/path/to/backup.dump.gz \
node --input-type=module <<'EOF'
import { restoreBackup } from './src/backup/manager.js';
const result = await restoreBackup(process.env.BACKUP_FILE);
console.log('Restore status:', result.status);
EOFPass targetDatabase to restore into a fresh database without touching production:
cd backend
DATABASE_URL="postgresql://future_admin:password@localhost:5432/future_restore_drill" \
BACKUP_FILE=/path/to/backup.dump.gz \
node --input-type=module <<'EOF'
import { restoreBackup } from './src/backup/manager.js';
const result = await restoreBackup(process.env.BACKUP_FILE, {
targetDatabase: 'future_restore_drill',
});
console.log(result);
EOFIf WAL archiving is configured, pass targetTime (ISO 8601) to stop recovery at a specific moment:
cd backend
DATABASE_URL="postgresql://..." \
BACKUP_FILE=/path/to/backup.dump.gz \
TARGET_TIME="2025-06-01T12:00:00Z" \
node --input-type=module <<'EOF'
import { restoreBackup } from './src/backup/manager.js';
const result = await restoreBackup(process.env.BACKUP_FILE, {
targetTime: process.env.TARGET_TIME,
});
console.log(result);
EOFAfter restoring to any database, apply outstanding Prisma migrations:
cd backend
DATABASE_URL="postgresql://user:password@host:5432/dbname" npx prisma migrate deployQuery a known table to confirm data integrity:
psql "$DATABASE_URL" -c "SELECT COUNT(*) FROM \"User\";"The CI workflow .github/workflows/backup-verification.yml runs every Sunday at 03:00 UTC. It:
- Seeds a source database, creates a backup via the backup manager
- Verifies the backup checksum
- Restores the backup to a fresh database instance
- Runs
prisma migrate deployagainst the restored database - Queries the restored database for expected data
- Opens a GitHub issue and notifies watchers on failure