How Privacy Works Today
A precise map of the existing visibility, capability, and auth machinery in Seed, including the surprises any redesign must account for.

Before redesigning anything, here is what actually exists, from a close read of the backend. The headline: Seed already has most of a permissions system, spread across five mechanisms that grew separately. The critique and synthesis build directly on this map.

The signed statements that already exist

Every permanent blob embeds the same base: {type, signer, ts, sig}, signed as canonical DAG-CBOR (backend/blob/blob.go). On top of that:

    Ref (blob_ref.go): a signed claim binding a space and path to head Change CIDs, with genesisBlob, generation, redirect, and, most important here, a visibility field. Deletion is a Ref with empty heads. A redirect is a Ref with a redirect target. A Ref is already a signed publish envelope: CID + timestamp + visibility + signature, plus tombstone and redirect semantics.

    Change (blob_change.go): deps, depth, ops. No visibility field. Changes are private by default and become public only when a public Ref reaches them through link propagation.

    Capability (blob_capability.go): issuer (signer), delegate, path, role, optional audience. Roles are exactly WRITER and AGENT (an EDITOR is a commented-out TODO). No expiry field, no revocation (RevokeCapability is a commented-out TODO). Chains resolve at depth 2 at most: owner→delegate, or owner→agent→delegate.

    Comment (blob_comment.go): carries its own visibility field, copied from the target document at creation time.

    Profile, Contact and Capability blobs are hardcoded as always public at indexing.

Visibility: one table, four rules, monotone forever

blob_visibility (blob_id, space) is the whole access model. space = 0 means public. space = N means visible to space N. Rows are seeded from the signed visibility field of a Ref or Comment, and propagated down the DAG by a four-row rule table: Change→dep, Ref→head, anything→DagPB, anything→Raw (schema.sql, index_visibility.go).

Three properties deserve a close look:

    It's a grant table that doesn't know it. (blob, space=0) means "everyone may read". (blob, space=N) means "space N's people may read". The system already thinks in audiences. It just has only two.

    It's monotone. Rows are only ever added (INSERT OR IGNORE). Once a blob has a public row it is public forever. Flipping a document private only changes the document_generations.visibility register, which gates listings. The old blobs are still served to anyone with the CID. There is no unpublish at the blob layer.

    The seeds are only Refs and Comments. Everything else inherits. So the Ref really is the sole authority on publicness. The proposal's instinct that "the publish must carry the visibility claim" describes the current design, and adds no new requirement.

Reading private content: write access in a trenchcoat

There is no read permission anywhere. Every private-read gate reuses the write-capability check:

    HTTP blockstore reads: an authenticated caller passes if they own the space or hold a root-scoped WRITER or AGENT capability (dbBlobCanCallerAccess → SQLCanWriteRootByOwnerID). A WRITER scoped to the very path being read is denied, because path-scoped caps don't count (documented pitfall, issue #618).

    API listings: dg.visibility IS NOT 'Private' OR <caller can write root>.

    P2P and Bitswap: CanPeerAccessCID accepts space owners and site servers, but through a different rule that accepts WRITER only (no AGENT) and doesn't require root scope. Two subsystems answer the same question differently.

Authentication itself is solid and layered. Browsers hold a non-extractable WebCrypto session key and receive a delegation Capability from the Vault. They sign a short-lived assertion (an ephemeral, never-published Capability with the daemon's peer ID as audience, in a ±5 min window) and exchange it for an encrypted 30-day bearer token in an httpOnly cookie. P2P peers do the same dance with ±1 min windows. The agents service already has a full SignedActionEnvelope pattern: a signed CBOR action that carries the delegating capability's CID inside the signature. The transport layer for a grants system already exists three times over. See Sign in with Seed.

Sync: private blobs do replicate

Private blobs sync to authenticated peers of the space and to the space's designated site server. RBSR reconciliation filters fingerprints so unauthorized peers can't even detect private items. Pushes temporarily allowlist specific CIDs for the receiving site server. But collaborator devices never discover each other directly: the site server is the hub. And one sharp edge: Bitswap's filter fails open when a blob has no visibility rows at all, while the HTTP blockstore fails closed on the same condition.

The cracks (each one is a design input)

    CreateRef hardcodes VisibilityPublic. The server API path can't create private refs at all. Only client-signed refs carry privacy. Open VULN-5.

    Nothing enforces "visibility only at first publish." The client signs the Ref, so a modified client can flip any doc's visibility on any publish. The server merely indexes what arrives.

    Timestamps are attacker-controlled, and security depends on them. The alive-vs-deleted decision is max(ref timestamps), and visibility is a last-writer-wins register keyed on Ref ts, with no index-time validation at all. A far-future timestamp wins forever. The rabbit-holes doc treats this as a hypothetical risk. Here it is already live.

    Privacy is opt-in per deployment. Every gate is inert unless the daemon runs -public-only. On a default desktop daemon, any local caller reads all private documents (open issue #664). The agents service HTTP API has no auth at all, and locality is the only control.

    Private docs are structurally one path segment. There are no private subtrees.

    Comment visibility never reconciles with later visibility changes on the target document. It is frozen at creation, in the signed blob.

    Raw IPFS uploads are anonymous. POST /ipfs/file-upload accepts unsigned bytes. Nothing claims them until a document links them.

    The .dagjson endpoint correctly enforces public-only and honors bearer auth. But it returns a different error message for "exists but private" than for "not found", which makes a small existence oracle.

What this map means

The proposal's three pillars land differently against reality. The envelope already exists (Ref). The read capability really does not exist: read is derived from write, root-only, with no expiry or revocation. Link propagation already exists for visibility (the four-rule table) but has never been generalized to audiences. Meanwhile the real deficits are monotone visibility, unvalidated timestamps, inconsistent gates, and opt-in enforcement, and no new blob type fixes those by itself. That framing drives the critique.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime