blockchainsv
Web3 Integration & APIs·July 07, 2026·17 min read

Web3 development company stack: why custom indexing is key

A dApp that reads every position, transfer, vote, fill, stake, and liquidation through direct RPC calls has already accepted a failure mode. The failure is not theoretical.

Web3 development company stack: why custom indexing is key

This is where a serious Web3 development company separates architecture from glue code. Smart contracts mutate state. Nodes expose state and logs. They do not provide product-grade filtering, aggregation, pagination, denormalized views, or deterministic query latency for a frontend under load. If the application needs those properties, custom indexing is not a convenience layer. It is part of the system boundary.

Direct RPC querying fails at the data access layer

The first mistake is treating an RPC node as a backend. It is not one.

An Ethereum-compatible node can answer narrow questions well. Current balance. Current owner. Transaction receipt. Block header. Event logs within a range, subject to provider limits. It can execute eth_call against a specific block. It can return deterministic data if the request is precise.

That does not make it a query engine.

A frontend that needs “all open positions for this account, sorted by health factor, including last collateral update and realized fees” is asking for an application view. The chain does not store that view. It stores state transitions. The node exposes those transitions through constrained primitives.

The attack vector is not always a contract exploit. Sometimes it is architectural. The invariant is simple:

A read path must not require unbounded historical scanning during user interaction.

Break that invariant, and the system degrades under normal use.

Direct node access creates several hard limits:

  • Historical reads scale poorly. Event logs exist, but complex filtering across contracts, topics, block ranges, and derived fields is not native database work. The application ends up scanning, chunking, retrying, and stitching.
  • Frontend latency becomes chain-history dependent. A user opening a dashboard should not pay the cost of reconstructing six months of events.
  • Provider limits become product limits. Public and commercial RPC providers cap block ranges, rate limits, payload sizes, and concurrent requests. The application inherits those ceilings.
  • Derived state becomes inconsistent across clients. If every client reconstructs the same derived view independently, one missed event or failed retry creates divergent UI state.
  • Pagination becomes fake. RPC pagination over historical events is not the same as database pagination over indexed application entities.

A dApp can survive this pattern at low scale. A prototype can tolerate it. A narrow mint page can tolerate it. A governance interface with years of proposals and votes cannot. A derivatives dashboard cannot. A marketplace with bids, cancellations, fills, royalties, and collection filters cannot.

RPC is a transport for chain facts. It is not an application data model.

This is the core reason custom indexing sits inside the stack of a competent dApp development agency. The indexer is not decorative middleware. It converts append-only execution history into queryable application state.

What custom indexing actually does

Custom indexing listens to chain events, transaction receipts, and sometimes direct contract reads at known block heights. It transforms those inputs into entities the application can query.

The contract emits facts:

  • PositionOpened
  • CollateralAdded
  • DebtRepaid
  • PositionLiquidated
  • Transfer
  • OrderFilled
  • VoteCast

The indexer builds application objects:

  • position
  • account portfolio
  • order book row
  • liquidation record
  • governance proposal
  • token holder snapshot
  • fee accrual history

That transformation is a controlled state mutation outside the EVM. It must be deterministic. It must be replayable. It must handle reorgs. It must preserve the relationship between source event, block number, transaction hash, and derived entity.

The Graph made this model common. Its subgraphs index blockchain data and expose the result through GraphQL. Instead of asking a node to search history repeatedly, the application asks a query layer for the exact entities it needs.

The distinction matters.

Read requirementDirect RPC patternCustom indexed pattern
Get current token balancebalanceOf() callUsually unnecessary unless combined with history
Show all historical trades for a userScan events over block rangesQuery indexed Trade entities by account
Sort positions by riskPull positions, call contracts, compute client-sideQuery denormalized position entities with risk fields
Paginate marketplace listingsChunk logs and reconstruct active listingsQuery active listing entities with cursor pagination
Build analytics viewRepeated historical RPC callsAggregate during indexing or query indexed entities
Recover after frontend reloadRe-run reconstructionRead persisted indexed state

The correct read path depends on the data shape. Current canonical state belongs on-chain. Historical and derived views belong in an indexed layer.

This does not replace contract events. It depends on them. A contract that fails to emit sufficient events forces the indexer to infer state from weaker signals. That is brittle. Event design is part of blockchain integration architecture.

A useful event is not a console log. It is a public interface for off-chain state reconstruction. It should include stable identifiers, indexed fields where appropriate, and enough data to avoid unsafe inference.

Bad event:

Updated(address user, uint256 value)

Better event:

CollateralUpdated(bytes32 indexed positionId, address indexed account, address indexed asset, uint256 collateralBefore, uint256 collateralAfter, uint256 blockTimestamp)

The second event has weight. It gives the indexer a deterministic transition. It reduces calls back into the contract. It makes historical reconstruction sane.

The Graph and the GraphQL boundary

The Graph uses subgraphs to index blockchain data and make it queryable with GraphQL. The operating model is straightforward: define the contracts, events, handlers, and entities; then process chain data into a store.

The value is not GraphQL syntax. The value is controlled shape.

A frontend should not know how to reconstruct a lending position from seven events across three contracts. It should query positions(where: { account:... }) and receive the fields that the product needs.

That boundary has consequences.

A subgraph mapping becomes part of the application’s correctness surface. If the mapping mishandles a burn event, the UI lies. If it ignores reorg behavior, the UI lies. If it stores numbers with unsafe conversions, the UI lies. There is no harmless indexing bug in a financial interface. There are only delayed state failures.

A Web3 API integration workflow should treat the subgraph as versioned infrastructure:

1. Define entities from product reads, not contract names.

Position, Market, AccountSnapshot, and Liquidation are better application entities than a flat mirror of every Solidity struct. The chain structure and the read model are not the same artifact.

2. Map every entity field to a source.

A field comes from an event, a contract call at a block, a deterministic calculation, or a constant. If that source is unclear, the field should not exist.

3. Store provenance.

Important entities should retain blockNumber, transactionHash, and log identity where relevant. Debugging without provenance becomes guesswork.

4. Design for replays.

An indexer must rebuild from genesis or deployment block and produce the same derived state. Non-deterministic calls, wall-clock dependencies, and mutable external APIs break that property.

5. Separate display formatting from indexed values.

Store raw integer values. Format decimals in the client. Precision loss in indexing is a silent defect.

GraphQL then becomes the read contract. It lets the frontend request specific fields without over-fetching. It supports filtering and pagination over indexed entities. It gives a stable API to React components, server-side rendering layers, analytics jobs, and monitoring.

The failure mode is over-indexing. Not every field deserves persistence. Not every dashboard metric belongs in the subgraph. If a field can be derived cheaply from two returned values, it can remain client-side. If it requires scanning history, it belongs in the index.

Cold rule: index what removes unbounded work from the user path.

Viem, Wagmi, and the modern frontend read split

Ethers.js v6 remains widely used. It provides contract abstractions, providers, signers, ABI encoding, and transaction flows. It is familiar. Familiar does not mean optimal for every modern React stack.

Viem gained significant adoption in 2023 as a lower-level, modular alternative to ethers.js. Its design favors type safety, explicit clients, and smaller composable actions. Wagmi builds on Viem and exposes React Hooks for wallet connection, account state, network switching, reads, writes, and transaction tracking.

The important part is not library branding. It is read-path separation.

A mature frontend should not push all data through one pipe. It should split reads by source of truth.

Data typePreferred sourceTypical tool
Wallet account, connector state, chain IDWallet providerWagmi hooks
Current contract state needed for executionRPC callViem or Wagmi contract reads
Historical user activityIndexerGraphQL client
Derived product viewsIndexerGraphQL client
Pending transaction statusRPC receipt pollingViem or Wagmi
External real-world input used by contractsOracle feedChainlink feed contract reads
Static metadataCDN, IPFS gateway, app backendStandard HTTP client

This split reduces false coupling. A wallet connection library should not become the analytics backend. A subgraph should not sign transactions. An RPC client should not paginate a marketplace.

Wagmi is useful because it abstracts React-specific wallet and contract interaction logic. But abstraction is not immunity. The same invariants apply:

  • Do not trigger broad historical RPC reads inside component render paths.
  • Do not bind critical UI state to a wallet hook if the data is not wallet-specific.
  • Do not refetch indexed history after every block unless the view needs it.
  • Do not treat optimistic UI state as confirmed chain state.
  • Do not mix pending transaction state with indexed final state without clear labels.

Viem is useful where explicitness matters. A public client reads. A wallet client signs. Actions are scoped. Types can be inferred from ABIs. That reduces accidental misuse, especially in larger teams where frontend engineers are integrating contracts they did not write.

Still, no client library fixes a broken data model. If the dApp asks the browser to reconstruct protocol history, the library only changes the shape of the failure.

A faster client does not repair an unbounded query.

The practical pattern is simple. Use Viem or Wagmi for live chain interactions. Use The Graph or a custom indexer for historical and derived state. Keep the boundary visible in code.

A component named UserPositionsTable should not contain log-scanning logic. It should call a query hook backed by indexed data. A component named RepayButton can use contract reads to validate current allowance, debt, and transaction simulation. Those are different jobs.

Oracles are not indexers

Chainlink Data Feeds are the standard pattern for bringing decentralized price or asset information into smart contracts. They solve a different problem from indexing.

An oracle brings external data into the on-chain execution environment. An indexer takes on-chain execution history and makes it usable off-chain. Confusing these layers creates bad architecture.

Example: a lending protocol needs asset prices for collateral checks. The contract reads a Chainlink price feed. That value affects whether a borrow, repay, or liquidation transaction succeeds. It is part of the state transition logic.

The frontend then needs to show a user’s liquidation history, current risk, collateral changes, and past repayments. That is indexing territory. The indexer may also read price-related events or store values observed at transaction time if emitted. But it is not the authority that decides whether the loan is liquidatable on-chain.

For teams integrating non-price external data, the same boundary applies. Weather, sports, shipping, identity, and IoT inputs do not become safe because they are displayed in a frontend. If the data affects contract execution, it needs an oracle design. A useful companion example is this guide to integrating off-chain weather APIs into Solidity smart contracts, which sits on the oracle side of the boundary rather than the indexing side.

The separation should be explicit:

LayerDirection of dataSecurity concern
OracleOff-chain data into smart contractData authenticity, aggregation, update conditions
IndexerOn-chain data into query storeDeterministic replay, reorg handling, mapping correctness
RPC clientDirect access to node stateProvider limits, latency, finality assumptions
Frontend cacheUI-local performance layerStale state, invalidation, pending transaction labeling

An oracle error can mutate contract outcomes. An indexing error can misrepresent contract outcomes. Both are dangerous. They are not the same danger.

Event design is the indexing foundation

A custom indexer cannot recover information the contract never exposes.

Storage can be read, but historical storage is not a convenient public dataset. Events are the durable integration surface. They are cheap compared with forcing every downstream system to infer transitions from transaction traces and storage diffs.

A Web3 development services team should review events before deployment with the same discipline used for access control and arithmetic. The question is not “does this emit something.” The question is “can an independent indexer reconstruct the application state from emitted facts.”

A strong event set has these properties:

  • Stable entity identifiers. Positions, orders, vaults, proposals, and markets need IDs that do not change when display data changes.
  • Indexed topics for primary filters. Account, asset, market, and entity ID often deserve indexed fields. Topic limits force tradeoffs. Use them deliberately.
  • Before-and-after values where transitions matter. Deltas are compact, but absolute post-state values reduce reconstruction risk.
  • No dependence on event order across unrelated contracts unless documented. Multi-contract workflows need clear correlation IDs or transaction-level handling.
  • Explicit cancellation and terminal states. Off-chain systems should not infer that an order is inactive only because a later fill appears.
  • Version awareness. Upgrades and migrations need events that let indexers distinguish schema and semantics.

Minimal contracts often omit this layer. That saves gas and creates permanent integration debt. The cost returns later as custom tracing, archive-node dependency, support tickets, and inconsistent analytics.

There is also a security angle. Poor event design makes monitoring weaker. Detection systems consume events. Risk dashboards consume events. Incident response consumes events. If the protocol cannot emit precise state transitions, it cannot be observed cleanly.

When direct RPC is still correct

Custom indexing is not a religion. It is a response to data shape.

Some applications do not need it. A small mint page can read total supply, mint price, account allowance, and sale state through direct calls. A simple staking interface with one pool and a small user base may survive with contract reads and limited event history. A token-gated page may only need ownership verification.

The wrong move is installing an indexing layer because it looks professional. Every new subsystem adds failure modes. Deployment. Backfill. Schema migration. Monitoring. Hosted service dependency. Query cost. Reorg logic. Data corruption. Operational ownership.

Use direct RPC when the reads are bounded and current-state oriented:

  • one account reading its own current balance;
  • one contract exposing a compact view function;
  • transaction simulation before a write;
  • receipt polling after a submitted transaction;
  • chain ID and network state;
  • small event ranges for recent activity.

Use indexing when the reads are historical, relational, aggregated, or product-critical:

  • user activity across long block ranges;
  • protocol-wide analytics;
  • active listings reconstructed from creates, cancels, and fills;
  • governance records across proposals, votes, delegates, and snapshots;
  • liquidation history and risk views;
  • leaderboards, rankings, and time-series charts;
  • multi-contract portfolio views.

This is not aesthetic. It is complexity placement. Either the system pays complexity once in an indexing layer, or every user pays it repeatedly through slow, fragile reads.

The implementation sequence that does not rot

A durable indexing implementation starts before frontend integration. It starts at the contract interface.

First, define the read models. Not the storage layout. The screens, API consumers, and operational monitors. If the product needs “open positions by account,” that is an entity. If risk needs “markets ordered by utilization,” that is a query. If support needs “all failed user actions,” that may require event design or transaction indexing outside a subgraph.

Second, audit the events against those read models. Every required indexed entity must have a deterministic source. If it does not, change the contract before deployment. After deployment, the workaround tax is permanent.

Third, build the subgraph or custom indexer with replay as a hard requirement. A local rebuild should produce the same entities from the same chain input. Any dependency on mutable HTTP APIs during indexing is suspect. Any floating-point conversion is suspect. Any handler that depends on processing outside chain order is suspect.

Fourth, define finality rules. Different networks have different reorg profiles and confirmation assumptions. The frontend should not present freshly indexed data as final if the underlying chain can reorganize it. Pending, confirmed, and finalized are different states.

Fifth, split frontend hooks by responsibility. A clean React stack using Wagmi and Viem might have wallet hooks, contract write hooks, indexed query hooks, and transaction status hooks. They should not collapse into one generic “Web3 data” module. Generic modules hide state boundaries.

Sixth, monitor the indexer like infrastructure. Lag by block height. Handler failures. Entity counts. Query latency. Reorg events. Backfill status. Schema version. These are production signals.

A compact implementation map looks like this:

PhaseArtifactFailure if skipped
Contract designEvents aligned to read modelsIndexer infers missing state
Index schemaEntities and relationshipsFrontend receives contract-shaped noise
Mapping logicDeterministic handlersRebuilds produce divergent state
Frontend boundarySeparate RPC and GraphQL hooksComponents perform hidden historical scans
OperationsLag and error monitoringStale data ships as live data
Migration planVersioned schemas and backfillsUpgrades break historical continuity

This is the practical stack. Contracts emit. Indexers derive. GraphQL serves. Viem and Wagmi execute live interactions. The frontend composes data without pretending one interface can do every job.

Centralized APIs versus custom indexing

Some teams place a conventional backend between the frontend and chain. That can be correct. It can also become an opaque trust layer.

A centralized API can aggregate data, hide provider complexity, enforce rate limits, and serve product-specific views. For some applications, it is faster to build and easier to operate than a decentralized indexing protocol. It also gives the operator power to filter, rewrite, delay, or fabricate responses unless clients verify against chain data.

The correct question is not whether centralized APIs are impure. The correct question is what trust boundary the product claims.

If the frontend shows non-critical analytics, a centralized cache may be acceptable. If it shows balances, liquidation risk, claimable rewards, or governance power, the data path needs auditability. Users do not need to inspect every query. But the system should retain provenance and allow independent reconstruction.

The Graph-style model gives stronger reproducibility because mappings and schemas can be inspected and replayed. A custom backend can reach the same standard, but only if it is built that way. Most are not by default.

A Web3 development company that treats indexing as backend convenience will miss this point. Indexing is not just performance. It is verifiable data access.

The standard should be blunt:

  • The UI must identify which values come from current contract reads.
  • The UI must identify which values come from indexed history.
  • The system must retain source transaction and block references for derived records.
  • The index must be rebuildable.
  • The frontend must not silently substitute centralized API data for canonical execution state.

This is how the architecture stays inspectable.

The final boundary

Custom indexing is key because blockchains are execution systems, not application databases. They provide deterministic state transitions. They do not provide arbitrary product queries with stable latency.

A production stack accepts that boundary. It does not fight it in the browser. It does not bury it inside a provider wrapper. It does not ask React components to become archive-node clients.

The durable pattern is narrow:

Contracts emit precise events. Oracles supply external facts when execution needs them. RPC clients read current state and submit transactions. Indexers reconstruct historical and derived views. GraphQL exposes those views. Frontend libraries like Viem and Wagmi handle wallet state, reads, writes, and transaction lifecycle without pretending to be databases.

The security guarantees are equally narrow.

No unbounded historical scan in the user path. No derived entity without a deterministic source. No indexed value without provenance. No oracle data treated as an index. No indexer treated as the chain.

Keep those invariants intact, and the Web3 API integration workflow remains legible. Break them, and the system still may render a page. It just cannot be trusted.

FAQ

Why is direct RPC querying insufficient for a dApp?
RPC nodes are designed to expose state transitions and logs, not to provide product-grade filtering, aggregation, or pagination. Relying on them for complex queries forces the frontend to perform slow, historical data reconstruction that scales poorly.
What is the difference between an oracle and an indexer?
An oracle brings external data into the on-chain execution environment to influence contract logic, whereas an indexer takes on-chain execution history and transforms it into queryable data for off-chain use.
How should a modern frontend handle data fetching?
A mature frontend should split reads by source: use Wagmi or Viem for live contract state and wallet interactions, and use a GraphQL client to fetch historical or derived data from an indexer.
What makes an event 'useful' for indexing?
A useful event acts as a public interface for state reconstruction by including stable identifiers, indexed fields for filtering, and sufficient data to avoid unsafe inference during historical processing.
When is it acceptable to use direct RPC instead of an indexer?
Direct RPC is appropriate for bounded, current-state tasks such as checking a user's balance, simulating a transaction, or reading a single contract value, where historical scanning is not required.

By Caleb North