Migration guide
0.7.0
This release introduces a new schema defintion and database API, and natively supports offchain data.
Drizzle schema definition
- Import
onchainTable
and database helper functions from@ponder/core/db
- Declare tables with
onchainTable
, - Export tables from
ponder.schema.ts
- Specify primary key columns with
.primaryKey()
- Specify not null columns with
.notNull()
import { onchainTable, text, integer } from "@ponder/core/db";
export const author = onchainTable("author", {
name: text("name").primaryKey(),
age: integer("age").notNull(),
});
evmHex
andevmBigint
are special columns exported from@ponder/core/db
import { onchainTable, evmHex, evmBigint } from "@ponder/core/db";
export const account = onchainTable("account", {
address: evmHex("address").primaryKey(),
balance: evmBigint("balance").notNull(),
});
Example
import { createSchema } from "@ponder/core";
export default createSchema((p) => ({
Account: p.createTable({
id: p.hex(),
daiBalance: p.bigint(),
totalUsdValue: p.float(),
lastActiveAt: p.int(),
isAdmin: p.boolean(),
graffiti: p.string(),
extra: p.json(),
}),
}));
import {
boolean,
evmBigint,
evmHex,
integer,
json,
onchainTable,
real,
string,
} from "@ponder/core/db";
export const account = onchainTable("account", {
address: evmHex("address"),
daiBalance: evmBigint("dai_balance"),
totalUsdValue: real("total_usd_value"),
lastActiveAt: integer("last_active_at"),
isAdmin: boolean("is_admin"),
graffiti: text("grafitti"),
extra: json("extra"),
});
Updated database API
In order to provide a unified developer experience, the database API (context.db) has been updated.
- Run
pnpm codegen
to update types - Import tables from
ponder.schema.ts
import { account } from "../ponder.schema";
- Replace
.findUnique()
with.find()
- await context.db.Account.findUnique({ id: event.args.from });
+ await context.db.find(account, { address: event.args.from });
- Replace
.create()
with.insert()
- await context.db.Account.create({
- id: event.args.from,
- data: { balance: 0n },
- });
+ await context.db
+ .insert(account)
+ .values({ id: event.args.from, balance: 0n });
- Replace
.createMany()
with.insert()
- await context.db.Account.createMany({
- data: [
- { id : event.args.from, balance: 0n},
- { id : event.args.to, balance: 0n},
- ],
- });
+ await context.db
+ .insert(account)
+ .values([
+ { id : event.args.from, balance: 0n},
+ { id : event.args.to, balance: 0n},
+ ]);
- Update
.update()
- await context.db.Account.update({
- id: event.args.from,
- data: ({current}) => ({ balance: current.balance + 100n }),
- });
+ await context.db
+ .update(account, { address: event.args.from })
+ .set((row) => ({ balance: row.balance + 100n }));
- Update
.upsert()
- await context.db.Account.upsert({
- id: event.args.from,
- create: { balance: 0n },
- update: ({current}) => ({ balance: current.balance + 100n }),
- });
+ await context.db
+ .upsert(account, { address: event.args.from })
+ .insert({ balance: 0n })
+ .update((row) => ({ balance: row.balance + 100n }));
- Update
.delete()
- await context.db.Account.delete({ id: event.args.from });
+ await context.db.delete(account, { address: event.args.from });
- Use low-level SQL client for advanced queries
context.db.sql
provides a drizzle interface which can be used to make arbitrary sql queries.
import { desc } from "@ponder/core/db";
import { account } from "../ponder.schema";
ponder.on("...", ({ event, context }) => {
const result = await context.db.sql
.select()
.from(account)
.orderBy(desc(account.balance))
.limit(1);
});
Example
import { ponder } from "@/generated";
ponder.on("ERC20:Transfer", async ({ event, context }) => {
const { Account, TransferEvent } = context.db;
await Account.upsert({
id: event.args.from,
create: {
balance: BigInt(0),
isOwner: false,
},
update: ({ current }) => ({
balance: current.balance - event.args.amount,
}),
});
});
import { ponder } from "@/generated";
import { account } from "../ponder.schema";
ponder.on("ERC20:Transfer", async ({ event, context }) => {
await context.db
.upsert(account, { address: event.args.from })
.insert({ balance: 0n, isOwner: false })
.update((row) => ({
balance: row.balance - event.args.amount,
}));
});
Offchain Tables
-
Offchain tables are different from onchain tables because they:
- are not automatically created or dropped
- are not affected by reorgs
-
Offchain tables can be written in api function, but can't be written in indexing functions
onchain tables | offchain tables | |
---|---|---|
indexing functions | read + write | read |
api functions | read | read + write |
- Define offchain tables with
offchainTable()
inponder.schema.ts
import { offchainTable, text, integer } from "@ponder/core/db";
export const author = offchainTable("author", {
name: text("name").primaryKey(),
age: integer("age").notNull(),
});
- Use
offchainSchema()
to create offchain tables in a specific schema
import { offchainSchema, serial, evmHex } from "@ponder/core/db";
export const offchain = offchainSchema("offchain");
export const metadata = offchain.table("metadata", {
id: serial("id").primaryKey(),
authors:
account: evmHex("account").notNull(),
});
- Add
drizzle-kit
as a dependency - Add migration generation script to
package.json
{
"scripts": {
"generate": "drizzle-kit generate --dialect postgresql --schema ./ponder.schema.ts --out migrations"
}
}
- Run
pnpm generate
to generate migrations for offchain tables - Run
pnpm dev
orpnpm start
to automatically apply migrations
Example
import {
boolean,
evmBigint,
evmHex,
offchainSchema,
onchainTable,
serial,
} from "@ponder/core/db";
export const account = onchainTable("account", {
address: evmHex("address").primaryKey(),
balance: evmBigint("balance").notNull(),
isOwner: boolean("is_owner").notNull(),
});
export const metadata = schema.table("metadata", {
id: serial("id").primaryKey(),
account: evmHex("account").notNull(),
});
import { ponder } from "@/generated";
import { replaceBigInts } from "@ponder/core";
import { desc, eq } from "@ponder/core/db";
import { getAddress } from "viem";
import { account, metadata } from "../../ponder.schema";
ponder.get("/register/:address", async (c) => {
const account = c.req.param("address");
await c.db.insert(metadata).values({ account });
return c.text("Success", 200);
});
ponder.get("/user-balances", async (c) => {
const result = await c.db
.select({
address: account.address,
balance: account.balance,
})
.from(account)
.innerJoin(
metadata,
eq(account.address, metadata.account),
)
.orderBy(desc(account.balance))
.limit(10);
return c.json(replaceBigInts(result, (b) => formatEther(b)));
});
0.6.0
Updated viem
to >=2
This release updates the viem
peer dependency requirement to >=2
. The context.client
action getBytecode
was renamed to getCode
.
pnpm add viem@latest
Simplified Postgres schema pattern
Starting with this release, the indexed tables, reorg tables, and metadata table for a Ponder app are contained in one Postgres schema, specified by the user in ponder.config.ts
(defaults to public
). This means the shared ponder
schema is no longer used. (Note: The ponder_sync
schema is still in use).
This release also removes the view publishing pattern and the publishSchema
option from ponder.config.ts
, which may disrupt production setups using horizontal scaling or direct SQL. If you relied on the publish pattern, please get in touch on Telegram and we'll work to get you unblocked.
Added /ready
, updated /health
The new /ready
endpoint returns an HTTP 200
response once the app is ready to serve requests. This means that historical indexing is complete and the app is indexing events in realtime.
The existing /health
endpoint now returns an HTTP 200
response as soon as the process starts. (This release removes the maxHealthcheckDuration
option, which previously governed the behavior of /health
.)
For Railway users, we now recommend using /ready
as the health check endpoint to enable zero downtime deployments. If your app takes a while to sync, be sure to set the healthcheck timeout accordingly. Read the Railway deployment guide for more details.
Metrics updates
Please see the changelog for specifics.
0.5.0
hono
peer dependency
Breaking: This release adds Hono as a peer dependency. After upgrading, install hono
in your project.
pnpm add hono@latest
Introduced API functions
This release added support for API functions. Read more.
0.4.0
This release changes the location of database tables when using both SQLite and Postgres.
It does not require any changes to your application code, and does not bust the sync cache for SQLite or Postgres.
Please read the new docs on direct SQL for a detailed overview.
SQLite
Ponder now uses the .ponder/sqlite/public.db
file for indexed tables. Before, the tables were present as views in the .ponder/sqlite/ponder.db
. Now, the.ponder/sqlite/ponder.db
file is only used internally by Ponder.
Postgres
Ponder now creates a table in the public
schema for each table in ponder.schema.ts
. Before, Ponder created them as views in the ponder
schema.
Isolation while running multiple Ponder instances against the same database also works differently. Before, Ponder used a schema with a pseudorandom name if the desired schema was in use. Now, Ponder will fail on startup with an error if it cannot acquire a lock on the desired schema.
This also changes the zero-downtime behavior on platforms like Railway. For more information on how this works in 0.4
, please reference:
Postgres table cleanup
After upgrading to 0.4.x
, you can run the following Postgres SQL script to clean up stale tables and views created by 0.3.x
Ponder apps.
Note: This script could obviously be destructive, so please read it carefully before executing.
DO $$
DECLARE
view_name TEXT;
schema_name_var TEXT;
BEGIN
-- Drop all views from the 'ponder' schema
FOR view_name IN SELECT table_name FROM information_schema.views WHERE table_schema = 'ponder'
LOOP
EXECUTE format('DROP VIEW IF EXISTS ponder.%I CASCADE', view_name);
RAISE NOTICE 'Dropped view "ponder"."%"', view_name;
END LOOP;
-- Drop the 'ponder_cache' schema
EXECUTE 'DROP SCHEMA IF EXISTS ponder_cache CASCADE';
RAISE NOTICE 'Dropped schema "ponder_cache"';
-- Find and drop any 'ponder_instance_*' schemas
FOR schema_name_var IN SELECT schema_name AS schema_name_alias FROM information_schema.schemata WHERE schema_name LIKE 'ponder_instance_%'
LOOP
EXECUTE format('DROP SCHEMA IF EXISTS %I CASCADE', schema_name_var);
RAISE NOTICE 'Dropped schema "%"', schema_name_var;
END LOOP;
END $$;
0.3.0
No breaking API changes.
Moved SQLite directory
Note: This release busted the SQLite sync cache.
The SQLite database was moved from the .ponder/store
directory to .ponder/sqlite
. The old .ponder/store
directory will still be used by older versions.
Moved Postgres sync tables
Similar to SQLite, the sync tables for Postgres were moved from the public
schema to ponder_sync
. Now, Ponder does not use the public
schema whatsoever.
This change did NOT bust the sync cache; the tables were actually moved. This process emits some WARN
-level logs that you should see after upgrading.
0.2.0
Replaced p.bytes()
with p.hex()
Removed p.bytes()
in favor of a new p.hex()
primitive column type. p.hex()
is suitable for Ethereum addresses and other hex-encoded data, including EVM bytes
types. p.hex()
values are stored as bytea
(Postgres) or blob
(SQLite). To migrate, replace each occurence of p.bytes()
in ponder.schema.ts
with p.hex()
, and ensure that any values you pass into hex columns are valid hexadecimal strings. The GraphQL API returns p.hex()
values as hexadecimal strings, and allows sorting/filtering on p.hex()
columns using the numeric comparison operators (gt
, gte
, le
, lte
).
Cursor pagination
Updated the GraphQL API to use cursor pagination instead of offset pagination. Note that this change also affects the findMany
database method. See the GraphQL pagination docs for more details.
0.1.0
Config
- In general,
ponder.config.ts
now has much more static validation using TypeScript. This includes network names incontracts
, ABI event names for the contractevent
andfactory
options, and more. - The
networks
andcontracts
fields were changed from an array to an object. The network or contract name is now specified using an object property name. Thename
field for both networks and contracts was removed. - The
filter
field has been removed. To index all events matching a specific signature across all contract addresses, add a contract that specifies theevent
field without specifying anaddress
. - The
abi
field now requires an ABI object that has been asserted as const (cannot use a file path). See the ABIType documentation for more details.
Schema
- The schema definition API was rebuilt from scratch to use a TypeScript file
ponder.schema.ts
instead ofschema.graphql
. Theponder.schema.ts
file has static validation using TypeScript. - Note that it is possible to convert a
schema.graphql
file into aponder.schema.ts
file without introducing any breaking changes to the autogenerated GraphQL API schema. - Please see the
design your schema
guide for an overview of the new API.
Indexing functions
event.params
was renamed toevent.args
to better match Ethereum terminology norms.- If a contract uses the
event
option, only the specified events will be available for registration. Before, all events in the ABI were available. context.models
was renamed tocontext.db
- Now, a read-only Viem client is available at
context.client
. This client uses the same transport you specify inponder.config.ts
, except all method are cached to speed up subsequent indexing. - The
context.contracts
object now contains the contract addresses and ABIs specified inponder.config.ts
, typed as strictly as possible. (You should not need to copy addresses and ABIs around anymore, just usecontext.contracts
). - A new
context.network
object was added which contains the network name and chain ID that the current event is from.
Multi-chain indexing
- The contract
network
fieldponder.config.ts
was upgraded to support an object of network-specific overrides. This is a much better DX for indexing the same contract on multiple chains. - The options that you can specify per-network are
address
,event
,startBlock
,endBlock
, andfactory
. - When you add a contract on multiple networks, Ponder will sync the contract on each network you specify. Any indexing functions you register for the contract will now process events across all networks.
- The
context.network
object is typed according to the networks that the current contract runs on, so you can write network-specific logic likeif (context.network.name === “optimism”) { …
Vite
- Ponder now uses Vite to transform and load your code. This means you can import files from outside the project root directory.
- Vite’s module graph makes it possible to invalidate project files granularly, only reloading the specific parts of your app that need to be updated when a specific file changes. For example, if you save a change to one of your ABI files,
ponder.config.ts
will reload because it imports that file, but your schema will not reload. - This update also unblocks a path towards concurrent indexing and granular caching of indexing function results.