docs(roadmap): storage and backup locations may share a drive

Resolves open question 2. One big disk holding both the app data and its
snapshots is what most people actually have, and two facts make it cheap:
reconcile() already chowns CONTAINERS_DIR and BACKUPS_DIR to the same
container user, so there is no permission negotiation; and sibling dirs
on one filesystem don't nest, so §3 admits them today unchanged.

The registries stay separate — that was always about trust and lifecycle,
not hardware — so §1's non-goal is reworded rather than dropped.

One hard rule survives: same drive yes, nested never. A storage location
containing a backup repo is a recursive-inclusion trap, and §3's nesting
refusal already covers both directions. What needs work is the error —
pointing storage at /mnt/bigdisk when /mnt/bigdisk/backups exists fails
the empty-dir rule, and the message must suggest a subdirectory rather
than saying "not empty", because that is the likely first attempt.

Shared fate gets the §6.1 treatment: durable badge on both locations and
a line in the backup summary, stated accurately rather than moralised —
a same-drive backup still covers accidental deletion, bad updates and
ransomware; what it doesn't survive is the disk dying. Also names the
compounding case, since nobody pictures it: a shared drive that is also
removable takes the apps and the restore path away at the same moment.

Checks: free space becomes per-device (two locations on one filesystem
draw from one pool, so a growing repo can starve the apps), plus a new
shared-device check that warns and never refuses.

Notes that this makes the naming collision worse and suggests the
resolution — a Disks view with one row per device showing which roles
LibrePortal has on it, registries separate underneath.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-24 03:51:33 +01:00
parent 5efdae0167
commit 5491455290

View File

@ -18,7 +18,7 @@ Nextcloud's 4 TB of photos go on the spinning disk. Vaultwarden and the control
- ❌ A general volume manager. We don't format, partition, mount, or write `/etc/fstab`. The drive must already be mounted; we validate and use it.
- ❌ Striping/tiering/RAID-alikes. One app's data lives on exactly one location. No splitting an app across two.
- ❌ Per-*volume* placement inside an app (`./data` here, `./db` there). Location granularity is the app directory. Revisit only if a real need shows up.
- ❌ Making backup locations and storage locations the same registry. They overlap on disk but differ in trust, lifecycle and ownership (see §11, §13.2).
- ❌ Merging the backup-location and storage-location **registries**. They differ in trust, lifecycle and ownership, so they stay separate — but they may freely share a *drive*, which is a supported and expected setup (§6.2).
## 2. What already works in our favour
@ -158,8 +158,9 @@ Split them deliberately: security checks refuse, fitness checks grade. And **a f
| 6 | **Persistence across reboot**`findmnt --fstab`, else a systemd `.mount` | **loud, durable warning — never refuse** | hand-mount a disk, register it, install Nextcloud, reboot, and the apps there won't start. That's worth saying clearly and repeatedly; it isn't worth blocking (§6.1). We don't write `fstab` (§1), but we say exactly what to add |
| 7 | **Removable / hot-plug**`lsblk -o RM,HOTPLUG` | **loud, durable warning** + marker discipline | an external drive is a supported setup. The user gets told plainly what happens when it's absent — once at registration, and standing on the location afterwards |
| 8 | **Distinct device** — compare `st_dev` with the primary root | warn | same disk ⇒ the location buys nothing. Usually a misunderstanding, not an error |
| 9 | **Free space** | refuse under a floor, warn under a fraction | |
| 9 | **Free space — per *device*, not per location** | refuse under a floor, warn under a fraction | locations can share a filesystem with each other and with a backup repo (§6.2), so they draw on one pool. Sum every LibrePortal consumer on the device |
| 10 | **Encryption at rest** — is it a LUKS/dm-crypt mapping? | informational | privacy-first product; worth surfacing, never worth blocking |
| 11 | **Shared device with a backup location**`st_dev` vs every registered backup location | **warn, never refuse** | shared fate: one disk failure takes the data and its only snapshots (§6.2) |
Two more things this wants:
@ -181,6 +182,33 @@ So the obligation moves from *prevent* to *inform*, and "loud" has to mean **dur
A one-time acknowledgement at registration ("I understand apps here won't start unless this drive is mounted") is probably worth it too — it makes the choice deliberate without making it hard. That's a UX call, not an architectural one.
### 6.2 — Sharing a drive with a backup location
**Supported, deliberately.** One big disk holding both the app data and its snapshots is a completely reasonable home setup, and it's what most people will actually have. The registries stay separate (§1) — that's about trust and lifecycle, not hardware — but nothing stops the two from landing on the same device, and two facts make it cheap:
- **Ownership already matches.** `reconcile()` chowns both `CONTAINERS_DIR` and `BACKUPS_DIR` to the same container user, so a shared drive has one owner and no permission negotiation.
- **The existing nesting rule is already the right rule.** Sibling directories on one filesystem — `/mnt/bigdisk/apps` and `/mnt/bigdisk/backups` — don't nest, so §3 admits them today with no change.
**The one hard rule: same drive yes, nested never.** A storage location that *contains* a backup repo (or the reverse) is a recursive-inclusion trap — the backup engine walking a tree that holds its own repository. §3's nesting refusal already covers it in both directions and must not be relaxed for the shared-drive case. What does need work is the error: pointing storage at `/mnt/bigdisk` when `/mnt/bigdisk/backups` already exists fails the empty-directory rule, and the message must say *"use a subdirectory such as `/mnt/bigdisk/apps`"* rather than a bare "not empty" — that's the single most likely first attempt.
**The honest warning: shared fate.** One disk failure loses the data *and* the only copy of its backups. That deserves saying plainly and permanently, with the same durable-not-a-toast discipline as §6.1 — a standing badge on both the storage and backup location, and a line in the backup summary along the lines of *"3 apps have no copy off this device."*
Say it accurately, though, rather than moralising: a same-drive backup still protects against **accidental deletion, a bad update, a botched migration, and ransomware** (with append-only enabled). What it does not survive is the disk dying. Both halves are true and the user should get both — the failure it doesn't cover is specific and nameable, not a general "this is wrong."
Worth noting the compounding case explicitly, because it's the one people don't picture: if the shared drive is also *removable* (§6.1), then unplugging it takes the apps **and** the restore path away at the same moment. Neither warning implies the other; a location that is both should say both.
**Two things this changes in the checks:**
- **Free space becomes per-device, not per-location** (check 9). Two locations on one filesystem draw from one pool, so a growing restic repo can starve the apps sharing the disk — a real new failure mode where before there were two independent budgets. Account for every LibrePortal consumer on the device, keep a reserve, and warn on the *device*, not the location.
- **Add a shared-device check**`st_dev` of a candidate storage location against every registered backup location, and vice versa. Never refuse; emit the shared-fate warning above.
**Two things it enables**, both nearly free:
- the picker on either side can **offer known drives** as suggestions, with the shared-fate note inline, instead of making the user type a path twice
- the storage marker (§3.1) is a strictly better mount test than the backup subsystem's `findmnt` probe — it catches a mountpoint shadowed by a stale directory, which `findmnt` reports as mounted. A backup location on a drive that also hosts a storage location can borrow it.
**Consequence for §13.1 (naming).** Sharing makes the collision worse, not better: the user now genuinely sees "bigdisk" in two places meaning two things. The natural resolution is to stop treating the two registries as the top-level concept in the UI and introduce a **Disks** view — one row per device, showing which roles LibrePortal has on it (*app data*, *backups*, or both), free space for the device as a whole, and mount state. The registries stay separate underneath; the user just stops having to hold that distinction to understand their own hardware. Recommended, but a bigger UI call than this doc should make alone.
## 7. Surfaces — first-run wizard, config panels, CLI
**First-run wizard.** The setup wizard (`core/setup/js/setup-wizard.js`) currently runs `Experience → Identity → Domains → Recommended → Metrics`, and already has the pattern for a step that isn't always shown: Metrics is advanced-only, and `_effectiveTotalSteps()` makes the count dynamic. A **Storage** step slots in **before Recommended** — locations must exist before apps get placed on them — and follows the same conditional rule, with a better trigger:
@ -312,7 +340,7 @@ Each phase is independently shippable and independently verifiable. Phase 0 carr
## 13. Open questions
1. **Naming.** "Storage location" vs "backup location" in the same UI — is that confusing enough to want a different word (*volume*? *disk*? *pool*?) for one of them?
2. **Can a storage location double as a backup location?** Same drive, different subdirectory, two registries. Recommend keeping them separate but having the picker offer known paths. Backing an app up onto the same drive its data lives on should at least warn.
2. ~~**Can a storage location double as a backup location?**~~**resolved (2026-08-24):** yes, sharing a drive is supported. Registries stay separate, sibling directories only (never nested), with a durable shared-fate warning and per-device space accounting. See §6.2.
3. **Does the `libreportal` app itself ever get to move?** Currently pinned. If the primary root fills up, that's a reinstall — acceptable, or worth solving?
4. **Per-instance vs per-type placement.** Instances inherit the model for free, but should `instance create` offer a location up front (likely yes — "work Nextcloud on the big disk, family on the SSD" is a good demo)?
5. ~~**`--storage-dir=` install flags**~~**resolved:** build it. The wizard covers the interactive case, but unattended installs have no wizard, and the flag is a few lines once the helper exists (§7).