Backend API for Predictify — a Stellar/Soroban prediction-markets dApp.
This service indexes on-chain market state from the Predictify Soroban contract, exposes a REST API for the frontend, handles wallet-based authentication, and ships notifications + leaderboards.
- Node.js 20 + TypeScript
- Express for HTTP
- Drizzle ORM + PostgreSQL for persistence
- BullMQ + Redis for async job queues (webhook delivery, backup verification, reconciliation, market resolution)
- zod for env + request validation
- pino for structured logging
- JWT (jsonwebtoken) for wallet-based session auth
- Stellar SDK for Soroban RPC + Horizon
- Jest + supertest for tests
cp .env.example .env # copy the template
# Edit .env — set JWT_SECRET, DATABASE_URL, and PREDICTIFY_CONTRACT_ID
# (all other keys have working testnet defaults)
npm install
npm run check-env # validate .env before touching the DB
npm run db:migrate
npm run dev # predev hook re-runs check-env automaticallyOnce running:
- Swagger UI → http://localhost:3000/docs (non-production only; set
ENABLE_DOCS=trueto enable in production) - OpenAPI JSON → http://localhost:3000/openapi.json (always available)
- Audit export →
GET /api/admin/audit/exportstreams admin audit logs asapplication/x-ndjson
- Default JSON request body limit:
256kb. - Route-level overrides are applied in middleware using
createBodyLimitMiddleware(...). - Webhook routes may opt into a larger limit of
1mb. - Requests exceeding the configured limit return HTTP
413with the standard error envelope, including correlation and request IDs.
The gap-scan worker detects missing ledger ranges in indexer_events between the durable cursor and chain tip, emits indexer_gap_detected_total{from,to}, and self-heals via backfillRange:
npm run indexer:gap-scanConfigure via INDEXER_GAP_SCAN_INTERVAL_MS, INDEXER_REWIND_LEDGERS, and INDEXER_BACKFILL_CHUNK_SIZE in .env.
Slow and IO-bound jobs (webhook delivery, backup verification, market resolution, reconciliation) run in BullMQ workers backed by Redis. See docs/job-queue.md for the full design, enqueue patterns, environment variables, and testing guide.
Quick start:
docker run -p 6379:6379 redis:7-alpine # local Redis
# Set REDIS_URL=redis://localhost:6379 in .env
npm run devsrc/
config/ env + logger
routes/ health, markets (more to come)
services/ domain services
workers/ indexer gap scan worker
metrics/ in-process counters (indexer_gap_detected_total)
middleware/ errorHandler, auth (planned)
workers/ long-running processes (Soroban indexer)
db/ drizzle schema, client, repositories
tests/ jest tests
drizzle/ generated migrations + meta
scripts/ dev helpers (check-drizzle-drift.ts)
.github/
workflows/ CI pipeline (lint, test, drift check, migrate)
This starter is intentionally minimal. The full backlog is tracked in GitHub Issues under the OFFICIAL CAMPAIGN label. Major themes:
- Wallet-based auth (Stellar address challenge/signature → JWT)
- Market CRUD + caching layer
- Soroban-RPC indexer with reorg/gap handling
- Predictions + claims endpoints
- Leaderboards & user profiles
- Webhook delivery + DLQ
- Observability (metrics, tracing, /readyz with deep checks)
- OpenAPI spec + contract tests
POST /api/auth/refreshaccepts{ "refreshToken": "<opaque token>" }, revokes the presented refresh token, and returns a freshaccessTokenplus a rotatedrefreshToken.- Refresh tokens are stored only as SHA-256 hashes in the
refresh_tokenstable. The raw bearer token is generated once and is never persisted. - If a revoked refresh token is presented again, the service treats it as suspected theft and revokes every still-active token in the same
familyId. POST /api/auth/logoutaccepts the same body and revokes the remaining active tokens in that refresh-token family.
Run the unit test suite:
npm test # Run all tests
npm run test:unit # Run unit tests only (excludes E2E)
npm run test:coverage # Run with coverage reportEnd-to-end tests validate the complete prediction lifecycle on Stellar testnet:
npm run test:e2e # Run E2E tests
npm run test:e2e:coverage # Run E2E tests with coverageE2E tests validate:
- User authentication with wallet signatures
- Market creation on testnet
- Placing predictions
- Market resolution
- Claiming winnings
- Data consistency across the lifecycle
Setup required:
- Funded Stellar testnet account
- Deployed Predictify contract on testnet
- Test database (separate from dev/prod)
See tests/e2e/README.md and docs/e2e-testing.md for detailed setup and usage.
CI/CD:
- E2E tests run nightly at 2 AM UTC
- Manual trigger via GitHub Actions
- Automatic issue creation on failure
npm test -- tests/refreshToken.test.tsThe refresh-token test suite covers rotation, expiry handling, reuse detection, logout family revocation, and hash-only storage.
Follow graph mutations are exposed at:
POST /api/users/:addr/followDELETE /api/users/:addr/follow
These endpoints require authentication, enforce users.is_private, update
cached followers_count and following_count values transactionally, and
write structured audit entries with the request correlation ID.
You can spin up the entire Predictify stack (API, Indexer, and PostgreSQL) using Docker Compose.
- Docker installed and running.
- A local
.envfile generated from.env.example. EnsureDATABASE_URLis set topostgres://postgres:postgres@db:5432/predictifyfor Docker compatibility.
-
Start the stack:
docker compose up --build
-
Verify the services: Once booted, the API will be available at http://localhost:3001. Check the health endpoint:
curl localhost:3001/health # Expected response: 200 OK
-
The migrate service runs automatically on startup to ensure the database schema is up-to-date before the API and Indexer start.
-
The indexer service runs as a persistent container; check the logs with docker compose logs -f indexer if you encounter sync issues.
- Performance: Multi-stage builds reduce the final image size by excluding source code and dev dependencies.
- Security: By using
USER nodeandslimbase images, we reduce the attack surface. - Resilience: The
depends_oncondition usingservice_healthyorservice_completed_successfullyensures the database is ready and migrations are applied before application services boot, preventing race conditions. - Supply-Chain: The base image is pinned by a specific digest. Important: When you run this, verify the digest matches your local build requirements, or update it to the latest
node:20-bookworm-slimdigest if you prefer the absolute latest patch version.
MIT