DubheSuiIndexer

Dubhe Rust Indexer

dubhe-indexer is a high-performance Rust binary that listens to Sui blockchain events emitted by your Dubhe contract and writes them into a PostgreSQL database. It exposes a built-in GraphQL endpoint and a gRPC endpoint for querying indexed data in real time.


How it works

The indexer is event-driven — it processes the events emitted by the Dubhe framework and writes them into PostgreSQL. All Dubhe events are emitted by the framework package (not your DApp package); the indexer keeps only those whose dapp_key matches your config, and for resource tables, only those listed in your dubhe.config.json.

In v2 the indexer handles far more than the three core Store events — it also tracks the extended storage (objects/scenes), scene permits, the marketplace, session keys, and DApp runtime/fee/revenue state. The full event → table mapping:

Event familyEventsDestination table(s)
Store (records)StoreSetRecord, StoreSetField, StoreDeleteRecord, StoreDeleteFieldyour resource tables (player, stats, …)
User storageUserStorageCreateduser_storages
Object storageObjectCreated, ObjectDestroyed, ObjectSetField, ObjectDeleteFieldobject_storages, object_storage_fields
Scene storageSceneCreated, SceneDestroyed, SceneSetField, SceneDeleteFieldscene_storages, scene_storage_fields
Scene permitsScenePermitCreated, ScenePermitAccept, ScenePermitJoin, ScenePermitLeave, ScenePermitExpirescene_permits, scene_permit_participants
MarketplaceItemListed, ItemSold, ListingCancelled, ListingExpiredmarketplace_listings
Session keysSessionActivated, SessionDeactivatedsessions
DApp runtimeDappCreated, DappUpgraded, CreditRecharged, DappPausedChanged, DappRuntimeUpdate, DappRevenueShareSetdapp_runtime_state
Fees / revenueMarketplaceFeeSettled, DappFeeStateUpdated, DappRevenueStateUpdateddapp_marketplace_fees, dapp_fee_state, dapp_revenue_state

Your own resource tables still require a matching entry in dubhe.config.json. The framework system tables above are created and populated automatically — no config needed.

Important: The indexer can only process events it has seen. If start_checkpoint in dubhe.config.json is set later than your deployment checkpoint, events before that point are permanently missed and will not appear in the database.


Installation

Install dubhe-indexer using the Dubhe CLI:

pnpm dubhe doctor

The doctor command checks your environment and installs required binaries, including dubhe-indexer. You can also download the binary directly from GitHub releases.


Prerequisites

  1. PostgreSQL — the indexer stores all indexed data in PostgreSQL.
  2. dubhe.config.json — generated from dubhe.config.ts via dubhe convert-json.
  3. A running Sui node or access to a remote checkpoint store (testnet/mainnet).

Configuration file

Generate dubhe.config.json from your TypeScript config before starting the indexer:

dubhe convert-json --config-path dubhe.config.ts --output-path dubhe.config.json

The JSON file must include start_checkpoint — the checkpoint from which the indexer begins reading. Set it to the checkpoint at or just before your contract was deployed:

{
  "name": "my_game",
  "description": "My on-chain game",
  "packageId": "0x123...",
  "originalPackageId": "0x123...",
  "start_checkpoint": "1000000",
  "resources": {
    "level": "u32",
    "stats": {
      "fields": { "attack": "u32", "hp": "u32" }
    }
  }
}

start_checkpoint is read from the config file. The --first-checkpoint CLI argument is present in --help output but is not used by the current binary — always set it in dubhe.config.json.


CLI reference

dubhe-indexer [OPTIONS]

Options:
  -c, --config-json <PATH>       Path to dubhe.config.json         [default: dubhe.config.json]
      --database-url <URL>       PostgreSQL connection string       [default: postgres://postgres@localhost:5432/postgres]
      --checkpoint-url <URL>     Checkpoint source: local directory (e.g. .chk) or
                                 remote http URL (e.g. https://checkpoints.testnet.sui.io)
                                                                    [default: .chk]
      --use-rpc-ingestion        Use gRPC to fetch checkpoints from --rpc-url instead
                                 of local path / remote store       [default: false]
      --rpc-url <URL>            gRPC endpoint (used with --use-rpc-ingestion)
                                                                    [default: http://localhost:9000]
      --port <PORT>              Proxy server port (GraphQL + gRPC via proxy, health, playground)
                                                                    [default: 8080]
      --grpc-port <PORT>         Direct gRPC backend port           [default: 8085]
      --graphql-port <PORT>      Independent GraphQL backend port   [default: 8089]
      --force                    Clear the database before starting [default: false]

Service ports

When the indexer starts, it launches three services:

ServiceDefault portDescription
Proxy server8080Serves /graphql, /playground, /health, /metadata, /welcome; forwards gRPC requests
gRPC backend8085Direct gRPC endpoint for DubheGrpcClient
GraphQL backend8089Independent PostGraphile GraphQL service

For most use cases, connect your client to the proxy at port 8080:

  • GraphQL queries: http://localhost:8080/graphql
  • GraphQL playground: http://localhost:8080/playground
  • gRPC (via proxy): http://localhost:8080 (prefix /dubhe_grpc.*)
  • Direct gRPC: http://localhost:8085

--grpc-port and --graphql-port must be different values. The binary exits immediately if they match.


Checkpoint sources

The --checkpoint-url argument controls where the indexer reads checkpoints from:

ValueSourceWhen to use
.chk (default)Local directoryLocalnet — the directory where dubhe node writes checkpoint files
https://checkpoints.testnet.sui.ioRemote storeTestnet
https://checkpoints.mainnet.sui.ioRemote storeMainnet
--use-rpc-ingestion + --rpc-urlgRPC from a full nodeWhen you have a full node with gRPC checkpoint API

Localnet

Use pnpm dubhe node to start the local Sui node. It writes checkpoint files to .chk by default, and the indexer reads from .chk by default — no extra flags needed:

# Terminal 1: start local node (writes checkpoints to .chk)
pnpm dubhe node
 
# Terminal 2: start indexer (reads from .chk by default)
dubhe-indexer \
  --config-json dubhe.config.json \
  --database-url postgres://postgres:postgres@localhost:5432/postgres

To clear the database and start fresh (useful after redeploying):

dubhe-indexer \
  --config-json dubhe.config.json \
  --database-url postgres://postgres:postgres@localhost:5432/postgres \
  --force

Testnet / Mainnet

Pass the remote checkpoint store URL via --checkpoint-url:

# Testnet
dubhe-indexer \
  --config-json dubhe.config.json \
  --checkpoint-url https://checkpoints.testnet.sui.io \
  --database-url postgres://user:password@localhost:5432/dubhe_prod
 
# Mainnet
dubhe-indexer \
  --config-json dubhe.config.json \
  --checkpoint-url https://checkpoints.mainnet.sui.io \
  --database-url postgres://user:password@localhost:5432/dubhe_prod

Framework system tables

Beyond your own resource tables, the indexer auto-creates and maintains a set of framework system tables. These hold the extended storage, marketplace, session, and DApp runtime state, and are queryable just like resource tables. In GraphQL they are exposed under dubhe-prefixed camelCase plural field names (via PostGraphile), and most have a dedicated typed helper on the GraphQL client.

PostgreSQL tableGraphQL fieldClient helperHolds
user_storagesdubheUserStoragesgetDubheUserStoragesper-user registrations (canonical owner, session key, …)
object_storagesdubheObjectStoragesgetObjectStoragesObjectStorage<T> named entities (guilds, bosses)
object_storage_fieldsdubheObjectStorageFieldsobject field values
scene_storagesdubheSceneStoragesgetSceneStoragesSceneStorage<T> scene metadata (PvP matches, dungeons)
scene_storage_fieldsdubheSceneStorageFieldsgetSceneStorageFieldsscene field values
scene_permitsdubheScenePermitsScenePermit<T> objects
scene_permit_participantsdubheScenePermitParticipantspermit participants
marketplace_listingsdubheMarketplaceListingsgetMarketplaceListingsactive/closed marketplace listings
sessionsdubheSessionsgetDubheSessionsactive/expired session keys
dapp_runtime_statedubheDappRuntimeStatesadmin, version, credit pool, paused/suspended flags
dapp_marketplace_feesdubheDappMarketplaceFeessettled marketplace fees
dapp_fee_statedubheDappFeeStatewrite-fee parameters and settled totals
dapp_revenue_statedubheDappRevenueStateaccrued DApp revenue

Tables without a dedicated helper are still queryable with getAllTables('<dubheGraphqlField>', …). See GraphQL Client for the typed helpers.


Querying indexed data

GraphQL client

import { createDubheGraphqlClient } from '@0xobelisk/graphql-client';
 
const client = createDubheGraphqlClient({
  endpoint: 'http://localhost:8080/graphql',
  subscriptionEndpoint: 'ws://localhost:8080/graphql'
});
 
// Your own resource table
const players = await client.getAllTables('player', {
  filter: { hp: { greaterThan: '50' } },
  orderBy: [{ field: 'hp', direction: 'DESC' }],
  first: 10
});
 
// Framework system tables — typed helpers
const scenes = await client.getSceneStorages({ dappKey, sceneType: 'pvp_match', first: 50 });
const guilds = await client.getObjectStorages({ dappKey, objectType: 'guild' });
 
// Or any system table generically by its GraphQL field name
const runtime = await client.getAllTables('dubheDappRuntimeState', { first: 1 });

gRPC client

import { DubheGrpcClient } from '@0xobelisk/grpc-client';
 
const grpc = new DubheGrpcClient({ baseUrl: 'http://localhost:8085' });
 
const result = await grpc.dubheGrpcClient.queryTable({
  tableId: 'player',
  filters: [],
  pagination: { page: 0, pageSize: 20 }
});

Via createClient

If you use @0xobelisk/client, both clients are created automatically:

import { createClient } from '@0xobelisk/client/sui';
 
const { graphqlClient, grpcClient } = createClient({
  network: 'localnet',
  packageId: PackageId,
  metadata,
  endpoints: {
    graphql: 'http://localhost:8080/graphql',
    grpc: 'http://localhost:8085'
  }
});

Monitoring and troubleshooting

Logging

# Info level (recommended)
RUST_LOG=info dubhe-indexer --config-json dubhe.config.json --database-url ...
 
# Debug level (verbose)
RUST_LOG=debug dubhe-indexer --config-json dubhe.config.json --database-url ...

Common issues

No data indexed, no errors

  • Check that start_checkpoint in dubhe.config.json is at or before the checkpoint of your contract deployment.
  • Check that originalPackageId matches the package ID from your first deployment (not an upgraded package ID).
  • Check that resource names in dubhe.config.json match the table names emitted by the contract.

Database connection refused

  • Verify PostgreSQL is running: pg_isready -h localhost.
  • Pass the connection string via --database-url, not via an environment variable — the indexer binary reads only the CLI argument.

Checkpoints not arriving (localnet)

  • Confirm pnpm dubhe node is running and writing to the .chk directory.
  • If you restarted the node, run with --force to clear stale data.

Production deployment

Docker

FROM ubuntu:22.04
 
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
 
COPY dubhe-indexer /usr/local/bin/dubhe-indexer
RUN chmod +x /usr/local/bin/dubhe-indexer
 
COPY dubhe.config.json /app/dubhe.config.json
WORKDIR /app
 
CMD ["dubhe-indexer", \
     "--config-json", "dubhe.config.json", \
     "--checkpoint-url", "https://checkpoints.mainnet.sui.io", \
     "--database-url", "postgres://user:password@db-host:5432/dubhe_prod", \
     "--port", "8080"]

Logging in production

RUST_LOG=info dubhe-indexer \
  --config-json dubhe.config.json \
  --checkpoint-url https://checkpoints.mainnet.sui.io \
  --database-url postgres://user:password@db-host:5432/dubhe_prod