A blockchain explorer for the Neurai network, engineered for efficiency and scalability. It combines a Rust backend for high-throughput block synchronization with a Next.js frontend for the user interface.
- Syncer Backend: Implemented in Rust using Tokio for asynchronous event processing. Handles block indexing with reduced memory overhead.
- Frontend Architecture: Built on Next.js 16 (App Router) and React 19, leveraging Server Components for optimized rendering.
- Analytics: Data visualization for network statistics, including difficulty and hashrate, utilizing Recharts.
- Asset Management: Native indexing and display of Neurai Assets (Tokens), including metadata and transfer ledgers.
- User Interface: Responsive layout constructed with Tailwind CSS, including system-aware theme support.
- Data Persistence: PostgreSQL database managed via SQLx for the syncer and Prisma for frontend queries.
- Architecture Overview
- Tech Stack
- Services
- Getting Started
- Configuration
- Development
- Project Structure
- Contributing
- License
The explorer follows a microservices architecture with four main components communicating through Docker's internal network.
| Component | Technology | Version |
|---|---|---|
| Frontend Framework | Next.js (App Router) | 16.x |
| UI Library | React | 19.x |
| Language | TypeScript | 5.x |
| Styling | Tailwind CSS | 4.x |
| State Management | TanStack Query | Latest |
| Component | Technology | Version |
|---|---|---|
| Syncer Runtime | Rust | 2021 Edition (rust 1.92) |
| Async Runtime | Tokio | Latest |
| Database Driver | SQLx | Latest |
| Database | PostgreSQL | 18.1 |
| ORM (Frontend) | Prisma | Latest |
| Component | Technology | Purpose |
|---|---|---|
| Containerization | Docker Compose | Service orchestration |
| Blockchain Node | Neurai Core | Network connectivity |
| Node Image | neuraiproject/neurai-node |
Published Neurai node image (Docker Hub) |
Web interface for blockchain data visualization and interaction.
- Framework: Next.js 16 (App Directory structure)
- Features:
- Block exploration with pagination support
- Transaction inspection and search functionality
- Network statistics dashboard
- Asynchronous state management via React Query
- UI Components:
- Icons provided by Lucide React & React Icons
- Data visualization using Recharts
- Conditional class utility via clsx
Rust-based service responsible for blockchain synchronization and indexing.
- Architecture: Asynchronous execution using the Tokio runtime
- Database: Direct PostgreSQL interaction via SQLx with compile-time query verification
- Performance:
- Memory usage: ~500MB (efficient resource management)
- Zero-copy deserialization implemented where applicable
- Database connection pooling
- Communication: JSON-RPC interface to the Neurai Node
PostgreSQL instance for indexed blockchain data persistence.
- Engine: PostgreSQL 18.1 (Alpine Linux variant)
- Schema Management: SQL migrations in
syncer/migrations/, applied by the syncer at startup (the frontend's Prisma schema only mirrors them) - Indexed Data:
- Block headers (height, hash, timestamp, transaction count, txids)
- Transactions (decoded JSON, raw bytes, position in block)
- Per-transaction history of what each address received/spent, in XNA and in assets; address and asset ledgers are sums of that history
Neurai blockchain daemon instance.
- Image: neuraiproject/neurai-node
v1.0.6, pulled from Docker Hub (no local build) - Source: NeuraiProject/Neurai 1.0.6 release
- Configuration: full indexes (
txindex,assetindex,addressindex,timestampindex,spentindex), REST enabled, wallet disabled, no inbound P2P - Ports:
19001: JSON-RPC interface, reachable only inside the Compose network
- Docker >= 24.0
- Docker Compose >= 2.20
- 8GB+ RAM recommended
- 50GB+ disk space for blockchain data
# Clone the repository
git clone https://github.com/your-org/neurai-explorer.git
cd neurai-explorer
# Optional: review credentials, ports and node settings before the first start
cp .env.example .env
# Build and start all services (the node image is pulled from Docker Hub)
docker compose up --build -d
# View logs
docker compose logs -f| Service | URL | Description |
|---|---|---|
| Explorer UI | http://localhost:3000 | Web interface |
| PostgreSQL | localhost:5432 | Database (internal) |
| Node RPC | node:19001 | Blockchain RPC (internal, Compose network only) |
All variables that docker-compose.yml reads from .env are listed with their
defaults and comments in .env.example: node image tag, shared
RPC credentials and capacity, node database cache, PostgreSQL credentials, and
the frontend host port. Copy it to .env and edit it before the first start;
.env is ignored by git.
The sections below describe the variables each service receives internally.
# RPC Connection
RPC_HOST=node
RPC_PORT=19001
RPC_USER=neuraiuser
RPC_PASS=neuraipassword
# Database
DATABASE_URL=postgres://user:pass@postgres:5432/neurai
# Logging
RUST_LOG=info,sqlx=warn,reqwest=warn
# Fetch blocks/previous transactions through the node's REST interface (default 1)
RPC_USE_REST=1
# Attempts per node request for transient failures (transport, HTTP 5xx/429)
# and base backoff between attempts (doubles, with jitter, honours Retry-After)
RPC_RETRIES=3
RPC_RETRY_DELAY_MS=200At startup the syncer waits for the node to answer RPC (it logs a
Node not ready, retrying line every few seconds while the node loads its
indexes or reindexes), so it can be started together with the node.
Sync tuning lives in syncer/config.json (rebuild the image after changing it):
| Option | Default | Meaning |
|---|---|---|
batchSize |
250 | Blocks fetched and written per database transaction |
blockFetchConcurrency |
16 | Concurrent block requests to the node |
inputFetchConcurrency |
32 | Concurrent previous-transaction requests |
prefetchBatches |
2 | Batches fetched ahead while the previous one is written |
asyncCommit |
true | Commit batches with synchronous_commit=off (a crash only re-syncs the last batches; the database never ends up inconsistent) |
supplyInterval |
600000 | ms between gettxoutsetinfo calls (scans the node's whole UTXO set) |
bulkModeThreshold |
20000 | Blocks behind the tip from which the syncer runs in bulk mode (see below); 0 disables |
indexBuildMem |
512MB | maintenance_work_mem used to rebuild the deferred indexes |
During the initial sync the log prints a Sync progress line every 10 s with
the current height, blocks/s, the share of time spent writing to PostgreSQL
(db_pct) and an ETA.
Bulk mode. While the database is more than bulkModeThreshold blocks
behind the tip, the syncer drops the secondary indexes whose keys are random
(idx_txaddr_address_time, idx_txaa_*_height, idx_asset_events_name,
idx_addr_balance, idx_addr_asset_bal) and pauses autovacuum on the big
tables — maintaining them insert by insert is most of the write cost of the
initial load and nothing queries them yet. Primary keys and the time /
block_height indexes stay. When it gets within the threshold it rebuilds
the indexes in one go (a few minutes at most), re-enables autovacuum and
analyzes the tables. What was dropped is recorded in sync_state.bulk_mode,
so a restart at any point resumes correctly. While bulk mode is on,
/api/status reports initialSync: true and the address, rich-list and asset
holder pages are slow (sequential scans); the rest of the explorer works.
-
Amounts are parsed exactly from the node's JSON (no
f64step), stored asNUMERICwith 8 decimals, and written back intoraw_dataand the API as decimal strings ("21000000000.12345678"), so JavaScript consumers keep satoshi precision.transactions.total_outputandtransactions.feeare computed exactly by the syncer. -
blocks.raw_dataholds the block header withtxas a list of txids; the decoded transactions are intransactions.raw_data(ordered bytx_index), and the serialized bytes intransactions.raw_hex. -
tx_addresses.received/sentandtx_address_assets.deltarecord what each address moved in each transaction, soaddresses/address_assetsbalances are sums of the history andaddresses.tx_countcounts distinct transactions. -
asset_eventskeeps every issuance/reissuance output; theassetsrow is the fold of them.
A syncer whose schema version differs from the one stored in the database
refuses to start; set RESYNC_ON_SCHEMA_CHANGE=1 for one start to wipe the
indexed data and resync from genesis.
Because balances are sums of the history rows, a chain reorg is undone exactly: the syncer subtracts the history of the orphaned blocks, deletes them, rebuilds the affected assets from their remaining events and resumes from the fork. The same operation is available by hand (for example after restoring an older node datadir):
docker compose run --rm syncer neurai-syncer --rollback 1234567 # undo blocks >= 1234567
docker compose up -d syncer # resync from there# Database (Prisma)
DATABASE_URL=postgres://user:pass@postgres:5432/neurai
# API Configuration
NEXT_PUBLIC_API_URL=http://localhost:3000/apiThe node runs the published neuraiproject/neurai-node image. Its
neurai.conf is generated by the image entrypoint from the NEURAI_*
environment variables set in docker-compose.yml, and only on the first start
of an empty node-data volume. The values that can be overridden from .env
(see .env.example) are:
NEURAI_IMAGE_TAG=v1.0.6 # Docker Hub tag to run
RPC_USER=neuraiuser # shared with syncer and frontend
RPC_PASS=neuraipassword
RPC_WORKQUEUE=1024
RPC_THREADS=64 # HTTP worker threads shared by RPC and REST
NODE_DB_CACHE=128 # MB; raise it for a faster initial syncTo change an existing node, edit /data/neurai.conf inside the node-data
volume and restart the node service:
docker compose exec node sh -c 'cat /data/neurai.conf'
docker compose restart nodeEnabling or disabling indexes on an already synced node requires reindexing
(docker compose run --rm node -reindex).
Earlier versions built the node from node/ and stored data in
/data/node inside the node-data volume. The published image stores data in
/data and runs as an unprivileged neurai user. To keep the existing chain
data, move it and fix ownership once, with the stack stopped:
docker compose down
docker run --rm --entrypoint sh -v neurai-explorer_node-data:/data \
neuraiproject/neurai-node:v1.0.6 -c \
'mv /data/node/* /data/ && rmdir /data/node && chown -R neurai:neurai /data'
docker compose up -dOtherwise remove the volume (docker volume rm neurai-explorer_node-data) and
let the node sync from scratch.
The layout of the indexed data has a version (sync_state.schema_version,
currently 4). When a new syncer image needs a different layout, it does not
try to convert the existing rows: it stops at startup with a message asking
for a resync. Rebuild the images and resync in one go:
git pull
docker compose build syncer frontend
docker compose stop syncer frontend
RESYNC_ON_SCHEMA_CHANGE=1 docker compose up -d syncer # wipes the indexed data, syncs from genesis
docker compose up -d frontend
docker compose logs -f syncer # "Sync progress" lines show blocks/s and ETARESYNC_ON_SCHEMA_CHANGE=1 is only honoured when the stored version differs,
and the syncer warns at every start while it is set, so it can also live in
.env; put it back to 0 once the resync has started. network_stats
(prices, peers) is kept; everything else is rebuilt from the node.
Removing the database volume instead (docker compose down,
docker volume rm neurai-explorer_pg-data, docker compose up -d) has the
same effect and also resets PostgreSQL itself.
The node is not touched by an explorer upgrade; only the explorer tables are re-read from it. With the batched syncer and REST fetching the full resync of mainnet is a matter of an hour or so, mostly bounded by the node.
Default Docker resource configuration (docker-compose.yml):
| Service | Memory Limit | Memory Reservation |
|---|---|---|
| Syncer | 12GB | 2GB |
| Frontend | - | - |
| PostgreSQL | - (tuned via PG_SHARED_BUFFERS / PG_EFFECTIVE_CACHE_SIZE) |
- |
| Node | - (NODE_DB_CACHE sets its database cache) |
- |
The syncer itself needs a few hundred MB during the initial sync (a couple of batches in flight plus the previous-outputs cache); the limit is generous.
docker compose downdocker compose up --build -d <service-name># All services
docker compose logs -f
# Specific service
docker compose logs -f neurai-syncerdocker compose exec neurai-postgres psql -U neuraiuser -d neuraineurai-explorer/
├── frontend/ # Next.js application
│ ├── src/app/ # App Router pages and /api routes
│ ├── src/components/ # React components
│ ├── src/lib/ # API client, exact-amount helpers (utils.ts)
│ ├── src/lib/services/ # DB queries shared by pages and API routes
│ ├── src/types/ # Shared TypeScript types (amounts are strings)
│ └── prisma/ # Prisma schema (mirrors the syncer's migrations)
├── syncer/ # Rust syncer service
│ ├── migrations/ # SQL migrations (applied at startup)
│ ├── src/
│ │ ├── main.rs # Entry point, --rollback command
│ │ ├── rpc/ # RPC/REST client, NodeClient trait
│ │ ├── sync/ # Engine (batches, reorgs), writer, rollback, stats
│ │ ├── db/ # Pool, schema guard, repositories
│ │ └── types/ # Node JSON types, Amount
│ └── Cargo.toml
├── docker-compose.yml # Service orchestration (node pulled from Docker Hub)
├── .env.example # Configurable variables with defaults
└── README.md
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

