aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_DESIGN.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
-rw-r--r--docs/MESHBAY_DESIGN.md86
1 files changed, 64 insertions, 22 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 33ab6fa..76c8b08 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -30,6 +30,8 @@
| Read | For |
|---|---|
| **this document** | the architecture, the trust model, and the reason each decision is what it is |
+| `QUICKSTART.md` | one machine to a working group, for somebody who has installed nothing |
+| `USERGUIDE.md` | using a group and running a node, for the person who does either |
| `MESHBAY_NODE_PROTOCOL.md` | the MNP wire format, message by message |
| `transfers-v1.md` | the transfer system's failure-mode analysis, kept because a synthesis cannot carry "every way a slot can be lost" |
| `playlists.md` | the playlist design and its interface in full, with what building it corrected (§9.10) |
@@ -38,10 +40,15 @@
| `PACKAGING-GUIDE.md`, `HTTPS.md`, `MAIL-SERVER.md`, `windows-build.md` | installation and server operations |
| `CLAUDE.md` | project conventions and the engineering lessons that govern how changes are made |
-**There is no user guide.** The one that existed described the system before
-per-node identity, named roots and the sealed wire, and was retired rather than
-repaired — a document a reader cannot tell the sound parts of is worse than none.
-Writing a new one starts here.
+**The user documentation is `QUICKSTART.md` and `USERGUIDE.md`** (written
+2026-09-18). The guide that preceded them described the system before per-node
+identity, named roots and the sealed wire, and was retired rather than repaired
+— a document a reader cannot tell the sound parts of is worse than none. The
+replacements are written from this document and from the code, address a reader
+who is not a developer, and cover Linux and the browser; Windows stays in
+`PACKAGING-GUIDE.md`. **They state what is not built as readily as what is**
+(§15.2, §15.3), which is what keeps them from drifting into the same condition
+as the one they replace.
### 0.2 Reference labels
@@ -289,9 +296,10 @@ comes from the hash binding:
| Attempts per connection | 5, then a node-wide lockout |
| Filing a request | requires the account to have at least one pinned identity already |
-`member unpin <user>` removes **every** device. `device revoke` marks one rather
-than deleting the row, because a deleted row is a key the node would happily pin
-again.
+`member unpin <user>` removes **every** device of an account, and is the only
+form the CLI has (§6.7). Revoking **one** device is `revoke_device` over MNP,
+driven from a device the node has already pinned, and it *marks* the row rather
+than deleting it — a deleted row is a key the node would happily pin again.
**Which device is on a connection** is proved separately from which account. The
handshake proves the account; `device_hello` — signed over a transcript naming the
@@ -676,7 +684,7 @@ unique `(device, nonce)` — a replay is a validly signed copy, so nothing about
signature refuses it.
**Epochs.** A new epoch is opened when, and only when, the set of devices that may
-read *future* messages shrinks: `member revoke`, `member unpin`, `device revoke`,
+read *future* messages shrinks: `member revoke`, `member unpin`, `revoke_device`,
`gek_rotate`, or an explicit `chat rotate`. Epoch 1 is opened at group load — a
group with no epoch is a group nobody can speak in.
@@ -1005,7 +1013,7 @@ decisions:
| Bound | Default | Why it exists |
|---|---|---|
-| Node-wide concurrent transfers | 8 | operator's machine |
+| Node-wide concurrent transfers | 8 **per kind** | operator's machine. Downloads and uploads are **separate pools** (`max_concurrent_downloads`, `max_concurrent_uploads`, §6.8), because the two cost different things and one queue for both makes each cap mean nothing. Streaming is a third pool again (`max_concurrent_streams`): a member watching a film is not charged a download slot |
| Per account, per group | 2 | absent means this, not "unlimited" |
| Grant deadline | 30 s | a grant nobody takes up is a slot nobody can use |
| Idle timeout | 120 s | catches a peer that vanished without the connection noticing |
@@ -1326,9 +1334,15 @@ Five protections, and they are the substance:
and `stat()` is itself case-insensitive on NTFS and exFAT, so this already holds
there;
- **strict chunk ordering**;
-- a **size cap** — 4 GB per file. There is deliberately no aggregate quota yet, and
- that gap is named in §15.3 rather than left to be discovered: a member can still
- fill the operator's disk one capped file at a time;
+- a **size cap** — 8 GB per file by default, and **the operator's to set**
+ (`max_upload_gb` in node.toml, on the Node page, or `meshbay-node transfers
+ max-size`), because the disk that fills is theirs and a Pi and a film server
+ are not the same machine. It is read from the transport context on every
+ chunk, so a change applies to an upload already running. A file past the
+ ceiling is refused at the chunk that crosses it and its `.part` is deleted.
+ There is deliberately no aggregate quota yet, and that gap is named in §15.3
+ rather than left to be discovered: a member can still fill the operator's
+ disk one capped file at a time;
- the target root must be **writable and available**, enforced by the node.
**There is no quarantine subdirectory.** A folder appearing beside the operator's
@@ -1500,26 +1514,36 @@ Two personas need different tools, and the headless one is the normal deployment
| Desktop | the desktop client's Node page |
| **Headless / SSH** | the CLI |
-**Every operation is reachable over SSH with no browser on the host.** `status`
+**Every operation is reachable over SSH with no browser on the host**, with one
+exception named in §15.3 rather than left to be discovered: **revoking a single
+device of a member is an interface action**, and the CLI's only lever is
+`member unpin`, which removes all of that account's devices at once. `status`
deliberately reads the keystore and config directly so it works while the daemon is
stopped — the state an operator is most often in, since the daemon will not stay up
before its key is linked or a group exists.
```
meshbay-node status
-meshbay-node group add <name> --dir <path> [--no-writable]
+meshbay-node group list|add|remove
meshbay-node root list|add|remove|set|eject|plug
meshbay-node gek init|rotate
meshbay-node operator pair
meshbay-node member list|invite|revoke|unpin
-meshbay-node member device list|revoke
-meshbay-node denylist show|clear
meshbay-node file list|rm
-meshbay-node chat status|prune|encrypt-history
+meshbay-node video rematch
+meshbay-node chat status|rotate|encrypt-history|prune
+meshbay-node transfers show|set|max-size|per-member
+meshbay-node denylist show|clear
meshbay-node stun list|add|remove|reset
-meshbay-node reload
+meshbay-node reload|restart-daemon
```
+**This block is a list of verbs that exist.** It carried
+`member device list|revoke` for as long as device linking has been built, and no
+such verb was ever written — `device` falls through to the username resolver and
+fails. A command list in a specification is read as a promise by whoever writes
+the next document from it, which is exactly how it was found (§15.3).
+
`member revoke`/`unpin` resolve a username against the roster and **refuse an
unknown one** rather than acting on nobody — a typo must not look like success.
Revocation tells the operator what it does *not* do: the ex-member stops receiving
@@ -1557,7 +1581,7 @@ a real capability reduction, not an oversight.
### 6.8 Node settings
-Five `[node] `settings affect what the node does rather than how it starts, and
+Eight `[node] `settings affect what the node does rather than how it starts, and
their value is invisible until something goes wrong — so they are surfaced on the
Node page:
@@ -1567,8 +1591,16 @@ Node page:
| `pair_ttl_hours` | 24 | how long an operator pairing code stays valid |
| `device_request_ttl_minutes` | 60 | how long a device request waits for approval. Comfort, not security: the code is bound to the keys by its hash |
| `max_concurrent_streams` | 8 | simultaneous video streams. One process per viewer, ~50 MB each; a slot is held for the length of a film, so this counts viewers |
+| `max_concurrent_downloads` | 8 | node-wide download leases (§5.5) |
+| `max_concurrent_uploads` | 8 | node-wide upload leases. A separate pool from downloads, because the two cost different things and one queue for both makes each cap meaningless |
+| `max_upload_gb` | 8 | the largest single upload, §6.4. Fractions are allowed, and it is read per chunk so a change reaches an upload already running |
| `transcode_incompatible_video` | true | whether browser-incompatible video is transcoded during streaming. Unlike remuxing this costs real CPU per viewer |
+`hardware_video_encode` (default true) governs whether that transcode may use the
+GPU. It is `node.toml` only and not on the page: the capability is established by
+encoding and reading the result back (`hwaccel.py`), so the setting means "use it
+if it works" and there is nothing for an operator to decide from a panel.
+
> **Turning transcoding off does not mean the same thing for every source.** A
> codec with an MSE codec string falls back to a copy and the viewer's own decoder
> decides; a codec with none has nothing to fall back to and the stream is refused,
@@ -2383,14 +2415,23 @@ it.
### 9.9 Photos
An album browser over the image files in the group's configured folders. It is
-**smaller** than Videos and Music, deliberately, in three ways:
+**smaller** than Videos and Music, deliberately, in two ways:
-- **Several root folders rather than one.**
- **One album-grid view, no mode toggle**, because there is nothing to fall back
from.
- **No third-party service at all** — there is nothing to match a photo *to*. It
already is what it is, per its own folder and filename.
+> **"Several root folders rather than one" used to be the third**, and is not a
+> difference any more. The generic `app_directories` op (§9.3) gave every
+> application a *set* of folders, so what Photos needed — a library scattered
+> across directories — is what Videos and Music get as well. The pane that
+> chooses them is the same shared widget in `multi` mode for all three; Chat's
+> is the only `single` one, because an attachment is written to exactly one
+> place. It is worth recording because the per-application shapes converging is
+> the whole point of §9.3, and this section had been left stating the
+> difference it removed.
+
An **album is a directory**, exactly as a season is a folder in Videos. Thumbnails
are node-side, orientation-corrected, and delivered through the same chunk path.
@@ -3080,13 +3121,14 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| **C4** for browser-using accounts | Open until the signed bundle opt-out ships (O3) |
| **T3** for browser users | **Accepted permanently.** Removed for native clients, and that removal's value depends on reproducible builds |
| **Hub identity pinning** (O13) | Nothing pins the hub's key. Bounded, because a substituted hub can neither read content nor ship code to a native client |
-| **Aggregate upload quota** | Per-file caps exist; a per-user or per-group total does not |
+| **Aggregate upload quota** | Per-file caps exist and the operator sets theirs (§6.4); a per-user or per-group total does not |
| **A signed upload transcript** | Ownership is recorded by the node and verifiable by nobody else (§5.4). Making it provable is a transcript the uploader signs, stored with the entry — designed in outline, not built |
| Forward secrecy in group chat | **Given up deliberately and on the record** (§4.5). If it becomes a requirement it belongs in 1:1 DM |
| Metadata at the hub | Membership, and who posted in which group and when. A known leak, not a solved problem (§7.1) |
| The exact-hash content check | Structural, not functional (§7.5) |
| **QUIC** | Off by default, and **not at parity**: it serves the index and file chunks with no transfer lease, no leaseless ceiling and no root-availability check, does its file I/O on the event loop, and returns exception text to the peer (**L3**). No client speaks it. Either it comes to parity or it goes; until then §5.1's "chat is the only gap" is the one sentence here that overstates the code |
| **The relay registry** | **Closed in the code**: `relay.RELAYS_ENABLED` is False and every `/v1/relays` route answers 503, as federation does. Nothing in the tree calls them, node or client, and §11.1 measured two ISPs with no TURN relay needed. Kept code that nothing calls is what **L7** says not to keep; it stays only as the proof-of-possession design (**AV6**) until a node needs a relay or it is deleted |
+| **Per-device revocation has no CLI** | A device is revoked over MNP (`roster.revoke_device`), from a device the node has already pinned. On a headless node the operator's only lever is `member unpin`, which removes **every** device of that account — so the per-device control the roster is built around is reachable from an interface and from nowhere else. §6.7 listed a `meshbay-node member device list\|revoke` verb that was never written, and that listing is how this was found: `USERGUIDE.md` was the first document written by reading the CLI rather than this specification, and the verb it copied out did not run |
| **Migrations run on SQLite only** | The chain reaches head and agrees with the models there (§12), which is not where it ships. **The exposure is one revision deep, not the whole chain**: every revision behind the first packaged release was development that no installation ever ran, so nothing replays them on PostgreSQL. What is unguarded is the *next* migration — a default, an index type or a constraint PostgreSQL refuses reaches a deploy without the suite saying so |
---