Migrate to HyperIndex V3
15 full months have passed since the official HyperIndex v2.0.0. Since then, we have shipped 32 minor releases and multiple patches with zero breaking changes to the documented API. We also received PRs from 6 external contributors, grew from 1 GitHub star to over 470, and saw many big projects rely on HyperIndex.
HyperIndex V3 focuses on modernizing the codebase and laying the foundation for many more months of development. This guide walks you through upgrading from V2 to V3.
New Features
Unified Handlers API
In V3 all handler registrations now happen through a single indexer value. Contract-specific exports (ERC20.Transfer.handler, UniV3.PoolFactory.contractRegister, etc.) have been removed in favor of indexer.onEvent, indexer.contractRegister, and indexer.onBlock.
Event handlers with indexer.onEvent:
import { indexer } from "envio";
indexer.onEvent(
{
contract: "ERC20",
event: "Transfer",
wildcard: true,
where: ({ chain }) => ({
params: [
{ from: chain.Safe.addresses },
{ to: chain.Safe.addresses },
],
}),
},
async ({ event, context }) => {
// Handler logic
},
);
Dynamic contracts with indexer.contractRegister:
import { indexer } from "envio";
indexer.contractRegister(
{
contract: "UniV3",
event: "PoolFactory",
},
async ({ event, context }) => {
context.chain.Pool.add(event.params.poolAddress);
},
);
Block handlers with indexer.onBlock consolidate across chains in a single call:
import { indexer } from "envio";
indexer.onBlock(
{ name: "EveryBlock" },
async ({ block, context }) => {
// Handler logic
},
);
For chain-specific or interval-based block handlers, use the where callback:
indexer.onBlock(
{
name: "Ranges",
where: ({ chain }) => {
if (chain.id !== 1) return false;
return {
block: {
number: {
_gte: 20_000_000,
_lte: 22_000_000,
_every: 100,
},
},
};
},
},
async ({ block, context }) => {
// Handler logic
},
);
Per-Event Start Block
Handlers can specify custom start blocks per chain via where.block.number._gte, overriding contract and chain configuration:
indexer.onEvent(
{
contract: "UniV4",
event: "Pool",
where: ({ chain }) => {
let startBlock: number;
switch (chain.id) {
case 1:
startBlock = 18_000_000;
break;
case 8453:
startBlock = 2_000_000;
break;
default: {
const _exhaustive: never = chain.id;
return false;
}
}
return {
block: { number: { _gte: startBlock } },
};
},
},
async ({ event, context }) => {
// Handler logic
},
);
CommonJS → ESM
We migrated HyperIndex from CommonJS-only to ESM-only. This enables:
- Using the latest versions of libraries that have long since abandoned CommonJS support
- Top-level await in handler files
Top-Level Await
Thanks to the migration to ESM, you can now use await directly in handler and other files:
import { indexer } from "envio";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
// Load data before registering handlers
const addressesFromServer = await loadWhitelistedAddresses();
indexer.onEvent(
{
contract: "ERC20",
event: "Transfer",
wildcard: true,
where: {
params: [
{ from: ZERO_ADDRESS, to: addressesFromServer },
{ from: addressesFromServer, to: ZERO_ADDRESS },
],
},
},
async ({ event, context }) => {
// ... your handler logic
},
);
3x Historical Backfill Performance
Achieved by adding chunking logic to request events across multiple ranges at once. This also fixed overfetching for contracts with a much later start_block in the config, as well as speeding up dynamic contract registration. If you had data fetching as a bottleneck, 25k events per second is now a standard.
Automatic Handler Registration (src/handlers)
We introduced automatic registration of handler files located in src/handlers.
Previously, you needed to specify an explicit path to a handler file for every contract in config.yaml. Now you can remove all of the paths from config.yaml and simply move the files to src/handlers. You can name the files however you want, but we suggest using contract names and having a file per contract.
If you don't like src/handlers, use the handlers option in config.yaml to customize it.
The explicit handler field in config.yaml still works, so you don't need to change anything immediately.
RPC for Realtime Indexing
Built by an external contributor @cairoeth to allow specifying realtime mode for an RPC data source to embrace low-latency head tracking:
rpc:
- url: https://eth-mainnet.your-rpc-provider.com
for: realtime
In this case, the RPC won't be used for historical sync but will be used as the primary source once the indexer enters realtime mode.
Chain State on Context
The Handler Context object provides chain state via the chain property:
import { indexer } from "envio";
indexer.onEvent(
{ contract: "ERC20", event: "Approval" },
async ({ context }) => {
console.log(context.chain.id); // 1 - The chain id of the event
console.log(context.chain.isRealtime); // true - Whether the indexer entered realtime mode
},
);
Indexer State & Config
As a replacement for the deprecated and removed getGeneratedByChainId, we introduce the indexer value. It provides nicely typed chains and contract data from your config, as well as the current indexing state, such as isRealtime and addresses. Use indexer either at the top level of the file or directly from handlers. It returns the latest indexer state.
With this change, we also introduce new official types: Indexer, EvmChainId, FuelChainId, and SvmChainId.
import { indexer } from "envio";
indexer.name; // "uniswap-v4-indexer"
indexer.description; // "Uniswap v4 indexer"
indexer.chainIds; // [1, 42161, 10, 8453, 137, 56]
indexer.chains[1].id; // 1
indexer.chains[1].startBlock; // 0
indexer.chains[1].endBlock; // undefined
indexer.chains[1].isRealtime; // false
indexer.chains[1].PoolManager.name; // "PoolManager"
indexer.chains[1].PoolManager.abi; // unknown[]
indexer.chains[1].PoolManager.addresses; // ["0x000000000004444c5dc75cB358380D2e3dE08A90"]
On indexer restart, reading indexer at the top level of a handler file returns values restored from the database — including dynamically registered contract addresses — rather than only what's declared in config.yaml:
import { indexer } from "envio";
// Includes initial + dynamically registered addresses persisted in the DB
console.log(indexer.chains.eth.Pool.addresses);
Conditional Event Handlers
Now it's possible to return a boolean value from the where function to disable or enable the handler conditionally.
import { indexer } from "envio";
indexer.onEvent(
{
contract: "ERC20",
event: "Transfer",
wildcard: true,
where: ({ chain }) => {
// Skip all ERC20 on Polygon
if (chain.id === 137) {
return false;
}
// Track all ERC20 on Ethereum Mainnet
if (chain.id === 1) {
return true;
}
// Track only whitelisted addresses on other chains
return {
params: [
{ from: ZERO_ADDRESS, to: WHITELISTED_ADDRESSES[chain.id] },
{ from: WHITELISTED_ADDRESSES[chain.id], to: ZERO_ADDRESS },
],
};
},
},
async ({ event, context }) => {
// ... your handler logic
},
);
Automatic Contract Configuration
Started automatically configuring all globally defined contracts. This fixes an issue where addContract crashed because the contract was defined globally but not linked for a specific chain. Now it's done automatically:
contracts:
- name: UniswapV3Factory
events: # ...
- name: UniswapV3Pool
events: # ...
chains:
- id: 1
start_block: 0
contracts:
- name: UniswapV3Factory
address: 0x1F98431c8aD98523631AE4a59f267346ea31F984
# UniswapV3Pool no longer needed here - auto-configured from global contracts
- id: 10
start_block: 0
contracts:
- name: UniswapV3Factory
address: 0x1F98431c8aD98523631AE4a59f267346ea31F984
# UniswapV3Pool no longer needed here - auto-configured from global contracts
ClickHouse Storage (Experimental)
HyperIndex can now run with multiple storage backends at the same time. Postgres remains the primary database, and entities can additionally be written to a ClickHouse database that is restart- and reorg-resistant. Prometheus metrics carry a storage-name label so you can distinguish backends.
Enable both backends in config.yaml:
storage:
postgres: true
clickhouse: true
envio dev automatically spins up a ClickHouse Docker container for local development. For envio start, provide your own connection via the environment variables ENVIO_CLICKHOUSE_HOST, ENVIO_CLICKHOUSE_DATABASE, ENVIO_CLICKHOUSE_USERNAME, and ENVIO_CLICKHOUSE_PASSWORD. Currently supported only on Dedicated Plan.
Do not run multiple indexers writing to the same ClickHouse database at the same time.
HyperSync Source Improvements
Multiple updates on the HyperSync side to achieve smaller latency and less traffic:
- Server-Sent Events instead of polling to get updates about new blocks
- CapnProto instead of JSON for query serialization
- Cache for queries with repetitive filters - huge egress saving when indexing thousands of addresses
- Improved connection establishment behind a proxy
- Configurable log level support via
ENVIO_HYPERSYNC_LOG_LEVELenvironment variable - Automatic rate-limiting handling on the client side
- Better reconnection logic, logging, and fallbacks for HyperSync SSE and RPC WebSocket height streaming for more stable indexing at the chain head
Fuel Block Handler Support
Block handlers are now supported for Fuel indexing.
Solana Support (Experimental)
HyperIndex now supports Solana with RPC as a source. This feature is experimental and may undergo minor breaking changes. Solana exposes its block-stream handler as indexer.onSlot (rather than onBlock) to match Solana's slot-based model.
To initialize a Solana project:
pnpx envio@3.0.0-rc.0 init svm
See the Solana documentation for more details.
pnpx envio init Improvements
- Removed language selection to prefer TypeScript by default
- Cleaned up templates to follow the latest good practices
- Added new templates to highlight HyperIndex features:
Feature: Factory Contract,Feature: External Calls - Pre-configured GitHub Actions workflow for running tests and initialized git repository
- Generated projects include Cursor/Claude skills to support agent-driven development
Block Handler Only Indexers
Now it's possible to create indexers with only block handlers. Previously, it was required to have at least one event handler for it to work. The contracts field became optional in config.yaml.
Flexible Entity Fields
We no longer have restrictions on entity field names, such as type and others. Shape your entities any way you want. There are also improvements in generating database columns in the same order as they are defined in the schema.graphql.
Unordered Multichain Mode by Default
Unordered multichain mode is now available and the default behavior. This provides better performance for most use cases. If you need ordered multichain behavior, you can explicitly set multichain: ordered in your config.
Preload Optimization by Default
Preload optimization is now enabled by default, replacing the previous loaders and preload_handlers options. This improves historical sync performance automatically.
TUI Improvements
We gave our TUI some love, making it look more beautiful and compact. It also consumes fewer resources, shares a link to the Hasura playground, and dynamically adjusts to the terminal width.
The TUI is now auto-disabled in CI environments and when running under AI agents, so logs stay clean without manual configuration. The legacy TUI_OFF=true environment variable was renamed to ENVIO_TUI=false.

New Testing Framework
HyperIndex ships a purpose-built testing framework powered by createTestIndexer(). Write tests against the same indexer that runs in production — no database, no Docker, no manual mock wiring.
The framework integrates with Vitest, replacing the previous mocha/chai setup with a single package that doesn't require configuration by default and includes snapshot testing out-of-the-box. It also provides typed test assertions and utilities to read/write entities in-between processing runs.
Three ways to feed events
1. Auto-exit — processes the first block with matching events, then exits. Each subsequent call continues where the last one stopped. Zero config needed.
import { describe, it } from "vitest";
import { createTestIndexer } from "envio";
describe("ERC20 indexer", () => {
it("processes the first block with events", async (t) => {
const indexer = createTestIndexer();
const result = await indexer.process({ chains: { 1: {} } });
// Auto-filled by Vitest on first run — just review and commit
t.expect(result).toMatchInlineSnapshot(`
{
"changes": [
{
"Transfer": {
"sets": [
{
"blockNumber": 10861674,
"from": "0x0000000000000000000000000000000000000000",
"id": "1-10861674-23",
"to": "0x41653c7d61609D856f29355E404F310Ec4142Cfb",
"transactionHash": "0x4b37d2f343608457ca...",
"value": 1000000000000000000000000000n,
},
],
},
"block": 10861674,
"chainId": 1,
"eventsProcessed": 1,
},
],
}
`);
});
});
2. Explicit block range — pin to specific blocks for deterministic CI snapshots.
const result = await indexer.process({
chains: {
1: {
startBlock: 10_861_674,
endBlock: 10_861_674,
},
},
});
3. Simulate — feed typed synthetic events for pure unit tests. No network, no block ranges.
await indexer.process({
chains: {
137: {
simulate: [
{
contract: "Greeter",
event: "NewGreeting",
params: { greeting: "Hello", user: "0x123..." },
},
],
},
},
});
Key capabilities
- Snapshot-driven assertions —
result.changescaptures every entity set/delete per block. Pair withtoMatchInlineSnapshotfor auto-generated, reviewable snapshots. - Direct entity access —
indexer.Entity.get(),.getOrThrow(),.getAll(), and.set()for reading and presetting state. - Real pipeline, real confidence — tests exercise the full indexer pipeline including dynamic contract registration, multi-chain support, and handler context.
- Parallel test execution via worker thread isolation.
The test indexer also exposes chain information:
const indexer = createTestIndexer();
indexer.chainIds; // [1, 42161]
indexer.chains[1].id; // 1
indexer.chains[1].startBlock; // 0
indexer.chains[1].ERC20.addresses; // ["0x..."]
// Read/write entities between processing runs
await indexer.Account.set({ id: "0x123...", balance: 100n });
const account = await indexer.Account.get("0x123...");
See the Testing documentation for more details.
Podman Support
Beyond Docker, HyperIndex now supports Podman for local development environments. This provides an alternative container runtime for developers who prefer Podman or have it available in their environment.
Nested Tuples for Contract Import
The envio init command now supports contracts with nested tuples in event signatures, which was previously a limitation when importing contracts.
PostgreSQL Update for Local Docker Compose
The local development Docker Compose setup now uses PostgreSQL 18.1 (upgraded from 17.5).
contractName and eventName on Event
Events now include contractName and eventName fields, making it easier to identify which contract and event you're working with in handlers:
import { indexer } from "envio";
indexer.onEvent(
{ contract: "ERC20", event: "Transfer" },
async ({ event }) => {
console.log(event.contractName); // "ERC20"
console.log(event.eventName); // "Transfer"
},
);
New Official Exported Types
Generated code now exports official generic types for entities, enums, and events. These replace the previous contract-specific type exports:
import type {
MyEntity, // Still exported but Entity<"MyEntity"> is preferred
Entity, // Generic entity type — use as Entity<"MyEntity">
Enum, // Generic enum type — use as Enum<"MyEnum"> (replaces direct MyEnum export)
EvmEvent, // Generic event type — use as EvmEvent<"ERC20", "Transfer">
// Access specific fields: EvmEvent<"ERC20", "Transfer">["block"]
} from "envio";
Support for DESC Indices
A nice way to improve your query performance as well:
type PoolDayData
@index(fields: ["poolId", ["date", "DESC"]]) {
id: ID!
poolId: String!
date: Timestamp!
}
RPC Source Improvements
Added polling_interval option for RPC source configuration. Also added missing support for receipt-only fields (gasUsed, cumulativeGasUsed, effectiveGasPrice) that are not available via eth_getTransactionByHash. HyperIndex will additionally perform the eth_getTransactionReceipt request when one of the fields is added in field_selection.
WebSocket Support (Experimental)
Experimental WebSocket support for RPC source to improve head latency. Please create a GitHub issue if you come across any problems.
chains:
- id: 1
rpc:
url: ${ENVIO_RPC_ENDPOINT}
ws: ${ENVIO_WS_ENDPOINT}
for: realtime
Prometheus Metrics for Data Providers
Added a Prometheus metric to track requests to data providers, providing better observability into your indexer's data fetching patterns.
GraphQL-Style getWhere API
The getWhere query API has been redesigned using GraphQL-style syntax:
// Before
const transfers = await context.Transfer.getWhere.from.eq("0x123...");
// After
const transfers = await context.Transfer.getWhere({ from: { _eq: "0x123..." } });
Additionally, three new filter operators are available following Hasura-style conventions:
context.Entity.getWhere({ amount: { _gte: 100n } })
context.Entity.getWhere({ amount: { _lte: 500n } })
context.Entity.getWhere({ status: { _in: ["active", "pending"] } })
Direct RPC Client
Replaced Ethers.js with a direct RPC client implementation, reducing dependencies and improving performance.
Block Lag Configuration
A per-chain block_lag option to index behind the chain head by a specified number of blocks. Replaces the global ENVIO_INDEXING_BLOCK_LAG environment variable. Defaults to 0. This is for advanced use cases — only use it if you know what you're doing.
chains:
- id: 1
block_lag: 5
Official /metrics Endpoint
Prometheus metrics are now official. We cleaned up metric names, switched time units to seconds instead of milliseconds, and followed Prometheus naming conventions more closely. Metrics also cover data points previously available only via the --bench feature. A separate /metrics/runtime endpoint with a dedicated Prometheus registry is available for runtime metrics, isolated from the default /metrics endpoint.
Starting from the v3.0.0 release, Prometheus metrics will follow semver and be documented.
Breaking changes:
- Cleaned up metric names and switched time units from milliseconds to seconds
- Removed
--benchsupport — use the/metricsendpoint instead
Use the new envio metrics CLI command to fetch the Prometheus metrics of a locally running indexer without curling the endpoint manually.
Continue on Config Change
HyperIndex can now keep indexing through some config.yaml changes — rpc configuration is the first to land — instead of erroring out on every restart. Where a change is incompatible, the CLI prints exactly which fields were touched and offers two clear options (revert, or envio dev -r to wipe and re-index). More flexibility will be unlocked over time; open a GitHub issue if you need a specific field supported.
Double Handler Registration
It's now possible to register multiple handlers for the same event with similar filters:
import { indexer } from "envio";
indexer.onEvent(
{ contract: "ERC20", event: "Transfer" },
async ({ event, context }) => {
// Your logic here
},
);
indexer.onEvent(
{ contract: "ERC20", event: "Transfer" },
async ({ event, context }) => {
// And here
},
);
Improved Multiple Data-Sources Support
After switching to a fallback source, HyperIndex now attempts to recover to the primary source 60 seconds later. Previously, it would stay on the fallback until the fallback was down or the indexer was restarted. The source selection logic has also been improved for better indexing resilience and stricter enforcement of the realtime mode configuration.
Updated Dev Docker Flow
envio dev no longer uses a generated Docker Compose file and manages containers, network, and volumes directly for greater flexibility. For example, disabling Hasura with ENVIO_HASURA now prevents envio dev from pulling the Hasura image. Use envio dev --restart (or -r) to forcefully clear the database even if there are no config changes detected.
Envio Dev Update
envio dev no longer automatically resets the database on incompatible config or schema changes. Use envio dev -r to explicitly allow this.
Envio Start Update
envio start now has a clear role: to run HyperIndex in the production environment. Use envio dev for local development to enable debugging with Dev Console.
Optimized envio codegen
envio codegen is now near-instant. We no longer run pnpm i for the generated package, and we no longer recompile ReScript every time you change config.yaml or schema.graphql. The output is also a lot quieter.
Smaller envio Package (-88MB)
By eliminating dynamically generated ReScript code, we no longer need to ship or run a ReScript compiler at runtime. The published npm package shrank from 141MB to 53MB.
No Hard pnpm Requirement
Internal use of pnpm is gone. The generated package no longer has its own dependency tree, so HyperIndex works with whichever package manager you prefer.
Bun Support
Run HyperIndex on Bun:
bun --bun envio dev
Choose Your Package Manager on envio init
envio init now accepts --package-manager=pnpm|npm|bun|yarn so you can scaffold projects without committing to pnpm.
Better Tuples Developer Experience
Solidity struct components used to be generated as positional tuples in handler params, which made handler code awkward. They are now generated as objects with named fields:
struct CreateEventCommon {
address funder;
address sender;
address recipient;
Lockup.CreateAmounts amounts;
IERC20 token;
bool cancelable;
bool transferable;
Lockup.Timestamps timestamps;
string shape;
address broker;
}
event CreateLockupTranchedStream(
uint256 indexed streamId,
Lockup.CreateEventCommon commonParams,
LockupTranched.Tranche[] tranches
);
// Before
event.params.commonParams[5];
event.params.commonParams[3][0];
// After
event.params.commonParams.cancelable;
event.params.commonParams.amounts.deposit;
Improved Multichain Backfill
For large multichain indexers, HyperIndex now throttles chains that have already reached the head so they don't compete for resources while the rest finish backfilling. Once every chain has caught up, throttling is lifted and all chains continue indexing equally.
Toolchain Upgrades
- ReScript upgraded from v11 to v12 (internally and in
envio inittemplates) - TypeScript upgraded from v5 to v6 (internally and in
envio inittemplates)
Breaking Changes
Node.js & Runtime
- Node.js 22 is now the minimum required version, while 24 is the recommended version
- Changes in handler files don't trigger codegen on
pnpm dev
Handler API Changes
- Unified handler registration via
indexer— contract-specific exports were removed. ReplaceContract.Event.handler(handler, options)withindexer.onEvent({ contract, event, ...options }, handler). ReplaceContract.Event.contractRegister(...)withindexer.contractRegister({ contract, event }, ...). The old API has been hard removed and no longer works. - Dynamic contract registration —
context.add<ContractName>(address)is replaced withcontext.chain.<ContractName>.add(address). eventFiltersrenamed towhere— thewherecallback receives{ chain }(not{ chainId }) and returnsfalse,true, or{ params: [...], block?: { number: { _gte, _lte, _every } } }. The previous array shorthand is no longer accepted at the top level — wrap it in{ params: [...] }.- Block handlers consolidated — the standalone
onBlockexport and per-chainforEachregistration are gone. Use a singleindexer.onBlock({ name, where? }, handler)call. For chain-specific or interval-based handlers, return{ block: { number: { _gte, _lte, _every } } }fromwhere, orfalseto skip a chain. - Per-event start block — handlers can now override the configured start block per chain via
where.block.number._gte. - Removed
experimental_createEffectin favor ofcreateEffect - Renamed transaction field
kindtotype - For block handlers:
block.chainIdis removed in favor ofcontext.chain.id Addresstype changed fromstringto`0x${string}`- Removed
transaction.chainIdfrom field selection — usecontext.chain.idorevent.chainIdinstead getWhereAPI redesigned — changed fromcontext.Entity.getWhere.fieldName.eq(value)tocontext.Entity.getWhere({ fieldName: { _eq: value } })using GraphQL-style filter syntax- Events now include
contractNameandeventNamefields
config.yaml Changes
- Renamed
networkstochains - Renamed
confirmed_block_thresholdtomax_reorg_depth - Removed
unordered_multichain_modeflag, replaced withmultichain: ordered | unordered(default:unordered) - Removed
loadersoption (now always enabled via Preload Optimization) - Removed
preload_handlersoption (now always enabled) - Removed
preRegisterDynamicContractsoption - Removed
event_decoderoption (the Rust-based decoder is now the only implementation) - Removed
rpc_configin favor ofrpc, which now supports multiple URLs,formode (sync,realtime,fallback), and WebSocket configuration (see RPC for Realtime Indexing) - Removed the
outputflag — generated types are always emitted to.envio/at the project root
HyperSync API Token Required
Indexers using HyperSync as a data source now require an ENVIO_API_TOKEN environment variable. You can obtain a free API token at envio.dev/app/api-tokens.
export ENVIO_API_TOKEN=your_token_here
Environment Variable Changes
- Removed
UNSTABLE__TEMP_UNORDERED_HEAD_MODEenvironment variable - Removed
UNORDERED_MULTICHAIN_MODEenvironment variable - Removed
MAX_BATCH_SIZEenvironment variable (usefull_batch_sizein config.yaml instead) - Renamed
ENVIO_PG_PUBLIC_SCHEMAtoENVIO_PG_SCHEMA(the old name is still supported until v4) - Renamed
TUI_OFF=truetoENVIO_TUI=false(TUI is also auto-disabled in CI environments and when running under AI agents)
Generated Code Changes
- Removed
chaintype in favor ofChainId(now a union type instead of a number) - Removed internal
ContractTypeenum (allows longer contract names) - Removed
getGeneratedByChainId(useindexervalue instead) - Lowercased entity types removed: Generated code no longer exports lowercased entity types (e.g.,
transfer). Use capitalized names instead (e.g.,Transfer) - Entity array field values are now typed as
readonly— update any code that directly mutates array fields S.nullableschema type now returnsnullinstead ofundefined
CLI Behavior Changes
envio devno longer auto-resets the database. Useenvio dev --restart(or-r) to clear it explicitly.envio startis now production-only — useenvio devfor local development.
Deprecated: MockDb Testing API
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 |
Deprecated: Contract-Specific Type Exports
Generated code no longer exports contract-specific event log types (e.g., ERC20_Transfer_eventLog) or direct enum types (e.g., MyEnum). Use the new generic types instead:
| Old | New |
|---|---|
ERC20_Transfer_eventLog | EvmEvent<"ERC20", "Transfer"> |
ERC20_Transfer_block | EvmEvent<"ERC20", "Transfer">["block"] |
MyEnum (direct export) | Enum<"MyEnum"> |
MyEntity | Entity<"MyEntity"> (preferred) |
Postgres Column Updates
raw_events.event_id:NUMERIC→BIGINTraw_events.serial:SERIAL→BIGSERIALenvio_chains.events_processed:INTEGER→BIGINTenvio_checkpoints.id:INTEGER→BIGINT- Deprecated
envio_chains._num_batches_fetched— always returns 0 for backward compatibility
Fixes
- Fixed an issue where the indexer stops progressing without any error (PostgreSQL client update)
- Fixed checksum for addresses returned by RPC in lowercase
- Fixed incorrect validation of transactions
tofield returned by RPC - Fixed OOM error on RPC request crashing loop
- Fixed an edge case where a multichain indexer could freeze during a rollback on reorg (also backported to v2.32.10)
- Fixed external Postgres database support via
ENVIO_PG_HOST - Fixed
S.nullableschema type to beT | nullinstead ofT | undefined
Migration Guide
Step 0: Prepare on V2 (Recommended)
Before upgrading to V3, we recommend preparing your project while still on V2:
- Upgrade to v2.32.6 and enable Preload Optimization:
# config.yaml
preload_handlers: true
-
If you were using loaders, migrate them to Preload Optimization following the Migrating from Loaders guide.
-
Verify your indexer works correctly with
pnpm devbefore proceeding to V3.
This step ensures a smoother migration by validating Preload Optimization works with your handlers before the V3 upgrade.
Step 1: Update Dependencies
Node.js
Update Node.js to version 22 or higher.
package.json
Update your package.json with the following changes:
{
"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"
}
}
Adding "type": "module" is required for V3. Without it, your project will fail to start due to ESM import errors.
Remove the generated package. As of v3.0.0-alpha.24, the local generated package no longer exists — types are emitted to .envio/types.d.ts (git-ignored) and wired up via a small envio-env.d.ts file at the project root. Drop the entry from package.json if you still have it:
- "optionalDependencies": {
- "generated": "./generated"
- },
Re-run envio codegen after upgrading; everything you previously imported from generated is now exported from envio.
If you use testing with Mocha (recommended: migrate to Vitest):
We recommend migrating from mocha/chai to Vitest, which offers a better testing experience with the new HyperIndex testing framework:
pnpm remove ts-mocha ts-node mocha chai @types/mocha @types/chai
pnpm add -D vitest@4.0.16
Update your package.json:
{
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"vitest": "4.0.16"
}
}
Move and refactor your test files:
- Move
test/Test.tstosrc/indexer.test.ts - Update imports from
mocha/chaito usevitest:
// Before (mocha/chai)
import { describe, it } from "mocha";
import { expect } from "chai";
// After (vitest)
import { describe, it, expect } from "vitest";
import { createTestIndexer } from "envio";
If you prefer to keep Mocha:
Remove ts-mocha and ts-node, then install tsx:
pnpm remove ts-mocha ts-node
pnpm add -D tsx@4.21.0
Update your test script in package.json:
{
"scripts": {
"mocha": "tsc --noEmit && NODE_OPTIONS='--no-warnings --import tsx' mocha --exit test/**/*.ts"
}
}
If you use ts-node for start script:
Replace with:
{
"scripts": {
"start": "envio start"
}
}
Step 2: Update tsconfig.json
Update your tsconfig.json to support ESM:
{
/* For details: https://www.totaltypescript.com/tsconfig-cheat-sheet */
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
/* For running Envio: */
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
/* Code doesn't run in the DOM: */
"lib": ["es2022"],
"types": ["node"]
}
}
This includes additional strictness options like verbatimModuleSyntax and noUncheckedIndexedAccess. You can disable them to simplify the migration.
Step 3: Update config.yaml
Rename networks to chains:
# 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)
Update multichain mode (if applicable):
If you had unordered_multichain_mode: true, remove it — this is now the default. If you need ordered multichain behavior, explicitly set:
multichain: ordered
Rename config options:
confirmed_block_threshold→max_reorg_depth
Remove deprecated options:
Remove the following options from your config if present:
loaders— now always enabled via Preload Optimizationpreload_handlers— now always enabledpreRegisterDynamicContracts— no longer neededunordered_multichain_mode— replaced withmultichainoptionevent_decoder— the Rust-based decoder is now the only implementationrpc_config— replaced withrpc(see Breaking Changes)
New option for batch size:
If you were using MAX_BATCH_SIZE environment variable, use the new config option instead:
full_batch_size: 5000
Automatic Handler Registration (optional):
Optionally move your handler files to src/handlers/ and remove the explicit handler paths from config.yaml.
Step 4: Update Environment Variables
Add required environment variables:
If your indexer uses HyperSync (the default data source), you need to set up an API token:
- Get a free API token at envio.dev/app/api-tokens
- Set the environment variable:
export ENVIO_API_TOKEN=your_token_here
For local development, you can add it to a .env file:
ENVIO_API_TOKEN=your_token_here
Remove deprecated environment variables if present:
UNSTABLE__TEMP_UNORDERED_HEAD_MODEUNORDERED_MULTICHAIN_MODEMAX_BATCH_SIZE— usefull_batch_sizein config.yaml instead
Step 5: Update Handler Code
Migrate to the unified indexer API:
All contract-specific handler exports (ERC20.Transfer.handler, Greeter.NewGreeting.contractRegister, etc.) have been removed. Register every handler through indexer.onEvent, indexer.contractRegister, or indexer.onBlock.
// 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 }) => {
// ...
},
);
Migrate dynamic contract registration:
// Before
UniV3.PoolFactory.contractRegister(async ({ event, context }) => {
context.addPool(event.params.poolAddress);
});
// After
indexer.contractRegister(
{ contract: "UniV3", event: "PoolFactory" },
async ({ event, context }) => {
context.chain.Pool.add(event.params.poolAddress);
},
);
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 handlers or interval/range filters, use the where callback (see Unified Handlers API).
Rename deprecated APIs:
| V2 (Deprecated) | 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 |
chain type | ChainId |
transaction.chainId | context.chain.id or event.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 } }) |
Removed APIs:
getGeneratedByChainId— use theindexer.chains[chainId]instead (see Indexer State & Config)getWhereAPI — update to the new 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 } });
- Lowercased entity types — use capitalized names instead:
// Before
import { transfer, approval } from "generated";
// After
import { Transfer, Approval } from "envio";
CLI behavior changes:
envio devno longer auto-resets the database. If you relied on auto-reset, runenvio dev -r(or--restart) explicitly.envio startis now production-only — keep usingenvio devfor local development.
Step 6: Test Your Migration
After making all changes, run codegen and start your indexer:
pnpm envio codegen
pnpm dev
Quick Migration Checklist
Prepare (on V2):
- Upgrade to
envio@2.32.6 - Enable
preload_handlers: truein config.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 the
optionalDependencies.generatedentry frompackage.json(alpha.24+) - Update
engines.nodeto>=22.0.0inpackage.json - Update
tsconfig.jsonfor ESM support - Migrate from mocha/chai to vitest (recommended) or replace
ts-mocha/ts-nodewithtsx
config.yaml:
- Rename
networkstochains - Rename
confirmed_block_thresholdtomax_reorg_depth - Replace
rpc_configwithrpc - Remove
unordered_multichain_mode(now default) - Remove
loadersandpreload_handlersoptions - Remove
preRegisterDynamicContractsoption - Remove
event_decoderoption - Remove the
outputoption (types are always written to.envio/) - If using ClickHouse, add
storage: { postgres: true, clickhouse: true }(env vars are still required for the connection)
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_sizein config.yaml) - Rename
TUI_OFF=truetoENVIO_TUI=falseif you set it
Handler Code:
- Migrate event handlers from
Contract.Event.handler(...)toindexer.onEvent({ contract, event, ...options }, handler) - Migrate dynamic contract registration from
Contract.Event.contractRegister(...)toindexer.contractRegister({ contract, event }, handler) - Migrate
context.add<Contract>(addr)tocontext.chain.<Contract>.add(addr) - Convert
eventFiltersto the newwherecallback returning{ params: [...] } - Migrate block handlers from per-chain
onBlockloops to a singleindexer.onBlockcall (usewherefor chain-specific or interval filters) - Use the new
where.block.number._gteto override per-event start blocks if needed - Replace
experimental_createEffectwithcreateEffect - Replace
block.chainIdwithcontext.chain.idin block handlers - Replace
transaction.kindwithtransaction.type - Update usage of
chaintype toChainId - Replace
getGeneratedByChainIdwithindexer.chains[chainId] - Update code expecting
Addresstype to bestring(now`0x${string}`) - Replace
transaction.chainIdwithcontext.chain.idorevent.chainId - Replace lowercased entity type imports with capitalized versions (e.g.,
transfer→Transfer) - Update
getWherecalls to new GraphQL-style filter syntax (e.g.,getWhere({ field: { _eq: value } })) - Update any
S.nullableschema usage — now returnsnullinstead ofundefined - Migrate from
MockDbtocreateTestIndexer()(see MockDb Migration Cheat Sheet) - Replace contract-specific type exports (
ERC20_Transfer_eventLog) with generic types (EvmEvent<"ERC20", "Transfer">)
CLI:
- If you relied on
envio devresetting the database automatically, switch toenvio dev -r - Use
envio devfor local development (envio startis now production-only)
Verify:
- Run
pnpm envio codegenandpnpm devto verify
Getting Help
If you encounter any issues during migration, join our Discord community for support.
Release Notes
For detailed release notes, see:
- v3.0.0-rc.0
- v3.0.0-alpha.24
- v3.0.0-alpha.23
- v3.0.0-alpha.22
- v3.0.0-alpha.21
- v3.0.0-alpha.20
- v3.0.0-alpha.19
- v3.0.0-alpha.18
- v3.0.0-alpha.17
- v3.0.0-alpha.16
- v3.0.0-alpha.15
- v3.0.0-alpha.14
- v3.0.0-alpha.13
- v3.0.0-alpha.12
- v3.0.0-alpha.11
- v3.0.0-alpha.10
- v3.0.0-alpha.9
- v3.0.0-alpha.8
- v3.0.0-alpha.7
- v3.0.0-alpha.6
- v3.0.0-alpha.5
- v3.0.0-alpha.4
- v3.0.0-alpha.3
- v3.0.0-alpha.2
- v3.0.0-alpha.1
- v3.0.0-alpha.0