Read API and WebSocket relay for Savage Summit frontend clients.
For AI-oriented coding guidance and deeper architecture notes, read AGENTS.md in this folder. For shared architecture/mechanics, read ../README.md.
- Node.js
22 - TypeScript
5.7.0 - Hono
4.6.0 - Drizzle ORM
0.38.0 - PostgreSQL (
pg8.13.0) ws8.18.0
- Server + routes:
src/index.ts - DB pool/client:
src/db/client.ts - DB schema mirror:
src/db/schema.ts - WS subscription hub:
src/ws/subscriptions.ts - Beast metadata helpers:
src/lib/beastData.ts
DATABASE_URL(required)DATABASE_SSL("true"or"false"; required in production)DB_POOL_MAX(default15)PORT(default3001)NODE_ENV(productionhides debug entries from/discovery payload)
Production note:
- API startup fails fast when
NODE_ENV=productionandDATABASE_SSLis unset.
cd api
pnpm installCreate api/.env:
DATABASE_URL="postgresql://user:password@localhost:5432/summit"
# optional
# DATABASE_SSL="false"
# DB_POOL_MAX="15"
# PORT="3001"Then start the server:
pnpm devVerify runtime status:
curl http://localhost:3001/health- Dev:
pnpm dev - Build:
pnpm build - Start:
pnpm start - Typecheck only:
pnpm exec tsc --noEmit
GET /discovery payloadGET /healthGET /beasts/allGET /beasts/:ownerGET /beasts/stats/countsGET /beasts/stats/topGET /logsGET /diplomacyGET /diplomacy/allGET /leaderboardGET /quest-rewards/totalGET /adventurers/:player
GET /beasts/all
- params:
limit(default25, max100),offset,prefix,suffix,beast_id,name,owner,sort(summit_held_seconds|level) - returns:
{ data: Beast[], pagination: { limit, offset, total, has_more } }
GET /logs
- params:
limit(default50, max100),offset,category,sub_category,player category/sub_categoryaccept comma-separated values- returns:
{ data: LogEntry[], pagination: { limit, offset, total, has_more } }
GET /beasts/stats/top
- params:
limit(default25, max100),offset - returns: paginated top beasts sorted by summit hold time, bonus XP, death timestamp
GET /diplomacy
- params:
prefix(required),suffix(required) - returns HTTP
400if either is missing
GET /beasts/stats/counts
- returns total/alive/dead using alive definition:
last_death_timestamp < now - 86400
GET /leaderboard
- returns owner-grouped reward sums
- amounts are divided by
100000for display
GET /adventurers/:player
- returns distinct adventurer IDs for the normalized player address
Root discovery notes:
- In development mode (
NODE_ENV != production),/includes debug endpoint hints. - This service currently does not define corresponding
POSThandlers insrc/index.ts.
- Endpoint:
ws://localhost:3001/ws - Channels:
summit,event - Message types:
subscribe,unsubscribe,ping
Subscribe example:
{"type":"subscribe","channels":["summit","event"]}Unsubscribe example:
{"type":"unsubscribe","channels":["event"]}Ping example:
{"type":"ping"}Server responses:
{"type":"subscribed","channels":[...]}{"type":"unsubscribed","channels":[...]}{"type":"pong"}
Realtime pipeline:
Indexer -> PostgreSQL NOTIFY (summit_update, summit_log_insert) -> SubscriptionHub LISTEN -> API WS broadcast
- Address inputs are normalized to lowercase 66-char
0x-padded form. - API is public read-only (no auth layer).
- No dedicated caching layer is used.
- Graceful shutdown closes WS subscriptions/listeners on
SIGINT/SIGTERM.
- Docker image uses multi-stage Node 22 Alpine build.
- Container runs as non-root and includes a healthcheck against
/health.
API CI runs:
pnpm exec tsc --noEmit -> pnpm build.