Skip to content

Architecture

Swarm ID has three parts. Your page runs the library. The library embeds a hidden iframe from the Swarm ID site, and that iframe does all the talking to Swarm. When the user connects, a popup opens on the Swarm ID site, unlocks the account, and hands the iframe a secret made for your app alone. The secret that controls the account never leaves the Swarm ID site.

┌───────────────────────────────────────────────────────────────────┐
│ Your page (https://my-app.example) │
│ │
│ SwarmIdClient │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Hidden iframe (https://swarm-id.snaha.net/proxy) │ │
│ │ │ │
│ │ SwarmIdProxy │ │
│ │ - holds this app's secret and the drives it may spend │ │
│ │ - signs and sends every Swarm request for the app │ │
│ │ - a peer on the account bus │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ▲ postMessage, both ways, every message validated │
│ │ │
│ connect() / the iframe's own button ──────────▶ window.open │
└───────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Connect popup (https://swarm-id.snaha.net/connect) │
│ │
│ - unlocks the account: passkey, Ethereum wallet, or password │
│ - derives this app's secret from the master key │
│ - same-site: writes the connection to the shared store, │
│ and the iframe picks it up from a storage event │
│ - partitioned: posts the secret and the account view to │
│ the iframe through window.opener │
└────────────────────────────────────────────────────────────────┘

The library your page uses. It:

  • creates and embeds a hidden iframe from the Swarm ID site
  • offers a connect button inside the iframe, or a connect() method for your own
  • exposes the Swarm operations: upload, download, chunks, single-owner chunks, feeds, GSOC and ACT
  • talks to the iframe only through postMessage

Runs inside the hidden iframe, on the Swarm ID site. It:

  • authenticates from the shared store when your page is same-site with it, or from the popup’s handover when the browser partitions its storage
  • holds your app’s secret and the drives it may spend
  • signs and sends every Swarm request, and reports progress and connection changes back to the page
  • validates every message from the page before acting on it
  • is a peer on the account bus, so a revoke or a rename made elsewhere reaches it while it is open

The /connect route of the identity site. It:

  • unlocks the account with its access method: a passkey, an Ethereum wallet, or a password
  • derives your app’s secret from the master key
  • writes the connection to the shared store, or posts it to the iframe when storage is partitioned
  • records your app under the account’s connected apps, with the session length from the account’s settings

The rest of swarm-id.snaha.net: account creation from a recovery phrase, the keystore, drives, connected apps, devices, backup and restore, and the developer tools at /dev.

Every key the account makes derives from its recovery phrase. The phrase gives a wallet whose private key is the master key and whose address is the account id. The master key stays on the identity site, encrypted at rest under the access method. From it come the per-app secrets and a stored derivationKey, and from that the Swarm encryption key, the backup signer that owns every feed, the postage signer, the sharing key and the account bus keys. The full tree, with every context string, is on Key Derivation.

An app’s secret is HMAC-SHA256(masterKey, appOrigin):

  • Deterministic: the same account always gives the same app the same secret
  • Unique per app: two apps never share one
  • No cross-app leakage: one app’s secret says nothing about another’s
  • The master key stays home: it never leaves the Swarm ID site

The identity site keeps the account in its own local storage: the encrypted vault, the account record with its connected apps, drives and devices, and settings. Your page cannot read any of it. The iframe is a different origin, and that boundary plus the per-app secret is what isolates apps from each other.

Whether the iframe can read that same store depends on the browser’s notion of site. The iframe reads the Swarm ID site’s own storage whenever the page embedding it is same-site with it. Ports do not count, so swarm-demo.snaha.net embedding swarm-id.snaha.net is same-site, and so is anything on localhost. Every same-site app then shares one store, and the iframe keeps one session per app origin in it.

Where the two are cross-site, or the browser partitions storage regardless (Safari’s Intelligent Tracking Prevention, strict privacy settings elsewhere), the iframe gets its own store. The connect popup hands it the account’s synced view (drives and signer keys, never the vault) through window.opener, and the iframe keeps the session in its own partition, so a reload does not repeat the handshake. That iframe is a full writer and a peer on the account bus, which is how a revoke or a rename made anywhere else still reaches it while it is open. #277 has the background.

The Storage Access API is not used: it governs cookies, not localStorage, so it can neither unpartition the iframe’s store nor say whether it is partitioned (#613). The iframe finds out by reading a marker the identity site leaves in its first-party store; no prompt is involved, and your app requests nothing on the iframe’s behalf.

Everything between your page and its iframe is postMessage, validated with Zod on both sides. The message kinds:

Group Messages
Session parentIdentify, checkAuth, connect, disconnect, isConnected, deriveAppSecret
Node getNodeInfo, getPostageBatch, getDrives
Data uploadData, downloadData, uploadFile, downloadFile, uploadChunk, downloadChunk
SOC socUpload, socDownload, socRawUpload, socRawDownload, socGetOwner
Feeds epochFeedUploadReference, epochFeedDownloadReference, seqFeedUploadReference, seqFeedDownloadReference, seqFeedUploadPayload, seqFeedDownloadPayload, seqFeedUploadRawPayload, seqFeedDownloadRawPayload, seqFeedGetOwner, feedGetOwner, createFeedManifest
GSOC gsocMine, gsocSend
ACT actUploadData, actDownloadData, actAddGrantees, actGetGrantees, actRevokeGrantees
Message Purpose
proxyReady the iframe is initialised
initError the iframe could not initialise
authStatusResponse answer to checkAuth
authSuccess a session was established
connectionInfoChanged the app-visible connection state changed
uploadProgress progress of an upload in flight
…Response one response kind per request kind above, for example uploadDataResponse
error a request failed
Message Purpose
setSecret the derived app secret and the account view, sent through window.opener when the iframe’s storage is partitioned

The account bus has its own message kinds: account snapshots, partition handover and presence. They travel between the account’s live contexts rather than between a page and its iframe.

Three places, and nowhere else. Swarm reads and writes, the popup handshake, and the page-to-iframe protocol are all HTTP or postMessage.

Use Endpoint When
Chunk upload stream to Bee the Bee node’s /chunks/stream, with http swapped for ws only when an upload passes useWebSocket: true. The iframe stamps each chunk itself and keeps up to 32 chunks in flight, waiting for Bee’s acknowledgement of each. The default upload path is HTTP with 8 chunks in flight
Account bus signaling wss://swarm-id.snaha.net/bus in production, ws://localhost:5520 in dev whenever an account context needs to reach peers outside its own storage partition. The socket carries join, signal and relay frames; after a WebRTC data channel is up, messages go peer to peer and the socket only relays what cannot
The signaling server signaling/, the bus-signaling service on DigitalOcean the server side of the row above. It forwards frames between the sockets of one room and stores nothing

A build with no signaling URL configured runs the bus over BroadcastChannel only.

Three ways an account’s state survives a device: an encrypted .swarmid file, the account’s own feeds on Swarm, and live propagation over the account bus.

Written and read by the identity site only; an app never sees it. A JSON envelope:

  • format: "swarm-id-backup", version: 1
  • salt: 32 random bytes, hex
  • payload: the account’s portable projection (name, settings, devices, connected apps, drives), AES-GCM-256 encrypted, hex. Never the seed vault, the access method, or live per-app session material

The file key is HKDF-SHA256 over the recovery-phrase entropy with that salt, so the file is useless without the phrase. It is not part of the Key Derivation tree: nothing else derives from it, and a fresh salt means two exports of the same account share no key.

  1. The user unlocks with their access method, which yields the seed entropy
  2. Serialize the portable projection (serializeSyncedAccount)
  3. Draw a salt, derive the file key from the entropy, encrypt with a random IV
  4. Write the envelope
  1. The user picks the file and enters the recovery phrase
  2. Derive the file key from the phrase’s entropy and the file’s salt, decrypt
  3. Validate the payload against the portable schema and check that the phrase’s address is the account id
  4. Choose an access method for this device and store the account

When a device signs in to an account it has never seen, it rebuilds the account from Swarm:

  1. Derive derivationKey, swarmEncryptionKey and the backup signer from the master key
  2. Read the account’s device roster, an append-only feed that lists every device that ever joined
  3. Read each device’s own state feed, which holds that device’s latest full view of the account
  4. Fold those views into one account with last-writer-wins rules and tombstones

The per-device feeds and the fold are described on Postage Batch Sharing.

  • Master key: only on the identity site, encrypted at rest under the access method
  • App secrets: derived per app; one app’s page never sees another’s
  • Cross-app isolation: the iframe’s storage is on a different origin from every app page
  • XSS in an app: cannot reach the master key or another app’s secret; the iframe is a different origin
  • A malicious app: only gets the secret derived for its own origin
  • Man in the middle: HTTPS is required for every deployment
  • Message spoofing: every message is validated against a schema, and the iframe answers only its own parent
  • Use HTTPS in production
  • Check that iframeOrigin is the Swarm ID site you expect
  • Keep the Swarm ID site secure and audited, above all when you host your own
  • Use a Content Security Policy to restrict who may embed the iframe