What you need first
A Nineveh project: a backend that is already following a contract and has tables in it. It gives you the two inputs on this page, and they are the only two.
1. The project's API URL — the address its tables are served from. It is everything
before /v1:
https://api.nineveh.dev/projects/myproject
Everything hangs off it: /v1/tables lists your tables — which is exactly what the
hooks read — /v1/tables/<name> reads one, and /v1/changes is the live feed. Studio's
Overview prints it under Your API, without any of those paths on the end, because this
is the part you paste.
2. An API key for that project — created in Studio under Settings, and shown once:
nvk_a1b2c3d4e5f6…
The key can only read. Not because it is a restricted kind of key — there is one kind — but because reading is all the API does: it lists your tables and returns their rows, and there is nothing else to ask it for. Your tables are written inside the backend, by reducers, and nothing out here can touch them.
Don't have a project? Make one in Studio: paste a contract address, tick what to follow, and it builds while you watch. Your first backend is that walk-through. If someone has handed you a URL and a key, you already have everything.
Scaffold it
npm create nineveh-app@latestpnpm create nineveh-app@latestyarn create nineveh-appbun create nineveh-app@latestIt asks for a folder, the URL and the key, then asks the project what tables it holds and writes an app around one of them:
leaderboard/
├─ src/
│ ├─ App.tsx a live table, built from your real columns
│ ├─ main.tsx the provider, reading .env
│ ├─ nineveh-tables.d.ts generated: every table and column, typed
│ └─ styles.css yours to replace
├─ .env your URL and key, already gitignored
└─ package.json vite, react, @nineveh/react
It installs and generates the types for you, so:
cd leaderboard && npm run devWhat you just made
A working Aptos frontend. Not a placeholder with sample data in it — the table on screen is one of yours, the columns are the ones your contract actually writes, and it is live: when the chain changes a row, the row changes and flashes.
From here it is an ordinary React app, and the two things worth doing first are both in
src/App.tsx:
- Show a different table. Change the name in
useLiveTable— the generated types will tell you which ones exist, and which columns each has. - Show something the contract never stored. A total, a count, a leaderboard. That is a reducer, written in Studio; the table it produces reads from here exactly like any other.
The rest of this page is the same thing done by hand, for an app you already have.
Point it at your project
In an app of your own, install the package:
npm install @nineveh/reactpnpm add @nineveh/reactyarn add @nineveh/reactbun add @nineveh/reactand wrap it once:
import { NinevehProvider } from "@nineveh/react";
export function App() {
return (
<NinevehProvider
project="https://api.nineveh.dev/projects/myproject"
apiKey={import.meta.env.VITE_NINEVEH_KEY}
>
<Leaderboard />
</NinevehProvider>
);
}project is the API URL from above — everything before /v1, because the hooks append
the rest themselves.
The key reaches the browser, and that is intended — it can only read, so there is nothing for it to break. Make each frontend its own in Studio and revoke it when that frontend is done; revoking one leaves the rest working.
Read a table
import { useTable } from "@nineveh/react";
function Leaderboard() {
const { data, loading, error } = useTable("sellers", {
order: ["revenue.desc", "seller"],
limit: 10,
});
if (loading) return <p>Reading…</p>;
if (error) return <p>{error.message}</p>;
return (
<ol>
{data.map((row) => (
<li key={row.seller}>
{row.seller} — {row.revenue}
</li>
))}
</ol>
);
}That is the whole loop. useTable asks the REST API one question and hands back the
answer with the three states a fetch has. The query is the REST query and no wider:
where, order, limit, offset, count.
Two columns in order because one is rarely enough — a leaderboard on revenue has ties,
and which row wins a tie decides what page two shows.
Keep it current
Change one import and the same rows follow the chain:
const { data, connected } = useLiveTable("sellers", {
order: ["revenue.desc", "seller"],
limit: 10,
});connected is whether the change feed is actually open, so a "live" dot can be honest
rather than decorative. Every live hook on a page shares one connection.
A change tells the hook that something moved; it does not carry the new rows. On a
change they are read again, which is simpler and always right — a change can reorder
rows or push one out of a limit: 10 window, and only the server knows which.
Let the compiler check your column names
The scaffolder did this for you; in your own app it is one command:
npx nineveh-types https://api.nineveh.dev/projects/myproject --key nvk_…
# wrote src/nineveh-tables.d.ts: 5 tables, 25 columnsNow a mistyped table or column name stops compiling, and every column arrives as the right type — which matters most for large numbers, where getting it wrong is silent. Nothing requires it; Types has the detail.
Where to go next
- Hooks — all five, with the query language in full.
- Types — generated declarations, and the one rule about wide integers.
- Why Nineveh — the model underneath: sources, reducers, state tables.
- Your first backend — the other half, if the project is yours to build.