diff --git a/Cargo.lock b/Cargo.lock
index fe06723f..1ed4038c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -486,7 +486,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "c0mpute-api"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"c0mpute-proto",
@@ -501,7 +501,7 @@ dependencies = [
[[package]]
name = "c0mpute-bench"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"blake3",
@@ -512,7 +512,7 @@ dependencies = [
[[package]]
name = "c0mpute-cli"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"axum",
@@ -546,7 +546,7 @@ dependencies = [
[[package]]
name = "c0mpute-core"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"aes-gcm",
"anyhow",
@@ -568,6 +568,7 @@ dependencies = [
"c0mpute-verify",
"directories",
"flate2",
+ "futures",
"hex",
"hkdf",
"hmac",
@@ -596,7 +597,7 @@ dependencies = [
[[package]]
name = "c0mpute-doctor"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"serde",
@@ -608,7 +609,7 @@ dependencies = [
[[package]]
name = "c0mpute-envelope"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"blake3",
"bs58",
@@ -621,7 +622,7 @@ dependencies = [
[[package]]
name = "c0mpute-gateway"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"axum",
@@ -643,7 +644,7 @@ dependencies = [
[[package]]
name = "c0mpute-market"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"c0mpute-envelope",
"serde",
@@ -653,7 +654,7 @@ dependencies = [
[[package]]
name = "c0mpute-net"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"async-trait",
@@ -671,7 +672,7 @@ dependencies = [
[[package]]
name = "c0mpute-placement"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"async-trait",
@@ -692,7 +693,7 @@ dependencies = [
[[package]]
name = "c0mpute-proto"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"blake3",
"hex",
@@ -704,7 +705,7 @@ dependencies = [
[[package]]
name = "c0mpute-sealed"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"aes",
"aes-gcm",
@@ -726,7 +727,7 @@ dependencies = [
[[package]]
name = "c0mpute-secure-chat"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"aes-gcm",
"anyhow",
@@ -749,7 +750,7 @@ dependencies = [
[[package]]
name = "c0mpute-store"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"blake3",
@@ -767,7 +768,7 @@ dependencies = [
[[package]]
name = "c0mpute-transcode"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"c0mpute-proto",
@@ -780,7 +781,7 @@ dependencies = [
[[package]]
name = "c0mpute-update"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"hex",
@@ -795,7 +796,7 @@ dependencies = [
[[package]]
name = "c0mpute-verify"
-version = "0.3.6"
+version = "0.3.7"
dependencies = [
"anyhow",
"blake3",
diff --git a/Cargo.toml b/Cargo.toml
index a59cc73c..ed7ea4f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,7 @@ members = [
]
[workspace.package]
-version = "0.3.6"
+version = "0.3.7"
edition = "2024"
license = "MIT"
repository = "https://github.com/profullstack/c0mpute"
diff --git a/apps/tui/package.json b/apps/tui/package.json
index b7fe7a9c..8f3b4ff5 100644
--- a/apps/tui/package.json
+++ b/apps/tui/package.json
@@ -1,6 +1,6 @@
{
"name": "@c0mpute/tui",
- "version": "0.3.6",
+ "version": "0.3.7",
"private": true,
"type": "module",
"bin": {
diff --git a/apps/web/package.json b/apps/web/package.json
index de9fb97c..7ae4b78d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "c0mpute",
- "version": "0.3.6",
+ "version": "0.3.7",
"private": true,
"type": "module",
"scripts": {
diff --git a/apps/web/src/app/getting-started/page.tsx b/apps/web/src/app/getting-started/page.tsx
index 8bdeda43..2456a525 100644
--- a/apps/web/src/app/getting-started/page.tsx
+++ b/apps/web/src/app/getting-started/page.tsx
@@ -92,8 +92,11 @@ $ c0mpute hosting ls # sites a gateway knows, with live copy counts`}
can publish new versions: run the same command with the same{" "}
--name to update it. Any gateway serves it at{" "}
/s/<site_id>/, for example{" "}
- https://gateway.c0mpute.com/s/<site_id>/. Limits:
- 256 MiB and 10,000 files per site.
+ https://gateway.c0mpute.com/s/<site_id>/, with
+ byte ranges so video seeks. Limits: 64 GiB per free site, 4 TiB
+ with --disks auto. Sites over 1 GiB are striped over the
+ network in 64 MiB groups; run the same command again to resume an
+ interrupted publish.
point a Moshpit name at it (free to register):
diff --git a/apps/web/src/app/releases/latest.json/route.ts b/apps/web/src/app/releases/latest.json/route.ts
index d02f75ef..fcf78e57 100644
--- a/apps/web/src/app/releases/latest.json/route.ts
+++ b/apps/web/src/app/releases/latest.json/route.ts
@@ -28,7 +28,7 @@ export const dynamic = "force-dynamic";
* this in sync with the workspace version in `Cargo.toml` on each release so a
* freshly built CLI reports "already latest" rather than a phantom upgrade.
*/
-const FALLBACK_VERSION = "0.3.6";
+const FALLBACK_VERSION = "0.3.7";
const FALLBACK_MIN_REQUIRED = "0.0.1";
type Channel = "stable" | "beta" | "nightly";
diff --git a/docs/hosting.md b/docs/hosting.md
index 997a4e1d..2e3e4c6e 100644
--- a/docs/hosting.md
+++ b/docs/hosting.md
@@ -9,11 +9,21 @@ c0mpute hosting publish ./public --name my-site # first version
c0mpute hosting publish ./public --name my-site # same name again = new version
c0mpute hosting publish ./public --name my-site --domain blog.example.org
c0mpute hosting ls # sites a gateway knows, with copy counts
+c0mpute hosting get movies/a.mp4 # download one file, shows MB/s
+c0mpute hosting rm --name my-site # take it down (publishes an empty version)
```
`publish` signs the site with this machine's identity (`/buyer/identity.key`),
-seeds it, and stays up until three hosting nodes hold copies (default wait 10 min).
-Only the machine holding that key can publish new versions of the site.
+seeds it, and stays up until three hosting nodes hold copies (default wait 10 min; for a
+large site, 10 min without progress). Only the machine holding that key can publish new
+versions of the site.
+
+The folder is scanned in place: nothing is copied, and the publisher serves chunks
+straight from the files while the network takes its copies, so publishing 50 GiB of
+video does not need another 50 GiB of local disk. Hashes of unchanged files are kept in
+`/hosting/scan-cache.json`, so running the same command again is quick. That
+is also how to resume: if `publish` stops (Ctrl-C, laptop asleep), run it again. The
+record already on the network is reused and holders keep every group they have.
The site is then served by every gateway at `http://:7777/s//`.
Paths resolve like a static host: `/` and directories map to `index.html`, an
@@ -30,18 +40,113 @@ is used for misses.
| Healing | A site with fewer than 3 live copies: each hosting node ranks the candidates (rendezvous hashing on site id + peer id). The top-ranked ones fetch a copy. A node that stops announcing stops counting after ~200 s and another node takes over. Above 5 copies the lowest-ranked holder lets go. A publisher seeding its own site is a source, not a copy. |
| Serving | Any gateway: local chunks when it holds them, otherwise fetched over libp2p from the fastest live holder (measured round-trip, round-robin among equally fast ones), failing over to the next and then to the DHT. Every chunk is checked against its hash. |
-Limits per site: 1 GiB, 10,000,000 files. Up to 5,000 files the manifest is one chunk;
-above that it is a root listing parts of 4,000 files each (by first path), so a
-gateway fetches only the part a request needs. Nodes before v0.3.0 cannot read
-parted manifests and skip those sites. A node holds at most `hosting.max_bytes`
-(default 5 GiB) of other people's sites, and at most 1 GiB per owner key.
+## Limits
+
+| | |
+|---|---|
+| Site, paid (`--disks`, a v2 record) | 4 TiB |
+| Site, free (v1 record) | 64 GiB |
+| Files per site | 10,000,000 |
+| One file | 128 GiB |
+| Small sites other features publish (ad creatives, app images, podcast shows, git repos) | 1 GiB, built in memory |
+| Node budget | `[hosting] max_bytes`; `0` (default) = what it holds plus 25% of the free space above the free-disk floor |
+| Free-disk floor | a copy must leave max(10 GiB, 5% of the disk), capped at 50 GiB, free |
+| Per owner, per node | 16 GiB of free sites; sites the node is a preferred (paid) host for do not count |
```toml
# ~/.config/c0mpute/config.toml
[hosting]
-max_bytes = 5368709120
+max_bytes = 0 # 0 = follow the disk; or a fixed number of bytes
```
+The budget advertised to d1sks (`max_gib`) is the same number, so `--disks auto` only
+picks disks with room for their share of the site.
+
+Errors say which limit was hit and what to do (`--disks auto` for a free site over
+64 GiB, split the folder above 4 TiB). A hosting node logs the limit that made it
+refuse a copy (`over this node's hosting budget`, `disk nearly full`, `per-owner limit`).
+
+## Large sites: striped placement
+
+A site up to 1 GiB is replicated whole, exactly as before. Above that it is
+**striped**: its chunks are bucketed by hash into groups, one group per 64 MiB of the
+site up to 8,192 groups (512 GiB); a bigger site keeps 8,192 groups and makes each one
+bigger (512 MiB at the 4 TiB cap), so a group bitmap on the gossip stays at most 1 KiB.
+The count comes from the size in the signed record, and every group is placed on its own:
+
+* The hosting nodes that rank highest for `(site, group)` (rendezvous hashing, the same
+ answer on every node) take that group until it has three copies. No node needs the
+ whole site; a 50 GiB folder spreads over the whole network.
+* Hosting nodes gossip which groups they hold (a bitmap per site) and how many bytes
+ they can still take (`room`). Only nodes that announce `room` are counted on for a
+ group, and only when they have room for it. A group that stays short for 3 minutes
+ brings in the next-ranked node, so a chosen node that cannot take it never stalls it.
+* Healing, copy counting, garbage collection, proof-of-storage challenges (a chunk of a
+ group the holder says it holds), budgets and the free-disk floor all work per group.
+ A node holds each group as soon as it is complete, so a restart resumes group by group.
+* Paid sites: preferred hosts take every group when there are at most three of them (a
+ paid host holds the whole site), else the three that rank highest per group; each is
+ paid for its share (`size x 3 / preferred`). A multi-TiB site therefore needs disks
+ with that much room each; `--copies 5` cuts each host's share to 60% of the site.
+* `c0mpute hosting ls` and `/hosting/v1/sites` show, for a striped site, `copies` = the
+ fewest copies of any group, plus `groups`, `short_groups` and `missing_groups`.
+
+Manifests: up to 5,000 files and 1 GiB the manifest is one chunk, byte for byte what
+older nodes read. Above either, it is a root listing parts (by first path) of at most
+4,000 files and 65,536 chunk hashes each, so every part is one chunk fetch even for a
+4 TiB site; a gateway fetches only the part a request needs. Every holder of any group
+keeps the root and all parts.
+
+Groups are defined over chunk hashes only (the group of a chunk is its hash modulo the
+group count), never file contents. A gateway finds the holders of any chunk from the
+record alone, and the same layout works for a site whose chunks are ciphertext.
+
+**Older nodes** refuse records over their own cap (256 MiB before v0.3.0, 1 GiB in
+v0.3.0 to v0.3.2), so they never see a striped site: they neither hold nor serve it (a
+gateway that old answers 404 for it), and they are never counted on for a group because
+they do not announce `room`. Sites up to 1 GiB are unchanged and those nodes keep holding
+and serving them.
+
+## Streaming and Range requests
+
+Gateways answer `Range` requests on `/s//...` and on custom domains: `206 Partial
+Content` with `Content-Range`, only the chunks the range covers are fetched, and
+`Accept-Ranges: bytes` is on every file, so `