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 parts
Section titled “The parts”SwarmIdClient
Section titled “SwarmIdClient”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
SwarmIdProxy
Section titled “SwarmIdProxy”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 popup
Section titled “The connect popup”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 identity site
Section titled “The identity site”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
Storage
Section titled “Storage”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.
Message protocol
Section titled “Message protocol”Everything between your page and its iframe is postMessage, validated with Zod on both sides. The message kinds:
Page to iframe
Section titled “Page to iframe”| 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 |
Iframe to page
Section titled “Iframe to page”| 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 |
Popup to iframe
Section titled “Popup to iframe”| 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.
Where WebSocket is used
Section titled “Where WebSocket is used”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.
Backup and Recovery
Section titled “Backup and Recovery”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.
The .swarmid file
Section titled “The .swarmid file”Written and read by the identity site only; an app never sees it. A JSON envelope:
format:"swarm-id-backup",version:1salt: 32 random bytes, hexpayload: 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.
Export
Section titled “Export”- The user unlocks with their access method, which yields the seed entropy
- Serialize the portable projection (
serializeSyncedAccount) - Draw a salt, derive the file key from the entropy, encrypt with a random IV
- Write the envelope
Import
Section titled “Import”- The user picks the file and enters the recovery phrase
- Derive the file key from the phrase’s entropy and the file’s salt, decrypt
- Validate the payload against the portable schema and check that the phrase’s address is the account id
- Choose an access method for this device and store the account
Restore from Swarm
Section titled “Restore from Swarm”When a device signs in to an account it has never seen, it rebuilds the account from Swarm:
- Derive
derivationKey,swarmEncryptionKeyand the backup signer from the master key - Read the account’s device roster, an append-only feed that lists every device that ever joined
- Read each device’s own state feed, which holds that device’s latest full view of the account
- 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.
Security
Section titled “Security”What is protected
Section titled “What is protected”- 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
What that covers
Section titled “What that covers”- 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
What you do
Section titled “What you do”- Use HTTPS in production
- Check that
iframeOriginis 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