The Seed daemon
The Go node at the centre of every Seed installation, where it stores data, what it listens on, which flags matter, and what its local API does and does not protect.

The Seed daemon, seed-daemon, is the program that implements the Hypermedia protocol. It stores signed blobs, verifies and indexes them, keeps account keys, syncs with other nodes over libp2p, and serves everything to the apps through gRPC and HTTP. The desktop app spawns one for you. A site runs one in a container. The CLI and the agents service never need one, because they talk to a site's Seed API.

Where the code is

The daemon lives in backend/, written in Go. The entry point is backend/cmd/seed-daemon. The main packages are backend/blob (blob types, verification, the indexer), backend/api (the gRPC services), backend/hmnet (libp2p, sync, the file gateway), backend/storage (SQLite schema and migrations), backend/core (keys and signing) and backend/config (flags). The protobuf definitions are in proto/ and generate both the Go server and the TypeScript clients. Run ./dev gen after changing them.

Other binaries beside the daemon: monitord (health checks of sites), relayd (a libp2p relay), pingp2p (connectivity test), seed-sqlite and mkdb (database tooling).

What it listens on

Listener

Flag

Default

Serves

libp2p

-p2p.port

55000

peer-to-peer sync and the P2P RPCs

HTTP

-http.port

55001

gRPC-web for every service, /ipfs/*, /hm/api/config, /debug/*

gRPC

-grpc.port

55002

the same services over plain gRPC

Both gRPC and HTTP bind wildcard addresses. In default mode, runtime admission accepts only loopback socket peers; server mode bypasses admission and relies on deployment networking. The desktop app passes its own ports, listed on its page. A daemon you run yourself keeps the 55000 defaults. Every flag can also be set as an environment variable with the SEED_ prefix, so -p2p.port is SEED_P2P_PORT. SEED_DAEMON_FLAGS prepends extra flags.

Flags that matter to operators

Flag

Default

Meaning

-data-dir

~/.mtt

Where everything is stored. The desktop app passes its own data directory.

-server-mode

false

Trust the deployment network and filter anonymous private reads. Hosted sites and gateways use this mode; the Docker image sets SEED_SERVER_MODE=true.

-api.allowed-origins

empty

Additional exact browser origins accepted in default mode.

-keystore-dir

empty

Use a file-based keystore in place of the OS keychain or vault. It is marked insecure. Sites use it inside their container.

-p2p.testnet-name

empty

Joins a named testnet in place of mainnet by adding a suffix to the protocol id.

-p2p.no-relay

false

Disables circuit relay.

-p2p.bootstrap-peers

the Seed gateways and the public IPFS bootstrap peers

The peers a fresh node connects to first.

-syncing.no-discovery

false

Refuse to fetch content the node does not have, turning off discovery.

-syncing.no-pull

false

Disable the sync scheduler, which also runs every DiscoverEntity request and subscription rerun.

-storage.sqlite-soft-heap-limit

0

Bytes of heap memory SQLite may hold across all connections. Above it page caches recycle their oldest pages instead of growing; nothing fails. 0 means no limit. See Memory.

-log-level

info

debug, info, warning or error.

Network lists every timeout and constant. Self-hosting shows the flags a site container uses.

What it stores

The data directory holds three things.

    keys/libp2p_id_ed25519 is the device key. It is separate from every account key.

    vault.json is the encrypted local keystore for account keys. A secret in the OS keychain decrypts it.

    db/db.sqlite is the SQLite database. It holds every blob, the index derived from them, full-text search, and the domain store.

The schema in backend/storage/schema.sql is the source of truth, and migrations only go forward. Reindexing rebuilds everything derived from the blobs and can take a long time on a large node. A debug flag forces a reindex with profiling.

Load and the read pool

Every read RPC takes a connection from the SQLite read pool before it runs. The pool has max(NumCPU, 12) connections, set in storage.go. A query that holds a connection for seconds removes that share of the node's read capacity for every other caller, so a call that costs microseconds of CPU can wait many seconds for a connection. When a node is pinned, measure cost per call rather than request volume. A CPU profile from /debug/pprof/profile?seconds=20 names the RPC that holds the connections, /debug/sqlite shows the pool, and counting calls by method over the same window gives the cost of one call. Clients must not enumerate every page of an expensive listing such as ListCitations for a heavily cited document. The web app caps those at one page. A restart only drains the backlog for a few minutes. Treat a fix as verified only after a long window of real traffic, never from the quiet minutes after a restart. The saturation of hyper.media on 2026-08-11 is written up in daemon-saturation-incident.md in the repository history.

Memory

Most of the daemon's resident memory is C heap, not Go. Every pooled connection keeps a private SQLite page cache of up to 256 MiB (cache_size in sqlite.go), and the same budget bounds the in-memory sort buffer of each running ORDER BY or GROUP BY. SQLite frees that memory promptly: in WAL mode a reader drops its whole cache when it starts a read after a commit, and a sort buffer goes with its statement. glibc does not pass the freed chunks back to the kernel, though. It keeps them in the arena they came from and only trims the top of each heap, and because cgo calls run on whichever thread is free the chunks spread over dozens of arenas, so the process ratchets up to the sum of every arena's peak. On Linux the daemon therefore calls malloc_trim(0) once a minute, which releases whole free pages from every arena and touches nothing still in use. /debug/sqlite shows SQLite's heap, its peak and every connection's page cache with hit and miss counts, and /debug/malloc shows the resident set, glibc's totals, the trim record and the per-arena report. -storage.sqlite-soft-heap-limit caps the live total: above it page caches recycle their oldest pages instead of growing. The hyper.media daemon was OOM-killed twice in October 2026 at 10 to 11 GB resident with under 1 GB of Go heap; that is the case these pages are for.

The HTTP surface

Besides gRPC-web, the HTTP port serves the file gateway and a few utility routes.

    GET /ipfs/<cid> returns a file or block. If the node does not have it, the daemon searches the network for up to a minute. GET /ipfs/<cid>.dagjson decodes a DAG-CBOR blob and pretty-prints it. It is the quickest way to inspect a change, ref or comment. POST /ipfs/file-upload chunks a file into UnixFS and returns its CID. POST /ipfs/<cid> stores one raw block. Both uploads accept up to 150 MiB and require no authentication. See Files.

    GET /hm/api/config on the daemon returns its peer id, addresses and protocol id. The web app's version of the same route adds the registered account.

    GET /debug/version reports the build. All /debug/* pages share the ordinary API admission policy. They cover metrics, pprof, the p2p and network reports, the SQLite pool and its memory, the C allocator and an embedded grpcui.

Authentication, plainly

In default mode, the daemon admits only loopback socket peers and trusts local tools after admission: they can read everything it holds and write as any key it keeps, because signing happens inside the daemon. HTTP requires the Host hostname to be exactly localhost or a literal loopback IP and checks browser fetch metadata and exact origins; media and frames without Origin use the exact Referer origin. -api.allowed-origins registers additional trusted browser origins, including the desktop renderer's origin, without widening peer or Host admission.

Server mode (-server-mode, or SEED_SERVER_MODE=true) trusts the deployment network instead of applying default-mode ingress checks. Keep the daemon's HTTP and gRPC API ports private, behind the web app, and proxy only the public /ipfs/* routes. Anonymous private reads are filtered; authorized bearer callers may read private content. Bearer tokens never gate writes. The Docker image enables server mode through its environment. Integrity explains the trust model.

Working with it

In the Seed app

The desktop app starts the daemon, waits for /debug/version, and then starts its own API bridge and the local agents server. Its settings show the daemon's peer id and addresses.

CLI

The CLI does not need a local daemon. seed-cli space dev talks to the desktop's API bridge and the daemon's HTTP port to publish a folder into the running app; see Publish a folder.

SDK

The SDK targets the Seed API. It does not call the daemon. The daemon's protobuf messages are available to TypeScript through the generated clients in @shm/shared.

Web API

The web app answers every site's /api/<Key> routes by calling the daemon over gRPC-web. Over plain HTTP, the daemon itself serves only /ipfs and /hm/api/config. Building with gRPC lists the services for direct callers.

Agents

Seed Agents read and write through a site's Seed API and fetch media from the daemon's /ipfs route. In the desktop app they use the bundled daemon through the API bridge. Hosted agents use hyper.media.

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

Unsubscribe anytime