Ponder indexes EVM data with TypeScript functions and a Postgres schema
The backend framework for crypto apps
At a glance
- What is it?
- Ponder is a TypeScript framework for indexing EVM data. You declare the contracts and chains in a config file, describe the tables in a schema file, and write ordinary TypeScript functions that handle events and insert rows, and the framework generates a GraphQL API over whatever you inserted. The examples directory doubles as a feature catalogue.
- Who is it for?
- Adopt Ponder when you need a custom indexer over contract events and your application is already TypeScript, because the indexing functions are ordinary functions and the query API is generated from your schema. Do not adopt it if you need a general purpose subgraph or a chain agnostic synchronisation layer, since Ponder is deliberately a framework for custom indexing logic you write.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A TypeScript framework for reading a chain
Ponder is an open-source TypeScript framework for EVM data indexing, and the tagline the repository carries is that it is the backend framework for crypto apps.
The problem it solves is the one every crypto application hits: the chain is the source of truth, but an application cannot query a chain efficiently. Reading events directly from an RPC provider during a request is slow and expensive, and a chain does not know anything about your application's data model. So the pattern is to index the chain into a database once, and serve your application from that.
The feature list is five lines and they describe the whole pipeline. Index any contract or account on any EVM-compatible chain. Write indexed data to Postgres. Query indexed data over HTTP using GraphQL or SQL. Build rapidly with a powerful local development server. Deploy anywhere that runs Node.js or Bun.
The last one is the deployment story and it is deliberately unfussy, because the output is a Node or Bun process talking to Postgres, so it fits wherever your other services already run rather than requiring a platform of its own.
The implementation is TypeScript, the licence is MIT, the site is ponder.sh, and the last push was on 2026-10-15 with the 0.18.2 release the same day. Support is through a Telegram chat rather than a forum, and the documentation, guides and API reference live on the project site.
Scaffolding and the development server
The quickstart has six steps, and the first one is a scaffolding command.
You run create-ponder, and you are asked for a project name and, if you are using a template, which is the recommended option. After the prompts the CLI creates the project directory, installs the dependencies and initialises a git repository.
bun create ponder
# or
pnpm create ponder
# or
npm init ponder@latestThree package managers, and the npm form is the init variant rather than the create one, which is a detail worth knowing before you paste the wrong command.
The second step is the development server, and its behaviour is what makes this a pleasant framework to work in. Ponder has a development server that automatically reloads when you save changes in any project file, and it also prints console.log statements and errors encountered while running your code.
bun dev
# or
pnpm dev
# or
npm run devThe console.log part is the detail that matters. A framework that swallows the output of your indexer leaves you debugging blind, and one that prints it inline while the server reloads is a much tighter loop. Since the indexing functions are just TypeScript that runs against real data, being able to print from them and see the result immediately is what makes the workflow bearable.
The reload is on any project file, not just the ones you are editing, which covers the config and the schema as well as the handlers.
The config file names the chains and the contracts
The third step is the configuration, and it is where the chain side of the system is described.
Ponder fetches event logs for the contracts in ponder.config.ts and passes those events to the indexing functions you write.
// ponder.config.ts
import { createConfig } from "ponder";
import { BaseRegistrarAbi } from "./abis/BaseRegistrar";
export default createConfig({
chains: {
mainnet: {
id: 1,
rpc: "https://eth-mainnet.g.alchemy.com/v2/...",
},
},
contracts: {
BaseRegistrar: {
abi: BaseRegistrarAbi,
chain: "mainnet",
address: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
startBlock: 9380410,
},
},
});Three things are worth noticing in that file. The chains block names a chain with its id and an RPC url, and the url is a placeholder for whatever provider you use. The contracts block names the contract with its ABI, the chain it belongs to, its address and a startBlock.
The startBlock is the one that has real consequences. It is where indexing begins, and setting it too low means Ponder fetches a lot of history you do not need, and setting it too high means you miss the events that happened before it. For a contract that has existed for years, that number is a real decision rather than a formality.
The ABI is imported from a local file, which means the contract interface is a checked in artefact and the events you handle are the ones the type system knows about.
The schema file is the contract between your data and the API
The fourth step is the schema, and the readme is explicit about what it is for. The ponder.schema.ts file specifies the database schema, which should match the shape of your application's data model.
// ponder.schema.ts
import { onchainTable } from "ponder";
export const ensName = onchainTable("ens_name", (t) => ({
name: p.text().primaryKey(),
owner: p.text().notNull(),
registeredAt: p.integer().notNull(),
}));The shape of it is a table definition with a name, the table name as a string, and a callback that describes the columns. Each column is a typed constructor call, so p.text().primaryKey() is a text column that is the primary key, p.text().notNull() is a required text column, and p.integer().notNull() is a required integer.
The design decision is that the schema is the source of truth for the query API. Step six in the quickstart says Ponder automatically generates a GraphQL API based on your ponder.schema.ts file, so a column you add is a field you can query, and a table you define is a query you can write.
That is what makes the framework more than a set of helpers for reading events. The schema is a typed description of your application's data, and everything downstream of it, the SQL tables, the GraphQL types and the response shapes, is derived from it rather than written separately.
Indexing functions are ordinary TypeScript
The fifth step is where the actual work happens, and the readme frames it in a way that is worth repeating.
Files in the src/ directory contain indexing functions, which are TypeScript functions that process a contract event. The purpose of these functions is to write indexed data to the database.
// src/BaseRegistrar.ts
import { ponder } from "ponder:registry";
import schema from "ponder:schema";
ponder.on("BaseRegistrar:NameRegistered", async ({ event, context }) => {
const { name, owner } = event.params;
await context.db.insert(schema.ensName).values({
name: name,
owner: owner,
registeredAt: event.block.timestamp,
});
});The imports are the interesting part. The registry is a virtual module, ponder:registry, and the schema is another one, ponder:schema, so the framework injects the type of the event and the type of your tables without a runtime import.
The handler itself is a plain async function that receives an event and a context. The event carries the decoded parameters and the block, so event.params gives you the arguments of the log and event.block.timestamp gives you the time, and both are typed. The context carries the database handle, and context.db.insert(schema.ensName).values(...) is a typed insert, so a column name that does not exist in the schema is a type error rather than a runtime failure.
So the work is ordinary server code. There is no framework-specific way of writing a handler beyond registering it, and the payoff is that the data you write is queryable immediately.
The GraphQL API comes from the schema, the SQL comes from Postgres
The sixth step is querying, and the generated API is shown as a query and a response.
{
ensNames(limit: 2) {
items {
name
owner
registeredAt
}
}
}The response is a JSON object with a list of items, each with the three fields from the schema.
The query name is the plural of the table, the argument is the limit, and the fields are the columns. All of that is derived from the schema file, which is the point of the generated API being described in the readme as based on your ponder.schema.ts file.
The alternative to GraphQL is SQL over the same Postgres tables, and the feature list mentions both, which matters for a team whose reporting stack already speaks SQL. The database is an ordinary Postgres, so anything that can query Postgres can query your indexed data, whether that is the generated API, a BI tool, or a psql session.
That is a significant property of the design. The index is not locked inside the framework. It is a Postgres schema that other tools can read, with an API generated on top for convenience.
The examples directory is the feature catalogue
The top level listing shows a workspace, and the examples directory is the most useful part of it.
The examples are named by what they demonstrate. There are feature examples for accounts, API functions, blocks, call traces, factories, filters, multichain, proxies and reading a contract. There are reference examples for the token standards, ERC1155, ERC20, ERC4626 and ERC721. And there are project examples, a friendtech one and a Uniswap v3 flash one.
Then there are integration examples, with-client, with-foundry, with-nextjs, with-offchain and with-trpc, which show how Ponder sits next to a front end, a Solidity test suite, a Next.js application, an off-chain data source and a tRPC API.
The feature and reference split is a good way to read them. The reference examples are the smallest and show one thing, and the project examples are the largest and show what a finished application looks like.
The rest of the tree is the monorepo. There is a packages/ directory for the published packages, which are ponder itself, @ponder/client, @ponder/react, @ponder/utils, create-ponder and eslint-config-ponder. There is a benchmark/ directory with scripts that compare Ponder against a subgraph implementation, a simulation-test/ directory, an etc/ directory, a docs/ directory, and a pnpm-workspace.yaml with a pnpm-lock.yaml.
The toolchain is modern and specified. The workspace uses pnpm 12.10.1 and requires Node 22 or newer, formatting and linting are done with Biome, releases are managed with changesets, and the root scripts show parallel test runs across the packages, a separate bun test variant, and recursive typecheck and build steps per package.
Editorial conclusion
Adopt Ponder when you need a custom indexer over contract events and your application is already TypeScript, because the indexing functions are ordinary functions and the query API is generated from your schema. Do not adopt it if you need a general purpose subgraph or a chain agnostic synchronisation layer, since Ponder is deliberately a framework for custom indexing logic you write. Verify first that the chain and the RPC provider you plan to use are supported, that your Node version is 22 or newer since the workspace requires it, and that the GraphQL shape generated from your schema matches what your front end expects.
Frequently asked questions
What is Ponder?
It is an open-source TypeScript framework for EVM data indexing. You can index any contract or account on any EVM-compatible chain, write the indexed data to Postgres, query it over HTTP using GraphQL or SQL, and deploy anywhere that runs Node.js or Bun.
How do I start a new Ponder project?
Run create-ponder, which asks for a project name and optionally a template. You can use bun create ponder, pnpm create ponder or npm init ponder@latest, and the CLI then creates the directory, installs the dependencies and initialises a git repository.
What do the two configuration files do?
ponder.config.ts names the chains and the contracts to fetch event logs for, with the ABI, the chain, the address and a startBlock. ponder.schema.ts specifies the database schema, with tables and typed columns, and it is what the GraphQL API is generated from.
What are Ponder indexing functions?
They are ordinary TypeScript functions in the src/ directory that process a contract event and write data to the database. They are registered with ponder.on, receive a typed event and context, and insert rows through a typed context.db.insert call.
What is the Ponder development server like?
It automatically reloads when you save changes in any project file, and it prints console.log statements and errors encountered while running your code, so the loop of editing and seeing the output is a single step.
What are the requirements for running Ponder?
The workspace requires Node.js 22 or newer and pnpm 12 or newer. The project is MIT licensed open source software, and the published packages are ponder, @ponder/client, @ponder/react, @ponder/utils, create-ponder and eslint-config-ponder.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ponder-sh-ponder)Was this useful?