This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
AR.IO Node — Arweave gateway for accessing and indexing blockchain data, with caching, ANS-104 bundle unbundling, and multi-source data retrieval.
| Task | Command |
|---|---|
| Install | yarn install |
| Init/upgrade SQLite | yarn db:migrate up |
| Run the service | yarn start (add START_HEIGHT=N only right after a fresh migration) |
| Watch mode | yarn watch |
| Unit + property tests | yarn test |
| Single test file | yarn test:file src/path/to/file.test.ts |
| E2E (needs Docker) | yarn test:e2e |
| Lint | yarn lint:check / yarn lint:fix |
| Typecheck (incl. tests) | yarn typecheck |
| Build | yarn build |
| Circular deps / dupes | yarn deps:ci / yarn duplicate:check |
build-core.yml runs on every push (any branch) and runs build,
lint:check and test:ci — not typecheck. Its docker build and test:e2e
steps run only on develop. test-core.yml (manual dispatch only) is the one
workflow that also runs typecheck. Nothing runs on pull_request. yarn build
uses tsconfig.prod.json, which excludes *.test.ts, so yarn typecheck is
the only thing that type-checks test code — run it locally. It currently fails
with hundreds of pre-existing errors, so the useful check is "no new errors
compared with develop", not a clean exit.
E2E tests (test/end-to-end/) come in two kinds. getCoreContainer suites
run the image tagged core. composeUp suites run whatever image
docker-compose.yaml resolves, ghcr.io/ar-io/ar-io-core:${CORE_IMAGE_TAG:-latest}.
Without USE_PREBUILT_IMAGE=true, both kinds build from the local Dockerfile.
If you set it, give one build both tags (see build-core.yml) and export
CORE_IMAGE_TAG. Otherwise the composeUp suites quietly pull the published
latest and test old code.
For test-layer selection (unit / property / e2e / auto-verify / parquet /
load), use the testing skill in .claude/skills/; release and
ar-io-gateway-operator live there too.
- Documentation index —
docs/INDEX.md - Env vars —
docs/envs.md(keep this anddocker-compose.yamlin sync when adding or removing env vars) - Architecture diagrams —
docs/diagrams/ - Release & worktree tooling —
tools/README.md - Reference repos (arweave, ao, HyperBEAM, etc.) —
.mrconfig; runmr updateto clone/update - OpenAPI —
docs/openapi.yaml
-
src/system.tsis the central DI wiring — all services, workers, data sources, resolvers, and lifecycle cleanup handlers are constructed here. -
The indexing pipeline is event-driven:
src/system.tswires workers together through a sharedeventEmitterusing the names insrc/events.ts. Block importer →TX_INDEXED→ unbundle-filter match →ANS104_BUNDLE_INDEXED→ unbundler → data-item indexer. Once a data item's data is indexed (ANS104_DATA_ITEM_DATA_INDEXED), a filter match re-emitsANS104_BUNDLE_INDEXED, so nested bundles recurse through the same path. To trace a stage, grepsystem.tsfor its event. -
SQLite is split into five DBs under
data/sqlite/:core(L1 blocks/txs),data(content hashes, cache metadata),bundles(data items),moderation,chunks. Each hasnew_*(unstable, near tip) andstable_*tables. Migrations are named<timestamp>.<db>.<name>.sql; the<db>segment chooses the target DB file. -
GraphQL goes through
gqlQueryableinsystem.ts: SQLite, optionally wrapped byCompositeClickHouseDatabase(ClickHouse for stable history, fed byClickHouseStreamer) and optionally merged with upstream gateways viaGatewaysGqlQueryable. -
src/data/uses composite sources with fallback chains (cache → S3 → AR.IO peers → trusted gateways → Arweave nodes). Retrieval order is configurable viaON_DEMAND_RETRIEVAL_ORDERandBACKGROUND_RETRIEVAL_ORDER. -
Database access runs in a worker thread (
StandaloneSqlite). The main process queues operations via message passing — never call SQLite synchronously from the main thread. -
Filters (
ANS104_UNBUNDLE_FILTER,ANS104_INDEX_FILTER,WEBHOOK_INDEX_FILTER) share a composable JSON filter system — seedocs/filters.md. -
Responses include trust headers indicating verification status.
-
HTTPSIG signs response headers (RFC 9421);
Content-Digestis inCO_SIGNABLE_HEADERSso when present it binds the body to the signature. Cached and HEAD responses always emit it from the stored hash; small uncached responses (≤HTTPSIG_BODY_DIGEST_BUFFER_MAX_BYTES, default 2 MiB) buffer + hash to emit it. Larger uncached bodies stream without a body digest. Chunks are bounded at 256 KiB so they always carry one. Index band files (/ar-io/indexes) are signed through theX-AR-IO-Index-Filetrigger;Repr-Digestis co-signable too, so a signed 206 is bound to the whole file. -
The
index-swarmsidecar (src/index-swarm/) shares signed index bands between gateways over HTTP and, with the torrent engine, BitTorrent. Both start by default since Release 85, subscribed to the ar.io publisher's root-TX bands (INDEX_SWARM_SUBSCRIBE=[]opts out). Anything a publisher, peer or engine supplies is untrusted: files are checked against signed digests, a torrent against signed infohashes and file list, and a torrent download is copied and hashed out ofswarm/before install, never installed in place.
- Branching: branches are cut from and merged back to
develop;mainonly moves at release time. Open PRs againstdevelop, notmain. - ESM + NodeNext: relative imports must carry a
.jsextension even in TypeScript (import { x } from '../lib/y.js'). Node 20 (.nvmrc),"type": "module", run viats-node/esm+ SWC (register.js). - License header: every
.tsfile must start with the AGPL header inresources/license.header.js—eslint-plugin-headerfails the build without it. Copy it when creating a new file. - Node protocol imports:
node:fs, notfs(unicorn/prefer-node-protocol). strict-boolean-expressionsis on: write explicit!== undefined/.length > 0rather than relying on truthiness.- Design principles (README): code to interfaces, keep IO separate from logic, make processes idempotent, and keep every component runnable in a single process. In tests, use in-memory implementations and sociable tests in preference to mocks and stubs.
- Stage specific files. Do not use
git add .orgit commit -A. - Use Conventional Commits format
(
feat:,fix:,refactor:,chore:,docs:,test:, etc.).
./tools/wt add <branch> symlinks .env and CLAUDE.local.md from the main
checkout into the worktree but gives each worktree its own clean data/
directory (not shared).
When iterating against the local service: stop it, clear
logs/service.log (rm logs/service.log && touch logs/service.log), then
restart. Service logs are JSONL; OTEL spans are in logs/otel-spans.jsonl.
Always use createTestLogger() from test/test-logger.ts in test files —
never winston.createLogger({ silent: true }). Test output is written to
logs/test.log (overwritten each run), not the console.
Five coordinated edits are required:
- SQL statement in
src/database/sql/<schema>/(named via-- statementNamecomment) - Worker implementation in
StandaloneSqlite - Queue wrapper in the main database class
- Case handler in the worker message handler
- Interface signature in
types.d.ts
- One
ALTER TABLEper column (no comma-separated columns) - Drop indexes before dropping their columns
- Avoid
DEFAULTinALTER TABLE ADD COLUMN— it rewrites the entire table - Prefer
NULLS FIRST/NULLS LASToverCOALESCEinORDER BYto preserve index usage - Run
./test/dump-test-schemasafter applying migrations so the test SQL files stay current. Down migrations go inmigrations/down/with the same filename.
src/lib/parquet-l1/chain.ts reimplements part of Arweave's consensus
hashing in TypeScript: the hash_list_merkle recurrence across its eras,
tx_root with the fork-2.5 padding, and the v1 data_root chunking.
That is deliberate — it is what lets an index be checked against the
chain without running a node — but it means upstream can change a rule
and leave us quietly wrong, which is worse than having no verifier.
test/arweave-consensus-pin.ts records each upstream function and a
digest of its text; chain-drift.test.ts re-reads them from the
repos/arweave checkout and fails when one changes. It skips when
there is no checkout, so CI is unaffected; run mr update to get one.
A failure is a prompt to read the upstream diff, not a verdict: a comment and a consensus change look identical to it. If our rule still holds, update the pin and say so in the commit.
Two rules that cost real time to get wrong, worth knowing before editing
chain.ts:
tx_rootdoes not bind transaction ids. Arweave drops the id before building the tree (ar_block.erl,generate_tx_tree/1), so an id reaches it only through the sort order.- A v1
data_rootis notarweave-js'scomputeRootHash. Arweave splits withar_tx:chunk_binary— fixed chunks, remainder last, no rebalancing — whilearweave-jsrebalances the last two chunks under 32 KB. Using the wrong one made 9 of 150 sampled mainnet blocks look corrupt.
Schema changes to SQLite stable_* tables, the Parquet export, or the
ClickHouse transactions table must be reflected in the corresponding
adapter under src/tests/auto-verify/sources/ and in the canonical types
in src/tests/auto-verify/types.ts. The adapters project each source into
a shared canonical shape for comparison, so a silent divergence shows up
as field_mismatch / missing_in_source discrepancies rather than a
build error. Also re-check the staging/final table list in
gateway-control.ts's cleanClickHouseTables when tables are added or
removed. See docs/auto-verify.md.
For testing uploads without broadcasting to Arweave, see
ARWEAVE_POST_DRY_RUN and ARWEAVE_POST_DRY_RUN_SKIP_VALIDATION in
docs/envs.md.
When changing behavior that affects documented contracts (env vars, APIs,
CLI tools), update the relevant file in docs/ in the same PR. Use
docs/INDEX.md to find the right doc. Add new terms and concepts to
docs/glossary.md. Add or improve TSDoc comments on code you touch.