REST API for wagent, a Stellar-based payment service. It handles merchant accounts, API-key authentication, payment orders, supported tokens, currency conversion, JWT auth with optional TOTP two-factor, and transactional email.
- Runtime: Node.js 20+ (Docker image uses Node 24 LTS), TypeScript 5.9
- Web: Express 4
- Database: MongoDB via Mongoose 9
- Auth: JWT (
jsonwebtoken), bcrypt password hashing (bcryptjs), TOTP 2FA (speakeasy) - Validation: Zod
- Stellar:
@stellar/stellar-sdk(address validation) - Email: Nodemailer (SMTP)
- Logging: Bunyan
- API docs: Swagger / OpenAPI (
swagger-jsdoc+swagger-ui-express)
- Node.js >= 20 and npm
- A running MongoDB instance (local or remote)
# 1. Install dependencies
npm install
# 2. Create your environment file
cp .env.example .env
# then edit .env and fill in real values (see Environment variables below)
# 3. Start MongoDB (any local instance works), e.g. via Docker:
docker network create wagentapi # once, if it does not already exist
docker compose up -d mongo-wagent
# set DB_URI=mongodb://localhost:27016 in .env for this Compose database
# 4. Run in watch mode (recompiles + restarts on change)
npm run devThe server listens on PORT (default 5000). Environment variables are loaded
from .env automatically (via dotenv) and validated on startup — the process
exits immediately if a required variable is missing.
| Script | Description |
|---|---|
npm run dev |
Watch-compile (tsc --watch) and restart via nodemon |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run the compiled app (node dist/index.js) |
npm run typecheck |
Type-check without emitting |
npm test |
Build and run logger and HTTP route tests |
npm run lint |
Lint with ESLint (flat config) |
npm run lint:fix |
Lint and auto-fix |
npm run format |
Format with Prettier |
npm run format:check |
Check formatting without writing |
npm ci
npm run build
NODE_ENV=production npm startLogs always go to stdout, including in production. Production also writes the
same logs to LOG_FILE_PATH. HTTP request logs include the method, route template,
status, duration and request ID. Request bodies, headers, query strings and URL
tokens are excluded from these request logs.
npm test uses a temporary MongoDB instance with fresh data. Install mongod
locally, or set MONGOD_BIN to its executable path. Blux and SMTP are simulated;
tests never use the configured application database or send real emails.
The API and MongoDB 7.0 are defined together in compose.yaml. The API waits for
MongoDB's health check before starting. Both use the external wagentapi network,
which is also shared with the transaction monitor.
Set LOG_DIRNAME and LOG_FILENAME in .env if needed. They default to ./logs
and api.log.
# 1. Create the shared network once
docker network create wagentapi
# 2. Prepare the API log directory (use LOG_DIRNAME if customized)
mkdir -p ./logs
sudo chown 1000:1000 ./logs
# 3. Pull MongoDB, then build and start both services
docker compose pull mongo-wagent
docker compose up -d --buildIf upgrading from the separate MongoDB Compose file, follow the data migration below before running these commands.
The image is a multi-stage build on node:24-alpine, runs as the non-root
node user, ships only production dependencies, and defines a HEALTHCHECK
against /health.
Notes:
- Because the API runs as a non-root user, the host directory mounted for
logs (
LOG_DIRNAME) must be writable by UID1000(thenodeuser). swagger-jsdocreads its annotations from the TypeScript sources at runtime, sosrc/swagger/**is included in the runtime image on purpose.
Compose uses mongo:7.0. After changing the image version, run the pull and
startup commands above to recreate the container.
If MongoDB 8.0 already initialized the data directory, do not reuse those files with 7.0. Community Edition does not support an in-place binary downgrade. Restore a compatible MongoDB 7.0 backup into a separate data directory and keep the original files. See MongoDB's downgrade policy.
The old file mounted the host data directory at /data/db/data, but MongoDB
stores its database at /data/db by default. The combined file corrects this to
~/storage/mongodb/data:/data/db. If the old container has data, it may be in an
anonymous Docker volume instead of the host directory.
Before recreating the old container, stop it and inspect its mounts:
docker stop mongo-wagent
docker inspect mongo-wagent --format '{{json .Mounts}}'If it contains existing data, copy /data/db to a new backup directory while the
container is stopped:
mkdir -p ~/storage/mongodb/backup-before-merge
docker cp -a mongo-wagent:/data/db/. ~/storage/mongodb/backup-before-merge/For a database last used by MongoDB 7.0, verify the backup and restore its files
into ~/storage/mongodb/data before recreating the container. For 8.0 data, follow
the version notes above. Keep the backup and old volume until the API connects
and the expected records are confirmed. If the old container never initialized
and has no data, no restore is needed.
After checking or restoring the data, remove only the old container so Compose can recreate it under the combined project:
docker rm mongo-wagent
docker compose pull mongo-wagent
docker compose up -d --buildAll variables are documented in .env.example. The required
ones are validated at startup in src/config/envs.ts.
Highlights:
| Variable | Required | Purpose |
|---|---|---|
PORT |
yes | HTTP port |
NODE_ENV |
yes | development or production |
DB_URI, DB_NAME |
yes | MongoDB connection |
JWT_SECRET |
yes | Signs 30-day JWTs — use a long random value |
EMAIL_USER, EMAIL_PASS |
yes | SMTP credentials (smtp.hostinger.com:587) |
FRONTEND_URL |
yes | Base URL for email links |
PAYMENT_URL |
yes | Base URL returned for created orders |
SWAGGER_API_URL |
yes | Server URL shown in Swagger UI |
LOG_FILE_PATH |
yes | Log file used in production |
ADMIN_PASSWORD |
yes | Currently required but unused (vestigial) |
CORS_ORIGIN |
no | Comma-separated allowlist; defaults to all origins |
Interactive Swagger UI is served at:
GET /swagger
Two authentication schemes are used:
apiKeyAuth— anapiKeyrequest header (merchant API key). Used byPOST /orders.bearerAuth—Authorization: Bearer <JWT>. Used by the authenticated user/admin endpoints.
GET /health -> { "status": "ok", "db": "connected", "uptime": <seconds> }
Always returns 200 while the process is responsive; the db field reports the
Mongoose connection state. Used by the Docker HEALTHCHECK.
src/
config/ env validation, db, logger
constants/ static config (tokens, currencies, dates, email)
middlewares/ auth, api-key, admin, validation, response envelope, asyncHandler
models/ Mongoose models (User, Order, Token, Transaction, ...)
routes/ HTTP route handlers (orders, tokens, users, users/auth, swagger)
swagger/ OpenAPI JSDoc annotations (read at runtime)
templates/ HTML email templates
utils/ helpers (jwt, email, currency conversion, authenticator, ...)
index.ts app bootstrap
These are pre-existing behaviours intentionally left unchanged. Review before relying on them:
src/utils/contract/getTransactionDetails.tsis a stub (returns hardcoded values) and is not imported anywhere.- Public payment endpoints have no JWT auth by design:
GET/PUT/DELETE /orders/:idandPUT /users/subscriptionare reachable with only an order id / capability token, which the public payment page relies on. If you need to lock these down, do so deliberately and update the payment frontend.