Your first backend

Build a working backend against a live contract, from nothing to querying real data. Follow it top to bottom — every step produces something you can see.

The hosted service isn't live yet, so this runs Nineveh on your own machine. You need Postgres, Rust, and a free Aptos stream key from geomi.dev.

Get it running

Three commands, once:

git clone https://github.com/thewoodfish/Nineveh.git && cd Nineveh
cargo build --release -p nineveh-cli

createdb nineveh
export NINEVEH_DATABASE_URL=postgres:///nineveh
export APTOS_API_KEY_TESTNET=your_geomi_key      # https://geomi.dev

target/release/nineveh up                        # the control plane, on :4000

And in a second terminal, the dashboard:

cd studio && npm install && npm run build && npm start    # http://localhost:3000

Running Nineveh yourself explains what each of those is doing, and covers the version without a dashboard.

Pick something that is definitely busy

The hardest part of a first project is not knowing whether silence means you got it wrong or the contract is quiet. So start with something that is never quiet: the Aptos framework itself, published on every network, emitting a block event every few hundred milliseconds.

Open Studio at http://localhost:3000. On your own machine there's no sign-in.

Create the project

New project → network testnet → address 0x1 → Inspect.

Nineveh reads the contract and lists everything it could follow — for 0x1 that's around three hundred things, which is far too many for a first look.

Click None in every section, then find Events and tick NewBlockEvent (from the 0x1::block module).

Tick NewBlockEvent, not NewBlock. They are different types, and testnet only emits the first. A source that matches nothing is not an error — you get a project that runs perfectly and stays empty, which is exactly what a silent contract looks like. This is the single most common way to lose an afternoon.

Name it blocks. Choose From now on rather than the contract's whole history — starting at the tip means data in seconds instead of a long backfill.

Create backend with 1 table.

Watch it fill

The Overview shows a cursor climbing, the chain head it's chasing, and the gap between them. Within a few seconds it reads following the chain, and new_block_event gains a row every time testnet produces a block — about a dozen a second.

You now have a backend. It has a URL:

http://127.0.0.1:4000/projects/blocks

Query it

BASE=http://127.0.0.1:4000/projects/blocks

curl $BASE/v1/tables                       # what tables exist, and their columns
curl "$BASE/v1/tables/new_block_event?limit=3"

Then something more specific — the ten most recent blocks, newest first:

curl "$BASE/v1/tables/new_block_event?limit=10&order=height.desc"

Look closely at height in the response:

{ "height": "381947", "epoch": "9821" }

It's a string, not a number. Move integers go up to 256 bits and JavaScript numbers stop being exact at 2⁵³. Nineveh returns them as strings so nothing is silently rounded, which means in your app:

const height = BigInt(row.height)   // right
const height = Number(row.height)   // wrong above 9 quadrillion, silently

Watch changes arrive

In a second terminal:

curl -N "$BASE/v1/changes"

Every row change is pushed as it commits:

event: change
id: 11292175483.0
data: {"version":"11292175483","table":"new_block_event","op":"insert","key":{…},"row":{…}}

Kill it, wait a moment, then resume from where you stopped:

curl -N "$BASE/v1/changes?after=11292175483.0"

Nothing is missed. That id is how a browser resumes too — EventSource sends it automatically as Last-Event-ID.

Make a table of your own

So far the table is a log: one row per event, exactly as it arrived. That is useful, but it isn't what an app usually wants. Apps want totals, current state, per something.

That is what a reducer is for. In Studio, New state table, and pick Count per row over proposer. You get a table with one row per proposer and a count that goes up — and Studio writes it as a reducer you can read:

export const blocks_per_proposer = table({
  key:     { proposer: address },
  columns: {
    count:     u64.default(0),
    last_seen: u64.default(0),
  },
})

on(new_block_event, (r) => {
  const b = blocks_per_proposer.row(r.proposer)
  b.count += 1
  b.last_seen = tx.timestamp
})

Save it. Nineveh rebuilds the new table from the records it already has — no re-reading the chain — and your old table keeps serving the whole time. Within seconds:

curl "$BASE/v1/tables/blocks_per_proposer?order=count.desc&limit=5"

That block is the whole of Reducers, and it is where the rest of your time goes.

What you just learned

  • A source is chain data you follow. A state table is what you build from it.
  • A log table keeps every record; a reduce table folds them into something smaller and more useful.
  • Changing a reducer replays your stored records in seconds. Adding a source is the expensive one, because no history exists for something you never followed.
  • Wide integers are strings. Parse them as BigInt.

Next: Reducers, to write the fold yourself rather than picking a template.