<!-- nornir:class: history -->
# Data root — `<home>/.nornir`
## TL;DR (the whole thing in four lines)
- nornir keeps **everything in `~/.nornir`**. That's it.
- **One folder per workspace** under `~/.nornir/workspaces/<name>/`.
- The **server is identical**, just under `/home/nornir/.nornir` (because the
daemon runs as the `nornir` user).
- Run **`nornir root`** to print where it is. No env-var to set; containers just
set `$HOME`.
## The layout
nornir's data root is **derived from the running user's home directory**. ONE
rule for everyone, **no path env-vars**. Everything is **per-workspace** — there
is no global warehouse or funnel; a workspace is one self-contained folder:
```
<home>/.nornir/
workspaces/
registry.redb # the ONLY global file (the index: which workspaces exist)
<name>/ # e.g. holger, korp, matrix — one folder per workspace
git/ # member source: server clones here / fat = your external checkout
builds/
warehouse/ # THIS workspace's Iceberg warehouse
funnel/ # THIS workspace's idea→plan funnel
```
The payoff: **fat and server modes are the identical layout** — the only
difference is who fills `git/` (you check it out, or the server clones it). A
`workspace_<name>` bundles as many repos as you want, so breadth lives *inside* a
workspace, not in a global pile.
`<home>` is the **running user's** home directory:
| who runs it | `$HOME` | data root |
| --- | --- | --- |
| a login user `rickard` | `/home/rickard` | `/home/rickard/.nornir` |
| the server (system user `nornir`) | `/home/nornir` | `/home/nornir/.nornir` |
The **same code** computes both — there is no special case for the server.
## How home is resolved
`config::nornir_home()` calls the [`home`](https://crates.io/crates/home) crate's
`home_dir()`, which reads `$HOME` and falls back to `getpwuid` for the current
uid. That makes it correct for:
- a normal interactive login (`$HOME` set);
- the systemd service (`Environment=HOME=/home/nornir` in the unit);
- `sudo -H -u nornir …` (the `-H` resets `$HOME` to nornir's, and getpwuid backs
it up).
The canonical resolvers live in `src/config.rs`:
```rust
pub fn nornir_home_from(home: &Path) -> PathBuf // pure: home.join(".nornir")
pub fn nornir_home() -> PathBuf // nornir_home_from(home::home_dir())
pub fn registry_root() -> PathBuf // nornir_home().join("workspaces")
// per-workspace, on `Loaded` (keyed by the workspace_<name> in the config path):
pub fn Loaded::workspace_name(&self) -> String // "matrix" from …/workspace_matrix/…
pub fn Loaded::warehouse_root(&self) -> PathBuf // registry_root()/<name>/builds/warehouse
pub fn Loaded::funnel_root(&self) -> PathBuf // registry_root()/<name>/funnel
```
Every root/warehouse/funnel default routes through these. An explicit
`[storage].local_path` still overrides the per-workspace default (a deliberate
pin). `nornir root` prints the resolved `nornir_home()` — "where does nornir keep
its data?".
## Why no path env-vars
There used to be `NORNIR_ROOT` (registry), `NORNIR_WAREHOUSE_ROOT`, and
`NORNIR_FUNNEL_ROOT`. They are **gone**. The home rule subsumes them: a process
inherits its user's `$HOME`, so there is exactly one place data lives per user
and nothing to forget to set in sync.
**Containers** get the right path the standard way — **set the user's `$HOME`**
(or run as a user whose passwd home is what you want). No nornir-specific env var
is needed or honored.
### What's still configurable (and what isn't)
- An **absolute `[storage].local_path`** in `nornir.toml` still pins a warehouse
to an explicit path (e.g. a fast local disk) for the **embedded CLI/MCP** — that's
a deliberate per-config override, not a filesystem-root env-var.
- **The server has no warehouse override.** `NORNIR_WAREHOUSE` is **no longer read
by `nornir-server`** — the server's warehouse is `$HOME`-derived per-workspace at
`<home>/.nornir/workspaces/<name>/builds/warehouse`. (There is no install flag
left that could pin one: `install add` has no `--warehouse`, and both live
install paths hand the unit renderer no warehouse — so no
`Environment=NORNIR_WAREHOUSE=` line is written; unit tests assert its absence.
No generator cleanup is pending — the old-flag wiring was removed in
`0c240f9`/`9ea97c4`.)
- **Non-path** env-vars the *server* honors are exactly two: `NORNIR_SERVER_TOKEN`
(auth) and `NORNIR_SERVER_ADDR` (bind). `NORNIR_SERVER` is the *client*'s endpoint.
`NORNIR_POLL` / `NORNIR_MONITOR` are **no longer read** (poll cadence is the
per-workspace registry field; workspaces are registered at runtime). `NORNIR_CONFIG`
remains an **embedded-CLI/MCP** config-resolution var (the server has no boot
config). Behavior vars like `NORNIR_TEST_STALL_SECS`, `NORNIR_VIZ_*` are unaffected.
## Erasing it — THE UNINSTALL LAW
`nornir install remove` — **no flags** — erases the data root and **keeps the
identity**:
```
<home>/.nornir/
workspaces/ … ← ERASED by every uninstall
cache/, funnel/, jobs/ … ← ERASED by every uninstall
ssh/ ← KEPT (the deploy key the git remotes have registered)
token ← KEPT (the server bearer token)
```
(System scope: the key is at `<service home>/.ssh`, so the uninstall spares that
directory *and* the `nornir` account that owns it, while erasing the data root and
any other state in the home.)
**The root is resolved by scope, never hardcoded** — the same
`nornir_home_from($HOME)` rule above, with system scope reading the service
account's home out of `/etc/passwd`. Two sibling apps once erased a hardcoded
`/home/<app>` regardless of scope: on a per-user install that path did not exist,
so the uninstall wiped nothing and *printed success anyway*. An eraser and a
resolver that disagree about where state lives means one of them is lying.
That is the suite rule (`workspace_nordisk/.nornir/suite-uninstall-law.md`):
*uninstall ALWAYS kills the data and ALWAYS keeps the ssh keys* — not behind a
flag, not opt-in, not `--purge`. Destroying the identity is the one explicit
opt-in `--wipe-identity`; afterwards a reinstall mints a **new** key you must
register on every remote again. `--purge` and `--keep-identity` are deprecated
no-ops kept only so old scripts still parse. Deletes run through skidbladnir's
guarded primitive `systemd::guarded_remove_dir_all`, which refuses `.ssh`
components, real users' homes, system directories, and symlinks out of the tree.
An uninstall that removed nothing prints `NOTHING REMOVED`, never success.
## Proof
`tests/root_resolution.rs::root_is_home_derived_and_same_for_any_user` asserts
the in-process resolution equals `$HOME/.nornir`, and — gated on passwordless
`sudo -n -H -u nornir` — runs the built `nornir root` **as the server user** and
asserts it prints `/home/nornir/.nornir`. Same code, same rule, two users.