Migrate to HyperIndex V3
This guide is a plain, step-by-step checklist of every change required to upgrade an existing HyperIndex V2 project to V3. For an overview of new V3 capabilities, see What's New in V3.
Follow the steps in order. Each step is independent enough to skim, but Step 0 (preparation on V2) is strongly recommended before you start touching V3 code.
Step 0: Prepare on V2 (Recommended)
Before upgrading to V3, prepare your project while still on V2:
-
Upgrade to
envio@2.32.6. -
Enable Preload Optimization in
config.yaml:preload_handlers: true -
If you were using loaders, migrate them to Preload Optimization following the Migrating from Loaders guide.
-
Verify your indexer still works with
pnpm dev.
Step 1: Update Node.js
Update Node.js to 22 or higher (24 is recommended). Earlier versions are no longer supported.
Step 2: Update package.json
-
Add
"type": "module"(required — without it the project will fail to start with ESM import errors). -
Set
engines.nodeto>=22.0.0. -
Update the
enviodependency to the latest v3 release. -
Remove the
optionalDependencies.generatedentry — the localgeneratedpackage no longer exists. Types are emitted to.envio/types.d.ts(git-ignored) and wired up via a smallenvio-env.d.tsfile at the project root. Everything previously imported fromgeneratedis now exported fromenvio.- "optionalDependencies": {
- "generated": "./generated"
- }, -
Update dev tooling:
{
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"envio": "3.0.0-rc.0"
},
"devDependencies": {
"@types/node": "24.12.2",
"typescript": "6.0.3",
"vitest": "4.1.0"
}
} -
If you used
ts-nodefor the start script, replace it withenvio start:{
"scripts": {
"start": "envio start"
}
}
Test runner
Option A — Migrate to Vitest (recommended).
pnpm remove ts-mocha ts-node mocha chai @types/mocha @types/chai
pnpm add -D vitest@4.0.16
{
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"vitest": "4.0.16"
}
}
Move tests from test/Test.ts to src/indexer.test.ts and update imports:
// Before (mocha/chai)
import { describe, it } from "mocha";
import { expect } from "chai";
// After (vitest)
import { describe, it, expect } from "vitest";
import { createTestIndexer } from "envio";
Option B — Keep Mocha. Replace ts-mocha/ts-node with tsx:
pnpm remove ts-mocha ts-node
pnpm add -D tsx@4.21.0
{
"scripts": {
"mocha": "tsc --noEmit && NODE_OPTIONS='--no-warnings --import tsx' mocha --exit test/**/*.ts"
}
}
Step 3: Update tsconfig.json
Update for ESM:
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"lib": ["es2022"],
"types": ["node"]
}
}
verbatimModuleSyntax and noUncheckedIndexedAccess are extra strictness. You can disable them to simplify the migration.
Step 4: Update config.yaml
Renames
networks→chainsconfirmed_block_threshold→max_reorg_depthrpc_config→rpc(now supports multiple URLs,for: sync | realtime | fallback, and WebSocket configuration)
# Before
networks:
- id: 1
contracts:
- name: MyContract
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
# After
chains:
- id: 1
contracts:
- name: MyContract
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
Removals
Remove these options if present:
unordered_multichain_mode— unordered is now the default. If you need ordered behavior, setmultichain: ordered.loaders— Preload Optimization is now always enabled.preload_handlers— now always enabled.preRegisterDynamicContracts— no longer needed.event_decoder— the Rust-based decoder is now the only implementation.output— generated types are always emitted to.envio/.
Replacements for environment variables
If you were using the MAX_BATCH_SIZE environment variable, switch to the config option:
full_batch_size: 5000
Optional: Automatic handler registration
Move handler files to src/handlers/ and remove the explicit handler paths from config.yaml. The explicit handler field still works if you'd rather not move files immediately.
Optional: ClickHouse storage
If using ClickHouse, add:
storage:
postgres: true
clickhouse: true
The connection environment variables (ENVIO_CLICKHOUSE_HOST, ENVIO_CLICKHOUSE_DATABASE, ENVIO_CLICKHOUSE_USERNAME, ENVIO_CLICKHOUSE_PASSWORD) are still required for envio start.
Step 5: Update Environment Variables
Add
If your indexer uses HyperSync (the default), set an API token:
-
Get a free API token at envio.dev/app/api-tokens.
-
Set it in your environment:
export ENVIO_API_TOKEN=your_token_hereOr in a local
.envfile:ENVIO_API_TOKEN=your_token_here
Remove
UNSTABLE__TEMP_UNORDERED_HEAD_MODEUNORDERED_MULTICHAIN_MODEMAX_BATCH_SIZE(usefull_batch_sizeinconfig.yamlinstead)ENVIO_INDEXING_BLOCK_LAG(use the per-chainblock_lagconfig option instead)
Rename
TUI_OFF=true→ENVIO_TUI=false(TUI is also auto-disabled in CI and under AI agents)ENVIO_PG_PUBLIC_SCHEMA→ENVIO_PG_SCHEMA(the old name is still supported until v4)
Step 6: Update Handler Code
All contract-specific handler exports have been removed. Register every handler through the unified indexer value imported from envio.
Migrate event handlers
// Before
import { ERC20 } from "generated";
ERC20.Transfer.handler(
async ({ event, context }) => {
// ...
},
{
wildcard: true,
eventFilters: ({ chainId }) => [
{ from: ZERO_ADDRESS, to: WHITELIST[chainId] },
],
}
);
// After
import { indexer } from "envio";
indexer.onEvent(
{
contract: "ERC20",
event: "Transfer",
wildcard: true,
where: ({ chain }) => ({
params: [{ from: ZERO_ADDRESS, to: WHITELIST[chain.id] }],
}),
},
async ({ event, context }) => {
// ...
},
);
Notes:
eventFiltersis renamed towhere.- The
wherecallback receives{ chain }(not{ chainId }) and must returnfalse,true, or{ params: [...], block?: { number: { _gte, _lte, _every } } }. - The previous array shorthand at the top level is no longer accepted — wrap it in
{ params: [...] }.
Migrate dynamic contract registration
// Before
UniV3.PoolFactory.contractRegister(async ({ event, context }) => {
context.addPool(event.params.poolAddress);
});
// After
import { indexer } from "envio";
indexer.contractRegister(
{ contract: "UniV3", event: "PoolFactory" },
async ({ event, context }) => {
context.chain.Pool.add(event.params.poolAddress);
},
);
context.add<ContractName>(address) becomes context.chain.<ContractName>.add(address).
Migrate block handlers
// Before
import { indexer, onBlock } from "generated";
indexer.chainIds.forEach((chainId) => {
onBlock(
{ name: "EveryBlock", chain: chainId },
async ({ block, context }) => {
// ...
},
);
});
// After
import { indexer } from "envio";
indexer.onBlock(
{ name: "EveryBlock" },
async ({ block, context }) => {
// ...
},
);
For chain-specific or interval handlers, return { block: { number: { _gte, _lte, _every } } } from where, or false to skip a chain. Inside a block handler, replace block.chainId with context.chain.id.
Update the getWhere API
Switch to the GraphQL-style filter syntax:
// Before
const transfers = await context.Transfer.getWhere.from.eq("0x123...");
const bigTransfers = await context.Transfer.getWhere.value.gt(1000n);
// After
const transfers = await context.Transfer.getWhere({ from: { _eq: "0x123..." } });
const bigTransfers = await context.Transfer.getWhere({ value: { _gt: 1000n } });
New operators are also available: _gte, _lte, _in.
Rename and removal cheat sheet
| V2 (removed) | V3 |
|---|---|
Contract.Event.handler(...) | indexer.onEvent({ contract, event, ...options }, handler) |
Contract.Event.contractRegister(...) | indexer.contractRegister({ contract, event }, handler) |
onBlock({ chain, ... }, handler) | indexer.onBlock({ name, where? }, handler) |
context.add<Contract>(addr) | context.chain.<Contract>.add(addr) |
eventFilters option | where callback returning { params: [...] } |
experimental_createEffect | createEffect |
block.chainId (in block handlers) | context.chain.id |
transaction.kind | transaction.type |
transaction.chainId | context.chain.id or event.chainId |
chain type | ChainId (now a union type) |
getGeneratedByChainId(...) | indexer.chains[chainId] |
Entity.getWhere.field.eq(value) | Entity.getWhere({ field: { _eq: value } }) |
Entity.getWhere.field.gt(value) | Entity.getWhere({ field: { _gt: value } }) |
Entity.getWhere.field.lt(value) | Entity.getWhere({ field: { _lt: value } }) |
Lowercased entity types (e.g. transfer) | Capitalized (Transfer) |
ERC20_Transfer_eventLog | EvmEvent<"ERC20", "Transfer"> |
ERC20_Transfer_block | EvmEvent<"ERC20", "Transfer">["block"] |
MyEnum (direct export) | Enum<"MyEnum"> |
MyEntity (direct export) | Entity<"MyEntity"> (preferred; direct still exported) |
Other type changes:
Addressis now`0x${string}`instead ofstring.- Entity array fields are typed as
readonly— update any code that mutates them. S.nullableschema type now returnsT | nullinstead ofT | undefined.- The internal
ContractTypeenum was removed.
Step 7: Update Tests
The MockDb testing API has been removed. Migrate to createTestIndexer() with simulate.
-import { TestHelpers, type User } from "generated";
-const { MockDb, Greeter, Addresses } = TestHelpers;
+import { createTestIndexer, type User, TestHelpers } from "envio";
+const { Addresses } = TestHelpers;
it("A NewGreeting event creates a User entity", async (t) => {
- const mockDbInitial = MockDb.createMockDb();
+ const indexer = createTestIndexer();
const userAddress = Addresses.defaultAddress;
const greeting = "Hi there";
- const mockNewGreetingEvent = Greeter.NewGreeting.createMockEvent({
- greeting: greeting,
- user: userAddress,
- });
-
- const updatedMockDb = await Greeter.NewGreeting.processEvent({
- event: mockNewGreetingEvent,
- mockDb: mockDbInitial,
- });
+ await indexer.process({
+ chains: {
+ 137: {
+ simulate: [
+ {
+ contract: "Greeter",
+ event: "NewGreeting",
+ params: { greeting, user: userAddress },
+ },
+ ],
+ },
+ },
+ });
const expectedUserEntity: User = {
id: userAddress,
latestGreeting: greeting,
numberOfGreetings: 1,
greetings: [greeting],
};
- const actualUserEntity = updatedMockDb.entities.User.get(userAddress);
+ const actualUserEntity = await indexer.User.getOrThrow(userAddress);
t.expect(actualUserEntity).toEqual(expectedUserEntity);
});
MockDb migration cheat sheet
Old (MockDb) | New (createTestIndexer) |
|---|---|
MockDb.createMockDb() | createTestIndexer() |
Contract.Event.createMockEvent({...}) | Inline in simulate: [{ contract, event, params }] |
Contract.Event.processEvent({event,mockDb}) | indexer.process({ chains: { id: { simulate } } }) |
mockDb.entities.Entity.get(id) | await indexer.Entity.getOrThrow(id) |
mockDb.entities.Entity.set({...}) | indexer.Entity.set({...}) |
| Manual handler threading & event chaining | Automatic — pass multiple events in the simulate array |
Step 8: Update CLI Usage
envio devno longer auto-resets the database. If you relied on this, runenvio dev -r(or--restart) explicitly.envio startis now production-only. Continue usingenvio devfor local development.- Changes in handler files no longer trigger codegen on
pnpm dev.
Step 9: Run Codegen and Verify
pnpm envio codegen
pnpm dev
Postgres column type changes (raw_events.event_id: NUMERIC → BIGINT, raw_events.serial: SERIAL → BIGSERIAL, envio_chains.events_processed: INTEGER → BIGINT, envio_checkpoints.id: INTEGER → BIGINT) are applied automatically — no action required. The deprecated envio_chains._num_batches_fetched column always returns 0.
Quick Migration Checklist
Prepare (on V2):
- Upgrade to
envio@2.32.6 - Enable
preload_handlers: trueinconfig.yaml - Migrate from loaders if applicable (guide)
- Verify indexer works with
pnpm dev
Dependencies:
- Update Node.js to
>=22 - Add
"type": "module"topackage.json← Required for V3 - Update
enviodependency to the latest v3 release - Remove
optionalDependencies.generatedfrompackage.json - Update
engines.nodeto>=22.0.0 - Update
tsconfig.jsonfor ESM support - Migrate from mocha/chai to vitest (recommended) or replace
ts-mocha/ts-nodewithtsx
config.yaml:
- Rename
networks→chains - Rename
confirmed_block_threshold→max_reorg_depth - Replace
rpc_configwithrpc - Remove
unordered_multichain_mode(now default) - Remove
loadersandpreload_handlers - Remove
preRegisterDynamicContracts - Remove
event_decoder - Remove
output(types always written to.envio/) - If using ClickHouse, add
storage: { postgres: true, clickhouse: true }
Environment variables:
- Set
ENVIO_API_TOKENif using HyperSync (get token) - Remove
UNSTABLE__TEMP_UNORDERED_HEAD_MODE - Remove
UNORDERED_MULTICHAIN_MODE - Remove
MAX_BATCH_SIZE(usefull_batch_size) - Remove
ENVIO_INDEXING_BLOCK_LAG(use per-chainblock_lag) - Rename
TUI_OFF=true→ENVIO_TUI=false - Rename
ENVIO_PG_PUBLIC_SCHEMA→ENVIO_PG_SCHEMA
Handler code:
- Migrate event handlers from
Contract.Event.handler(...)toindexer.onEvent({ contract, event, ...options }, handler) - Migrate dynamic contract registration to
indexer.contractRegister({ contract, event }, handler) - Replace
context.add<Contract>(addr)withcontext.chain.<Contract>.add(addr) - Convert
eventFilterstowherereturning{ params: [...] } - Migrate block handlers to a single
indexer.onBlockcall (usewherefor chain-specific or interval filters) - Use
where.block.number._gteto override per-event start blocks if needed - Replace
experimental_createEffectwithcreateEffect - Replace
block.chainIdwithcontext.chain.id - Replace
transaction.kindwithtransaction.type - Replace
transaction.chainIdwithcontext.chain.idorevent.chainId - Update
chaintype toChainId - Replace
getGeneratedByChainIdwithindexer.chains[chainId] - Update
Addressconsumers — type is now`0x${string}` - Replace lowercased entity imports with capitalized versions (e.g.
transfer→Transfer) - Update
getWherecalls to GraphQL-style filter syntax - Update any
S.nullableusage — now returnsnullinstead ofundefined - Replace contract-specific type exports with generics (
EvmEvent<"ERC20", "Transfer">)
Tests:
- Migrate from
MockDbtocreateTestIndexer()
CLI:
- Use
envio dev -rif you relied onenvio devresetting the DB automatically - Use
envio devfor local development (envio startis production-only)
Verify:
- Run
pnpm envio codegenandpnpm dev
Getting Help
If you encounter any issues during migration, join our Discord community for support.