Migrate from Ponder to HyperIndex
Need help? Reach out on Discord for personalized migration assistance.
Migrating from Ponder to HyperIndex is straightforward — both frameworks use TypeScript, index EVM events, and expose a GraphQL API. The key differences are the config format, schema syntax, and entity operation API.
If you are new to HyperIndex, start with the Quickstart guide first.
For an assistant-led workflow, see How to Migrate Using AI, which includes a shared process for Cursor and Claude Code.
Why Migrate to HyperIndex?
- Up to 158x faster historical sync via HyperSync
- Multichain by default — index any number of chains in one config
- Same language — your TypeScript logic transfers directly
Migration Overview
Migration has three steps:
ponder.config.ts→config.yamlponder.schema.ts→schema.graphql- Event handlers — adapt syntax and entity operations
At any point, run:
pnpm envio codegen # validate config + schema, regenerate types
pnpm dev # run the indexer locally
Step 0: Bootstrap the Project
pnpx envio@3.0.0-rc.0 init
This generates a config.yaml, a starter schema.graphql, and handler stubs. Use your Ponder project as the source of truth for contract addresses, ABIs, and events, then fill in the generated files.
Step 1: ponder.config.ts → config.yaml
Ponder
import { createConfig } from "ponder";
export default createConfig({
chains: {
mainnet: { id: 1, rpc: process.env.PONDER_RPC_URL_1 },
},
contracts: {
MyToken: {
abi: myTokenAbi,
chain: "mainnet",
address: "0xabc...",
startBlock: 18000000,
},
},
});
HyperIndex (v3)
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: my-indexer
contracts:
- name: MyToken
abi_file_path: ./abis/MyToken.json
events:
- event: Transfer
- event: Approval
chains:
- id: 1
start_block: 0
contracts:
- name: MyToken
address:
- 0xabc...
start_block: 18000000
v2 note: HyperIndex v2 uses
networksinstead ofchains. See the v2→v3 migration guide.
Key differences:
| Concept | Ponder | HyperIndex |
|---|---|---|
| Config format | ponder.config.ts (TypeScript) | config.yaml (YAML) |
| Chain reference | Named + viem object | Numeric chain ID |
| RPC URL | In config | RPC_URL_<chainId> env var |
| ABI source | TypeScript import | JSON file (abi_file_path) |
| Events to index | Inferred from handlers | Explicit events: list |
| Handler file | Inferred | Explicit handler: per contract |
Convert your ABI: Ponder uses TypeScript ABI exports (as const). HyperIndex needs a plain JSON file in abis/. Strip the export const ... = wrapper and as const and save as .json.
Field selection — accessing transaction and block fields
By default, only a minimal set of fields is available on event.transaction and event.block. Fields like event.transaction.hash are undefined unless explicitly requested.
events:
- event: Transfer
field_selection:
transaction_fields:
- hash
Or declare once at the top level to apply to all events:
name: my-indexer
field_selection:
transaction_fields:
- hash
contracts:
# ...
See the full list of available fields in the Configuration File docs.
Step 2: ponder.schema.ts → schema.graphql
Ponder
import { onchainTable, primaryKey, index } from "ponder";
export const token = onchainTable("token", (t) => ({
address: t.hex().primaryKey(),
symbol: t.text().notNull(),
balance: t.bigint().notNull(),
}));
export const transferEvent = onchainTable(
"transfer_event",
(t) => ({
id: t.text().primaryKey(),
from: t.hex().notNull(),
to: t.hex().notNull(),
amount: t.bigint().notNull(),
timestamp: t.integer().notNull(),
}),
(table) => ({
fromIdx: index().on(table.from),
}),
);
HyperIndex
type Token {
id: ID!
symbol: String!
balance: BigInt!
}
type TransferEvent {
id: ID!
from: String! @index
to: String!
amount: BigInt!
timestamp: Int!
}
Type mapping:
| Ponder | HyperIndex GraphQL |
|---|---|
t.hex() | String! |
t.text() | String! |
t.bigint() | BigInt! |
t.integer() | Int! |
t.boolean() | Boolean! |
t.real() / t.doublePrecision() | Float! |
t.hex().array() | Json! |
Primary keys: HyperIndex requires a single id: ID! string field on every entity. For composite PKs (e.g. owner + spender), construct the ID string manually: `${owner}_${spender}`.
Indexes: Replace index().on(column) with an @index directive on the field.
Relations: Replace Ponder's relations() call with @derivedFrom on the parent entity:
type Token {
id: ID!
transfers: [TransferEvent!]! @derivedFrom(field: "token_id")
}
See the full Schema docs.
Step 3: Event Handlers
Handler registration
Ponder
import { ponder } from "ponder:registry";
ponder.on("MyToken:Transfer", async ({ event, context }) => {
// ...
});
HyperIndex
import { MyToken } from "generated";
MyToken.Transfer.handler(async ({ event, context }) => {
// ...
});
Event data access
| Data | Ponder | HyperIndex |
|---|---|---|
| Event parameters | event.args.name | event.params.name |
| Contract address | event.log.address | event.srcAddress |
| Chain ID | context.chain.id | event.chainId |
| Block number | event.block.number | event.block.number |
| Block timestamp | event.block.timestamp (bigint) | event.block.timestamp (number) |
| Tx hash | event.transaction.hash | event.transaction.hash ⚠️ needs field_selection |
Entity operations
| Intent | Ponder | HyperIndex |
|---|---|---|
| Insert | context.db.insert(t).values({...}) | context.Entity.set({ id, ...fields }) |
| Update | context.db.update(t, pk).set({...}) | get → spread → context.Entity.set({ ...existing, ...changes }) |
| Upsert | .insert().values().onConflictDoUpdate() | context.Entity.getOrCreate({ id, ...defaults }) → set |
| Read (nullable) | context.db.find(table, pk) | context.Entity.get(id) |
| Read (throws) | manual null check | context.Entity.getOrThrow(id) |
Full handler example
Ponder
ponder.on("MyToken:Transfer", async ({ event, context }) => {
await context.db.insert(transferEvent).values({
id: event.id,
from: event.args.from,
to: event.args.to,
amount: event.args.amount,
timestamp: Number(event.block.timestamp),
});
await context.db
.update(token, { address: event.args.to })
.set((row) => ({ balance: row.balance + event.args.amount }));
});
HyperIndex
import { MyToken } from "generated";
MyToken.Transfer.handler(async ({ event, context }) => {
context.TransferEvent.set({
id: `${event.transaction.hash}_${event.logIndex}`,
from: event.params.from,
to: event.params.to,
amount: event.params.amount,
timestamp: event.block.timestamp,
});
const token = await context.Token.getOrThrow(event.params.to);
context.Token.set({
...token,
balance: token.balance + event.params.amount,
});
});
Important: Entity objects from
context.Entity.get()are read-only. Always spread (...existing) and set new fields — never mutate directly.
See the Event Handlers docs for the full API reference.
Extra Tips
Factory contracts (dynamic registration)
Replace Ponder's factory() helper in config with a contractRegister handler:
import { MyFactory } from "generated";
// Registers each newly deployed contract for indexing
MyFactory.ContractCreated.contractRegister(({ event, context }) => {
context.addMyContract(event.params.contractAddress);
});
In config.yaml, omit the address field for the dynamically registered contract.
External calls
Replace context.client.readContract(...) with the Effect API to safely isolate external calls from the sync path:
import { createEffect, S } from "envio";
export const getSymbol = createEffect(
{
name: "getSymbol",
input: S.schema({ address: S.string, chainId: S.number }),
output: S.string,
cache: true,
},
async ({ input }) => {
/* viem call here */
},
);
// In handler:
const symbol = await context.effect(getSymbol, {
address,
chainId: event.chainId,
});
Multichain
Add multiple entries under chains: and namespace your entity IDs by chain to prevent collisions:
const id = `${event.chainId}_${event.params.tokenId}`;
See Multichain Indexing for configuration details.
Wildcard indexing
HyperIndex supports wildcard indexing — index events by signature across all contracts on a chain without specifying addresses.
Validating Your Migration
Use the Indexer Migration Validator CLI to compare entity data between your Ponder and HyperIndex endpoints field-by-field.
Getting Help
- Discord: discord.gg/envio — fastest way to get help
- Docs: docs.envio.dev
- AI-friendly docs: HyperIndex complete reference