# Files and directories Every path nxdns reads or writes, and the mode it is created with. Source of truth: `src/cli.zig` (`DataDir`), `src/storage/querylog_schema.zig` (the preserved query-log databases), `src/filter/manager.zig` (the blocklist snapshots), `src/platform/logging.zig` (the log file). ## The data directory Default `/var/lib/nxdns`, overridable with `--data-dir DIR`. `nxdns run` and `nxdns import` create it and its parents at mode 0700 when it is missing; `nxdns check` and `nxdns export` do not create it. `export` fails if it is not there. `check` opens it only on the branch that resolved to the database, so an absent data directory is not in itself a failure: an explicit `--config FILE` never looks at the directory, and without one `check` falls back to the default configuration file, or prints "nothing to check" and exits 2 when neither source exists. No subcommand opens the directory read-only. `run`, `import`, `export` and a `check` that resolved to the database all go through the same `DataDir.openConfigDb`, which opens `config.db` read/write, chmods it to 0600, enables WAL — creating `config.db-wal` and `config.db-shm` — and then runs any pending schema migrations. So `nxdns export` and `nxdns check` write to the data directory, and on a database one schema version behind they migrate it. `export` additionally creates an empty `config.db` if the directory exists without one; `check` reaches the database branch only when `config.db` is already there. | Path | What it is | Mode | | --- | --- | --- | | `config.db` | The configuration database — the single source of truth, including `web.password_hash`. | 0600 | | `config.db-wal`, `config.db-shm` | SQLite write-ahead log and shared-memory index for `config.db`. Created when WAL is enabled, inheriting the main file's permissions. | 0600 | | `querylog.db` | The query log: every domain every client asked for. Expendable — if it is missing or unusable it is recreated empty. | 0600 | | `querylog.db-wal`, `querylog.db-shm` | WAL sidecars for `querylog.db`. | 0600 | | `querylog.db.corrupt-` | A `querylog.db` that could not be used, moved aside before an empty one was created in its place. Kept, never overwritten. | Whatever the renamed file had — no chmod reaches it | | `querylog.db.corrupt--` | The same, when the plain name is taken — `` counts from 1 and rises until the name is free. Two recreates within one second is the case it exists for. | The same | | `blocklists/` | Compiled blocklist snapshots, one subdirectory of the data directory. | 0700 | | `blocklists/.list` | Exact domains for blocklist source ``, one per line, behind a header. | 0600 | | `blocklists/.wild` | Wildcard entries for the same source. | 0600 | | `blocklists/.raw.tmp`, `.list.tmp`, `.wild.tmp` | Transient refresh state: the downloaded body and the two compile outputs before they are published by rename. | 0600 | `` is the `blocklist_sources` row id. A `.list` or `.wild` file whose id is no longer a `blocklist_sources` row is deleted by the orphan sweep; files of a live source are left alone whatever their state, and the sweep matches only the two published suffixes, so a `.tmp` file left by an interrupted refresh is not swept — the next refresh of that source overwrites it. A `querylog.db` is moved aside when it is missing nothing but usability: SQLite reports it corrupt or not a database, `PRAGMA quick_check` does not answer `ok`, or its `user_version` fingerprint does not match the schema. Only the main file is renamed — its `-wal` and `-shm` are deleted, because a stale WAL would be replayed into the fresh database. A missing `querylog.db` is created without any aside file. The rename happens inside `querylog_schema.open`, before the 0600 chmod, and that chmod names `querylog.db` and its two sidecars only — so an aside file keeps the mode the file had at rename time, which for a `querylog.db` nxdns itself created is 0600 and for one an operator put there is whatever they left it at. Nothing prunes the aside files; they accumulate until an operator removes them, and each one holds the same browsing history the live query log holds. The 0600 modes are not cosmetic. `config.db` holds the argon2id password hash and `querylog.db` holds the browsing history of every client on the LAN, so both are as sensitive as each other, and a WAL file holds the same rows as the database it belongs to. SQLite creates the main database at `0644 & ~umask`; nxdns chmods it to 0600 before enabling WAL, so the sidecars inherit 0600 rather than being created world-readable. ## The configuration file Default `/etc/nxdns/config.zon`, overridable with `--config FILE`. nxdns reads it and never writes it: it seeds an unconfigured database once and is ignored afterwards. nxdns does not create the file or its directory. `nxdns export --out FILE` writes a ZON file at mode 0600 through a temporary file and a rename. That file carries `web.password_hash`, so treat exports as secrets. Without `--out` the export goes to stdout, where permissions are the redirect's business. ## The log file Only when `logging.output = .file`. The path is `logging.file_path`, default `/var/log/nxdns/nxdns.log`, and validation requires it to be absolute. **nxdns does not create the log directory.** It must exist and be writable by the user the service runs as before the process starts; under systemd the unit's `LogsDirectory=nxdns` does that. Rotation triggers at `logging.max_size_mb` MiB and keeps `logging.max_files` files in total, the live one included, so the highest generation on disk is `logging.max_files - 1`. With the default 5 that is `nxdns.log` plus `nxdns.log.1` through `nxdns.log.4`. Rotation deletes the highest generation, renames each remaining one up by one, then renames the live file to `.1`. A `max_files` of 1 or 0 deletes the live file instead of renaming it. The directory holding the log file is also sampled by the disk monitor. | Path | What it is | Mode | | --- | --- | --- | | `logging.file_path` (default `/var/log/nxdns/nxdns.log`) | The live process log. Opened for append; created when it does not exist. | `0666 & ~umask` — nxdns never chmods it | | `.1` … `.` | Rotated generations, newest first. | Inherited from the live file they were renamed from | The log file is the one path here nxdns does not set a mode on. `config.db` and `querylog.db` are chmodded to 0600 whatever the umask; the log file is left at whatever the umask gives it. Under the shipped unit that is 0600, because `deploy/systemd/nxdns.service` sets `UMask=0077`. Run from a shell with the usual `umask 022` it is 0644, and `LogsDirectory=nxdns` leaves the directory at systemd's default 0755, so a log written there is readable by any local user. With `logging.output = .stderr` or `.syslog` nxdns writes to stderr and creates no file; under systemd the journal captures it. ## Certificate and key files `doh_server.cert_path` / `key_path` and `dot_server.cert_path` / `key_path`, conventionally under `/etc/nxdns`. nxdns reads them, never writes or creates them. Both must be readable by the user nxdns runs as; the key should be readable by its owner only, which `nxdns check` warns about when it is not. A watcher polls both files and swaps a renewed pair in without a restart. See [enable DoH and DoT](../how-to/enable-doh-and-dot.md).