Wikielele. DNS · self-hosted DNS analytics
docs for
back to the dashboard

Architecture

The four moving parts and what passes between them.

internalsHow it is built. Never needed to run it.page 12 of 24

Four moving parts in one container: a poller, a SQLite store, a rollup pass, and a Next.js app that only ever reads. Nothing is distributed, nothing is queued, and there is no cache layer.

In plain terms

One program copies your resolver’s log into a database file, over and over, forever. A second tidies up behind it, summarising old days so they can be compressed. The website you are looking at only ever reads that file — it never asks your resolver anything while you click around.

That is the whole system. It is one container on one machine, and if you stopped it right now your network would carry on resolving DNS exactly as before.

Device

asks a question

AdGuard Home

answers, logs, forgets

Poller

every 5s, backwards

SQLite

90 days, WAL

Dashboard

reads, never writes

One query's path. The poller is the only component that talks to AdGuard Home, and it only ever reads: nothing downstream of it can change what the network resolves.

The parts#

PartRunsTalks to
Ingest workerEvery 5s, started once at bootAdGuard Home’s API, SQLite (write)Pi-hole’s API, SQLite (write)
Rollup passInside the worker tick, hourly grainsSQLite (read and write)
Retention sweepOnce a day, inside the workerSQLite (delete)
Next.js appPer requestSQLite (read only)

The provider seam#

Only one of those four parts knows which resolver you run. The worker talks to a DnsProvider, chosen once from DNS_PROVIDER, whose whole obligation is to hand back normalised entries; from the batch write onwards nothing in the codebase can tell what produced a row. That is why every analytical screen is identical on both.

The control screens are the opposite, and are declared rather than assumed. Each provider publishes a set of capabilities, and a screen whose capability is absent is hidden from the navigation and refused by its route — not rendered broken. AdGuard Home publishes nearly all of them, which is why the control section is nine screens deep here.Pi-hole publishes far fewer — it has no equivalent of AdGuard’s blocked-services catalogue or its encryption screen — so most of the control section is simply not reachable, including destinationGeo, the one analytical capability in the set.

Why one process#

The worker boots from instrumentation.ts, which Next runs once per server process, and it holds the same SQLite handle the pages read through. That is deliberate: a separate worker container would need its own connection, its own copy of the enrichment caches, and a way to tell the web process that the datasets had finished loading. On a Raspberry Pi answering one household, the coordination would cost more than it buys.

Reads never write#

Every page is a server component running a prepared statement against the store. No route handler mutates the query log, and the only writes in the entire product are ingest, the rollup pass, retention, and the handful of control routes that call the resolver’s API and then resync the local mirror.

This is why the dashboard cannot corrupt your history, and why INGEST_ENABLED=false produces a perfectly functional read-only instance against an existing database. The demo you are reading this on is exactly that, taken one step further: no worker, no database at runtime, every page prerendered. See what is different here.

SQLite, and why it is the right answer#

  • , so the worker writes while pages read, without either blocking the other.
  • synchronous=NORMAL, which fsyncs at checkpoints rather than every commit. With WAL the only loss window is the last transaction on power failure: an acceptable trade for a query log, and a large reduction in SD-card write amplification.
  • A 32MB page cache and temp_store=MEMORY, to keep rollup aggregation off the card.
  • busy_timeout=5000, so a read that lands during a write waits rather than throwing.

There is no Postgres option and there will not be one. The access pattern is one writer, a handful of readers, and aggregate scans over a single fact table. A network round trip per query would make every page slower, not faster.

The front end#

Next.js App Router, React server components by default. Client components exist only where something genuinely needs the browser: the live stream and the map (an SSE connection), the command palette, the filter bar, and the preferences popover. Charts are Recharts with a house style; the map is d3-geo projected on the server, so 105KB of geometry and the projection library never reach the browser.

See also