Consume changes into PostgreSQL
This example copies USDC/WETH processor changes into PostgreSQL. It commits each change and its cursor in one database transaction, then acknowledges Leani. Restarting the consumer resumes from its acknowledgement without duplicating committed rows.
Start the matching Leani node
Follow query once, then follow through the
backfill and serve commands. Keep that node running at http://127.0.0.1:8080.
It must contain the usdc-weth-latest instance with delivery enabled.
The earlier windowed.toml event example has delivery disabled and cannot be
used for this consumer.
You also need Bun and the local SDK setup from Install Leani.
Prepare PostgreSQL
If you have PostgreSQL running, create an empty leani_example database and set
DATABASE_URL to its connection string. Alternatively, with Docker running:
docker run --name leani-example-postgres \ -e POSTGRES_PASSWORD=leani-example \ -e POSTGRES_DB=leani_example \ -p 127.0.0.1:54329:5432 -d postgres:17-alpinedocker exec leani-example-postgres pg_isready -U postgres -d leani_exampleWait until pg_isready reports that it is accepting connections. The container
is an isolated example database; use your own credentials for other deployments.
Run the consumer
In the terminal with DATABASE_URL, run:
cd "$LEANI_SOURCE"export LEANI_URL=http://127.0.0.1:8080export LEANI_PROCESSOR=usdc-weth-latestexport LEANI_CONSUMER=postgres-exampleexport LEANI_CONSUMER_CREDENTIAL="$(openssl rand -hex 32)"bun examples/sdk-postgres/index.tsThe application creates its tables and registers the named consumer with the secret you supplied. Registration does not generate this secret. Preserve it when restarting or moving to another terminal; do not generate a different secret for the same consumer identity.
Expect Consuming usdc-weth-latest into PostgreSQL; press Ctrl-C to stop.
The historical example then drains the retained changes and waits for new ones.
The runnable application consists of examples/sdk-postgres/index.ts and its
imported helper, examples/sdk-subscription/index.ts.
Inspect the result and restart
Using psql with your connection string:
psql "$DATABASE_URL" -c 'select count(*), max(sequence) from leani_applied_changes'psql "$DATABASE_URL" -c 'select processor, network, sequence from leani_cursors'curl -s http://127.0.0.1:8080/v1/processors/usdc-weth-latest/consumers/postgres-exampleFor the 1,000-block example, the checked run committed 491 changes. PostgreSQL’s
highest sequence should match Leani’s acknowledgedSequence, with lagChanges
zero after draining. A change is a processor mutation or finality transition;
it is not necessarily one row per Ethereum block.
Press Ctrl-C in the consumer terminal. Run bun examples/sdk-postgres/index.ts
again in that same terminal, keeping the exported identity and credential.
The row count and cursor should stay unchanged until new changes arrive.
Keep the transaction boundary
The destination implementation is:
class PostgresDestination implements Destination<unknown> { constructor( private readonly sql: SQL, private readonly processor: string, private readonly network: string, ) {}
async migrate(): Promise<void> { await this.sql` create table if not exists leani_applied_changes ( processor text not null, network text not null, sequence numeric(20, 0) not null, cursor text not null, operation text not null, kind text not null, entity_key text, payload jsonb, block_number bigint, primary key (processor, network, sequence) ) `; await this.sql` create table if not exists leani_cursors ( processor text not null, network text not null, cursor text not null, sequence numeric(20, 0) not null, updated_at timestamptz not null default now(), primary key (processor, network) ) `; }
async transaction( work: (transaction: DestinationTransaction<unknown>) => Promise<void>, ): Promise<void> { await this.sql.begin(async (sql) => { await work({ apply: async (change: ChangeEnvelope<unknown>) => { await sql` insert into leani_applied_changes ( processor, network, sequence, cursor, operation, kind, entity_key, payload, block_number ) values ( ${this.processor}, ${this.network}, ${change.sequence}, ${change.cursor}, ${change.operation}, ${change.kind}, ${change.key}, ${JSON.stringify(change.data)}::jsonb, ${change.block?.number ?? null} ) on conflict (processor, network, sequence) do nothing `; }, storeLeaniCursor: async (cursor: string, sequence: string) => { await sql` insert into leani_cursors ( processor, network, cursor, sequence, updated_at ) values ( ${this.processor}, ${this.network}, ${cursor}, ${sequence}, now() ) on conflict (processor, network) do update set cursor = excluded.cursor, sequence = excluded.sequence, updated_at = excluded.updated_at where leani_cursors.sequence <= excluded.sequence `; }, }); }); }}The (processor, network, sequence) key makes replay idempotent. The cursor is
stored in the same transaction as the event. A failed acknowledgement after
commit can cause redelivery; a committed event must remain safe to apply again.
This example stores a change log. When building domain tables, apply both
forward and inverse mutations to keep those tables correct through reorgs.
The reusable consumer loop explains leases,
reset handling, and commit-before-ack in detail. Stop this example’s Docker
database when finished with docker stop leani-example-postgres.