Skip to content

Running the Indexer

View as Markdown

Use this page when the stream definitions are ready and you need to wire the runtime, database, migrations, and process shape.

The typical Thru indexing stack looks like this:

Chain RPC -> @thru/replay ChainClient -> @thru/indexer runtime -> Postgres tables
|
v
app-owned queries/routes

The current @thru/indexer runtime assumes:

  • PostgreSQL-backed tables generated through Drizzle
  • a Drizzle database client passed as db
  • a clientFactory that returns a ChainClient from @thru/replay
  • Drizzle Kit or equivalent migration management
  • a standalone Node service to run the indexer continuously

If you are not using Postgres plus Drizzle, start with the package reference for @thru/indexer and validate whether the current runtime fits your stack.

import { Indexer } from "@thru/indexer";
import { ChainClient } from "@thru/replay";
import { db } from "./db";
import tokenAccounts from "./account-streams/token-accounts";
import tokenTransfers from "./streams/token-transfers";
export function createIndexer() {
return new Indexer({
db,
clientFactory: () => new ChainClient({ baseUrl: process.env.CHAIN_RPC_URL! }),
eventStreams: [tokenTransfers],
accountStreams: [tokenAccounts],
defaultStartSlot: 0n,
safetyMargin: 64,
pageSize: 512,
logLevel: "info",
});
}
OptionWhat it controls
dbThe Drizzle client used for inserts, updates, and checkpoints.
clientFactoryFresh replay client creation for backfill and live streaming.
eventStreamsAppend-only streams for event rows.
accountStreamsCurrent-state streams for account rows.
defaultStartSlotStarting slot when no checkpoint exists yet.
safetyMarginHow far behind the live tip replay should stay during backfill-to-live switchover.
pageSizeHow many records to request per backfill page.
logLevelRuntime verbosity.

Your Drizzle schema needs the checkpoint table plus every stream table.

export { checkpointTable } from "@thru/indexer";
export { tokenAccountsTable } from "./account-streams/token-accounts";
export { tokenTransferEvents } from "./streams/token-transfers";

Without checkpointTable, the runtime cannot resume safely after restarts.

In practice, most apps run the indexer as its own long-lived service:

  1. load environment and connect to Postgres
  2. run or verify migrations
  3. build the Indexer
  4. call await indexer.start()
  5. stop gracefully on SIGTERM or SIGINT
const indexer = createIndexer();
process.on("SIGINT", () => indexer.stop());
process.on("SIGTERM", () => indexer.stop());
await indexer.start();

The indexer writes standard Drizzle tables. Query those tables directly from your backend or expose routes owned by your application.

import { desc } from "drizzle-orm";
import { db } from "./db";
import { tokenTransferEvents } from "./schema";
export async function listRecentTransfers(limit = 50) {
return await db
.select()
.from(tokenTransferEvents)
.orderBy(desc(tokenTransferEvents.slot))
.limit(limit);
}