# Chainquery: turning the LBRY blockchain into a MySQL database you can query

> A Go service that ingests LBRY blocks from a full node, keeps them in MySQL through reorganizations, and exposes the result over HTTP with a public read only SQL endpoint.

**OdyseeTeam/chainquery** — Chainquery parses and syncs the LBRY blockchain data into structured SQL

- Repository: https://github.com/OdyseeTeam/chainquery
- Website: https://lbry.tech
- Stars: 2,206 · Forks: 36
- Language: Go
- License: MIT
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/odyseeteam-chainquery

## Why index a blockchain into MySQL in the first place

The README opens with the reason plainly. Chainquery reads the LBRY blockchain into a structured MySQL database so that blocks, transactions, addresses, and the full LBRY claim system covering channels, publishes, supports, tips and purchases, and tags can be queried quickly with plain SQL instead of walking the chain over RPC.

That last clause is the whole design argument. Every question you want to ask about a blockchain of this shape is a join: which addresses spent into which transaction, which claims a channel holds, which purchases a tip funded, how many claims sit in a list. Answering those over JSON-RPC means many round trips and repeated work. Answering them against a relational schema means one query and an index.

The claim system is the reason this is not a generic block indexer. LBRY stores content metadata as claims inside transaction outputs, and the README's data model section describes the base types of block, transaction, input, output, and address, enriched with claim, support, purchase, tag, claim_tag, claim_in_list, and abnormal_claim, plus bookkeeping tables for job_status and application_status. The schema in `model/` is generated, with SQLBoiler as the query layer, which is why you get typed Go structs for free.

Everything ships as one binary. The README states that everything runs from a single binary, `chainquery serve`, which starts the API server and the daemon together. There is no separate worker to deploy and no message broker between them.

```bash
chainquery serve
chainquery run <job>
```

## Strictly ordered blocks, parallel transactions, and a redo queue

The daemon lives in `daemon/daemon.go` and `daemon/processing/`, and the README describes its loop in concrete terms: on each iteration it asks the node for the current height and processes every block above the last height it stored. The interesting part is what happens inside a block.

Blocks are processed strictly in order. A channel handshake named blockQueue to blockProcessedChan plus a global BlockLock mutex guarantee block N+1 never starts before N is committed, and the README says out of order processing panics by design to avoid corrupting the data. Trading a loud crash for silent corruption is a defensible call in a sync daemon, and the code documents that it is intentional.

Transactions inside a block run in parallel through a worker pool governed by maxparalleltxprocessing, defaulting to NumCPU. That parallelism creates a dependency problem, since a transaction can spend an output created by another transaction in the same block. The answer is two mechanisms working together: transactions are pre-sorted by dependency depth using optimizeOrderToProcess so parents tend to run before children, and any transaction that still fails is pushed onto a redo queue and retried up to maxfailures times, default 1000.

Throttling is a config knob named daemonmode, with four documented values: beast (0, no delay), slow and steady (1, 100ms per block), delay (2, configurable), and daemon (3, one block per daemon iteration). Someone recovering a database that fell behind wants a mode that does not saturate the node; someone syncing from scratch wants the opposite. Having both in one config key is a small design choice that tells you the author has done both.

Entry points are named in the README as daemonIteration(), processing.RunBlockProcessing(stopper, height), and processing.ProcessTx(...).

## Reorganization handling is automatic and bounded

Any chain indexer that ignores reorganizations is a snapshot generator rather than a sync tool. Chainquery treats this as a first class case. Before processing a block, the daemon compares the stored previous block hash against the chain. On a mismatch it recursively deletes the diverged blocks up to a depth of 100, logs the reorg depth, and reprocesses from the divergence height.

Three details make this more than a paragraph of intent. The depth is bounded, so a pathological reorg cannot send the daemon into unbounded recursive deletion. The depth is logged, so an operator can see how deep the rollback went rather than discovering it as unexplained row loss. And the reprocess restarts from the divergence height rather than from the tip, which means the normal ordered pipeline rebuilds everything above the fork.

The v2.6.0 release notes describe this area as having been hardened, alongside bounded RPC, database, socket notification, and alert paths, incomplete head cleanup, and recovery coverage. The same notes mention improved reorg handling and added Slack app logging for reorg depth and affected heights. So the automatic behavior described in the README has a matching observability story, which is what you want when a rollback happens unattended at three in the morning.

One caveat belongs here. The README's opening paragraph says the service connects to lbcd in production, describing it as LBRY's Go full node implementation which the stack has migrated to, or the original lbrycrd. Every mechanism description further down, however, still names lbrycrd. The daemon section says it asks lbrycrd for the current height and keeps the database in sync with it, and the Claimtrie Sync job is described as pulling claim status from lbrycrd's ClaimTrie. Both statements are in the current README. Read together, they suggest a network whose production node moved while the prose describing the sync path was not fully updated, and the sensible move is to confirm which node your deployment points at before trusting either description.

## The HTTP surface, including a public SQL endpoint with no auth

The API server binds to apihostport, default 0.0.0.0:6300, and deliberately uses a separate read DSN named apimysqldsn rather than the daemon's connection. The route table in the README is worth reading as a security posture, not just an API list.

Three kinds of access appear. Routes with no auth are `/api/`, which returns Hello World!, `/api/sql`, `/api/addresssummary` for received, spent, and balance, `/api/status` for table names and sizes, and `/api/validate` for chain data validation. Routes requiring an API key are `/api/process`, which processes a block or range of blocks, `/api/sync/name`, `/api/sync/addresses`, and `/api/sync/txvalues`. `/metrics` for Prometheus is guarded by basic auth.

`/api/sql` deserves a second look. The README describes it as public read only SQL that runs the query param against MySQL with an injected MAX_EXECUTION_TIME and a maxsqlapitimeout cap. The mitigations are real and deliberate: the injection is what stops a runaway cartesian join from pinning the box, and it shows the author thought about the abuse case. But the endpoint is unauthenticated by default and it passes arbitrary query text to your database. Whether that is acceptable depends entirely on what sits behind it. Bound to a public interface, it is a convenient research endpoint and a liability at the same time.

The API key endpoints are rejected unless the supplied Key appears in the apikeys config, and the README notes this list is empty by default, which disables them. Failing closed on an empty list is the correct default here. `/metrics` uses promuser and prompass when set.

API documentation is generated from `swagger/apiserver/api/swagger.yaml` and published, with the README marking the effort as work in progress, at the GitHub Pages site for the project.  The repository also carries a small Node package purely to serve those docs, with `package.json` naming it swagger-docs, pointing main at main.js, requiring node >=24, and depending on express 3.x. A Node runtime inside a Go project is a slightly odd seam, but it is scoped to the docs site rather than to the service.

## Scheduled jobs cover what is too slow to compute inline

Some blockchain data is cheap to derive while ingesting and other data is not. The job scheduler in `daemon/jobs/` handles the second category, wired up by initJobs(), with a table of intervals and purposes in the README.

Claimtrie Sync runs every 15 minutes and pulls claim status and effective amount from the node's ClaimTrie. Mempool Sync runs every second for unconfirmed transactions, Certificate Sync every 5 seconds for channel data, and Chain Sync every 5 seconds for chain derived data with a code note saying it must run in under 2.5 minutes. Then the daily tier: Validate Chain for integrity validation, Address Balance Sync, Transaction Value Sync, and Claim Count in Channel, each at 24 hours.

The interval choices reveal the cost model. Anything touching the mempool or certificates is second scale because those change constantly. Chain Sync at 5 seconds carries an explicit budget in the code, which tells you the author measured it. The 24 hour jobs are recomputes of values that can be derived incrementally but are cheaper to rebuild in bulk.

Any of these can be triggered individually with `chainquery run <job>`, and the job_status table records the last run of each. Having a durable record of job execution is what makes a stalled daily sync diagnosable.

The fourth component is the upgrade manager in `daemon/upgrademanager/`, which runs data and schema upgrade scripts between versions on startup. It tracks the app, data, and api versions in the application_status table so it knows which scripts to apply. For a service whose whole job is to be the canonical SQL view of a chain, silently applying migrations on boot is a defensible choice, and the version table is what makes it repeatable.

## What the Go module, the dependency list, and the release gap reveal

The module is unambiguous about the runtime floor:

```go
module github.com/lbryio/chainquery
go 1.26
toolchain go1.26.4
```

Go 1.26 with a pinned toolchain is worth noticing because it says something about the project's age. The repository lists three releases: v2.6.0 published in June 2026, and v2.5.4 and v2.5.3 both published in October 2020, an hour apart, described as improving sync claims of the claim trie and increasing queue size. That is close to a six year gap between 2.5.4 and 2.6.0. The v2.6.0 notes read like a revival: GitHub Actions CI for Go 1.26 with MySQL backed race tests and generated migration bindata checks, Travis era release plumbing replaced by GitHub Releases publishing the Linux binary with a SHA-256 checksum, and removal of legacy autoupdate, Travis webhook, Twilio, and deprecated Slack webhook support.

The dependency list explains a lot of the stack. The MySQL driver go-sql-driver and sqlx alongside jmoiron handle the database, SQLBoiler provides the generated model layer, rubenv/sql-migrate for migrations, and kevinburke/go-bindata for embedding generated migration files, which is why CI checks generated migration bindata. prometheus/client_golang backs the metrics endpoint. cobra and viper handle the CLI and configuration, gorilla/mux the routing, logrus the logging, and OdyseeTeam/sockety provides a socket notification service. Several LBRY dependencies are pinned to pseudoversions from 2022 and earlier, which is consistent with a project that sat quiet for years.

On identity, the naming is layered. The repository itself lives under the OdyseeTeam organisation and its description reads that Chainquery parses and syncs the LBRY blockchain data into structured SQL, but the Go module path is github.com/lbryio/chainquery and every README badge points at the lbryio organisation, including the CI workflow, the Go Report Card, and the Code Climate maintainability badge. The homepage is lbry.tech. So the code, the CI, and the docs all still reference the older lbryio path while the repository has moved. Anyone scripting against the module path or the badge URLs should be aware of that split.

Activity is current rather than historical. The repository is not archived, the last push was in late August 2026, and it carries roughly 2,200 stars, 35 forks, and 41 open issues. A fork to star ratio this low is characteristic of a project people depend on rather than one they build on top of.

## Conclusion

Chainquery earns its place when your question about LBRY is a question about rows rather than about blocks. Walking the chain over JSON-RPC for every lookup does not scale, and this service exists to move that work into MySQL where you can join, aggregate, and index. The parts to understand before deploying are the ones the README is most careful about: strict block ordering, automatic reorg rollback, a separate read DSN for the API, and a public SQL route that ships with no authentication. Start with `chainquery serve` against a full node you control, watch the block processor catch up, and only then point a real query at the endpoint.

## FAQ

### What does Chainquery do?

It reads the LBRY blockchain into a MySQL database so blocks, transactions, addresses, and the claim system can be queried with plain SQL instead of walking the chain over RPC. A single binary, chainquery serve, runs the daemon and the API server together.

### Which LBRY node does Chainquery connect to?

The README is inconsistent here. Its opening paragraph says lbcd in production, the Go full node the stack migrated to, or the original lbrycrd, while the daemon and job sections still describe querying lbrycrd for height and reading its ClaimTrie. Confirm which node your deployment points at.

### How does Chainquery handle blockchain reorganizations?

Before processing a block the daemon compares the stored previous block hash against the chain. On mismatch it recursively deletes diverged blocks up to depth 100, logs the reorg depth, and reprocesses from the divergence height.

### Is the Chainquery SQL endpoint protected by authentication?

No. The /api/sql route is described as public read only SQL and requires no auth. It passes the query parameter to MySQL with an injected MAX_EXECUTION_TIME and a maxsqlapitimeout cap, which limits damage but does not restrict who can ask questions.

### Why are transactions in a block processed in parallel?

Because a worker pool sized by maxparalleltxprocessing, defaulting to NumCPU, speeds up large blocks. To handle transactions that spend outputs created earlier in the same block, transactions are pre-sorted by dependency depth and failures go onto a redo queue retried up to 1000 times.

### Which Go version does Chainquery need?

The go.mod file declares go 1.26 with toolchain go1.26.4. The v2.6.0 release added GitHub Actions CI for Go 1.26, including MySQL backed race tests and generated migration bindata checks.

## Sources

- [License: MIT](https://github.com/OdyseeTeam/chainquery/blob/master/LICENSE)
- [OdyseeTeam/chainquery on GitHub](https://github.com/OdyseeTeam/chainquery)
- [Project website](https://lbry.tech)
- [README](https://github.com/OdyseeTeam/chainquery/blob/master/README.md)
- [Releases](https://github.com/OdyseeTeam/chainquery/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/odyseeteam-chainquery
