Hyper distribution (feed-hyper)
Reference for @seedprotocol/feed-hyper: publishFeed, serveFeed, openFeed, seedFeed and watchFeed, the seed-feed command and its flags, the drive layout, and its known limits.
@seedprotocol/feed-hyper writes the documents @seedprotocol/feed generates into a Hyperdrive, announces the drive on Hyperswarm, replicates other people's drives, and serves a drive to ordinary feed readers over localhost HTTP. A feed's identity is the drive's public key, not a hostname. It's transport only: what goes into each document is decided by the feed package, as described in createFeed and handlers.
import { publishFeed, serveFeed, openFeed, seedFeed, watchFeed } from '@seedprotocol/feed-hyper'It runs on Node 20.18.1 or later only and depends on Holepunch native modules, which may crash under Bun. See the overview for installation.
DEFAULT_SEED_FEED_HYPER_KEY, reserved for the key of Seed's own feed, is an empty string (it's defined in @seedprotocol/arweave and re-exported here), so the package has no built-in feed to open. Everything on this page works with drives you publish yourself.
The drive
A published drive holds one file per schema and format, plus a registry:
/registry.json
/posts/rss.xml
/posts/atom.xml
/posts/feed.json
/posts/archive/2026/10/rss.xml only with includeArchives (library only)The collection is the lowercased, pluralized schema name, as in the HTTP routes. registry.json describes the drive:
type FeedRegistry = {
key: string // the drive key (z32)
version: number // see the warning below
updatedAt: string // ISO time of the publish
schemas: Array<{
schema: string // e.g. 'post'
formats: Array<'rss' | 'atom' | 'json'>
paths: string[] // drive paths written for this schema in this publish
}>
}Inside each document, the channel's feedUrl is hyper://<key>, so self and paging links look like hyper://<key>/posts/rss. siteUrl comes from the siteUrl option or the process site config, and the other channel fields, such as title and author, from the site config.
Things that hold for every drive:
- Single writer. The drive's private key lives in the Corestore directory (
storePath, or--store). Only that store can write the drive. Back it up: Hyperdrive keys can't be rotated, only replaced by a new drive under a new key. - One process per store. A second process on the same store fails with
File descriptor could not be locked. To serve a feed whilepublishkeeps announcing it, giveserveits own store; it replicates from the publisher. - Append-only. Republishing writes new versions of the files. Revoked items leave the current version, but peers keep the drive's history.
Known limits of publishFeed and seed-feed publish:
- Only page 1 is written. When that page is full, it still links
rel="next"(JSON:next_url) tohyper://<key>/posts/rss?page=2, which doesn't exist. Raise the page size if readers need more items. includeArchivesdoesn't build a real archive. It writes the same latest page as the current month's archive, linked only to the current feed.registry.json'sversionis one behind the drive version the publish ends on, because the registry is written last.- Old files stay. A publish only writes; files from an earlier publish with more schemas or formats remain in the drive, though the new registry no longer lists them.
The seed-feed command
seed-feed publish --schema <names> [--format rss,atom,json] [--store <path>]
[--drive-name <name>] [--page-size <n>] [--site-url <url>] [--no-announce]
seed-feed seed <key> [--store <path>]
seed-feed serve <key> [--store <path>] [--port 8080] [--host 127.0.0.1] [--no-announce]
seed-feed help | --help | -h| Flag | Commands | Default | Description |
|---|---|---|---|
--schema <names> | publish (required) | none | Comma-separated schema names, e.g. post,image. Matched exactly, so lowercase |
--format <list> | publish | rss,atom,json | Comma-separated. Unknown entries are dropped; if none are left, the command fails |
--store <path> | all | ./.seed/feed-store | Corestore directory, resolved against the current directory |
--drive-name <name> | publish | seed-feed | Name of the writable drive inside the store. The same store and name always give the same key |
--page-size <n> | publish | 25 | Items written per document. 0 or a non-number falls back to 25 |
--site-url <url> | publish | The site config's siteUrl (https://seedprotocol.io) | The channel home page in each document |
--port <n> | serve | 8080 | HTTP port. 0 falls back to 8080; use the library for a random port |
--host <host> | serve | 127.0.0.1 | HTTP bind address |
--no-announce | publish, serve | off | Don't join Hyperswarm. publish writes the drive and exits; serve serves only what the store already holds |
What each command does:
publishgenerates the documents once through@seedprotocol/feed, writes them, and prints the drive key (z32), thehyper://URL, the drive version and the paths written. Then it keeps announcing the drive until Ctrl+C (SIGINT or SIGTERM), unless you pass--no-announce. To update the feed, run it again with the same--storeand--drive-name.seedopens a drive by key (z32 or hex) and stays joined to its Hyperswarm topic until stopped. It always announces;--no-announceis ignored. See the warning under seedFeed.serveopens a drive by key and serves it over HTTP until stopped. It prints the address and an example URL forpostRSS.
Flags that don't apply to a command are accepted and ignored. A missing --schema or key and an unknown command print an error and the usage, and exit with status 1. An unknown flag prints only the error (Unknown option '--…') and exits with status 1. The CLI has no option for archives and no watch command; use the library for those. The generating commands read the same environment variables as the feed package (list).
publishFeed
Generates the feeds of one or more schemas and writes them into a drive.
function publishFeed(options: PublishFeedOptions): Promise<PublishFeedSession>Options
| Name | Type | Default | Description |
|---|---|---|---|
schemas | string[] | required | Schema names. Throws publishFeed: schemas must be non-empty if empty |
formats | FeedFormat[] | required | 'rss', 'atom', 'json'. Throws if empty |
storePath | string | required | Corestore directory |
driveName | string | 'seed-feed' | Name of the writable drive in the store |
pageSize | number | 25 | Items per document |
announce | boolean | true | true keeps the drive announced on Hyperswarm until close(). false writes, closes the store and returns |
includeArchives | boolean | false | Also write /<collection>/archive/<year>/<month>/… for the current month. See the limits above |
siteUrl | string | The site config's siteUrl | Channel home page |
fixtureContents | Record<string, string> | none | Skip generation and write these bodies, keyed by drive path. For tests. The registry lists them all under the first schema |
Returns
type PublishFeedSession = {
key: string // drive public key, z32
version: number // drive version after this publish
paths: string[] // drive paths written, ending with '/registry.json'
hyperUrl: string // 'hyper://<key>'
close: () => Promise<void> // leaves Hyperswarm and closes the store; safe to call twice
}With announce: false the session is already closed when it returns.
Example
A scheduled republish that writes and exits:
import { publishFeed } from '@seedprotocol/feed-hyper'
import { setSiteConfig } from '@seedprotocol/feed'
setSiteConfig({ title: 'My Blog', siteUrl: 'https://blog.example.com' })
const session = await publishFeed({
schemas: ['post'],
formats: ['rss', 'atom', 'json'],
storePath: '/var/seed/feed-store',
pageSize: 50,
announce: false,
})
console.log(`Published version ${session.version} of ${session.hyperUrl}`)A drive is only reachable while some peer announces it, so something else (a long-running publish, or a mirror) has to stay online between runs.
serveFeed
Opens a drive by key and serves it over HTTP with serve-drive, so RSS readers can subscribe to http://127.0.0.1:8080/posts/rss.xml.
function serveFeed(options: ServeFeedOptions): Promise<ServeFeedResult>| Option | Type | Default | Description |
|---|---|---|---|
key | string | required | Drive key, z32 or hex |
storePath | string | required | Corestore directory. Can be the publisher's store or a separate one |
port | number | 8080 | 0 picks a free port |
host | string | '127.0.0.1' | Bind address |
announce | boolean | true | Join Hyperswarm to fetch the drive from peers |
type ServeFeedResult = {
key: string
port: number // the bound port
host: string
baseUrl: string // e.g. 'http://127.0.0.1:8080'
close: () => Promise<void>
}Behavior:
- Both drive paths and the feed package's HTTP paths work:
/posts/rss.xmland/posts/rss,/posts/feed.jsonand/posts/json, and the same under/archive/<year>/<month>/. ?version=<n>serves a file as it was at drive versionn(a serve-drive feature). Other query parameters, such as?page=2, are ignored.- Content types come from the file extension:
application/xmlfor RSS and Atom (notapplication/rss+xml),application/json; charset=UTF-8for JSON Feed and the registry. - Missing files return
404. There's no authentication, so keephoston localhost unless you mean to publish the gateway. - Files are fetched from peers when first requested and kept in the store.
import { serveFeed, localFeedUrl } from '@seedprotocol/feed-hyper'
const gateway = await serveFeed({ key: process.argv[2]!, storePath: '.seed/feed-mirror', port: 8080 })
console.log(localFeedUrl(gateway.baseUrl, 'post', 'rss')) // http://127.0.0.1:8080/posts/rss.xml
// later: await gateway.close()localFeedUrl
function localFeedUrl(baseUrl: string, schemaName: string, format: 'rss' | 'atom' | 'json'): stringReturns baseUrl plus the drive path for that schema and format.
openFeed
Opens a drive by key to read it from Node.
function openFeed(options: OpenFeedOptions): Promise<OpenedFeed>| Option | Type | Default | Description |
|---|---|---|---|
key | string | required | Drive key, z32 or hex |
storePath | string | required | Corestore directory |
announce | boolean | true | Join Hyperswarm to find peers |
syncTimeoutMs | number | 15000 | When the store has nothing of this drive yet, wait up to this long for a peer before returning. 0 doesn't wait |
type OpenedFeed = {
key: string
version: number // drive version when opened
drive: Hyperdrive // the underlying drive (untyped)
get: (path: string) => Promise<string | null> // UTF-8 contents, null if missing
getRegistry: () => Promise<FeedRegistry | null> // parsed /registry.json, null if missing or invalid
close: () => Promise<void>
}openFeed doesn't throw when no peer shows up in time; it returns, and get then waits for peers for blocks that aren't local yet.
import { openFeed } from '@seedprotocol/feed-hyper'
const feed = await openFeed({ key: process.argv[2]!, storePath: '.seed/reader' })
const registry = await feed.getRegistry()
console.log(registry?.updatedAt, registry?.schemas.map((s) => s.schema))
const rss = await feed.get('/posts/rss.xml')
await feed.close()seedFeed
Joins a drive's Hyperswarm topic and replicates it until closed, without generating or serving anything. It's what seed-feed seed runs.
function seedFeed(options: { key: string; storePath: string }): Promise<{ key: string; close: () => Promise<void> }>seedFeed doesn't download the drive's files. It fetches the drive's metadata, but file contents are only stored when something reads them. A mirror that nobody reads through can't serve the feed once the publisher goes offline. Download the files yourself after each update:
import { openFeed, watchFeed } from '@seedprotocol/feed-hyper'
const feed = await openFeed({ key: process.argv[2]!, storePath: '/var/seed/feed-mirror' })
await feed.drive.download('/').done()
watchFeed(feed.drive, { onUpdate: () => feed.drive.download('/').done() })
// keep the process running; call feed.close() to stopRunning serve on the mirror and requesting each file once has the same effect for those files.
watchFeed
Calls you back when a drive gets a new version, for example when the publisher republishes and the change replicates.
function watchFeed(
drive: Hyperdrive,
options: { onUpdate: (version: number) => void | Promise<void> },
): () => voidPass feed.drive from openFeed. It listens for appends to the drive's core, so one republish can call onUpdate several times, once per append. Errors thrown by onUpdate are logged, not rethrown. It returns a function that stops watching.
Lower-level helpers
| Export | Signature | Description |
|---|---|---|
openFeedStore | (options: { storePath: string; driveName?: string; key?: string; announce?: boolean }) => Promise<FeedStoreHandles> | Opens a Corestore and a drive: by key (read-only replica), or the named writable drive (driveName, default seed-feed). With announce (default true) it joins Hyperswarm as client and server and replicates every connection. Returns { store, drive, swarm }, where swarm is null without announce |
closeFeedStore | (handles: FeedStoreHandles) => Promise<void> | Leaves Hyperswarm and closes the drive and store |
encodeKey | (key: Buffer | Uint8Array) => string | A key as z32 |
keyToBuffer | (key: string | Buffer | Uint8Array) => Buffer | A z32 or hex key as bytes |
replicateStores | (a: Corestore, b: Corestore) => () => void | Replicates two stores in the same process, without the network. Returns a stop function. For tests |
feedDrivePath | (schemaName, format, archive?: { year; month }) => string | ('post', 'json') → /posts/feed.json |
feedHttpPath | (schemaName, format) => string | ('post', 'json') → /posts/json |
resolveHttpToDrivePath | (pathname: string) => string | /posts/json → /posts/feed.json; other paths unchanged |
hyperFeedUrl | (keyZ32: string) => string | hyper://<key> |
REGISTRY_PATH | '/registry.json' | |
DEFAULT_SEED_FEED_HYPER_KEY | '' | Reserved for Seed's official feed key |
The types PublishFeedOptions, PublishFeedResult, PublishFeedSession, OpenFeedOptions, OpenedFeed, SeedFeedOptions, SeedFeedSession, ServeFeedOptions, ServeFeedResult, FeedRegistry, FeedRegistryEntry, WatchFeedOptions, FeedStoreHandles, OpenStoreOptions and ArchivePathOptions are exported. Hyperdrive, Corestore and Hyperswarm ship no type declarations, so drive, store and swarm are untyped.
Related
- Serve published items as a feed: the walkthrough
- @seedprotocol/feed overview: site config and environment variables used when publishing
- createFeed and handlers: what goes into each document
- Gateway Hyper: the related Hyper tunnel for gateways
createFeed and handlers
Reference for the HTTP handlers and generation functions in @seedprotocol/feed: routes, query parameters, status codes and headers, createFeed's nine arguments, how item fields are mapped, and the exported types.
Overview
What @seedprotocol/mapping does, how to install it, its two entry points and the types a mapping is made of.