Express 5 REST API for EquipChain — a decentralized utility meter monitoring and data access platform powered by Soroban smart contracts on Stellar.
- Project Overview
- Quick Start
- API Reference
- Architecture Overview
- Configuration Reference
- Development Guide
- Load Testing with k6
- Deployment Guide
- Contributing
- Related
- License
EquipChain is a blockchain-powered platform for monitoring, managing, and analyzing utility meter data. This backend service provides:
- REST API — Query meter status, contract data, and project information via a clean JSON API.
- Dashboard Analytics — Data aggregation endpoints for daily, monthly, custom-range summaries and fleet-wide metrics.
- Soroban Integration — Reads on-chain data from Stellar Soroban smart contracts for transparent, immutable meter records.
- Real-time Capabilities — Built on Express 5 with WebSocket support for live meter updates.
- Redis Caching — High-performance data caching for frequently accessed meter readings.
- Docker Support — Containerized deployment for consistent environments.
| Technology | Purpose |
|---|---|
| Express 5 | HTTP framework |
| Node.js 20+ | Runtime |
| Soroban (Stellar) | Smart contract integration |
| Redis | Caching layer |
| WebSockets | Real-time meter data |
| Docker | Containerization |
Current Phase: MVP — Core meter monitoring endpoints are operational. The API serves project metadata and on-chain contract data, and provides dashboard analytics aggregation. WebSocket event streaming for live meter readings is implemented. Future releases will add full CRUD for meters and admin management.
git clone https://github.com/EquipChain/EquipChain-backend.git
cd EquipChain-backendnpm installCopy the example environment file and adjust as needed:
cp .env.example .env| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
No | Server listen port |
CONTRACT_ID |
CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS |
No | Stellar Soroban contract ID |
REDIS_URL |
— | No | Redis connection string for caching |
WS_ENABLED |
false |
No | Enable WebSocket support |
NODE_ENV |
development |
No | Environment mode |
LOG_LEVEL |
info |
No | Logging verbosity |
npm startThe server starts at http://localhost:3000.
curl http://localhost:3000
# {"project":"Equipchain","status":"Monitoring Meters","contract":"CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS"}Note: On first startup in development mode, the server automatically seeds ~6,480 sample meter readings across 3 meters spanning 90 days. This provides test data for the analytics endpoints immediately. Set
SKIP_SEED=1to disable auto-seeding.
All endpoints return JSON. Base URL: http://localhost:3000 (development) or your deployed URL.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/ |
Project metadata (name, status, contract ID) | No |
GET |
/api/health |
Health check (uptime, status, timestamp) | No |
GET |
/health |
Service health + queue/scheduler stats + deploy identity (gitSha, deployedAt) |
No |
GET |
/health/live |
Liveness probe — dependency-free; failure means restart | No |
GET |
/health/ready |
Readiness probe — checks services + cache; failure removes the instance from LB rotation | No |
GET |
/metrics |
Prometheus scrape (HTTP counters/histograms, event-loop lag, build info, queue/cache gauges) | Optional bearer (METRICS_TOKEN) |
GET |
/api/system/rate-limits |
Caller's resolved rate-limit tier, remaining budget, window reset | No |
The Docker image carries a HEALTHCHECK probing /health/live; compose uses /health/ready.
Authentication is JWT-based (HS256, Authorization: Bearer <token>). Tokens carry a sub (identity), roles array, and a unique jti so they can be revoked server-side.
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/api/auth/challenge |
Dev convenience: mint a JWT for a wallet address. Disabled in production unless ENABLE_DEV_CHALLENGE=true (the production flow is wallet-signature verification, layered onto this endpoint). Brute-force guarded (5/min per IP). |
No |
GET |
/api/protected |
Protected sample route | Yes (JWT) |
POST |
/api/auth/logout |
Revoke the caller's own token server-side (jti denylist) | Yes (JWT) |
GET |
/api/analytics/readings |
Machine access to raw readings via API key | Yes (x-api-key) |
Note: The earlier revision of this table listed
/auth/login,/auth/register, and/auth/refresh— those endpoints never existed in this service and the table has been corrected. There is currently no password-based account system; identities come from the challenge flow (dev) or wallet-signature verification (production), and user accounts are managed by admins via/api/admin/users.
Service clients authenticate with an x-api-key header instead of a JWT. Keys are stored in the API-key repository with a status, an expiry, a permission scope list (read, write), and a rate-limit tier (standard/premium → premium tier, internal → internal tier). Lookup is timing-safe (hash-then-compare), and a key lacking the route's required permission is rejected with 403 even though it authenticates.
Every token has a jti. POST /api/auth/logout (and POST /api/admin/logout for admins) denylists the presented token's jti for its remaining TTL through the shared cache, so sign-out is real: the token stops authenticating immediately, on every instance, not just the client that dropped it.
Dashboard analytics and data aggregation endpoints for meter readings. Computed in-memory from stored readings.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/analytics/daily-summary |
Daily aggregated readings within a date range | No |
GET |
/api/analytics/monthly-summary |
Monthly aggregated readings within a date range | No |
GET |
/api/analytics/custom-range |
Aggregated readings with configurable granularity | No |
GET |
/api/analytics/fleet-summary |
Fleet-wide summary across all meters | No |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
startDate |
string (ISO 8601) | Required | Start date (e.g., 2026-01-01) |
endDate |
string (ISO 8601) | Required | End date (e.g., 2026-01-31) |
meterIds |
string or string[] | All meters | Filter by one or more meter IDs |
timezone |
string (IANA) | UTC |
Timezone for day boundaries |
aggregationType |
enum | avg |
count, sum, avg, min, max, p50, p95 |
compareWith |
enum | — | previous_period, year_over_year |
Example Response:
GET /api/analytics/daily-summary?startDate=2026-01-01&endDate=2026-01-03&aggregationType=sum
{
"data": [
{ "key": "2026-01-01", "value": 350, "count": 3 },
{ "key": "2026-01-02", "value": 225, "count": 2 },
{ "key": "2026-01-03", "value": 425, "count": 2 }
],
"meta": {
"startDate": "2026-01-01",
"endDate": "2026-01-03",
"granularity": "day",
"aggregationType": "sum",
"timezone": "UTC",
"totalReadings": 7
}
}Same parameters as daily-summary, returns monthly rollups.
Additional Parameter: granularity — hour, day, week, month
Example Response:
{
"fleet": {
"totalReadings": 6480,
"totalMeters": 3,
"value": 112.5,
"aggregationType": "avg"
},
"meters": [
{ "meterId": "METER-001", "value": 150.2, "readings": 2160 },
{ "meterId": "METER-002", "value": 85.3, "readings": 2160 },
{ "meterId": "METER-003", "value": 102.0, "readings": 2160 }
],
"topPerformer": { "meterId": "METER-001", "value": 150.2, "readings": 2160 },
"bottomPerformer": { "meterId": "METER-002", "value": 85.3, "readings": 2160 }
}All admin routes require a JWT with the admin role. Changes to users, devices, config, and webhooks are recorded in an audit trail readable at GET /api/admin/audit.
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users |
List users (paginated, filterable, searchable) |
POST |
/api/admin/users |
Create a user (duplicate email → 409) |
GET |
/api/admin/users/:id |
Get user details |
PATCH |
/api/admin/users/:id |
Update a user's roles |
DELETE |
/api/admin/users/:id |
Deactivate a user |
GET |
/api/admin/devices |
List registered devices |
POST |
/api/admin/devices |
Register a device (duplicate deviceId → 409) |
PATCH |
/api/admin/devices/:id |
Update device metadata |
DELETE |
/api/admin/devices/:id |
Remove a device |
GET |
/api/admin/webhooks |
List webhook endpoints (secrets omitted) |
POST |
/api/admin/webhooks |
Register a webhook (returns signing secret once) |
GET |
/api/admin/webhooks/:id |
Webhook details |
GET |
/api/admin/webhooks/:id/deliveries |
Recent delivery attempts (newest first) |
PATCH |
/api/admin/webhooks/:id |
Update webhook / pause via status: inactive |
DELETE |
/api/admin/webhooks/:id |
Delete webhook and its delivery logs |
GET |
/api/admin/config |
Get runtime config |
PATCH |
/api/admin/config |
Update whitelisted config keys (rateLimitPerMinute, maintenanceMode) |
POST |
/api/admin/config/reset |
Reset config to defaults |
GET |
/api/admin/audit |
Admin action audit trail (filter by action, admin) |
GET |
/api/admin/system/health |
System health snapshot |
GET |
/api/admin/system/stats |
Resource/connection stats |
GET |
/api/admin/system/ws-connections |
Active WebSocket connections |
POST |
/api/admin/logout |
Revoke the admin's own token |
| Key | Default | Effect |
|---|---|---|
maintenanceMode |
false |
true returns 503 on all /api/exports and /api/analytics routes (admin routes and health probes stay reachable so you can turn it back off) |
rateLimitPerMinute |
null (off) |
When set, caps every rate-limit tier at this many requests/min, live without restart |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/meters |
List all registered meters | Yes |
GET |
/meters/:id |
Get meter details by ID | Yes |
POST |
/meters |
Register a new meter | Admin |
PUT |
/meters/:id |
Update meter configuration | Admin |
DELETE |
/meters/:id |
Remove a meter | Admin |
GET |
/meters/:id/readings |
Get readings for a specific meter | Yes |
All export data comes from the live stores (readings store, device registry) — analytics summaries are computed from real stored readings.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/exports/readings |
Export meter readings (CSV/JSON/NDJSON) | Yes |
GET |
/api/exports/analytics/:summaryType |
Export analytics summaries (daily/weekly/monthly) | Yes |
GET |
/api/exports/system-report |
Export system-wide report (meters, readings, alerts, summary) | Admin |
GET |
/api/exports/meters |
Export the device registry | Yes |
All export endpoints support the following query parameters:
format- Output format:csv,json, orndjson(default:csv)fields- Comma-separated list of fields to include (e.g.,id,timestamp,value)startDate- Filter by start date (ISO format:2026-01-01or2026-01-01T00:00:00Z)endDate- Filter by end date (ISO format)pretty- Set totruefor pretty-printed JSON (default:false)
Additional parameters specific to endpoints:
/api/exports/readings:meterIds(comma-separated),status/api/exports/analytics/:summaryType: Date range filtering for daily summaries/api/exports/system-report:sections(comma-separated:meters,readings,alerts,summary)/api/exports/meters:status,location
# Export all readings as CSV
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=csv"
# Export specific fields as JSON
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=json&fields=id,timestamp,value"
# Export readings for specific meters and date range
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=csv&meterIds=meter-001,meter-002&startDate=2026-01-01&endDate=2026-06-01"
# Export daily analytics as NDJSON (streaming)
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/analytics/daily?format=ndjson"
# Export system report (admin only - admin role comes from the JWT, never a header)
curl -H "Authorization: Bearer ADMIN_TOKEN" \
"http://localhost:3000/api/exports/system-report?format=json§ions=meters,summary"All export endpoints use streaming to handle large datasets efficiently:
- Responses use
Transfer-Encoding: chunked - Data is streamed row-by-row for CSV and line-by-line for NDJSON
- Memory usage remains constant regardless of dataset size
- Suitable for exporting 10,000+ records
Webhook endpoints are registered by admins and delivered through the job queue with SSRF guards (private/reserved targets refused), bounded redirects, retries with backoff, and HMAC-SHA256 signatures.
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/api/admin/webhooks |
Register a webhook endpoint | Admin |
GET |
/api/admin/webhooks |
List registered webhooks | Admin |
GET |
/api/admin/webhooks/:id/deliveries |
Recent delivery attempts | Admin |
PATCH |
/api/admin/webhooks/:id |
Update or pause (status: inactive) |
Admin |
DELETE |
/api/admin/webhooks/:id |
Remove a webhook | Admin |
Every delivery to a registered webhook carries:
x-equipchain-signature: t=<unix_seconds>,v1=<hex_hmac>
The signature is HMAC-SHA256 over "<t>.<raw_body>" keyed with the secret returned at registration time. Because the timestamp is inside the signed material, a captured delivery cannot be replayed with a fresh timestamp — verify the HMAC and reject timestamps older than a few minutes:
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))
&& Math.abs(Date.now() / 1000 - t) < 300;The API is documented with OpenAPI 3 and served interactively via Swagger UI.
| Endpoint | Description |
|---|---|
GET /api/openapi.json |
Raw OpenAPI 3 specification (generated from @openapi JSDoc annotations) |
GET /api/docs |
Interactive Swagger UI for browsing and testing the API |
The specification covers all API endpoints (system, admin, analytics, exports) and the
security scheme (bearerAuth). Annotations live alongside the route handlers in src/,
so the docs stay in sync with the code.
GET /
{
"project": "Equipchain",
"status": "Monitoring Meters",
"contract": "CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS"
}List endpoints select a strategy with ?paginate=offset (default) or ?paginate=cursor.
Offset — ?page=2&limit=20. Familiar, supports jumping to an arbitrary page, and reports
total/totalPages. Best for small to medium result sets.
Cursor (keyset) — ?cursor=<opaque>&limit=20 forward, ?before=<opaque>&limit=20 backward.
Position is anchored to a value rather than a row count, which gives it two properties offset
paging cannot have: performance independent of how deep you are, and no page drift — rows
inserted or deleted between requests never cause a client to skip or repeat items. Use it for
large, append-heavy data such as meter readings, audit logs and webhook delivery logs.
| Parameter | Mode | Description |
|---|---|---|
paginate |
both | offset (default) or cursor |
limit |
both | Items per page, 1–100 (default 20) |
page |
offset | 1-based page number (default 1) |
cursor |
cursor | Page forward from this position |
before |
cursor | Page backward from this position |
sortBy / sortOrder |
both | Sort field and asc/desc |
GET /meters?paginate=cursor&limit=2
{
"data": [ { "id": 1 }, { "id": 2 } ],
"pagination": {
"limit": 2,
"cursor": null,
"nextCursor": "eyJ2IjoxLCJmIjoiaWQiLCJvIjoiYXNjIiwiayI6MiwiaWQiOjJ9",
"prevCursor": null,
"hasNext": true,
"hasPrev": false
}
}total and totalPages are absent in cursor mode: counting the full result set is the exact
cost cursor pagination exists to avoid. Endpoints that need a count can opt in via the
includeTotal option.
Cursors are opaque. Pass them back exactly as received — do not construct, parse or edit
them. A cursor records the sort it was issued for, so replaying one against a different
sortBy/sortOrder is rejected with a 400 rather than silently returning the wrong rows.
EquipChain-backend/
├── .github/
│ └── workflows/
│ └── ci.yml # CI (lint, Node 20/22 test matrix, audit, Docker gate)
├── k6/ # Grafana k6 load-test scenarios
├── scripts/
│ ├── seed-readings.js # Sample meter readings generator (auto-runs in dev)
│ ├── docker-build.sh # Docker build helper
│ └── docker-run.sh # Docker run helper
├── src/
│ ├── config/
│ │ ├── index.js # Frozen env config, fail-fast validation
│ │ ├── logger.js # Pino structured logger with redaction
│ │ ├── rateLimits.js # Tier definitions for the rate limiter
│ │ └── tracing.js # OpenTelemetry setup
│ ├── data/
│ │ └── adminStore.js # Admin-managed stores (users/devices/config/audit)
│ ├── docs/
│ │ └── openapi.js # OpenAPI 3 spec builder from @openapi annotations
│ ├── jobs/
│ │ ├── billing.job.js # Period consumption billing
│ │ ├── cacheWarm.job.js # Analytics cache warming
│ │ ├── reports.job.js # Daily/monthly report generation
│ │ ├── sync.job.js # Soroban contract-state sync
│ │ └── webhookRetry.job.js # Signed webhook delivery (SSRF-guarded)
│ ├── lib/
│ │ └── soroban.js # Soroban RPC adapter (real + mock backends)
│ ├── middleware/
│ │ ├── apiKeyAuth.js # API-key auth (timing-safe, scopes, expiry)
│ │ ├── auth.js # JWT verify + jti revocation denylist
│ │ ├── metrics.js # Prometheus instrumentation + /metrics render
│ │ ├── rateLimiter.js # Tiered rate limiting with bounded store
│ │ ├── requireAdmin.js # Admin role guard
│ │ └── validate.js # Zod request validation
│ ├── repositories/ # Repository layer (Base + domain repos)
│ ├── routes/
│ │ ├── admin/ # /api/admin/* (users, devices, webhooks, config, audit, system)
│ │ ├── analytics.js # /api/analytics/* aggregation + raw readings
│ │ ├── docs.js # /api/docs (Swagger UI), /api/openapi.json
│ │ ├── exports.js # /api/exports/* streaming exports
│ │ └── index.js # Router composition, health probes, logout
│ ├── schemas/ # Zod request schemas
│ ├── services/
│ │ ├── aggregator.js # In-memory readings store + aggregation engine
│ │ ├── cache.js # Redis cache with bounded memory fallback
│ │ ├── exporter.js # Streaming CSV/JSON/NDJSON export engine
│ │ ├── queue.js # Durable job queue (timeouts, backpressure, recovery)
│ │ ├── scheduler.js # Cron-style scheduler (chunked intervals)
│ │ ├── webhook.js # Webhook delivery service
│ │ └── websocket.js # socket.io gateway (JWT handshakes)
│ ├── utils/ # pagination, sanitization, errors
│ ├── app.js # Canonical Express app (middleware pipeline)
│ └── server.js # Server composition root (services, shutdown)
├── test/ # node:test suites (unit + integration)
├── index.js # Boot entry point (npm start, Docker CMD)
├── Dockerfile # Multi-stage build, non-root, HEALTHCHECK
├── docker-compose.yml # API + Redis stack
├── package.json # Project metadata and dependencies
└── README.md # You are here
The pipeline as actually mounted in src/app.js:
Request
│
▼
[Metrics] → Request counter + duration histogram (bounded route labels)
[/metrics] → Prometheus scrape endpoint (optional bearer token)
[Helmet] → Security headers (strict CSP, HSTS, no framing)
[CORS] → Origin allowlist (CORS_ORIGINS)
[Body parsing] → JSON/urlencoded with size limit + prototype-pollution strip
[Rate limiter] → Tiered per identity (free/premium/admin/internal + API keys)
[Auth guard] → Stricter limiter on /api/auth/challenge (brute force)
[Correlation ID] → Sanitized x-correlation-id + structured request logging
│
▼
[Router] → System routes, analytics, exports, docs, admin (JWT + role)
[Maintenance gate] → 503 kill-switch on public analytics/exports when enabled
│
▼
[Handlers] → Route handlers → services → repositories
│
▼
[404] → [Error handler] → Standard error envelope, correlation-ID reference for 500s
- Cache — Redis with a bounded in-memory fallback; single-flight
getOrSetprevents stampedes - Queue — Durable job queue: per-job timeouts, backpressure cap, crash recovery, retry ladder
- Scheduler — Cron-style recurring jobs (billing, reports, sync, cache warm)
- Webhook — Signed HTTP delivery through the queue
- WebSocket — socket.io gateway broadcasting meter readings (batched per meter)
| Repository | Backend | Purpose |
|---|---|---|
MeterReadingRepository |
In-memory / future DB | Meter readings |
UserRepository |
In-memory / future PostgreSQL | User accounts and roles |
DeviceRepository |
In-memory / future DB | Device registry |
ApiKeyRepository |
In-memory / future DB | API keys (scopes, expiry, tiers) |
WebhookRepository |
In-memory / future DB | Webhook endpoints + capped delivery logs |
ConfigRepository |
In-memory / future DB | Runtime config |
All configuration is via environment variables. Create a .env file in the project root.
| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
No | Port the HTTP server binds to |
HOST |
0.0.0.0 |
No | Host address to bind |
CONTRACT_ID |
(see below) | No | Stellar Soroban contract ID for meter data |
REDIS_URL |
— | No | Redis connection string (redis://...) |
WS_ENABLED |
false |
No | Set to true to enable WebSocket server |
WS_PATH |
/ws |
No | WebSocket endpoint path |
NODE_ENV |
development |
No | development, test, or production |
LOG_LEVEL |
info |
No | debug, info, warn, error |
SKIP_SEED |
— | No | Set to 1 to skip auto-seeding sample data |
OTEL_EXPORTER_OTLP_ENDPOINT |
— | No | OTLP collector endpoint for trace export |
OTEL_SERVICE_NAME |
equipchain-api |
No | Service name reported in traces |
Default CONTRACT_ID: CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS
The project has two entry files with distinct roles - keep them that way:
| File | Role | Used by |
|---|---|---|
index.js |
Full boot path: seeds dev data, installs process handlers, starts the server with all background services | npm start, the Docker CMD |
src/server.js |
Library module: exports startServer, installProcessHandlers, gracefulShutdown; also runs standalone when executed directly |
index.js, tests, tooling |
Historical note:
src/server.jsexports the boot contractindex.jsconsumes. Requiring the module (as tests do) must never mutate global process state - handlers are installed only viainstallProcessHandlers().
npm testTests use Node's built-in node:test and node:assert modules. No additional test framework is required.
Sample meter reading data spanning 90 days across 3 meters is auto-generated on server start in development mode. The seed data powers the analytics endpoints with realistic consumption patterns (morning ramp, peak hours, evening decline, night lows). To manually seed or re-seed:
node scripts/seed-readings.jsnpx eslint .npm ci --productiondocker build -t equipchain-backend:local .
docker run --rm -p 3000:3000 --env-file .env equipchain-backend:localRun the full local stack (API + Redis) with Docker Compose:
docker compose up --buildHelper scripts:
sh scripts/docker-build.sh
sh scripts/docker-run.shPerformance testing is implemented using Grafana k6, an open-source load testing tool to identify performance bottlenecks under stress.
Install k6 by following the official installation guide:
# macOS
brew install k6
# Ubuntu/Debian
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
# Windows (winget)
winget install k6Verify installation:
k6 versionAll test scripts are located in the k6/ directory and are parameterizable via environment variables.
| Test | File | Description | Command |
|---|---|---|---|
| Smoke | k6/smoke.js |
1 VU performing all API operations for 30s. Verifies basic functionality under no load. | npm run k6:smoke |
| Load | k6/load.js |
Ramp up to 50 VUs over 1 min, sustain for 3 min, ramp down over 1 min. 80% reads, 20% writes. | npm run k6:load |
| Stress | k6/stress.js |
Gradual increase from 10 → 50 → 100 → 200 → 500 VUs to identify the breaking point. | npm run k6:stress |
| Spike | k6/spike.js |
Sudden jump from 0 to 200 VUs in 10s, sustain for 1 min, then cool down. | npm run k6:spike |
| Soak | k6/soak.js |
50 VUs sustained for 30+ minutes to detect memory leaks and performance degradation. | npm run k6:soak |
| Quick | (all except soak) | Runs smoke, load, stress, and spike tests sequentially. | npm run k6:quick |
Override the base URL and other parameters via environment variables:
# Point to a different environment
k6 run k6/smoke.js -e BASE_URL=https://staging.example.com
# Override soak test duration and concurrency
k6 run k6/soak.js -e DURATION=60m -e VUS=100Each test measures and reports:
| Metric | Description |
|---|---|
| Request Rate (RPS) | Number of requests per second |
| Response Time Percentiles | p50, p75, p90, p95, p99 — median and tail latency |
| Error Rate | Percentage of failed/non-2xx requests |
| Checks | Application-level assertions (e.g., status is 200, body has required fields) |
Generate visual HTML reports for detailed analysis:
k6 run --out html=k6/results/load-report.html k6/load.jsThe standard CI workflow (.github/workflows/ci.yml) runs unit tests only (npm test). For load testing in CI, add a separate workflow step that installs k6 and runs the smoke test as a quick health check:
- name: Install k6
run: |
curl -fsSL https://github.com/grafana/k6/releases/download/v0.54.0/k6-v0.54.0-linux-amd64.tar.gz | tar -xz
sudo cp k6-v0.54.0-linux-amd64/k6 /usr/local/bin/
- name: Run k6 smoke test
run: k6 run k6/smoke.js
env:
BASE_URL: ${{ secrets.BASE_URL }}Test results (HTML reports, JSON summaries) are stored in k6/results/. This directory is gitignored and will not be committed to the repository.
docker build -t equipchain-backend:local .
docker run -d \
--name equipchain-api \
-p 3000:3000 \
--env-file .env \
equipchain-backend:localDocker Compose (API + Redis):
docker compose up -d --build| Platform | Instructions |
|---|---|
| Railway | Connect repo, set build command npm install, start command npm start |
| Render | Use Web Service, set runtime to Node, start command npm start |
| Fly.io | Run fly launch, configure internal_port = 3000 |
| AWS ECS | Push Docker image to ECR, configure task definition with env vars |
The included GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request to main:
- Checkout — Clone the repository
- Setup Node — Install Node.js 22 with npm cache
- Install —
npm ci - Test —
npm test
- Set
NODE_ENV=productionto disable debug logging and auto-seeding - Use a reverse proxy (nginx, Caddy) for SSL termination
- Configure health check monitoring on
/api/health
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure tests pass before submitting.
- EquipChain Contracts — Soroban smart contracts on Stellar
- EquipChain Frontend — Web dashboard and client app
MIT © EquipChain