Skip to content

Read and organize your library

Markdown · Agent index

The browser library lives at https://account.sponge.computer/library. Sign in with an email code, a passkey or your Sponge key; the browser uses a private HttpOnly session. One account continues to hold one library. The browser does not require a local CLI process to be running to read content already synced to the cloud.

A sticky top bar on every screen size holds the Sponge icon (home), search, Save and the account menu. The library is a reading queue in five views: Inbox, Later, Archive, Starred and Everything (keys 1–5), each with its count. A new item is in the Inbox until you read it, then in the Archive, unless you file it yourself: E archives the open item (or sends an archived one back to the Inbox), L saves it for later, S stars it, Shift+E marks it read and archives it in one step, and Z undoes the last filing. Hovering a row shows the same actions; the open item’s toolbar has them as toggles. Filing the open item out of the view opens the next one. A confirmation with Undo stays for six seconds, longer while the pointer or focus is on it. The list menu beside the title sorts by newest, oldest, shortest or longest and shows any, unread, in-progress or read items; rows show the reading time, or the time left with a progress bar once started on this browser. Search always looks across every view. Inline toggles show only private, only public or only PDF items (press again to clear). Filing is one reading-state change (location and starred beside status), so the CLI keeps it and any other device sees it. Numbered parts of one work, such as pages titled “Section 3”, gather under the work’s name in reading order when two or more of them are in the list. The saved reader renders CommonMark and GitHub-flavored Markdown: tables, nested task lists, strikethrough, automatic links, reference links and footnotes. Headings have permalinks and an expandable article outline. Code blocks have language labels, copy controls and syntax highlighting for common languages; unknown languages and unusually large blocks keep their complete, copyable source. Inline and display TeX render as native MathML, including matrices; unsupported or oversized equations stay readable as TeX. Highlighting and math load only when needed and yield between blocks. Recognized captured SVG diagrams, MathML, tables and superscript/subscript text render through a bounded passive-format allowlist. Damaged or unsupported captured block formats show a compact explanation, saved text and original markup disclosure instead of a page of tags. Embedded players link to the original article. Code examples and other raw HTML stay literal. Images load from the site that published them, without a referrer; an address that looks signed becomes a plain link. Saved source bytes are unchanged. Stored raster images and attachments use authenticated item-scoped routes; other file types download as attachments. PDFs also have an Open saved PDF link. Images sit centred in the column; one the column shrinks opens full size in a modal on click, Enter or Space, with its alt text as the caption when it is more than a file name, and closes on click or Escape. A page’s cover image, which capture appends last, opens the article instead when the page has no other picture. Media items expose their saved transcript; this MVP does not stream original media from third-party sites.

The reader bar shows reading progress and time left, and remembers where you stopped in each item on this browser. Reading settings choose a theme (dark, light, sepia or follow the system), sans or serif text, text size, line spacing and width; they are stored on this device only. On phones, including in landscape, the reader bar slides away as you read down and returns when you scroll up. Finishing an article offers Done, which marks it read, archives it and opens the next item, and a quieter Mark as read that leaves it where it is.

Items you have opened recently reopen instantly from memory and refresh in the background if they are more than 30 seconds old. Hovering, focusing or pressing a row fetches it ahead of the click, and the items next to the open one load while the browser is idle. This cache is in memory, any edit refetches the item, and signing out clears it.

Each item is one page: its accepted summary sits above the saved copy as a card that always shows its lead sentence and folds its key ideas away (folded by default on phones, remembered on this device), or that you can skip past; Highlights and notes follow the copy. Select text in the copy or summary to highlight it or comment on it; H highlights the current selection. A highlight is anchored to the saved Markdown of the current revision with the text around it, so it syncs to your devices as an ordinary local highlight. Selecting summary text that is not in the saved copy saves a comment as a note quoting it.

Notes about the whole piece, rather than a passage, open from the round note button in a bottom corner of the reader (or N): a dialog with the new-note field, Command/Ctrl+Enter to save, and the item’s notes to edit or delete. The button shows how many there are. Where the reader is narrow (a phone, or a tablet beside the list) it sits at the trailing edge, and on a phone it leaves with the toolbar while you read down. The end of the article lists highlights and notes together, with an Add a note button that opens the same dialog. Copy as Markdown, beside the item’s notes, copies its highlights and notes as one Markdown section.

Below that, Related in your library names up to three other saves closest in meaning to the one you just read. They come from the same in-browser index as Related by meaning in search: the comparison runs on this device, nothing is sent anywhere, and the section is left out when nothing in the library is close, when the library has fewer than four items, or when the browser asks to save data.

Highlights (H with nothing selected, the highlighter button in the top bar, or the command palette) opens a notebook of every highlight and note across the library: one entry per item, most recently annotated first, its highlights in reading order and then its notes. Search narrows it to highlighted passages, comments, notes and titles that hold every word. Choosing a passage opens the item and scrolls to that highlight. Export copies the whole notebook as Markdown or downloads it as sponge-highlights-YYYY-MM-DD.md: a heading per item, its author and linked site, each passage as a quote with its comment, and notes, ready to paste into Obsidian, Notion or Bear. The notebook is read from the browser’s own copy (GET /api/web/notebook, served by the copy like the library’s other reads), so opening, searching and exporting it cost no cloud request.

Create, edit and delete personal notes and highlight comments, change reading status from the reader toolbar, or set explicit public/private visibility. Notes are paginated rather than loading an entire annotation history into one response. Unfinished note text stays in memory across reader navigation, with an unload warning; it is not durable offline browser storage.

A change you make shows at once and stays shown until this browser’s copy has caught up with it: a list or item read that races the copy cannot undo it. An article you mark read keeps its row while it is open, so J/K keep their place, and leaves the Unread list when you move on. If another device’s sync is still being indexed, the change waits for the copy and is sent again with the same request ID.

The item list grows as you scroll; it loads the next page before you reach the end, and J past the last loaded row loads more and keeps going. Background refreshes re-read the rows already loaded, so a scrolled list never shrinks.

The compact desktop workspace scrolls the item list and reader separately. Focus hides the list for reading; Escape brings it back. On a phone, opening an item replaces the list and offers Back to library. Press / or Command/Ctrl+K to open search outside a text field, and use the arrow keys, Home and End while a library row has focus. Press ? for all shortcuts: j and k move to the next and previous item, m toggles read, o opens the original, n writes a note, h opens Highlights and f toggles focus. Enter opens the focused item. Changing filters preserves the current article and unfinished note; opening another item reuses an unchanged list rather than fetching and rebuilding it.

Web edits are rounds of the browser’s own device (The browser as a device), committed through the same sync route as a CLI device’s round and idempotent by request ID: a retry sends the bytes of the first attempt, so an uncertain commit is never written twice. Connected devices receive them at their next sponge sync or while sponge sync --watch runs; local edits reach the browser after sync. The page shows an edit at once; the copy takes its committed round as the log’s next when nothing came between, without fetching it back, and indexes it. Offline, an edit waits in the copy (“Saved in this browser · sends when online”) and is sent when the cloud next answers: a later choice of the same setting replaces a waiting one. While the cloud is busy or out of reach, it is tried again after its Retry-After, or after a wait that doubles from 5 seconds to 30 minutes. An edit the cloud refuses for good, or one still unsent after 7 days, ends: the footer says how many could not be sent, with Try again and Discard. A session that ends deletes the copy; only the edits still waiting are kept (sponge-unsent.json in the origin’s storage, each with the clock it was made at), for up to 7 days, until the same account signs in again and its new copy sends them. One a later edit of the same setting has overtaken meanwhile is let go and listed in the footer. Another account’s sign-in deletes them, and a signed-out account page offers to discard them. Signing out from the account page first asks an open library tab to send them, and if any are left asks whether to keep or discard them. A tab left open on an older version of Sponge holds the copy until it is reloaded or closed; a newer tab says so and takes over once it is. A browser without its own copy can neither read nor edit the library (503 browser-copy-unavailable): one without origin-private storage, as in some private windows, or an account whose library is not yet in its own storage. Profile, saves and devices still work there. Capture jobs, model packages and inference remain local.

Search opens as one wide palette over the library and, when an article is open, over that article too. Results come in two kinds:

  • Library results come from the keyword index of the library’s copy in this browser. With matches=1 the items API ranks items whose titles hold more of the words first, then returns up to three excerpts per item, read from the matched text chunks: windows around the densest runs of query words in the saved text, your highlights and your notes, each labelled by source, with the matched words as ranges. The indexed title, address and digest lines are skipped, and repeated passages show once. Each row shows the item’s summary sentence under its title.
  • Related by meaning results come from this device: a small static embedding model (Model2Vec potion-base-8M, int8, about 4 MB) loads once as an immutable cached file, and vectors for titles, leads and key ideas are built in memory from the compact corpus. Each related item shows the summary lines closest to the query (encoded on the device too) and how similar it is. Items that match both ways stay under Library, ranked higher. A question or a phrase of four or more words puts Related by meaning first.

Search adds nothing to browser storage and no text leaves the browser for ranking. With Save-Data on, or if the model cannot load, search uses keyword results only, and the palette’s footer says which mode is active. On wide screens a preview pane shows the focused result in full: every excerpt, why it relates, and its summary. Narrow screens show the first excerpt inline. Inside an article, exact matches come first, then passages that match by meaning; choosing one scrolls to it and paints it with the CSS Custom Highlight API. A pasted link offers Save this link. Search stays in the palette; there is no separate show-all list.

Save opens a dialog that queues a link as a save request (below) and lists the queue with its state, Retry for failures and Cancel for waiting requests. When no computer is connected, the dialog says so and links to setup. The account menu’s Save and connect page has copyable steps: install and connect the CLI, save from a terminal, and add the sponge mcp server to Claude Code, Codex or any MCP client, plus instructions to paste into an agent. An empty library shows the same two ways in.

Built assets are content-hashed and served immutable, so a returning visit downloads only the HTML. The library is installable from the browser (web manifest with standard and maskable icons).

A save request asks your devices to capture a URL. The browser never captures. A request holds the normalized http(s) URL, an optional note of up to 500 characters, its state and, when it settles, an item id or a short error code.

Each account can have at most 500 open requests. There is only ever one open request per URL, so adding the same URL again returns the existing request.

  • GET /api/web/saves returns { requests, devices, ingest }: open requests plus the 50 most recently settled, newest first. devices counts the connected CLI devices (unchanged). ingest says who captures them:

    {
    "leader": { "name": "Studio", "lastSeenAt": "2026-11-01T12:00:00.000Z",
    "capabilities": ["capture", "digest"], "live": true, "leader": true },
    "devices": [ /* the same shape, leader first, then most recently seen; at most 20 */ ]
    }

    leader is null when no device that can capture is online. lastSeenAt is null and capabilities empty for a device that has not checked in since the ingest leader shipped.

  • POST /api/web/saves takes { url, note? }. It returns { request } with status 201 for a new request, or 200 if one was already open.

  • DELETE /api/web/saves/:id cancels a queued request. It returns 409 if a device holds a live claim on it.

  • POST /api/web/saves/:id/retry moves a failed request back to queued.

Devices use bearer-token routes:

  • GET and POST /api/sync/saves list and queue requests, in the same shapes as the web routes above.
  • POST /api/sync/saves/claim takes { limit? } (at most 10) and returns { requests, leader }. Only the account’s live ingest leader (below) gets requests; a claim also counts as its heartbeat, and with no live leader the claiming device becomes it. It claims queued and lapsed requests for 10 minutes.
  • POST /api/sync/ingest is a device heartbeat: { capabilities: ["capture", "digest"] } in, { leader, leaseExpiresAt, ingest } out, where ingest has the shape above and each device also carries thisDevice.
  • POST /api/sync/saves/:id/complete takes { itemId } or { errorCode }. It must come from the claiming device and is idempotent.

sponge sync and sponge saves run claim requests, capture them with the normal add pipeline, report the result and sync again.

Settled requests are purged as other writes happen: each account keeps at most its 50 newest, and none settled more than 30 days ago. One write removes at most 100 rows.

One device per account captures queued saves and drafts summaries, so several connected computers never race each other. Each sponge sync (and every 30 seconds under sponge sync --watch, easing to 60 seconds while nothing changes) sends a heartbeat naming what the device can do: capture unless it runs with --no-saves, and digest when auto-digest is on and its local model was usable. The cloud keeps one lease per account and elects, among live devices that can capture, one with a summary model first, then the current holder, then the most recently seen. The lease lasts two minutes past the leader’s last recorded presence. To keep idle heartbeats free, presence is rewritten only when capabilities change or it is 30 seconds old, so the lease can end up to 30 seconds sooner than two minutes after the last heartbeat. After it lapses, the next capable device to check in takes over. Revoked or expired devices never lead.

On the leader, each round captures claimed saves, then queues summaries for recent captures (30 days) synced from other devices that have none, three per round, drafts within a two-minute budget and syncs the result. Other devices leave their own auto-digest jobs to it; a job whose revision already has a digest settles on that one. See docs/decisions/ingest-leader.md. The copy’s GET /api/web/corpus returns a compact, private listing for in-browser ranking: titles, sites, reading states, leads and accepted digest ideas.

Rows and the reader header show the author beside the site: the capture’s author frontmatter, or the @handle in the address for X posts. Saved X posts take their accepted summary sentence as their title instead of the post’s first words; a title set by hand still wins.

G, the top bar’s map button or the command palette opens a map of the concepts across your library: named groups of ideas that several saved articles share, placed so related concepts sit near each other and coloured by the wider region they belong to. Drag to pan, pinch, scroll with a mouse wheel or press + and − to zoom, 0 fits the whole map, and the arrow keys pan. Choosing a concept, on the map or in the side list, shows its description, its nearest concepts and the articles in it, each with the claims that placed it there; choosing an article opens it. Escape clears the choice, then closes the map. An item’s page lists the concepts it belongs to under its summary, and each opens the map there.

The leading CLI device builds the map from accepted digests with its own models and syncs it (sponge map, docs/library-map.md); the cloud computes nothing. The copy’s GET /api/web/map returns the newest map read against the library as it is now: deleted items, and concepts left with none, drop out, and titles, sites, reading states and claims come from the current items. Until a map has been built the pane says how to build one.

Your profile optionally claims one case-insensitive 2–30-character handle using letters, numbers, hyphens and underscores, with letters/numbers at both ends. Reserved handles and duplicates are rejected. A claimed handle is fixed in this MVP; display name and bio remain editable. Private-only accounts can leave it unset.

Claiming a handle publishes nothing by itself. Items start private; /@handle lists the items you publish: effectively public, not deleted, captured, with a resolvable sanitized source URI. Publish or unpublish an item from the reader or with sponge publish ITEM / sponge unpublish ITEM. Autopublish, in Your profile or sponge publish --auto on, publishes new saves from public web pages; it is a synced setting and off by default. Libraries from before 0.17.0 keep the items that were public then.

Your devices build /@handle, its feeds and embed as static files and upload the changed ones when they sync. The browser is one of them: after each edit it renders the same files from its copy, with the CLI’s renderer, and uploads what changed, so publishing, unpublishing and links set in the browser apply at once. Two devices that have read the same rounds render the same bytes. The cloud renders nothing; it refuses an upload older than the last one (publication-stale) or naming a round the account has not reached (publication-ahead). A profile change shows at the next upload, from the browser’s next edit or a device’s next sync. Devices before 0.17.0 must be updated: once publishing settings or links change, the cloud asks them to update instead of syncing.

Public output contains source metadata and accepted current digest/editorial summary fields. It excludes saved article bodies, files, local paths, account realms, personal annotations, unreviewed drafts and credentials. JSON Feed is at /@handle/feed.json; Atom is at /@handle/feed.xml. There is no shared discovery feed, follow graph or social commenting in this MVP.

A share link at /s/<id> (a random 128-bit id) shows chosen items to anyone who has it, optionally with your highlights: those you have when you make the link, so later highlights stay private. A link to one item takes that item’s title unless you give it one. Copy link in an item’s Share popover makes the link (the first time), publishes it and copies it; on a phone a link that already exists opens the share sheet instead (the sheet cannot wait for a new one). A link made or revoked in the browser appears or goes at once; one made with the CLI, at its next sync. The CLI makes them with sponge share ITEM... [--title T] [--highlights]; list them in Your profile or with sponge share list, and revoke with Revoke or sponge share revoke ID. Links are not listed anywhere and carry noindex.

Every published page carries link-preview tags (title, description, address and the Sponge card image) and ends with a quiet “Save your own reading with Sponge →” link.

Public pages are served from each Cloudflare data center’s cache in front of the bucket, revalidated by every view (ETag, so an unchanged page is a 304 with no bucket read). Publishing or revoking purges the changed pages at once in the data center the upload went through; the Cache API cannot purge other data centers, which drop their copy within a minute. A missing page is cached for 10 seconds.

/@handle/embed is a small, self-contained page of your recent public items that any site can show in an iframe. Copy embed code in the Share popover (and in Your profile) copies:

<iframe src="https://account.sponge.computer/@handle/embed" title="Name's reading on Sponge"
width="100%" height="360" loading="lazy" style="border:0"></iframe>

Each row shows the date the item was listed, its title linking to the original article (new tab, no referrer) and the source host. A footer links to your public page. The embed lists the newest 30 items of /@handle; limit trims the list in the page. /api/public/:handle/items now redirects to /@handle/feed.json.

Query parameter Values Default
limit 1–30 8
theme auto, light or dark auto

An unknown handle, or one with nothing published, returns 404.

The page background is transparent. Browsers paint an opaque background behind an iframe whose color scheme differs from the embedding page’s, so pick the theme that matches your site, or set color-scheme on the iframe element to match.

The embed loads no fonts, images or scripts from other origins. It runs one inline script that reports its height to the embedding page on load and whenever its content resizes:

// Message posted to window.parent with targetOrigin "*"; the height is not sensitive.
{ type: 'sponge:resize', height: 214 } // integer CSS pixels
// In the embedding page:
addEventListener('message', (event) => {
if (event.origin !== 'https://account.sponge.computer' || event.source !== frame.contentWindow) return;
if (event.data?.type === 'sponge:resize' && Number.isInteger(event.data.height))
frame.style.height = event.data.height + 'px';
});
// A listener added after the frame loaded asks for the current height:
frame.contentWindow?.postMessage({ type: 'sponge:measure' }, 'https://account.sponge.computer');

Only /@handle/embed may be framed (Content-Security-Policy: frame-ancestors *, no X-Frame-Options). Its policy otherwise allows only the hashed inline style and script. Every other Sponge page still refuses framing; the other public pages run no script at all. Public responses are noindex and cached for up to 60 seconds.

Unpublishing an item or revoking a link deletes the file at once from the browser, or at the device’s next sync from the CLI. Copies already fetched, by people, feed readers or caches, may remain.

Account → Delete account erases the account and everything Sponge Cloud keeps for it. It asks for recent proof (the Sponge key, a passkey or an email code, as replacing the key does) and for the account’s email or the word DELETE typed out. The request is POST /api/account/delete with { "confirm": "…" } from a signed-in browser, under the same Origin and X-CSRF-Token rules as every other account change.

The account stops working first: in one D1 batch the account is disabled, a deletion record is written, every browser session ends, every paired device is revoked and every connected app’s token is removed. From then on nothing signs in, syncs, saves or publishes. The browser is signed out, its copy of the library is removed, and the page says the account was deleted.

Then, in order, each step safe to repeat:

  1. Sign-in: sessions, passkeys, sign-in and passkey challenges, email codes, the verified email, pairing codes, connected-app tokens and queued operator jobs. The Sponge key’s hash is replaced by a random one, and every device is revoked.
  2. The account’s Durable Object: its whole SQLite database (deleteAll) — the library, the sync log and staged rounds, save requests, device leases, the search index and the copied profile.
  3. Sync files: every R2 object under accounts/<id>/ in the sync bucket (packs, chunks, the publication gate).
  4. Public pages: every page, feed, embed and shared link the account’s manifest names, then the manifest. Cached copies are purged as unpublishing purges them.
  5. Directory: every remaining D1 row naming the account — the handle and profile, billing entitlements, sync usage (the service total gives the bytes back), device records, account moves — then the account row.

If a step fails, the account is already shut, the deletion record keeps the error, and background maintenance (every 15 minutes) runs the steps again until they finish.

Subscriptions. Sponge holds no store credentials, so it cannot cancel a subscription itself. While a Stripe subscription would renew, deletion is refused (409 subscription-active) and the page links to Manage subscription (/plus/manage, the Stripe customer portal). One cancelled to end with the period does not block it. An App Store subscription does not block it either: the page says to cancel Sponge Plus in the Apple Account’s subscription settings, since deleting the account does not stop Apple renewing it.

What is not erased:

  • Copies on your own computers and phones: Sponge on a device keeps its local library. The CLI and apps can no longer sync it.
  • Payment records kept by Stripe and Apple, under their own policies, and Sponge’s record of which webhook and notification IDs it has processed (IDs and times only).
  • The deletion record: the account’s random ID and when deletion was requested and finished, so a retry knows what to finish.
  • A shared link’s address stays retired, with no account named in it, so nobody can take over a link you once shared. Retired records of links removed before the deletion still name the random account ID.
  • Rate-limit counters, which hold keyed hashes and expire on their own.
  • Edge-cached copies of public pages in other data centers expire within a minute.
  • Cloudflare’s point-in-time recovery history of D1 and of the account’s Durable Object, kept for 30 days and then gone. Sponge does not restore a deleted account from it.

Each device’s library, the browser’s copy included, is derived from canonical, account-scoped sync rounds and immutable packs and chunks; the log remains the recovery source. The cloud keeps no library of its own to read: no item, field, note or search rows. Its index records only which files the log names and which saved images and PDFs each revision has, which retiring files, pruning packs and serving saved images need. It checks bytes and content addresses as it goes. Checkpointed steps bound work and retain a lease through each atomic batch. Ordinary sync/write responses schedule a small background step. The cloud runs no schedule of its own, so after an exchange the syncing device asks POST /api/sync/index for one bounded pass at a time until the index is current (up to 10 minutes per sponge sync; the installed worker syncs every 15 minutes). An idle index answers from two reads without taking a lease. Stopping early loses nothing: the next sync continues from the same checkpoint.

A saved image is served by its content address (GET /api/web/items/<id>/assets/<sha256>), from any revision of the account that names it and until it is retired.

Accounts indexed before the cloud stopped projecting still hold their older projection rows in their object; nothing reads them. Dropping those tables is a later step, taken only after each account object’s point-in-time-recovery bookmark (the operator’s bookmark job, docs/decisions/account-objects.md).

A signed-in browser session is also a device of its account. POST /api/web/device registers it (idempotent). It is the session’s own: it lasts as long as the session, and signing out, or the session expiring, disconnects it. Browser devices are bounded by the account’s live sessions (at most 32), so they never count toward the limits on paired devices (32 signed in, 100 ever). GET /api/web/sync/{status,rounds,packs/:sha,chunks/:sha,retired} reaches the same sync handlers as a CLI device’s bearer token, authorized by the session cookie instead. Nothing else under /api/web/sync/ exists, and other methods are refused. Each request names the account the browser’s copy belongs to (X-Sponge-Device-Account); a session of another account gets 409 wrong-account before anything is read. These routes answer only inside an account’s own object; an account still stored in D1 gets 503 sync-unavailable.

A worker keeps the account’s library in SQLite-WASM on the origin-private file system: it applies the account object’s own schema, pulls rounds after its cursor, fetches each pack or chunk once and keeps it, and runs the cloud’s indexing code over it. The copy records its account and library once and refuses any other: a round of another library is an integrity failure. Each stored pack is hashed in full the first time the worker reads it, and a damaged one is fetched again. A rate-limited pull reports its code and Retry-After. An index pass that makes no progress ends the sync rather than repeating, and a lease left by a page that closed mid-pass is cleared on open. A request to the cloud that has not answered in 20 seconds gives up (its body gets 5 minutes), so a stalled one never holds the copy’s sync. After each index run the copy drops stored packs and chunks that no indexed file or unindexed round names any more. A cloud whose log is behind the copy (restored to an earlier point) starts the copy again.

Compiling SQLite-WASM needs 'wasm-unsafe-eval'. A dedicated worker runs under the policy of its own script response, so only the device worker’s response (/assets/device-worker.<hash>.js) carries script-src 'self' 'wasm-unsafe-eval'. Pages and every other script keep script-src 'self', and the deploy check enforces both. Chromium and WebKit apply the worker’s own policy (checked against the built worker); in a browser that applied the page’s policy instead, the worker could not start and the library could not be read in that browser.

The library reads that copy: its list, search, item pages, notes and the search corpus come from the worker, through the same read code the cloud runs, so responses are the same. The first visit pulls the library once and indexes it in the background; the list says the library is being brought to this browser, with file progress. Later visits open the copy at once and pull only what changed: with nothing new, one status request. A visible tab looks 1, 2, 4 and 8 minutes after the reader last acted and every 15 minutes after; showing or focusing the tab, or acting in it, looks soon; a hidden tab does not look. A browser that clears the copy (storage pressure, or site data cleared) gets it again the same way; the page asks the browser to keep its storage, and a full one stops syncing and says so (503 storage-full). Until a fresh copy has pulled once, its reads answer 503 library-indexing; after that the copy answers with what it has indexed while it catches up, and the page shows the progress. A first load fetches packs four at a time ahead of the index.

One tab holds the copy at a time: the first worker to take the sponge-device Web Lock opens it, and the other tabs pass their reads to it over a BroadcastChannel. When that tab closes, or its copy is wiped, the next takes the lock and opens the copy. Each passed read names its account. A tab holding another account’s copy (another account signed in on a shared computer) checks the session: once it is no longer its own, it wipes its copy and lets the lock go, so the new account’s tab opens its own; a tab left over from the old account is refused, and while the session cannot be checked it waits and asks again. Each sync answer is a look at the session (the cloud refuses another account’s or an ended one). With no look in 5 minutes the holding tab answers and checks the session behind it (GET /api/session/account); with none in 30 minutes (a tab left open, a hidden tab never syncs) it checks first, so a tab left open after its session ended or switched shows nothing from the copy; an account page signed in to another account tells every tab of the old one to wipe its copy. A read the holding tab has not answered in 10 seconds (12 at the page) is a failure, waited out as below. Messages carry a protocol version, and a call from another version is refused. Only the signed-in account’s copy is kept. Signing out (also while the copy is opening), or any page of the account finding the session signed out or expired, removes it from every tab. Edits are rounds of the browser’s device, committed by the tab holding the copy, which then publishes (Public handles). Profile, saves and saved images still go to the cloud.

The device reads at the current sync protocol (4, which reads checkpoints). A fresh copy starts from the latest checkpoint and the rounds after what it covers, as a CLI device does. A copy behind rounds whose packs the cloud has pruned, or one that meets a pruned pack while reading, empties itself and starts again from the latest checkpoint: its index records would otherwise point at packs that are gone, where the cloud’s own index points its records at the checkpoint’s copies first. Starting again downloads the library again, so a pruned pack met while reading starts it again at most once an hour (remembered across reloads); until then the reads that need it are waited out (503 index-busy).

A browser that stays open across a release that raises the account’s protocol is answered 426 sync-update-required like any older device, and the library reloads to get the current page. A web device is not counted among the devices that keep an account on an older protocol, because it is always the page’s version.

The library is unavailable in this browser for the session (503 browser-copy-unavailable) only when the copy cannot serve it: no OPFS, Web Locks or worker (some private windows), a copy of another account or library, an account with no object to sync from (503 sync-unavailable), or a disconnected device. Any other failure to pull, index or read (a response without a code, another 5xx, 429, the network, a pruned pack, storage full, a damaged pack, no answer in time) is waited out: that read and the reads after it answer 503 index-busy with a Retry-After for a jittered wait, from 5 seconds and doubling up to 5 minutes, never shorter than the cloud’s Retry-After, and the copy is asked again after it. Failures that are waited out never turn the copy off. A response without a code is cloud-unavailable, never sync-unavailable.

Cloud copies have TLS and provider storage protections, not end-to-end encryption. Account sessions and revocable device authority still control access. Keep the saved recovery key and independent local backups private.

Documentation drafted with AI and checked against the product source.