Skip to main content
This page is a definitive end-to-end guide into practical squid development. It uses templates to simplify the process. Check out Build an indexer from scratch for a more educational barebones approach.
Feel free to also use the template-specific sqd scripts defined in commands.json to simplify your workflow. See the sqd CLI cheatsheet for a short intro.

Prepare the environment

  • Node.js 20 or newer
  • Git
  • Squid CLI
  • Docker (if your squid will store its data to PostgreSQL)
See also the Environment setup page.

Understand your technical requirements

Consider your business requirements and find out
  1. How the data should be delivered. Options:
  2. What data should be delivered
  3. Which Substrate networks you’ll be indexing - see the supported networks page Note that you can use SQD via RPC ingestion even if your network is not listed.
  4. What exact data should be retrieved from blockchain(s)
  5. Whether you need to mix in any off-chain data

Example requirements

Suppose you want to create a BigQuery dataset with Kusama native tokens transfers.
  1. The delivery format is BigQuery.
  2. A single table with from, to and amount columns may suffice.
  3. Kusama is a Substrate chain.
  4. The required data is available from Transfer events emitted by the Balances pallet. Take a look at our Substrate data sourcing miniguide for more info on how to figure out which pallets, events and calls are necessary for your task.
  5. No off-chain data will be necessary for this task.

Start from a template

Although it is possible to compose a squid from individual packages, in practice it is usually easier to start from a template.
  • Native events emitted by Substrate-based chains
  • ink! smart contracts
  • Frontier EVM contracts on Astar and Moonbeam
After retrieving the template, prepare it for a local run:
1

Install dependencies

2

Configure v2 gateway access

The gateway-based Substrate templates above require an SQD Network data API key. Add it to the template’s existing .env file:
.env
SubstrateBatchProcessor reads SQD_API_KEY automatically when .setGateway() receives a URL.
The data API key is separate from the optional SQD Cloud deployment key used by sqd auth.
Test the template locally. The procedure varies depending on the data sink:
1

Start PostgreSQL

2

Build the squid

3

Apply the database migrations

4

Start the squid processor

You should see output that contains lines like these:
5

Start the GraphQL server

In a separate terminal, run:
Visit the GraphiQL console to verify that the GraphQL API is running.
When done, shut down and erase your database with docker compose down.

The bottom-up development cycle

The advantage of this approach is that the code remains buildable at all times, making it easier to catch issues early.
1

Regenerate task-specific utilities

Generate contract or runtime utilities for the data you need. See the detailed guidance.
2

Configure data requests

Choose the data source, filters, related data, and fields. See the detailed guidance.
3

Decode and normalize the data

Turn each batch into the records your application needs. See the detailed guidance.
4

Enrich the data when needed

Optionally add external data or direct chain state queries. See the detailed guidance.
5

Prepare the store

Define the destination schema, tables, and migrations. See the detailed guidance.
6

Persist transformed data

Write each processed batch efficiently to the selected destination. See the detailed guidance.

Regenerate the task-specific utilities

Configure the Substrate typegen at typegen.json, then regenerate the runtime-specific utilities with
See the Substrate typegen reference for the full config format, and the Substrate tutorial for a worked example. If your squid indexes ink! contracts, also generate the contract utilities with the ink! typegen.
These squids use both Substrate typegen and EVM typegen. To generate all the required utilities, configure the Substrate part, then save all relevant JSON ABIs to ./abi, then run
followed by
See the Frontier EVM guide for details.

Configure the data requests

Data requests are customarily defined at src/processor.ts. Edit the definition of const processor to
  1. Use a data source appropriate for your chain and task
    • Use a SQD Network gateway whenever it is available. RPC is still required in this case.
    • For networks without a gateway use just the RPC.
  2. Request all events and calls that your task requires, with any necessary related data (e.g. parent extrinsics).
  3. If your squid indexes any of the following: then you can use some of the specialized data requesting methods to retrieve data more selectively.
  4. Select all data fields necessary for your task (e.g. fee for extrinsics).
See the Substrate processor reference for more info. Processor config examples can be found in the tutorials:

Decode and normalize the data

Next, change the batch handler to decode and normalize your data. In templates, the batch handler is defined at the processor.run() call in src/main.ts as an inline function. Its sole argument ctx contains:
  • at ctx.blocks: all the requested data for a batch of blocks
  • at ctx.store: the means to save the processed data
  • at ctx.log: a Logger
  • at ctx.isHead: a boolean indicating whether the batch is at the current chain head
  • at ctx._chain: the means to access RPC for state calls
This structure (reference) is common for all processors; the structure of ctx.blocks items varies. Each item in ctx.blocks contains the data for the requested events, calls and, if requested, any related extrinsics; it also has some info on the block itself. See the Substrate batch context reference. Use the .is() and .decode() functions to decode the data for each runtime version, e.g. like this:
You can also decode the data of certain pallet-specific events and transactions with specialized tools:

Mix in external data and chain state call output (optional)

If you need external (i.e. non-blockchain) data in your transformation, take a look at the External APIs and IPFS page. If any of the onchain data you need is unavailable from the processor or inconvenient to retrieve with it, you can use direct storage queries.

Prepare the store

At src/main.ts, change the Database object definition to accept your output data. The methods for saving data will be exposed by ctx.store within the batch handler.
  1. Define the schema of the database (and the core schema of the OpenReader GraphQL API if it is used) at schema.graphql.
  2. Regenerate the TypeORM model classes with
    The classes will become available at src/model.
  3. Compile the models code with
  4. Ensure that the squid has access to a blank database. The easiest way to do so is to start PostgreSQL in a Docker container with
    If the container is running, stop it and erase the database with
    before issuing a docker compose up -d. The alternative is to connect to an external database. See this section to learn how to specify the connection parameters.
  5. Regenerate a migration with
You can now use the async functions ctx.store.upsert() and ctx.store.insert(), as well as various TypeORM lookup methods to access the database.See the Writing to PostgreSQL guide and the typeorm-store reference for more info.

Persist the transformed data to your data sink

Once your data is decoded, optionally enriched with external data and transformed the way you need it to be, it is time to save it.
For each batch, create all the instances of all TypeORM model classes at once, then save them with the minimal number of calls to upsert() or insert(), e.g.:
It will often make sense to keep the entity instances in maps rather than arrays to make it easier to reuse them when defining instances of other entities with relations to the previous ones.If you perform any database lookups, try to do so in batches and make sure that the entity fields that you’re searching over are indexed.See also the patterns and anti-patterns sections of the Batch processing guide.

The top-down development cycle

The bottom-up development cycle described above is convenient for initial squid development and for trying out new things, but it has the disadvantage of not having the means of saving the data ready at hand when initially writing the data decoding/transformation code. That makes it necessary to come back to that code later, which is somewhat inconvenient, for example when adding new squid features incrementally. The alternative is to do the same steps in a different order:
1

Update the store

2

Regenerate utility classes

Regenerate the task-specific utilities if the new feature requires them.
3

Update the processor configuration

4

Decode and normalize the added data

Transform the new batch items into the required records.
5

Retrieve external data when needed

6

Add the persistence code

Write the transformed records to the prepared store.

GraphQL options

Store your data to PostgreSQL, then consult Serving GraphQL for options.

Scaling up

If you’re developing a large squid, make sure to use batch processing throughout your code. A common mistake is to make handlers for individual event logs or transactions; for updates that require data retrieval that results in lots of small database lookups and ultimately in poor syncing performance. Collect all the relevant data and process it at once. You should also check the Cloud best practices page even if you’re not planning to deploy to SQD Cloud - it contains valuable performance-related tips. Many issues commonly arising when developing larger squids are addressed by the third party @belopash/typeorm-store package. Consider using it. For complete examples of complex squids take a look at the Giant Squid Explorer and Thena Squid repos.

Next steps