Getting started
By the end of this tutorial you will have ninho running on your machine, an audit event stored in PostgreSQL, and that event on screen. It takes about ten minutes. Nothing here touches a production system, and everything is thrown away at the end.
You need Go 1.23 or newer, Node 20 or newer, and podman. You do not need corvo: you will play the agent yourself with curl.
1. Get the code and the configuration
git clone https://git.rda.run/corvo/ninho
cd ninho
cp .env.example .env
.env is read by make dev and by make pg-up, and never in production. The copy you just made already works: it points at a local PostgreSQL that does not exist yet, with the ingest token development-token.
2. Start a database
make pg-up
This reads the user, password, port, and database name out of the NINHO_DSN in your .env, starts a PostgreSQL 18 container named ninho-pg, and waits until it accepts connections. The container is disposable: make pg-down at the end takes it and its data away.
3. Start ninho
make dev
Two processes come up together, the Go server and Vite. Watch the first lines:
level=WARN msg="NINHO_SESSION_KEY is empty, sessions die on restart and are not shared between instances"
level=WARN msg="NINHO_LDAP_URL is empty, the UI and the read API are open"
level=INFO msg="migration applied" name=001_init.sql
level=INFO msg="migration applied" name=002_roles.sql
level=INFO msg="ninho is up" version=0.1.0+... addr=:8080 ingest=/api/ingest
The two warnings are the point of this step. Startup told you, without being asked, that sessions are ephemeral and that reads need no sign-in. Both are correct for development and both are wrong for production, which is why they are warnings and not silence.
The migrations ran on their own. There is no separate migration command.
Open http://localhost:5173. Vite serves the UI and proxies the API to the Go server. You will land on the overview, and it will be empty, because nothing has arrived yet.
4. Send an event
In another terminal, play the agent. This is the same shape corvo sends: a JSON array of envelopes, with a bearer token.
curl -i -X POST http://localhost:8080/api/ingest \
-H 'Authorization: Bearer development-token' \
-H 'Content-Type: application/json' \
-d '[{
"v": 1,
"id": "tutorial-0001",
"host": "workstation",
"event": {
"@timestamp": "2026-01-15T10:30:00Z",
"category": "process",
"record_type": "EXECVE",
"result": "success",
"session": "42",
"summary": { "actor": { "primary": "ana" }, "action": "executed" },
"user": { "ids": { "auid": "1000", "uid": "0" } },
"process": { "exe": "/usr/bin/rm", "args": ["rm", "-rf", "/var/log/old"] }
}
}]'
You get 204 No Content. There is no response body on purpose: the agent has nothing to read, only a status code to act on.
Refresh the browser. The event is on the Events screen, and the overview now has one host and one command.
5. Look at what was stored
Click the event. The detail screen shows the extracted fields on the left and the whole raw JSON below. Both come from the same row: the columns exist so the trail can be queried and indexed, and the jsonb is the source of truth, kept whole and never edited.
Notice that the command reads rm -rf /var/log/old, and that it came from process.args. ninho never reads process.title, which the kernel truncates at exactly 128 bytes, mid-word and with no mark that a cut happened. The reason is in Reading the trail.
6. Watch the live tail
Go back to Events and turn on the live tail. Send the curl again, changing "id" to tutorial-0002 and the timestamp to something later. The new event appears on its own, without a refresh.
Now send tutorial-0002 a second time, unchanged. Nothing happens, and you still get a 204. corvo delivers at least once, so a duplicate is routine and not an error: the insert is an ON CONFLICT (id, occurred_at) DO NOTHING.
7. See the agent
Open Agents. There is one row, workstation, with the time since its last batch. This screen exists because of the failure mode that matters most in an audit trail: a wrong event is visible, but an event that stopped arriving looks exactly like a machine where nothing happened. See Reading the trail.
8. Clean up
Ctrl-C stops make dev, both processes together.
make pg-down
The container and its data are gone.
Where to go next
You have seen ingest, storage, and reading. What you have not seen is any authentication on the read side, because with no NINHO_LDAP_URL the UI and the read API are open, which is the development mode the warning told you about.
- To put this on a server: Deploy to production.
- To require a sign-in: Configure LDAP sign-in.
- To understand the shape of the thing you just ran: Architecture.