You don't normally write this file. Studio writes it as you add sources, tables and webhooks, and shows it under Settings to read rather than to edit. This page is here for when you want to know what Studio wrote.
It's also the file you do write by hand if you
run Nineveh yourself,
where your config lives in your own repository and is reviewed like code: nineveh init
reads it to pin the Move layouts it needs, and nineveh validate reports every problem
at the line it's on.
name: vault
network: mainnet
start_version: auto
reducers: ./vault.nineveh.ts
sources:
deposits: { event: 0xabc::vault::DepositEvent }
withdrawals: { event: 0xabc::vault::WithdrawEvent }
vaults: { resource: 0xabc::vault::Vault }
positions: { table: 0xabc::vault::Vault.positions }
state:
vaults: { mirror: vaults }
positions: { mirror: positions }
deposit_log: { log: deposits }
api: { rest: true }
webhooks:
my_backend:
url: https://myapp.example/hooks/nineveh
on: [balances.changed]balances isn't in state above. It's a fold — a running total per user, which no
single record holds — so it's declared in vault.nineveh.ts along with the handlers
that write it. Webhooks subscribe to it the same way regardless of which file declares
it.
Top level
| Key | Required | Meaning |
|---|---|---|
name |
yes | The project's name. |
network |
yes | mainnet, testnet or devnet. |
start_version |
no | auto (default) or a transaction version. nineveh init resolves auto to the first transaction that touched any of the sources' contract addresses, which is at or before their modules were published, and pins it in nineveh.lock. So nothing relevant is missed, and every build starts at the same place. |
sources |
yes | At least one source. |
state |
yes* | The mirror and log tables: copies of what the chain already holds. Optional when reducers declares the project's tables instead. |
reducers |
yes* | A reducers file, named relative to this file, holding the tables you fold yourself and the handlers that write them. Optional when state alone is enough. A project needs at least one table from one of the two. |
api |
no | rest, true by default. |
webhooks |
no | Where state changes are delivered. |
Names of the project, sources, tables and columns are lower snake case: a–z,
0–9 and _, starting with a letter, at most 63 characters. Names starting with _
are reserved for Nineveh's own columns.
Sources
Each source is one of:
| Kind | Example | Records |
|---|---|---|
event |
{ event: 0xabc::vault::DepositEvent } |
each event of that type |
resource |
{ resource: 0xabc::vault::Vault } |
each write and delete of that resource |
table |
{ table: 0xabc::vault::Vault.positions } |
each item written to or deleted from the Table, TableWithLength or SmartTable in that field |
A generic struct named without type arguments (0x1::coin::CoinStore) matches every
instantiation. With arguments (0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>), it
matches exactly that one. A table: source on a generic struct needs its type arguments.
A table: source follows exactly the tables held in that field. Nineveh learns each
table's handle when the struct holding it is written, so two tables with the same key
and value types, or another contract's table of the same types, never mix. The holding
struct must be stored as a resource or as a table value.
Two shapes a table: source can't follow yet:
BigOrderedMap. While it is small it keeps its entries inside the parent struct rather than in a table of its own, and Nineveh doesn't read those.- A table wrapped in a struct of your own —
Profile.positions.inner, wherepositionsis your type and the table is inside it. The field atable:source names has to hold the table itself.
Follow the parent with a resource: source meanwhile.
Resources are needed alongside events: many contracts expose their real state only as resource writes, and state kept in tables never appears as a resource write at all.
A project whose sources are all event: sources streams only the transactions that emit
those events. Any resource: or table: source means streaming every transaction,
because the stream can't filter on resource or table writes.
State tables
A table is built one of three ways. Two of them are copies of what the chain already holds, and they're configured here:
mirror: the latest value of a resource or a table item, one row each.log: every record a source produced, append-only, one row each.
The third is a reduce table — a fold you write, like a running total or a count per day. Those are declared in the reducers file, not here; see your own folds below.
mirror: the latest value
vaults: { mirror: vaults }Keeps the latest value of each resource (by address) or table item (by table and key)
from a resource or table source, and deletes it when the chain does. Columns come
from the source's layout: the key columns first, then one column per field of the value's
struct, typed as in Column types.
| Source | Key columns |
|---|---|
resource |
address, plus type for a generic struct named without type arguments |
table |
handle, key |
A table value that isn't a struct or an enum is stored in one value column. A field
that isn't a valid column name, or has the same name as a key column, is an error.
Build that table with reduce instead.
Enums. A Move enum value, such as a versioned V1/V2 event or resource, gets a
column per field that any of its variants declares, in the order the fields first
appear:
- A field every variant declares with the same type is typed like a struct's field.
- A field only some variants declare is nullable, and null for the others.
- A field declared with different types in different variants is
json.
An enum with more than one variant also gets a _variant column holding the value's
variant, such as "V2". Names starting with _ are Nineveh's, so no field clashes
with it.
log: every event
deposit_log: { log: deposits }One append-only row per event from an event source. The key is version and
event_index (the event's position in its transaction), followed by one column per
field of the event. An enum event gets its columns as described for
mirror.
Your own folds
Everything above is a copy of something the chain already holds. The tables that are
yours — a total per user, a count per day, a leaderboard — are reduce tables, and
they aren't written here. They're declared in the reducers file that
reducers: names, where a table's columns and the handlers that write them sit
together:
export const balances = table({
key: { user: address },
columns: { balance: u128.default(0) },
})
on(deposits, (d) => {
balances.row(d.user).balance += u128(d.amount)
})One file, both halves of the project: nineveh.yaml says what to follow and what to
serve, the reducers file says what to compute. Studio writes the first and you write
the second — and Reducers is the page for it.
Column types
| Type | Holds |
|---|---|
bool |
bool |
u8 … u256, i8 … i256 |
integers of exactly that width |
address |
address, and Object<T> (stored as its address) |
string |
0x1::string::String |
bytes |
vector<u8> |
json |
any struct, vector or other structured value |
These are the same type names the reducers file uses, so a column means the same thing whichever file declares it.
An Option<T> field fits a nullable column of T's type. Integers never widen
implicitly: put a u64 into a u128 column with an expression.
webhooks
Where Nineveh sends state changes. Each entry is a named endpoint, so several changes can share one URL, one secret and one delivery cursor:
webhooks:
my_backend:
url: https://myapp.example/hooks/nineveh
on: [balances.changed, holders.inserted] # or .updated, .deleted
rows: true # optional; see below| Key | Required | Meaning |
|---|---|---|
url |
yes | Where deliveries go. |
on |
yes | The changes it wants: <table>.changed, .inserted, .updated or .deleted. A log table only ever inserts, so .updated and .deleted on one never fire. |
rows |
no | Whether a delivery carries the changed row, not only its key. Default true. |
on may name any of the project's state tables — those state: declares and those a
reducers: file does, since both are the same kind of table once they're built.
URLs must use https; plain http is accepted only for a loopback host —
localhost, 127.0.0.1 or ::1 — so a webhook can be pointed at your own machine
while you build. Deliveries are signed with the endpoint's own secret, so URLs never
carry credentials.
rows: false sends only the key: "this row changed, come and look". That keeps
deliveries small, and it's self-correcting: however the deliveries are retried or
reordered, a fetch always returns current state.
Changing a project's webhooks never rebuilds its tables: they don't shape what's built.
YAML notes
- Duplicate keys are errors, not silent overrides.
- Booleans are exactly
trueandfalse;yes,no,onandoffare rejected. - Floating-point numbers are rejected everywhere.