This guide covers deploying AirFlex to a production-like environment using:
- Vercel — Next.js frontend
- Railway — Express API server + PostgreSQL database
- Prerequisites
- Frontend on Vercel
- Database (PostgreSQL) on Railway
- Server on Railway
- Connecting to Stellar Testnet
- Running Database Migrations
- Post-Deployment Checklist
- A Vercel account
- A Railway account
pnpminstalled locally (npm install -g pnpm)- The AirFlex repository forked/cloned
- A Paystack account with a live/test secret key
- A Termii account with an API key
- A Stellar keypair for the server admin (generate below)
# Generate a server admin Stellar keypair (save the output securely)
stellar keys generate --network testnet server-admin
stellar keys address server-adminDeploy the server and database to Railway in one click:
The button uses the
railway.jsontemplate at the repository root. After clicking, Railway will prompt you for the required environment variables listed in Section 3 before provisioning resources.
| Variable | Description | Example |
|---|---|---|
NEXT_PUBLIC_API_URL |
Full URL of your deployed Railway server | https://airflex-server.up.railway.app |
NEXTAUTH_SECRET |
Random secret for session signing (if using NextAuth) | openssl rand -hex 32 |
NEXT_PUBLIC_STELLAR_NETWORK |
testnet or mainnet |
testnet |
-
Push your repository to GitHub (or fork the official repo).
-
Go to vercel.com/new and import your GitHub repository.
-
In the Configure Project screen:
- Framework Preset: Next.js (auto-detected)
- Root Directory:
frontend - Build Command:
pnpm build(or leave as defaultnext build) - Output Directory:
.next
-
Expand Environment Variables and add each variable from the table above.
-
Click Deploy.
-
Once deployed, note your Vercel URL (e.g.
https://airflex.vercel.app). You will add this asCORS_ORIGINon the Railway server.
- Open your Vercel URL — the marketplace landing page should load.
- Open browser DevTools → Network tab → confirm API calls go to your
NEXT_PUBLIC_API_URL. - Navigate to
/auth/signupand verify the OTP flow initiates without CORS errors.
-
In your Railway project, click + New → Database → Add PostgreSQL.
-
Railway provisions a managed PostgreSQL instance automatically.
-
Click the PostgreSQL service → Variables tab → copy the
DATABASE_URLvalue. It follows the format:postgresql://<user>:<password>@<host>:<port>/<dbname> -
This value will be used as the
DATABASE_URLenvironment variable on the server service (see Section 3).
- In the Railway PostgreSQL service, open the Data tab or connect via
psql $DATABASE_URLand run\dtto list tables after migrations have been applied.
| Variable | Required | Description |
|---|---|---|
PORT |
No | Railway sets this automatically — do not override |
NODE_ENV |
Yes | Set to production |
DATABASE_URL |
Yes | Copied from the Railway PostgreSQL service |
JWT_SECRET |
Yes | Random string ≥ 32 characters (openssl rand -hex 32) |
ENCRYPTION_KEY |
Yes | Exactly 64 hex characters (32 bytes for AES-256-GCM) — node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
STELLAR_SERVER_SECRET |
Yes | Stellar secret key of the server admin account (starts with S) |
ESCROW_CONTRACT_ADDRESS |
Yes | Deployed Soroban escrow contract address |
STELLAR_NETWORK |
Yes | testnet or mainnet |
HORIZON_URL |
Yes | Horizon RPC endpoint (see Section 4) |
SOROBAN_RPC_URL |
Yes | Soroban RPC endpoint (see Section 4) |
PAYSTACK_SECRET_KEY |
Yes | Paystack live or test secret key |
TERMII_API_KEY |
Yes | Termii SMS API key |
CORS_ORIGIN |
Yes | Your Vercel frontend URL (e.g. https://airflex.vercel.app) |
REDIS_URL |
Yes | Redis connection string for BullMQ job queue (Railway Redis add-on) |
ENABLE_API_DOCS |
No | Set to true to serve Swagger UI in non-production environments |
-
In your Railway project, click + New → GitHub Repo → select your fork.
-
Railway auto-detects the
server/directory. Set the Root Directory toserver. -
In Settings → Build & Deploy:
- Build Command:
pnpm install --frozen-lockfile && pnpm build - Start Command:
pnpm start
- Build Command:
-
Go to the Variables tab and add every variable from the table above.
-
Click Deploy. Railway builds the TypeScript source and starts the server.
-
Add a Redis add-on for BullMQ:
- Click + New → Database → Add Redis.
- Copy the
REDIS_URLfrom the Redis service Variables tab and add it to the server service.
- Railway displays the public domain after deployment (e.g.
https://airflex-server.up.railway.app). - Visit
https://airflex-server.up.railway.app/health— should return:{ "status": "ok", "timestamp": "..." } - Visit
https://airflex-server.up.railway.app/ready— should return:{ "status": "ready", "db": "ok" }
Set the following environment variables to connect to the Stellar Testnet:
STELLAR_NETWORK=testnet
HORIZON_URL=https://horizon-testnet.stellar.org
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
ESCROW_CONTRACT_ADDRESS=CCBJ235OCBFZXBFSUUUT4PMG7RRCAXZXMUEB2L7CTTQ5NRSNO4P2SLNPSTELLAR_NETWORK=mainnet
HORIZON_URL=https://horizon.stellar.org
SOROBAN_RPC_URL=https://soroban-rpc.mainnet.stellar.gateway.fmThe STELLAR_SERVER_SECRET account must be funded before it can submit transactions:
# Fund via Friendbot (testnet only)
curl "https://friendbot.stellar.org?addr=<YOUR_SERVER_PUBLIC_KEY>"New user wallets are automatically funded via Friendbot on first login when
STELLAR_NETWORK=testnet. On mainnet, replace the Friendbot call with a funded
platform account that seeds new wallets with a minimum balance.
After the server is deployed for the first time — and after every schema change — run migrations against the production database.
# Export the Railway DATABASE_URL
export DATABASE_URL="postgresql://user:password@host:port/dbname"
cd server
pnpm db:migrate# Install the Railway CLI
npm install -g @railway/cli
# Login and link your project
railway login
railway link
# Run migrations in the Railway environment
railway run pnpm db:migrateThe migration script applies SQL files in server/migrations/ sequentially,
creating or altering tables:
users— phone, OTP fields, Stellar public key, deletion statewallets— Stellar key pairs (secret key AES-256-GCM encrypted)trade_offers— listings, escrow hashes, status transitionstransactions— audit log (retained with[deleted]sentinel on account deletion)
Always run migrations before starting a new server version that depends on schema changes.
Work through this checklist after every fresh deployment or promotion to production.
-
GET /healthreturns HTTP 200 with{ "status": "ok" } -
GET /readyreturns HTTP 200 with{ "status": "ready", "db": "ok" } - PostgreSQL connection is live (confirmed by
/readyresponse) - Redis connection is live (check Railway Redis service metrics)
- Database migrations have been applied (
pnpm db:migrate)
- Send a test OTP:
POST /api/v1/auth/request-otpwith a real phone number - Confirm the SMS arrives via Termii
- Verify the OTP:
POST /api/v1/auth/verify-otp— should return a JWT - Confirm a Stellar wallet was provisioned (check the
walletstable)
- Confirm Paystack webhook signature validation is active:
- Send a test webhook event from the Paystack dashboard
- Confirm the server responds with
200and logs show HMAC verification passed - A missing or invalid
x-paystack-signatureheader should return401
-
CORS_ORIGINis set to the exact Vercel frontend URL (not*) -
NODE_ENV=production— Swagger UI should be disabled -
JWT_SECRETis a strong random value (not the placeholder) -
ENCRYPTION_KEYis 64 hex chars and stored only in Railway's encrypted variables -
STELLAR_SERVER_SECRETis stored only in Railway's encrypted variables — never in.envcommitted to git
- Server admin account has sufficient XLM balance to pay transaction fees
-
ESCROW_CONTRACT_ADDRESSresolves on the configured network (testnet or mainnet) -
GET /api/v1/walletreturns a balance for a test user account
- On staging (
NODE_ENV != production),GET /api/docsopens Swagger UI -
GET /api/docs.jsonreturns a valid OpenAPI 3.1 document
| Symptom | Likely Cause | Fix |
|---|---|---|
/health returns 502 |
Server did not start | Check Railway build logs for TypeScript errors |
/ready returns { "db": "error" } |
DATABASE_URL wrong or DB not migrated | Verify variable + run pnpm db:migrate |
| OTP SMS not delivered | TERMII_API_KEY invalid or unset | Check Termii dashboard logs |
| Wallet balance fetch fails | HORIZON_URL unreachable | Confirm STELLAR_NETWORK and HORIZON_URL match |
release_payment fails |
STELLAR_SERVER_SECRET account not funded | Fund via Friendbot (testnet) or send XLM (mainnet) |
| CORS errors in browser | CORS_ORIGIN mismatch | Set CORS_ORIGIN to exact Vercel URL including protocol |
| BullMQ jobs not processing | REDIS_URL not set or Redis down | Add Redis add-on on Railway and set REDIS_URL |
Last updated: 2026 — maintained by the AirFlex core team.