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 Getting Started guide first.
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 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
handler: ./src/EventHandlers.ts
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