promise.md names Connect as a paid service for "keeping off-site backups", with
two constraints that are load-bearing rather than marketing: it never sees your
data, and every hosted service has a free equivalent in the open code. Nothing
was implemented — no endpoint, no account, no client support.
The client half turns out to be almost entirely there, because a Connect
destination is not a new kind of thing: it is a restic REST repository whose
password never leaves the machine. So `connect` resolves exactly like `rest` in
resticLocationUri — it IS one. It is a separate TYPE only so the UI can tell it
apart from a REST server someone runs themselves, which are identical on disk.
Availability is data, not code: CFG_BACKUP_CONNECT_ENDPOINT (empty) is reported
through the locations feed as connect:{available,endpoint}, and the wizard
renders from that — the option present but disabled, its panel saying what it
will be and that SFTP and S3 do the same job today. The day the service exists,
setting that one value turns it on with no release. Verified both ways.
The device code is a credential, so it goes through the secret channel as a
reference rather than travelling in the wizard payload, which is base64'd into a
world-readable task file.
Design, and what the service still has to provide, in
docs/roadmap/connect-backup-destination.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.5 KiB
LibrePortal Connect as a backup destination (design)
Status: client side built, greyed out. There is no service yet. · Scope: what Connect has to be for backups, and what already exists · Origin: "maybe add a section of adding LibrePortal's connect service" (2026-08-28)
0. What Connect is promised to be
From docs/guide/promise.md, which is where this starts and what it has to obey:
LibrePortal Connect — optional services for the tricky parts, like reaching your server from your phone or keeping off-site backups. We work like a courier carrying a sealed box: we move and store your data, but it stays locked and you hold the only key.
and:
Every paid service has a free, self-hostable equivalent in the open code.
Both of those are load-bearing here, not marketing. The first says the service must never be able to read a backup. The second says shipping Connect must not make anything worse for someone who never uses it.
1. What already exists
Almost all of it, because a Connect destination is not a new kind of thing.
| Piece | Status |
|---|---|
| Client-side encryption with a key the server never sees | ✅ restic, always on |
| A backend that stores packs over HTTP | ✅ rest location type |
| Per-location URI, engine, retention, append-only | ✅ the location model |
| A password the user keeps, escrowed locally | ✅ passwords.txt, 0600 |
| Somewhere to enter a credential without it leaking | ✅ the secret channel |
| A destination list in the wizard to hang it off | ✅ the Backups step |
| The service | ❌ does not exist |
So the client half is a REST repository with credentials in the URI. That is why
connect resolves exactly like rest in resticLocationUri — it is one.
It is a separate TYPE only so the UI can tell "LibrePortal Connect" apart from "a REST server I run myself", which are identical on disk. Anyone who wants the second can have it today, free, which is the promise's litmus test passing.
2. Shape of a Connect destination
CFG_BACKUP_LOC_<n>_TYPE=connect
CFG_BACKUP_LOC_<n>_URI=rest:https://<device>:<token>@connect.libreportal.org/v1/<repo>/
CFG_BACKUP_LOC_<n>_PASSWORD=<repository password — never sent>
The URI carries the device credentials; the PASSWORD is the encryption key and stays here. The service holds packs it cannot open. That is the sealed box, implemented rather than asserted.
The device code the user pastes is a credential, so it travels through the
secret channel (scripts/webui/webui_secret.sh) as a reference — never in the
wizard payload, which is base64'd into a world-readable task file.
3. The not-yet-available state
CFG_BACKUP_CONNECT_ENDPOINT decides it, and it is empty.
The locations feed reports connect: {available, endpoint}, and the wizard
renders from that: the option is present but disabled and labelled "not
available yet", and its panel says what it will be and — more importantly —
that SFTP and S3 do the same job today.
Deliberately data-driven: the day the service exists, setting that one value turns it on. No release, no code change. Verified by setting it and watching the option go from disabled to a working device-code field, then unsetting it.
Showing a disabled option at all is a judgement call: it advertises something that cannot be bought. It earns its place by answering the question the step otherwise raises — "is off-site a thing this can do?" — and by pointing at the free answer in the same breath.
4. What the service still has to provide
None of this is client work:
- accounts, and a device-code flow that yields a per-device credential
- a restic-compatible REST endpoint per repository, scoped to that device
- quota and retention policy the client can read, so the UI can say what is left
- revocation, so a lost machine can be cut off without rotating the repo password
5. Open questions
- Per-device or per-account credentials? Per-device is what allows revocation without re-keying every machine, but it means the service tracks devices — which is state about the user it does not otherwise need.
- Does Connect ever hold the repository password? It must not. But then a user who loses it loses the backup, and a hosted service is exactly what someone buys to avoid that responsibility. The honest answer is that it cannot be both; the escrow file and saying so plainly is the current answer.
- What does the wizard do about quota before an account exists? Nothing to show until the endpoint answers, so the step stays silent rather than guessing.