What I Learned Reading Production Envio Indexers
From: Kaustubh Agrawal — Growth Engineer candidate Companion docs: ENVIO_REVENUE_MODEL.md · ENVIO_REVENUE_MATH.md · ENVIO_GROWTH_PLAN.md · ENVIO_DECK_OUTLINE.md · ENVIO_FIRST_24_HOURS.md · ENVIO_VIDEO_SCRIPT.md
I wanted to ground my application in something more than my own Mirror Protocol experience, so I cloned and read two of your most prominent customer indexers — Sablier (
sablier-labs/indexers) and Velodrome (velodrome-finance/indexer). Below is what I learned. Sharing it because some of the observations might be useful as inputs into the Growth playbook we'll discuss, and because reading production Envio code at this depth is the kind of homework I'd want to be doing in week one anyway. Caveat throughout: I'm reading the public surface from outside; you have all the context I don't.
TL;DR — Six Patterns I Saw Repeatedly, And What They Tell Me
| # | Pattern | Where it shows up | What it tells me |
|---|---|---|---|
| 1 | Effect API + entity-cache-first preload pattern | Sablier preloadCreateEntities; Velodrome getTokenDetails + PriceOracle | The single most important pattern in production. Hard to discover. |
| 2 | Aggressive Effect-result caching for cost optimization | Sablier tokenMetadata cache file with the 0 alias trick | Real customers hit cache file size limits. Not in onboarding docs. |
| 3 | Codegen pipelines for multi-chain config sprawl | Sablier @sablier/devkit generates 27-deployment configs | At scale, hand-writing config.yaml doesn't work. |
| 4 | Aggregators as a first-class architectural layer | Velodrome LiquidityPoolAggregator.ts (~24KB), Aggregators/ directory | Indexers don't just store events; they compute derived state. |
| 5 | Hourly snapshots from aggregators | Velodrome Snapshots/ with Shared.ts epoch-alignment logic | Time-series state is what most analytics products actually need. |
| 6 | Dynamic contract registration via factory patterns | Both customers, called out in Velodrome's CLAUDE.md | Empty address: arrays in config + addAddress() at runtime. The pattern that breaks every newcomer. |
The shape of the observation: the patterns Envio's most sophisticated customers depend on are not the ones the docs lead with. That's a Growth-side opportunity, not a product complaint — every gap between "what docs cover" and "what production needs" is a template, a Loom, a guide, or a case study waiting to be made.
1. Reading the Two Repos: Surface Stats
Before the patterns, the rough shape of what these customers built.
Sablier (sablier-labs/indexers)
| Streams | Airdrops | Analytics | |
|---|---|---|---|
| Schema size | 905 lines | 550 lines | 506 lines |
| Chain deployments configured | 27 | (high) | (cross-cuts) |
| Handler files | many of 158 total .ts files | many | many |
| Standout architectural choice | Codegen pipeline (@sablier/devkit) | Codegen | Effect API for forex + Coingecko |
Sablier runs three separate Envio indexers (streams / airdrops / analytics), each with its own schema, all generated by a custom codegen toolchain. The config.yaml and schema.graphql files are autogenerated — both files literally start with # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. That's a meta-build pattern most onboarding flows wouldn't anticipate.
Sablier also runs The Graph subgraphs in parallel in the same monorepo (graph/ directory). The repo's existence is a real proof of "you can run Envio alongside your existing Graph stack" — and a hint that some of the most committed Envio adopters started as comparison runs.
Velodrome+Aerodrome (velodrome-finance/indexer)
| Metric | Value |
|---|---|
| Schema size | 817 lines |
| Chain deployments | 12 (Optimism, Base, plus Celo / Soneium / Ink / Mode / Lisk / Unichain / Fraxtal / Metal / Swell) |
| Config size | 28 KB hand-written |
| Architecture domains | 11 (Pools / Factories / Gauges / Voting / veNFT / NFPM / ALM / Pool Launcher / Superswaps / Swap Fee Modules) |
| Standout | Heavy Effect API usage for PriceOracle.ts, RPC fallback gateway, multi-protocol aggregators |
Velodrome's repo includes a CLAUDE.md file documenting their own internal Envio patterns. Reading it was the single most useful artifact in this whole exercise — more on that in section 6.
2. The Effect API + Entity-Cache-First Preload Pattern
The single most important pattern in production-grade Envio indexers. Both customers use it, in slightly different shapes, and it's not the way the docs walk a new user through their first indexer.
Sablier's version
In envio/streams/mappings/lockup/common/preload.ts, the create-stream handler does this before any business logic:
const [asset, batch, batcher, watcher] = await Promise.all([
context.Asset.get(assetId),
context.LockupBatch.get(batchId),
context.LockupBatcher.get(batcherId),
context.Watcher.get(watcherId),
]);
let assetMetadata: RPCData.ERC20Metadata;
if (asset) {
// already cached as an entity — reuse it
assetMetadata = { decimals: Number(asset.decimals), name: asset.name, symbol: asset.symbol };
} else {
// fall through to Effect API for the network call
assetMetadata = await context.effect(fetchTokenMetadata, {
address: params.asset,
chainId: event.chainId,
});
}Three things stack here:
- Parallel entity reads (
Promise.all) — the handler doesn't sequence reads serially. - Entity-cache-first — if the asset entity already exists, use the indexed metadata directly; only call the RPC via
context.effect()if it's actually a new asset. - Effect API for the network call, with cache enabled at the
createEffectdefinition.
Velodrome's version
In src/PriceOracle.ts, the same shape with an extra layer for stale-data refresh:
const tokenDetails = await context.effect(getTokenDetails, {
contractAddress: tokenAddress,
chainId,
});
// ... only refresh price if hour has passed ...Plus a fallback gateway in Effects/RpcGateway.ts for when the primary Effect path needs a different RPC.
Why this pattern matters for Growth
The Envio docs introduce the Effect API as an experimental feature. In both production customers it's load-bearing. Without it, handlers double-execute the network calls during preload. The pattern that makes a slow indexer fast is not a footnote — it's the default move.
If I were building the "First Indexer in 30 Minutes" pillar artefact (Play 1 from the strategy memo), the second 30 minutes — the upgrade path that gets a developer to a production-grade indexer — would be entirely about teaching this pattern. Not from scratch; from the working version of these two repos. "Here's how Sablier and Velodrome both structure their handler reads. Steal it."
3. Aggressive Effect-Result Caching (And the 0 Alias Trick)
One observation I didn't expect to find. From envio/common/effects/token-metadata.ts in Sablier:
// We alias the unknown token metadata as "0" to optimize the cache file size.
const TokenMetadata = S.union([
S.shape(S.schema(0), (_) => ({ decimals: UNKNOWN.decimals, name: UNKNOWN.name, symbol: UNKNOWN.symbol })),
{ decimals: S.number, name: S.string, symbol: S.string },
]);Reading the comment is the whole story: "We alias the unknown token metadata as 0 to optimize the cache file size." This is a real production optimization — Sablier hit a cache-file-size threshold and engineered around it by encoding the most common case (an unrecognized token) as a single byte instead of a serialized object.
That's the kind of detail that only emerges at scale. It's not in the docs because most users never need it. But it is a specific signal that:
- The Effect API cache file can grow large enough at production scale to need optimization
- Sablier hit it and figured this out themselves
- The same trick would be useful for any other production customer with high-cardinality token spaces (DEXes, bridges, multichain analytics)
A short Envio blog post titled "Optimizing your Effect cache at scale — a pattern from Sablier" writes itself. That's the case-study factory (Play 3) producing its first asset before I've even started.
4. Codegen Pipelines for Multi-Chain Config Sprawl
Sablier doesn't write config.yaml by hand. They have a codegen toolchain (@sablier/devkit) that produces it. The justfile reveals the build pipeline:
@codegen-config:
just root::codegen::envio-config $INDEXER_NAME
@codegen-bindings:
just root::codegen::envio-bindings $INDEXER_NAME
@codegen-schema:
just root::codegen::schema envio $INDEXER_NAMEThree layers — schema, config, bindings — all generated from a single source of truth in their devkit. The autogenerated config covers 27 contract deployments per indexer.
This is the "scale tax" of Envio. Once a customer has more than, say, ten contracts × multiple chains, hand-maintaining config.yaml becomes a mistake-magnet. A pattern that only the most sophisticated customers solve themselves.
The Growth opportunity: a published reference codegen toolchain (or a community template forking Sablier's approach) would dramatically lower the activation cost for multi-chain customers. That's a Play 1 artefact targeted specifically at the mid-market segment that has more than one chain to track.
5. Aggregators as a First-Class Architectural Layer
Velodrome's src/ has these top-level directories, in order of size:
src/
├── EventHandlers/ # raw event ingestion
├── Aggregators/ # derived state computation
├── Snapshots/ # hourly snapshots of aggregated entities
├── Effects/ # external calls (RPC, API)
├── PriceOracle.ts # token-price-with-refresh-interval
├── Helpers.ts # math, USD conversion, position calcs
└── Constants.ts # chain-specific configs, factory addressesThe Aggregators/ directory is a first-class layer of the indexer architecture. LiquidityPoolAggregator.ts is ~24KB — it's the single largest source file in the repo. It computes pool-level metrics (TVL, volume, fees, votes, emissions) from the raw events that the EventHandlers ingest.
This is a deliberate separation of concerns:
- EventHandlers: ingest, validate, set base entities
- Aggregators: compute derived state from base entities
- Snapshots: capture aggregator state at hourly epochs
Most newcomers to Envio (myself included on Mirror Protocol) collapse all three into the EventHandlers layer. That works for small indexers and breaks at scale because aggregator logic gets tangled with event-specific logic.
The Growth implication: a "production indexer architecture" template that ships these three directories pre-structured, with a README walking through the separation, would meaningfully accelerate any customer planning to build a non-trivial indexer. This is the kind of asset that turns a Production Small customer into a Production Medium one — the second dimension of expansion (Play 4 in the strategy memo).
6. Velodrome's CLAUDE.md Is a Wishlist for Envio's Onboarding Docs
This is the observation I'm most cautious about how I frame, because it could read as criticism of Envio's docs. It isn't. It's the opposite — it's a list of patterns that are already true in Envio, that Velodrome's team had to write down for themselves. Each one is a template-or-guide opportunity for Growth.
From velodrome-finance/indexer/CLAUDE.md, verbatim, the "Envio-Specific Patterns" section:
- Entity updates: Always spread the existing entity (
{ ...existing, field: newValue }) — entities are read-only/immutable.- External calls: Must use the Effect API (
createEffect+context.effect()) because handlers run twice during preload. Seesrc/Effects/.- Dynamic contracts: Many contracts (Pool, CLPool, Gauge, etc.) have empty
address:arrays inconfig.yaml— they are registered dynamically at runtime via factory events usingcontext.ContractName.addAddress().- Relationships: Use
entity_idstring fields (e.g.token0_id: String!), not direct object references. No entity arrays.- Timestamps: Always cast to BigInt:
BigInt(event.block.timestamp).- Addresses: Use lowercase keys in config objects for
.toLowerCase()lookups. Always checksum addresses for entity storage.
Each of these is a real Envio pattern. None of them is a Velodrome-specific quirk. They wrote this section because new contributors to their codebase kept making the same mistakes.
Read forward, this list is six potential canonical Growth-side artefacts:
- "Why entities are immutable in Envio (and how to update them correctly)" — short blog or doc-link card
- "Why handlers run twice during preload (and what to do about it)" — the Effect API explainer Velodrome wishes existed
- "Dynamic contract registration via factory events" — a guide with a working repo (Velodrome's open-source code is the reference)
- "Entity relationships in Envio: when to use
_idstrings vs. nested objects" — a 2-minute read for new users - "BigInt and timestamp gotchas" — FAQ entry; one paragraph
- "Address handling: lowercase config keys + checksum entity IDs" — quickref card
Six Growth assets, in priority order, derived directly from where one of your sophisticated customers had to embed institutional knowledge in their own repo. The half-life on each is years.
7. What This Exercise Tells Me About The Customer Base
Three things became clearer reading this code than they were from the docs alone.
First, the gap between the docs' default-path indexer and a production-grade indexer is large. The docs walk a developer from npx envio init to a single-event handler with one entity. Production customers — both the ones I read — have aggregator layers, snapshot layers, Effect API caching strategies, codegen pipelines, multi-domain handler hierarchies. The "second 30 minutes" of onboarding (Play 1's upsell guide) needs to bridge that gap. There's at least 8–12 specific guides waiting to be written from the patterns above.
Second, your most committed customers are doing real architecture work. Sablier's monorepo runs three Envio indexers + multiple Graph subgraphs side by side, with a codegen toolchain. Velodrome runs 12 chain deployments with 11 protocol domains and a custom price oracle. These aren't toy uses of the platform — they're the hardest indexing problems on EVM today, and they're being solved on Envio. That's the strongest customer-validation signal I saw.
Third, the case-study factory (Play 3) has free material sitting in plain sight. Every paragraph in this document could be a published blog post on Envio's site, ghost-written from the customer's perspective, with the customer's permission. The Sablier 0-alias trick alone is a five-minute-read post that would land hard with anyone running a multi-token indexer. Multiply that by the Effect API preload pattern, the dynamic contract pattern, the codegen toolchain, the aggregator/snapshot architecture — that's six to ten posts before the case-study factory has even interviewed a customer.
8. Why I'm Sharing This, Specifically
I wanted to send something more substantive than another strategy memo. The previous documents argued the shape of a Growth role at Envio. This one is the kind of artifact that would come out of the role in week three or four — a memo grounded in real code, not abstract analysis.
Two honest caveats:
- I read the surface I had access to. I haven't talked to the Sablier or Velodrome teams; I haven't seen any internal context; I'm reading the open-source code as a fresh outsider. Some of what I observed will be stuff you've already cataloged. I'd be unsurprised if you have an internal doc that overlaps significantly with this one.
- None of these observations is a criticism. Every gap between docs and production is also a template, a guide, a Loom, or a case study waiting to be made. That's exactly the kind of work I'd want to be the person doing.
If anything in here is useful as input into our planning conversation, great. If most of it is already on your radar, that's also a useful signal — it tells me your team is operating at a level where this kind of audit is table-stakes. Either way, I wanted you to see what I'd been doing in the last few days while we waited for the planning meeting.
— Kaustubh