Read and organize your library
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.
Read and manage
Section titled “Read and manage”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, save and connect
Section titled “Search, save and connect”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=1the 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).
Save requests
Section titled “Save requests”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/savesreturns{ requests, devices, ingest }: open requests plus the 50 most recently settled, newest first.devicescounts the connected CLI devices (unchanged).ingestsays 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 */ ]}leaderis null when no device that can capture is online.lastSeenAtis null andcapabilitiesempty for a device that has not checked in since the ingest leader shipped. -
POST /api/web/savestakes{ url, note? }. It returns{ request }with status 201 for a new request, or 200 if one was already open. -
DELETE /api/web/saves/:idcancels a queued request. It returns 409 if a device holds a live claim on it. -
POST /api/web/saves/:id/retrymoves a failed request back to queued.
Devices use bearer-token routes:
GETandPOST /api/sync/saveslist and queue requests, in the same shapes as the web routes above.POST /api/sync/saves/claimtakes{ 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/ingestis a device heartbeat:{ capabilities: ["capture", "digest"] }in,{ leader, leaseExpiresAt, ingest }out, whereingesthas the shape above and each device also carriesthisDevice.POST /api/sync/saves/:id/completetakes{ 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.
Ingest leader
Section titled “Ingest leader”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.
Library map
Section titled “Library map”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.
Public handles
Section titled “Public handles”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.
Share links
Section titled “Share links”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.
Embed your reading on another site
Section titled “Embed your reading on another site”/@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.
Deleting your account
Section titled “Deleting your account”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:
- 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.
- 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. - Sync files: every R2 object under
accounts/<id>/in the sync bucket (packs, chunks, the publication gate). - 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.
- 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.
Index and recovery
Section titled “Index and recovery”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).
The browser as a device
Section titled “The browser as a device”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.