Subgraphs Beta
Subgraphs let you build custom views of Stacks onchain data. Define the events you care about, write TypeScript handlers that transform them into SQL rows, and secondlayer indexes everything into queryable Postgres tables — your own slice of the chain, shaped exactly how your app needs it.
Install with bun add @secondlayer/subgraphs.
Subgraphs are declarative SQL tables that auto-index blockchain activity into queryable Postgres tables. Define a schema, write event handlers in TypeScript, deploy, and query — like a materialized view over the chain.
Ensure setup (skip any step already done, use the project's package manager):
- Skill: run the `skills` npm package to install — e.g. `npx|bunx|pnpm dlx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `@secondlayer/cli` installed globally
- Auth: `secondlayer auth login`
/secondlayer Help me create a subgraph. Ask me:
1. What contract do I want to index?
2. Which events or function calls should I track?
3. What columns do I need in my tables?
Scaffold the subgraph, let me review it, then deploy.Getting started
A subgraph definition has three parts: sources (what events to listen for), a schema (what tables to create), and handlers (how to process each event into rows).
import { defineSubgraph } from "@secondlayer/subgraphs"
export default defineSubgraph({
name: "token-transfers",
version: 1,
sources: [
{ type: "stx-transfer" },
],
schema: {
transfers: {
columns: {
sender: { type: "principal", indexed: true },
recipient: { type: "principal", indexed: true },
amount: { type: "uint" },
memo: { type: "text", nullable: true },
},
},
},
handlers: {
"*": async (event, ctx) => {
await ctx.insert("transfers", {
sender: event.tx.sender,
recipient: event.data.recipient,
amount: event.data.amount,
memo: event.data.memo,
})
},
},
})Schema
Each subgraph gets its own PostgreSQL schema (subgraph_<name>). Tables are defined declaratively with typed columns. System columns are added automatically: _id, _blockHeight, _txId, _createdAt.
schema: {
balances: {
columns: {
address: { type: "principal", indexed: true },
token: { type: "text", indexed: true },
amount: { type: "uint" },
},
uniqueKeys: [["address", "token"]], // enables upsert
indexes: [["token", "amount"]], // composite index
},
}Handlers
Handlers process events into table rows. Each handler receives the event and a context object with write and read operations. Use "*" as a catch-all or match specific source keys.
handlers: {
"*": async (event, ctx) => {
// Write operations (batched, flushed atomically):
await ctx.insert("transfers", { ... })
await ctx.upsert("balances", { ... }) // requires uniqueKeys
await ctx.update("balances", { amount: 0 }, { address: "SP..." })
await ctx.delete("balances", { address: "SP..." })
// Read operations (immediate):
const row = await ctx.findOne("balances", { address: "SP..." })
const rows = await ctx.findMany("balances", { token: "usda" })
// Block/tx metadata:
ctx.blockHeight // current block
ctx.txId // current transaction
ctx.timestamp // block timestamp
ctx.sender // tx sender
},
}Querying
Once deployed, query subgraphs through the SDK or CLI. The query API supports filtering, comparison operators, ordering, pagination, and field selection.
// Via SDK
const { data, meta } = await client.subgraphs.queryTable(
"token-transfers",
"transfers",
{
sort: "_block_height",
order: "desc",
limit: 25,
offset: 0,
filters: { sender: "SP1234..." },
}
)
// Comparison operators via dot notation
const { data } = await client.subgraphs.queryTable(
"token-transfers",
"transfers",
{ filters: { "amount.gte": "1000000" } }
)
// Get row count
const { count } = await client.subgraphs.queryTableCount(
"token-transfers",
"transfers",
{ filters: { sender: "SP1234..." } }
)
// Via CLI
sl subgraphs query token-transfers transfers --sort _block_height --order desc --limit 25
sl subgraphs query token-transfers transfers --filter sender=SP1234... --countTyped client
The SDK can infer TypeScript types from your subgraph definition, giving you typed queries with autocompletion for table names, column names, and filter operators.
import { getSubgraph } from "@secondlayer/sdk"
import mySubgraph from "./subgraphs/token-transfers"
const client = getSubgraph(mySubgraph, { apiKey: "sk-sl_..." })
// Fully typed — table names, column names, where operators
const rows = await client.transfers.findMany({
where: { sender: { eq: "SP1234..." } },
orderBy: { _blockHeight: "desc" },
limit: 25,
})
const total = await client.transfers.count({
sender: { eq: "SP1234..." },
})
// Or via the SecondLayer instance
const client = new SecondLayer({ apiKey: "sk-sl_..." })
const typed = client.subgraphs.typed(mySubgraph)
const rows = await typed.transfers.findMany({ ... })Search
Enable full-text search on any text column with the search: true flag. This creates a PostgreSQL trigram index (pg_trgm) for fast fuzzy matching.
schema: {
contracts: {
columns: {
name: { type: "text", search: true },
deployer: { type: "principal", indexed: true },
},
},
}
// Query with search via REST API
const { data } = await client.subgraphs.queryTable("contracts", "contracts", {
search: "token",
})Deploy
Deploy subgraphs via the CLI. The CLI bundles your handler code with esbuild and posts it to the API. Schema changes are diffed automatically — additive changes are applied in place, breaking changes require a reindex.
# Deploy to Second Layer
sl subgraphs deploy subgraphs/token-transfers.ts
# Dev mode — watches for changes, auto-redeploys
sl subgraphs dev subgraphs/token-transfers.ts
# Force reindex (drops and recreates schema)
sl subgraphs reindex token-transfers
# Reindex from a specific block range
sl subgraphs reindex token-transfers --from 150000 --to 160000
# Scaffold a subgraph from a deployed contract's ABI
sl subgraphs scaffold SP1234...::my-contract --output subgraphs/my-contract.ts