Installation
This tutorial shows how to setup an Apibara project from scratch. The goal is to start indexing data as quickly as possible and to understand the basic structure of a project. By the end of this tutorial, you will have a basic indexer that streams data from two networks (Ethereum and Starknet).
Installation
This tutorial starts with a fresh Typescript project. Let's start by creating it.
mkdir my-indexer
cd my-indexer
npm init -y
After that, you can add the Apibara NPM packages.
npm add --save apibara@next @apibara/protocol@next @apibara/indexer@next
added 325 packages, and audited 327 packages in 11s
73 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Apibara Config
Your indexers' configuration goes in the apibara.config.ts
file. You can
leave the configuration empty for now.
import { defineConfig } from "apibara/config";
export default defineConfig({});
EVM Indexer
Let's create the first EVM indexer. All indexers must go in the indexers
directory and have a name that ends with .indexer.ts
or .indexer.js
.
The Apibara CLI will automatically detect the indexers in this directory and
make them available to the project.
Since Apibara is chain-agnostic, we need to add the EVM package to stream Ethereum data.
npm add --save @apibara/evm@next
added 2 packages, and audited 329 packages in 4s
73 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Then you can create the first indexer.
mkdir indexers
touch indexers/rocket-pool.indexer.ts
import { EvmStream } from "@apibara/evm";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";
export default defineIndexer(EvmStream)({
streamUrl: "https://ethereum.preview.apibara.org",
startingCursor: {
orderKey: 21_000_000n
},
filter: {
logs: [{
address: "0xae78736Cd615f374D3085123A210448E74Fc6393"
}]
},
async transform({ block }) {
const logger = useLogger();
const { logs, header } = block;
logger.log(`Block number ${header?.blockNumber}`)
for (const log of logs) {
logger.log(`Log ${log.logIndex} from ${log.address} tx=${log.transactionHash}`)
}
},
});
Notice the following:
- The indexer file exports a single indexer.
- The
defineIndexer
function takes the stream as parameter. In this case, theEvmStream
is used. This is needed because Apibara supports multiple networks with different data types. streamUrl
specifies where the data comes from. You can connect to streams hosted by us, or to self-hosted streams.startingCursor
specifies from which block to start streaming.- The
filter
specifies which data to receive. You can read more about the available data for EVM chains in the EVM documentation. - The
transform
function is called for each block. It receives the block as parameter. This is where your indexer processes the data. - The
useLogger
hook returns an indexer-specific logger.
There are more indexer options available, you can find them in the documentation (TODO: link).
Running the indexer
During development, you will use the apibara
CLI to build and run indexers. For convenience,
you should add the following scripts to your package.json
file.
{
"scripts": {
"dev": "apibara dev",
"build": "apibara build",
"start": "apibara start"
}
}
dev
: runs all indexers in development mode. Indexers are automatically reloaded and restarted when they change.build
: builds the indexers for production.start
: runs a single indexer in production mode. Notice you must first build the indexers.
Now, run the indexer in development mode.
npm run dev
> [email protected] dev
> apibara dev
✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3587 ms
✔ Restarting indexers
rocket-pool | log Block number 21000071
rocket-pool | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool | log Block number 21000097
rocket-pool | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool | log Block number 21000111
rocket-pool | log Log 589 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xa01ec6551e76364f6cf687f52823d66b1c07f7a47ce157a9cd9e441691a021f0
...
Starknet indexer
You can index data on different networks from the same project. Let's add an indexer for Starknet. Like before, you need to add the package to support the network.
npm add --save @apibara/starknet@next
added 1 package, and audited 330 packages in 5s
73 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
After that, you can implement the indexer. In this case, the indexer listens for all events emitted by the STRK staking contract.
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";
export default defineIndexer(StarknetStream)({
streamUrl: "https://starknet.preview.apibara.org",
startingCursor: {
orderKey: 900_000n,
},
filter: {
events: [{
address: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
}],
},
async transform({ block }) {
const logger = useLogger();
const { events, header } = block;
logger.log(`Block number ${header?.blockNumber}`)
for (const event of events) {
logger.log(`Event ${event.eventIndex} tx=${event.transactionHash}`)
}
},
});
You can now run the indexer. In this case, you can specify which indexer you want to run
with the --indexers
option. When the flag is omitted, all indexers are run concurrently.
npm run dev -- --indexers strk-staking
> [email protected] dev
> apibara dev --indexers=strk-staking
✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3858 ms
✔ Restarting indexers
strk-staking | log Block number 929092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 929119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
...
Production build
The apibara build
command is used to build a production version of the indexer. There are two main
changes for the production build:
- No hot code reloading is available.
- Only one indexer is started. If your project has multiple indexers, it should start them independently.
npm run build
> [email protected] build
> apibara build
✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
◐ Building 2 indexers
✔ Build succeeded!
ℹ You can start the indexers with apibara start
Once the indexers are built, you can run them in two (equivalent) ways:
- The
apibara start
command by specifying which indexer to run with the--indexer
flag. In this tutorial we are going to use this method. - Running
.apibara/build/start.mjs
with Node. This is useful when building Docker images for your indexers.
npm run start -- --indexer rocket-pool
> [email protected] start
> apibara start --indexer rocket-pool
◐ Starting indexer rocket-pool
rocket-pool | log Block number 21000071
rocket-pool | log Log 239 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0xe3b7e285c02e9a1dad654ba095ee517cf4c15bf0c2c0adec555045e86ea1de89
rocket-pool | log Block number 21000097
rocket-pool | log Log 265 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
rocket-pool | log Log 266 from 0xae78736cd615f374d3085123a210448e74fc6393 tx=0x8946aaa1ae303a19576d6dca9abe0f774709ff6c3f2de40c11dfda2ab276fbba
...
Runtime configuration & presets
Apibara provides a mechanism for indexers to load their configuration from the apibara.config.ts
file:
- Add the configuration under the
runtimeConfig
key inapibara.config.ts
. - Change your indexer's module to return a function that, given the runtime configuration, returns the indexer.
You can update the configuration to define values that are configurable by your indexer. This example is going to store the DNA stream URL and contract address in the configuration.
import { defineConfig } from "apibara/config";
export default defineConfig({
runtimeConfig: {
streamUrl: "https://starknet.preview.apibara.org",
contractAddress: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
},
});
Then update the indexer to return a function that returns the indexer. Your editor is going to show a type error
since the types of config.streamUrl
and config.contractAddress
are unknown, the next session is going to
explain how to solve that issue.
import { StarknetStream } from "@apibara/starknet";
import { defineIndexer } from "@apibara/indexer";
import { useLogger } from "@apibara/indexer/plugins/logger";
import { ApibaraRuntimeConfig } from "apibara/types";
export default function (config: ApibaraRuntimeConfig) {
return defineIndexer(StarknetStream)({
streamUrl: config.streamUrl,
startingCursor: {
orderKey: 900_000n,
},
filter: {
events: [{
address: config.contractAddress as `0x${string}`
}],
},
async transform({ block }) {
// Unchanged.
},
});
};
Typescript & type safety
You may have noticed that the CLI generates types in .apibara/types
before
building the indexers (both in development and production mode).
These types contain the type definition of your runtime configuration. You can
instruct Typescript to use them by adding the following tsconfig.json
to your
project.
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": [
"**/*.ts",
".apibara/types"
],
"exclude": [
"node_modules"
],
}
After restarting the Typescript language server you will have a type-safe runtime configuration right into your indexer!
Presets
Having a single runtime configuration is useful but not enough for real-world indexers. The CLI provides a way to have multiple "presets" and select which one to use at runtime. This is useful, for example, if you're deploying the same indexers on multiple networks where only the DNA stream URL and contract addresses change.
You can any number of presets in the configuration and use the --preset
flag to select which one to use.
For example, you can add a sepolia
preset that contains the URL of the Starknet Sepolia DNA stream.
If a preset doesn't specify a key, then the value from the root configuration is used.
import { defineConfig } from "apibara/config";
export default defineConfig({
runtimeConfig: {
streamUrl: "https://starknet.preview.apibara.org",
contractAddress: "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a" as `0x${string}`,
},
presets: {
sepolia: {
runtimeConfig: {
streamUrl: "https://starknet-sepolia.preview.apibara.org",
},
}
},
});
You can then run the indexer in development mode using the sepolia
preset.
npm run dev -- --indexers=strk-staking --preset=sepolia
> [email protected] dev
> apibara dev --indexers=strk-staking --preset=sepolia
✔ Output directory .apibara/build cleaned
✔ Types written to .apibara/types
✔ Indexers built in 3858 ms
✔ Restarting indexers
strk-staking | log Block number 100092
strk-staking | log Event 233 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 234 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 235 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Event 236 tx=0x012f8356ef02c36ed1ffddd5252c4f03707166cabcccb49046acf4ab565051c7
strk-staking | log Block number 100119
strk-staking | log Event 122 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
strk-staking | log Event 123 tx=0x01078c3bb0f339eeaf303bc5c47ea03b781841f7b4628f79bb9886ad4c170be7
...
Storing data & persisting state across restarts
All indexers implemented in this tutorial are stateless. They don't store any data to a database and if you restart them they will restart indexing from the beginning.
You can refer to our storage section to learn more about writing data to a database and persisting the indexer's state across restarts.