<!-- grove:last-verified v1.98 -->
Grove sharing is peer-to-peer and cryptographic. When you share a file, you create a signed grant that gives the recipient permission to decrypt and access it. No central server involved.
1. You select a file in the Files tab and click ๐ Share (โฏ menu)
2. A modal shows a search box โ type to find friend cells (only friends can receive shares)
3. Select a recipient and click Share
4. Grove creates a ShareGrant for the recipient:
5. Grant is pushed to the recipient's cell via POST /api/receive-grant
6. Manifest and chunks are pushed so the file is immediately accessible
1. Grant arrives at your cell and is verified (Ed25519 signature check)
2. File appears in your Feed tab, and received shared folders also render as playable libraries in the ๐ฅ Shared with me Smart Folder (see below)
3. If chunks are already on your cell โ file is immediately viewable
4. Click ๐ฒ Add to Grove (tap twice to confirm) to save the file: decrypts it, saves to GroveHome preserving the sender's sub-folder layout, re-ingests as your own file, and syncs to your peers. Re-ingesting content you already hold is deduped (the #174 content-identity guard returns the existing manifest instead of minting a duplicate), so Add-to-Grove / re-share won't spawn double manifests for the same plaintext.
1. Click โฏ โ ๐ซ Unshare on a shared file (or from the Feed's outgoing section)
2. Grant is deleted from their cell via POST /api/revoke-grant
3. They lose access (though if they already clicked Add to Grove, they have their own copy)
Revoke is best-effort and immediate, but hard-gated (C1 invariant): /api/revoke-grant only deletes a grant when the request's granted_by matches the authenticated X-Grove-Sender โ only the original signer of a grant may revoke it. A peer holding a valid peer-secret can no longer delete someone else's grants. Revoke also purges the cached portal thumbnail so a revoked recipient can't keep pulling it.
Grant rows also self-heal even when a revoke never reaches the other cell โ the fix that matters for self-update family cells (nook/palooza/family) that never run grove gc. On a conservative ~daily background gate (_maybe_reclaim_old_versions), each cell runs sweep_orphan_grants, which reaps any grants-table row whose manifest_hash is no longer a live manifest here. This converges both sides of a dead share over time: the sender drops its own stale row once its manifest is gone, and the recipient drops the incoming row once the sender's tombstone propagates. A grant with an empty/None manifest_hash is kept (ambiguous โ never delete a live grant on a guess).
The same gate collapses byte-identical duplicate manifests (_collapse_phantom_manifests) โ but a duplicate whose content is shared (its content hash has a grant) is never collapsed, so reclamation can't break a live share.
When sharing a file from within a folder, you can enable ๐ Auto-share new files added to this folder:
shared_folders in configYou can also open Auto-Share Settings directly from the folder โฏ menu (๐ Auto-Share).
Select multiple files with checkboxes โ batch Share button โ share all to the same recipient.
Show in Portal lets the owner make a specific file or folder accessible through their linked WAN gateway portal โ without copying keys off the cell.
1. Set up Remote Access first (Settings โ Advanced โ Remote Access)
2. In the Files tab, click โฏ on any file or folder โ ๐ Show in Portal
3. The button toggles: when active it reads ๐ โ In Portal โ remove
4. The file is now visible in your portal account's file view at the gateway
Alternatively, drop files into ~/GroveHome/Remote to auto-expose them.
Endpoint: POST /api/portal/show-in-portal
A file's filename and path live encrypted inside its manifest (see ยง7, manifest-metadata privacy), and a portal account can't decrypt them โ so previously portal-shared media showed up as a flat list of "unknown" files.
Now, when you Show-in-Portal (or otherwise share to a portal account), the grant carries the file's display filename and its GroveHome-relative path (rel_path) in plaintext, per recipient. _share_to_portal_user sends them; the gateway stores them via POST /api/share-grant (new rel_path grant column); _get_portal_manifests falls back to these when the manifest's own name/path are encrypted.
The result: the portal rebuilds the real nested folder tree (e.g. Radiohead/OK Computer/โฆ, The Office/Season 1/โฆ), so a shared folder of music or video auto-detects its media role and plays in the portal's Files view โ the same library + player the owner sees on the dashboard.
rel_path is per-recipient โ only the people you actually shared with see those folder/file names. It's not exposed to other peers or replicators (consistent with ยง7). The gateway hosting the portal does hold it, the same as it already holds the display filename./portal/stream/<manifest> and is Range-aware, so seek/scrub works over WAN.The owner dashboard has a synthetic ๐ฅ Shared with me Smart Folder โ the owner-side parity of the portal folder tree above. Received shares are grouped by sender, and a shared folder of media auto-renders there as a browsable, playable library:
source_folder / rel_path, so a shared album keeps its real nested structure (e.g. Radiohead/OK Computer/โฆ) instead of a flat dump.source_folder / rel_path are unsigned grant metadata (they can't ride inside the signed manifest โ ยง7 keeps the real path encrypted), so they are traversal-sanitized on receipt (_swm_sanitize_relpath, reduced to safe segments / a bare basename) before they reach the DB or drive any Add-to-Grove placement. This blocks a forwarding relay from rewriting the path on an otherwise validly-signed grant to escape GroveHome.
A folder can be published as a web site at /site/<slug>/. This is separate from peer-to-peer sharing โ it serves the folder contents as a rendered web page.
From inside a folder in the Files tab, look for the ๐ก serve as site link, or use โฏ โ the folder actions. This opens the Serve as Site modal:
/site/recipes/Grove has a tiered access model for site content:
{
"manifest_hash": "content_addressed_manifest_hash",
"filename": "vacation.jpg",
"file_type": "image",
"total_size": 4521984,
"encrypted_file_key": "base64_file_key_sealed_for_recipient",
"granted_by": "base64_ed25519_pubkey_of_creator",
"shared_with": "base64_ed25519_signing_pubkey_of_recipient",
"recipient_encryption_pubkey": "base64_x25519_envelope_pubkey_of_recipient",
"signature": "base64_ed25519_signature",
"granted_at": "2026-03-28T15:00:00Z",
"thumbnail": "base64_jpeg_thumbnail_or_null",
"source_peers": ["pubkey1", "pubkey2"],
"source_folder": "Photos",
"rel_path": "Summer/vacation.jpg"
}
Note the two distinct recipient keys: shared_with is the recipient's canonical Ed25519 signing identity, while recipient_encryption_pubkey is their X25519 envelope key that encrypted_file_key is sealed for. granted_by is the creator's Ed25519 pubkey. source_folder / rel_path are unsigned (see Shared with me).
signature covers manifest_hash|shared_with|granted_at|granted_by and is verified with granted_by's Ed25519 public keyToggle between views with ๐ผ๏ธ / ๐ buttons. Preference saved.
/api/revoke-grant only deletes a grant when the caller's granted_by matches the authenticated X-Grove-Sender (C1 invariant) โ only the original signer can revoke. It also purges the cached portal thumbnail.source_folder / rel_path can't ride inside the signed manifest, so they are traversal-sanitized on receipt before touching the DB or Add-to-Grove placement โ a forwarding relay can't rewrite them to escape GroveHome.