Read-only, horizontally-scalable query API for the ixo blocksync database.
This service is the query half of what used to be a single ixo-blocksync
process: ixo-blocksync keeps
indexing the chain into Postgres (writes), while ixo-blocksync-api serves
GraphQL + REST reads from that same database. Because it holds no sync state,
you can run as many replicas as you need.
Built on PostGraphile 5 (Grafast) with the V4 compatibility preset, so the
GraphQL schema keeps the exact field names, arguments and response shapes of
the existing ixo-blocksync v4 GraphQL API.
| Path | Description |
|---|---|
POST /graphql |
GraphQL API (same schema shape as ixo-blocksync) |
GET /graphiql |
Ruru (GraphiQL) with Grafast plan visualisation |
GET /graphql/schema.graphql |
Exported SDL (written on boot) |
GET /api/claims/collection/:id/claims |
Paginated collection claims (same contract as ixo-blocksync) |
GET /api/ipfs/:cid |
Rate-limited IPFS gateway proxy |
GET / |
Plain liveness text ("API is Running") |
GET /healthz |
Liveness with a DB round-trip |
Copy .env.example to .env. DATABASE_URL is the only required variable —
point it at the blocksync database (read access is enough; the API never
writes).
Notes:
DATABASE_USE_SSL=1connects withrejectUnauthorized: falseto match how ixo-blocksync connects to the managed cluster. Prefer proper CA validation where possible.- pgbouncer: the pool intentionally avoids server startup parameters
(pgbouncer rejects them) - query timeouts are client-side plus per-request
SET LOCALinside transactions. LISTEN/NOTIFY does not traverse pgbouncer transaction pooling, so setBLOCK_CACHE_LISTEN_DATABASE_URLto the direct postgres service for instant cache invalidation; without it the poll backstop (default 3s) bounds staleness instead. RPCis only needed for thetokenomicsSupply*GraphQL fields (they read the chain, not the DB).- Block-aware response cache (
BLOCK_CACHE, default on): GraphQL POST responses are cached in-process and the entire cache is flushed the moment the indexer commits a block (pg_notify trigger on"Chain"- applyscripts/block-notify-trigger.sqlonce per database - plus aBLOCK_CACHE_BACKSTOP_MSpoll as a safety net). Because the database only changes when a block commits, a cached response can never be staler than the database itself: someone who transacts and immediately queries still sees their transaction, since that block flushed the cache. Mutations (none exist),tokenomics*operations (chain-RPC backed) and responses overBLOCK_CACHE_MAX_ENTRY_BYTESare never cached.X-Cache: HIT|MISSis set on cacheable requests;/healthzreports cache stats.
Every request (except / and /healthz probes) emits one structured JSON
line on stdout via pino, ready for any log
pipeline (Loki/Grafana, BigQuery, kubectl logs | jq):
{"level":"info","time":"2026-07-11T09:00:00.000Z","method":"POST","path":"/graphql","status":200,"durationMs":43.2,"bytes":26543,"ip":"1.2.3.4","ua":"node-fetch","gql":{"op":"GetEntityById","type":"query","roots":["entity"],"hash":"ab12cd34ef56ab12","cache":"HIT","errors":false},"msg":"request"}gql.rootsare the operation's top-level fields — the natural dimension for per-resolver latency/volume aggregation;gql.hash(sha256 of the query text) groups identical documents;gql.cacheisHIT/MISSfrom the block cache orBYPASSfor uncacheable operations;gql.errorsflags GraphQL errors (headers-based heuristic — errors precede data in the payload).bytesis the uncompressed response size (logging sits inside compression). Variables are deliberately never logged.- Responses with status ≥ 400 and requests slower than
LOG_SLOW_MS(default 1s) log atwarn, 5xx aterror, so slow/failing traffic can be alerted on by level alone. LOG_LEVELcontrols verbosity (silentdisables). Pretty-print locally withnpm run dev | npx pino-pretty.
npm install
npm run dev # tsx watch
npm run build && npm start- No GraphQL query batching (array-of-operations POST bodies). Removed in PostGraphile 5; no known ixo client uses it (verified against impacts-x-web, jambo, ixo-Mobile and ixo-matrix-appservice-rooms, July 2026). Use HTTP/2 or merge queries into one document with aliases.
nodeIdand connection cursor encodings differ from v4. Field names and shapes are unchanged; only the opaque values differ, so don't persist them across the migration./api/ipfs/:cidserves non-inline-safe content types (e.g.image/svg+xml, XML) asapplication/octet-streamwithnosniff+ a sandboxing CSP, so proxied IPFS content can never execute scripts on the API origin (v4 only blockedtext/html). Common image/json/pdf types are unaffected./api/claims/...no longer triggers the cellnode schema-type fetch (that is a write; the indexer's cron owns it). The "not loaded yet" response contract is unchanged.- No
/wswebsocket server (it belongs to the indexer, which sees new blocks), no swagger, no write/cron endpoints (/api/tokenomics/fetchAccountsstays on the indexer). - No Sentry yet — add the org-standard Sentry setup at deploy time if wanted.