Navin is a blockchain-powered logistics platform that improves supply chain visibility for enterprises through tokenized shipments, immutable milestone tracking, and automated settlements. By creating a zero-trust interface between logistics providers and their clients, Navin aims to ensure both parties access identical real-time data — removing information asymmetry and enabling seamless, dispute-free operations.
The backend service powers the off-chain layer of the platform, handling API logic, data aggregation, real-time streaming, and Stellar blockchain anchoring.
Chain integration status: shipment/telemetry hashes are anchored on-chain today via Horizon transactions. Soroban smart-contract integration (hash-and-emit events, escrow) is being co-designed with the navin-contracts repo — settlement flows are currently simulated placeholders. See
TODO.mdPart 3.
This is the shortest path to a complete local stack. It runs the API, MongoDB, Redis, and both Stellar workers on the Compose network.
docker compose -f docker-compose.yml up -d --build Get the Navin Backend running in less than 5 minutes:
docker compose -f docker-compose.yml ps curl http://localhost:3000/api/health
# 1. Clone the repository
The health request should return `success: true` and `data.status: "active"`.
The Compose file reads `.env` when it exists. It supplies the internal
container addresses for MongoDB and Redis, so `MONGO_URI` and `REDIS_URL` in
`.env` do not need to be changed for this workflow. The API is available at
`http://localhost:3000/api`.
On Windows PowerShell, use `Copy-Item .env.example .env` for step 2 and
`curl.exe http://localhost:3000/api/health` for step 4.
### Service topology
```mermaid
flowchart LR
client[Client] -->|HTTP :3000| app[app<br/>Express API]
app --> mongo[(mongo<br/>MongoDB :27017)]
app --> redis[(redis<br/>Redis :6379)]
worker[stellar-worker<br/>Stellar anchor worker] --> mongo
worker --> redis
indexer[stellar-indexer<br/>Confirmation indexer] --> mongo
indexer --> redis
mongo and redis are published on loopback for local inspection. The
workers publish no ports; they consume the same MongoDB and Redis services as
the API. The API and workers wait for healthy MongoDB and Redis containers
before starting.
The default Compose discovery rules automatically merge
docker-compose.override.yml, which mounts src/ and runs tsx watch.
Use this mode when you want hot reload:
docker compose up -d --build
docker compose logs -f appUse docker compose -f docker-compose.yml ... when you need the production
runner without the development override, as in the quickstart above.
See docs/environment-variables.md for the
complete environment matrix, validation rules, defaults, and optional
integration settings. JWT_SECRET must be at least 32 characters; the example
file contains a development placeholder that should be replaced locally.
appis unhealthy: inspectdocker compose -f docker-compose.yml logs app. The healthcheck calls/api/healthafter a 15-second startup grace period;docker compose ... psshows whether MongoDB and Redis became healthy first.- The API exits immediately: check
JWT_SECRETlength and the application logs. A missing or invalid required environment value makes the process fail fast during startup. - MongoDB or Redis is stuck in
starting: inspect the service logs withdocker compose -f docker-compose.yml logs mongo redis, then retry after the healthchecks pass. On a clean machine, the first image pulls can take a few minutes. - Port 3000, 27017, or 6379 is already in use: stop the conflicting process/container or change the published ports in a local Compose override.
- Workers restart or show no HTTP healthcheck: this is expected for the worker services. They do not listen on port 3000 and intentionally disable the inherited app healthcheck; inspect their logs instead.
- Reset local state:
docker compose -f docker-compose.yml down -vremoves the MongoDB volume. Start again with the quickstart command to rebuild the stack from an empty database.
If MongoDB and Redis are already installed locally, run the API directly:
# Create .env first, then install dependencies
npm ci
# Start the development server with hot reload
npm run dev# Check health endpoint (no auth required)
curl http://localhost:3000/api/health
# Expected response:
# { "success": true, "message": "OK", "data": { "status": "active", "uptime": 123, "timestamp": "2026-06-28T..." } }The Navin Backend uses JWT (JSON Web Tokens) for stateless authentication. All protected endpoints require a valid bearer token.
curl -X POST http://localhost:3000/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"name": "John Doe",
"password": "SecurePass123"
}'
# Response:
# {
# "success": true,
# "message": "Account created successfully",
# "data": {
# "user": {
# "id": "507f1f77bcf86cd799439011",
# "email": "user@example.com",
# "name": "John Doe",
# "role": "VIEWER"
# },
# "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# }
# }curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123"
}'
# Response: { "success": true, "data": { "user": {...}, "token": "eyJhbGciOi..." } }Store the token and include it in all subsequent requests:
# Example: List shipments
curl -X GET http://localhost:3000/api/shipments \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# If token is missing or invalid, you'll receive:
# { "success": false, "message": "Missing or invalid token", "data": null, "error": { "code": "ERR_AUTH_INVALID" } }curl -X POST http://localhost:3000/api/auth/logout \
-H "Authorization: Bearer <your_token>"
# Response: { "success": true, "message": "Logged out successfully", "data": null }Access tokens expire after 7 days by default. When expired, the API returns:
{
"success": false,
"message": "Token has expired",
"data": null,
"error": { "code": "ERR_AUTH_INVALID" }
}Action: Redirect user to login page. A POST /api/auth/refresh endpoint exists for session continuation; see docs/swagger.yaml for its contract.
User roles determine which endpoints they can access:
| Role | Permission Level | Typical Use Case |
|---|---|---|
SUPER_ADMIN |
Full system access | Internal DevOps, system administration |
ADMIN |
Organization administration | Company managers, billing access |
MANAGER |
Shipment & analytics management | Logistics coordinators |
VIEWER |
Read-only access | Auditors, read-only dashboards |
DRIVER |
Field operator (invitable today) | Drivers — assignable via invitations; no role-guarded routes yet |
CUSTOMER |
Minimal read access | External parties (tracking only) |
Signup always assigns
VIEWER; elevated roles are granted via invitations (POST /api/users/invitations) — never self-declared.
If your role lacks permissions, you'll receive:
{
"success": false,
"code": "ERR_PERMISSION_DENIED",
"message": "Insufficient permissions for this operation"
}Every successful API response follows this standard format:
{
"success": true,
"message": "Operation completed successfully",
"data": {
"_id": "507f1f77bcf86cd799439011",
"name": "Example Shipment",
"status": "IN_TRANSIT"
},
"meta": {
"page": 1,
"limit": 20,
"total": 150
}
}{
"success": false,
"message": "Human-readable error description",
"data": null,
"error": {
"code": "ERR_VALIDATION_FAILED"
},
"details": [
{ "path": "email", "message": "Invalid email address", "code": "invalid_string" }
]
}success(boolean) — Whether the operation succeededmessage(string) — Human-readable descriptiondata(object|array|null) — Response payload;nullon errorserror.code(object) — Machine-readable error code lives aterror.code(error responses only); see Error Codes Registrydetails(array, optional) — Field-level validation detailsmeta(object) — Pagination metadata (only for list endpoints)
Rate limiting:
429responses additionally include a top-levelretryAfter(seconds) and an RFC-8574Retry-Afterheader.
The API supports two pagination patterns: offset-based and cursor-based.
Use page and limit query parameters for offset pagination:
curl -X GET "http://localhost:3000/api/shipments?page=2&limit=20" \
-H "Authorization: Bearer <token>"
# Response includes meta:
# {
# "data": [...],
# "meta": {
# "page": 2,
# "limit": 20,
# "total": 150
# }
# }Parameters:
page(integer, default: 1) — Page number (1-indexed)limit(integer, default: 20, max: 100) — Records per page
For high-frequency data (anomalies, telemetry), use cursor-based pagination:
curl -X GET "http://localhost:3000/api/anomalies?limit=20" \
-H "Authorization: Bearer <token>"
# Response:
# {
# "data": [
# { "_id": "...", "type": "TEMPERATURE_EXCEEDED", ... },
# { "_id": "...", "type": "HUMIDITY_LOW", ... }
# ],
# "meta": {
# "nextCursor": "eyJfaWQiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEifQ",
# "hasMore": true
# }
# }Fetch next page:
curl -X GET "http://localhost:3000/api/anomalies?limit=20&cursor=eyJfaWQiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEifQ" \
-H "Authorization: Bearer <token>"When to use cursor-based:
- Real-time feeds (anomaly streams, telemetry)
- Data that changes frequently
- Mobile/streaming clients
All errors include a structured code field for programmatic handling. See the Error Codes Registry for the complete list.
| Status | Meaning | Example |
|---|---|---|
200 OK |
Request succeeded | GET, PATCH successful |
201 Created |
Resource created | POST successful |
400 Bad Request |
Malformed request | Missing fields, invalid types |
401 Unauthorized |
Missing/invalid token | ERR_AUTH_INVALID |
403 Forbidden |
Insufficient permissions | ERR_PERMISSION_DENIED |
404 Not Found |
Resource doesn't exist | ERR_SHIPMENT_NOT_FOUND |
409 Conflict |
Duplicate unique value | ERR_DUPLICATE_KEY (email already registered) |
500 Internal Server Error |
Unhandled exception | ERR_INTERNAL_SERVER_ERROR |
import axios from 'axios';
const api = axios.create({ baseURL: 'http://localhost:3000/api' });
api.interceptors.response.use(
response => response.data,
error => {
const code = error.response?.data?.error?.code;
if (code === 'ERR_AUTH_INVALID') {
// Handle expired session
localStorage.removeItem('authToken');
window.location.href = '/login';
} else if (code === 'ERR_PERMISSION_DENIED') {
// Handle insufficient permissions
console.error('Access denied:', error.response.data.message);
} else if (code === 'ERR_VALIDATION_FAILED') {
// Handle validation errors
console.error('Invalid input:', error.response.data.message);
}
throw error;
}
);The backend supports WebSocket (Socket.IO) connections for real-time updates on shipments and anomalies.
import io from 'socket.io-client';
const socket = io('http://localhost:3000', {
auth: {
token: 'your_jwt_token_here'
}
});
socket.on('connect', () => {
console.log('Connected to real-time server');
});
socket.on('disconnect', () => {
console.log('Disconnected from real-time server');
});// Listen for shipment status changes
socket.on('shipment:status', (shipment) => {
console.log('Shipment status updated:', shipment);
});
// Listen for new anomalies
socket.on('anomaly:detected', (anomaly) => {
console.log('Anomaly detected:', anomaly);
});For detailed WebSocket documentation, see WebSocket Features. For the IoT → BullMQ → Stellar → anomaly detection flow, see Telemetry Ingestion Pipeline.
Summary of the main surfaces — the full, authoritative reference is docs/swagger.yaml.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/health |
No | System health check |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/signup |
No | Register a new user (assigned VIEWER) |
POST |
/api/auth/login |
No | Authenticate and get JWT token |
POST |
/api/auth/refresh |
No | Refresh session token (old jti blocklisted) |
POST |
/api/auth/register/company |
No | Create company + first ADMIN atomically |
POST |
/api/auth/logout |
Yes | Revoke current session (blocklists jti) |
GET / DELETE |
/api/auth/sessions[/{jti}] |
Yes | List / revoke sessions |
POST / DELETE |
/api/auth/2fa/setup, /verify, /{...} |
Yes | TOTP 2FA lifecycle + backup codes |
GET / POST / DELETE |
/api/company/api-keys[/{id}] |
ADMIN+ | Machine-to-machine API keys |
The legacy
/api/auth/api-keys/*routes still work but are deprecated aliases of the module above.
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
GET |
/api/users/me |
Yes | Any | Current user profile |
GET |
/api/users |
Yes | MANAGER+ | List organization users (cursor or offset) |
POST / DELETE |
/api/users[/{id}] |
Yes | ADMIN+ | Create / soft-delete users |
POST |
/api/users/invitations |
Yes | ADMIN+ | Generate invitation link |
GET / POST |
/api/users/invitations/verify, /accept |
No | — | Verify token / accept invitation |
GET / POST / PATCH / DELETE |
/api/company/invitations[...] |
ADMIN+ | Manage persistent invitations (resend/revoke/info) |
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
POST / GET / GET·PATCH·DELETE {id} |
/api/organizations[/{id}] |
SUPER_ADMIN (+ADMIN read/update) | Organization CRUD |
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
GET |
/api/shipments |
Yes | VIEWER+ | List shipments (offset pagination) |
POST |
/api/shipments |
Yes | MANAGER+ | Create shipment (Stellar-tokenized) |
GET·PATCH·DELETE |
/api/shipments/{id} |
Yes | role varies | Details / update / soft-delete |
PATCH |
/api/shipments/{id}/status |
Yes | MANAGER+ | Status transition (state machine) |
POST |
/api/shipments/{id}/proof |
Yes | MANAGER+ | Upload proof of delivery (multipart) |
POST |
/api/shipments/{id}/documents, /{id}/photos |
Yes | MANAGER+ | Attach files (multipart) |
GET |
/api/shipments/{id}/timeline, /{id}/eta |
Yes | VIEWER+ | Milestone timeline / ETA |
POST |
/api/shipments/{id}/disputes |
Yes | MANAGER+ | Raise a dispute |
GET |
/api/shipments/export?format=csv |
Yes | ADMIN/MANAGER | CSV export (≤10k rows) |
| CRUD | /api/shipment-templates[/{id}] |
Yes | VIEWER+ read / MANAGER+ write | Reusable shipment templates |
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
GET |
/api/ledger/blocks, /blocks/{id} |
Yes | VIEWER+ | Immutable milestone ledger (envelope + meta) |
GET / POST |
/api/telemetry, /telemetry/bulk |
Yes | VIEWER+ | Time-series records / bulk ingest (hashed + anchored) |
GET / PUT |
/api/telemetry/thresholds |
Yes | VIEWER+ / ADMIN+ | Per-organization anomaly thresholds |
GET |
/api/anomalies, /anomalies/stats |
Yes | VIEWER+ | Anomalies (cursor-paginated) / aggregates |
PATCH |
/api/anomalies/{id}/resolve |
Yes | MANAGER+ | Resolve anomaly |
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
GET |
/api/analytics/performance, /summary |
Yes | MANAGER+ | KPIs / summary with sparklines |
GET |
/api/payments, /payments/{id} |
Yes | VIEWER+ | Payment records |
POST / PATCH |
/api/payments, /payments/{id}/status |
Yes | ADMIN+ | Create / update payments |
GET |
/api/settlements, /settlements/summary, /{id} |
Yes | VIEWER+ | Settlement views |
POST |
/api/settlements/{id}/dispute |
Yes | MANAGER+ | Dispute a settlement |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/notifications, /unread-count |
Yes | List notifications / unread badge |
PATCH / POST / DELETE |
/notifications/{id}/read, /read-all, /{id} |
Yes | Read-state management |
GET / PATCH |
/api/notifications/preferences |
Yes | Channel preferences |
POST |
/api/notifications/phone/send-otp, /verify-otp |
Yes | SMS OTP verification |
GET |
/api/activity · /api/audit-logs |
Yes | Activity feed (all roles) / audit trail (ADMIN+) |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/events (?token=) |
SSE auth | Server-Sent Events stream |
GET |
/api/events/poll |
Yes | Fallback polling stream |
POST |
/api/webhooks/iot |
API Key | Receive IoT telemetry |
POST |
/api/webhooks/stellar |
HMAC Signature | Stellar settlement callbacks |
Configuration is validated at boot with Zod (src/env.ts) — the process exits immediately if a required variable is missing or malformed.
Core variables:
| Variable | Required | Default | Description |
|---|---|---|---|
MONGO_URI |
✅ Yes | — | MongoDB connection string (mongodb://…) |
JWT_SECRET |
✅ Yes | — | JWT signing secret (min 32 characters) |
PORT |
No | 3000 |
HTTP listen port |
NODE_ENV |
No | development |
development | test | production |
REDIS_URL |
No | redis://127.0.0.1:6379 |
Redis (BullMQ queues, SSE, caches) |
STELLAR_NETWORK |
No | testnet |
testnet | public; selects the default Horizon/Soroban URLs |
HORIZON_URL |
Optional | (derived from STELLAR_NETWORK) |
Overrides the Horizon URL |
STELLAR_SECRET_KEY |
Optional | — | Horizon signing key (required for anchoring jobs) |
STELLAR_WEBHOOK_SECRET |
Optional | — | HMAC secret for /api/webhooks/stellar (min 16 chars) |
FRONTEND_URL |
No | http://localhost:3000 |
Invite / password-reset link base |
ALLOWED_ORIGINS |
No | (empty) | Comma-separated CORS allowlist |
TOTP_ENCRYPTION_KEY |
Optional* | — | 64-hex AES-256 key for 2FA secrets (required in production) |
STORAGE_PROVIDER |
No | mock |
mock | s3 | r2 | cloudinary |
The full matrix — SMTP, SendGrid, Twilio, S3/Cloudinary keys, Soroban placeholders, Sentry — lives in docs/environment-variables.md and .env.example. There are no JWT_EXPIRY, STELLAR_NETWORK_PASSPHRASE, or API_KEY_PREFIX variables; token TTL is fixed in code and the network passphrase derives from STELLAR_NETWORK. The Horizon URL also derives from STELLAR_NETWORK by default, but can be overridden with HORIZON_URL (see src/config/stellarNetwork.ts); an unrecognized STELLAR_NETWORK value fails fast at startup.
| Command | Purpose |
|---|---|
npm run dev |
Start development server with hot reload (tsx watch) |
npm run build |
Compile TypeScript to JavaScript in dist/ |
npm run start |
Run production build |
npm run typecheck |
Run TypeScript type checker |
npm run check:deps |
Check runtime imports against package.json dependencies |
npm run lint |
Run ESLint over src/ |
npm run lint:fix |
Auto-fix lint issues |
npm test |
Run test suite (Jest with ESM VM modules) |
npm run test:watch |
Run tests in watch mode |
npm run migrate:up |
Apply pending database migrations (migrate-mongo) |
npm run migrate:down |
Rollback last migration |
npm run seed |
Seed the database with development data |
npm run worker:stellar |
Standalone telemetry → Stellar anchoring worker |
npm run worker:stellar-indexer |
Standalone Stellar confirmation indexer worker |
We welcome contributions! Please see CONTRIBUTING.md for guidelines on branching, commits, pull requests, and our build/test requirements.
If you discover a security vulnerability, email navinxmr@gmail.com — do not open a public issue.
Built on the Stellar ecosystem.