Cross-Origin Storage API: Stop Downloading Files Twice

There was a time when loading React from a public CDN was a real performance trick. If enough sites pulled the same react.production.min.js off the same CDN URL, a first-time visitor to your site stood a good chance of already having it cached from some other site they’d visited. One download, reused everywhere. That was the whole pitch for shared CDNs.
Browsers took it away on purpose. To stop sites from using the cache as a cross-site tracking signal, they started partitioning the HTTP cache by top-level site. The attack they were closing: you can detect whether someone visited another site by timing whether a shared resource loads from cache. Chrome partitioned in version 86 in October 2020, Firefox in 85, Safari years before either. Now the same file at the same URL gets downloaded and stored separately for every site that uses it, and the old “just use a public CDN” advice mostly stopped making sense. The Cross-Origin Storage (COS) API is a WICG proposal to bring that shared cache back.
Its trick is to change what the shared cache is keyed on. Rather than the URL, which is what leaked, COS keys the file by its cryptographic hash, and only lets you ask for a file whose exact bytes you already know. That closes the tracking hole the old shared cache opened. One copy of React, or a web font, or a big WebAssembly binary, shared across every origin that wants it.
It’s early. Nothing ships it natively yet. But the problem it solves is real and getting worse, so it’s worth understanding now.
What is the Cross-Origin Storage API?
Cross-Origin Storage is a proposed browser API for storing and retrieving large files by their content hash rather than their URL, in a store that can be shared across origins with the user’s device acting as the shared cache. A file is identified by its SHA-256 digest, so the same bytes fetched by two different sites from two different URLs map to a single entry on disk.
The whole surface is one method on a new navigator.crossOriginStorage interface:
requestFileHandle(hash, options)
It returns a Promise<FileSystemFileHandle>, the same handle type as the File System API, so once you have it you read the file exactly the way you already know how. There’s no bespoke store()/retrieve() pair to learn; requestFileHandle() covers both directions, and the create flag decides which.
The proposal comes from Googlers Thomas Steiner and François Beaufort, together with Christian Liebel of Thinktecture, and it’s still at the incubation stage. The motivating cases are the files a lot of sites already share byte-for-byte: JavaScript libraries, web fonts, WebAssembly modules, game engines. A font served by Google Fonts gets requested by thousands of sites; store it once and any of them could read it locally instead of fetching it from a CDN on every visit.
The problem gets extreme with AI models. The explainer’s headline example is a single 8 GB model that two origins both need, which without COS means 16 GB downloaded and 16 GB sitting on disk for one file. The size list runs up from there: Gemma 2 at 1.35 GB, Llama-3.1-70B at 33 GB. That’s the group feeling the most pain, but the mechanism is the same one that would let two sites share a copy of React.
Why can’t the Cache API or IndexedDB share files across origins?
Because every existing storage mechanism is partitioned by origin, and every one of them is addressed by URL or key rather than by content. Those two facts are the whole problem.
| Store | Addressed by | Shared across origins? |
|---|---|---|
| Cache API | URL | No |
| IndexedDB | key | No |
| Origin Private File System | path | No |
| Cross-Origin Storage | content hash | Yes, within a declared scope |
The Cache API keys entries on the request URL, so the same copy of React served from cdn-a.com/react.js and cdn-b.com/react.js is two unrelated entries even if the bytes are identical, and cache partitioning means even the same URL is a separate entry per site. IndexedDB and the Origin Private File System are both walled inside a single origin by design. None of them can say “I don’t care where this came from, I care that it hashes to 8f43…”. Content addressing is the one thing that makes cross-origin sharing safe to attempt, because you can only ask for a file whose exact bytes you already know.
COS is explicitly not trying to replace any of them. It’s a fourth thing for a narrow case: big files that lots of sites legitimately share.
How does requestFileHandle() read and store files?
You compute the file’s SHA-256 hash, ask for a handle, and read it. Retrieving a file another origin already stored looks like this:
const hash = {
algorithm: "SHA-256",
value: "8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4",
};
const handle = await navigator.crossOriginStorage.requestFileHandle(hash);
const file = await handle.getFile();
If the file isn’t there, you fetch it the normal way and store it, passing create: true and the origins you’re willing to share it with:
const handle = await navigator.crossOriginStorage.requestFileHandle(hash, {
create: true,
origins: ["https://example.com", "https://example.org"],
});
const writable = await handle.createWritable();
await writable.write(fileBlob);
await writable.close();
The hash argument is a dictionary of { value, algorithm }, where value is a 64-character lowercase hex digest. SHA-256 is the algorithm the spec builds around. On write, the browser verifies the bytes you hand it actually hash to the value you claimed and throws a DataError if they don’t, so a stored file can never lie about its own identity. That verification is what lets a different origin trust the entry later without re-downloading it.
The origins option is the access-control knob. Omit it and the file is scoped to your own site. Pass an explicit list and only those origins can see it. Pass "*" and you’re offering it to the whole web, which is where the privacy design gets interesting.
How does Cross-Origin Storage avoid becoming a supercookie?
This is the question the whole design orbits, because a naive “do you have file X?” lookup is a cross-site tracking primitive. If a rare file were stored by exactly one obscure origin, any other site that could confirm its presence would learn you’d visited that origin. Content addressing alone doesn’t save you here; the answer to the query is the leak.
COS defends against it with layered gating rather than a permission prompt.
You can’t enumerate. There’s no “list what’s stored” call. As the spec puts it, developers “cannot enumerate the contents of Cross-Origin Storage or access a file without already knowing its hash.” You can only probe for bytes you already have.
Global files need a crowd. A "*"-scoped file’s presence is only confirmable to an outside origin if its hash is on the Public Hash List, a registry a file joins only after clearing a k-anonymity-style popularity bar, appearing byte-identical across a minimum number of independent origins (the reference public-hash-list currently uses roughly 100). If a file is popular enough that “you have it” tells an attacker nothing about you specifically, sharing its presence is safe. If it’s rare, the browser refuses to confirm.
The browser lies on purpose. For files where the presence signal would be sensitive, the user agent may apply GREASE’ing: “occasionally responding as if a disclosable entry were absent, even though [gating] would otherwise permit disclosure,” adding noise so a site can’t distinguish a real miss from a privacy-motivated one. There’s a nice pragmatic carve-out: the browser won’t GREASE gigabyte-scale weights, since forcing a spurious multi-gigabyte re-download to protect a signal nobody’s mining is a bad trade.
The upshot is that a NotFoundError is deliberately ambiguous. It might mean the file isn’t stored, or that you’re out of scope, or that the hash isn’t on the Public Hash List, or that the browser just decided not to tell you. The spec is explicit that it “does not prove the file is physically absent.”
One thing that surprised me, and corrects a lot of secondhand write-ups: there’s no per-file permission prompt, and no human-readable hash shown to the user. Earlier tellings of this idea imagined a dialog where you’d confirm a file by some readable fingerprint. The current design dropped that. A handle returned from COS is already fully authorized, so calling getFile() or createWritable() “never triggers an additional permission prompt.” User control lives in settings UI for inspecting and evicting stored files, not in an interstitial on every download.
How does the declarative crossoriginstorage attribute work?
Most sites shouldn’t have to hash their own script tags, so the proposal also sketches a declarative path where the browser does the COS lookup for you. It leans on the integrity attribute you may already use for Subresource Integrity (that hash is exactly what COS needs), plus a new crossoriginstorage attribute:
<script
src="popular-js-framework.js"
integrity="sha256-def456..."
crossoriginstorage="*"
></script>
The same idea is floated for JavaScript import attributes and a cross-origin-storage() modifier in CSS url(). These aren’t defined in the COS spec itself; each one has to land in its own host language’s spec (HTML, TC39, CSS), so treat them as direction, not API.
Is the Cross-Origin Storage API supported in browsers yet?
No. Cross-Origin Storage is not implemented in any browser, there’s no origin trial, and there’s no flag to flip. Emscripten’s own COS docs say it plainly: the API “has not yet shipped in any browser.” It is not Baseline, and it isn’t close.
What exists is experimentation around the edges. There’s a Chrome extension that polyfills the API so libraries can develop against it, and the AI-in-the-browser crowd is already wiring in opt-in support: Transformers.js gates it behind an experimental_useCrossOriginStorage flag, with WebLLM and wllama experimenting too. That’s the natural first constituency: a 33 GB model you download once and reuse across every site that runs it is a far better story than downloading it per origin.
On the standards side it’s genuinely early. Mozilla opened a standards-position issue on June 22, 2026, but hasn’t taken a formal stance yet; I couldn’t find a WebKit position or a W3C TAG review at all. So this is one browser’s proposal with no committed implementers.
A quick disambiguation while you’re searching, because the names collide: Cross-Origin Storage is not the Storage Access API. That one is about letting embedded third-party content reach its own cookies. COS is about sharing large content-addressed files. Different problem, confusingly adjacent name.
Should you use the Cross-Origin Storage API?
Not enough to write code against it, but enough to track it. The waste it targets only compounds: every site now pays full freight for its own copy of the same shared libraries and fonts since the cache got partitioned, and in-browser AI keeps pushing file sizes into the gigabytes, where it really starts to hurt. A content-addressed cache shared across origins is the obvious fix, and the interesting engineering is entirely in making it safe rather than making it work.
It also fits a pattern I keep noticing in these platform proposals, the same one behind WebMCP and the HTTP QUERY method: the web already had the raw capability, and the standards work is mostly about giving it a name and a safety model. COS is that for deduplicated storage. There’s a read-side version of this too, which I covered in getting a site ready for AI readers: that post is about the content agents consume, this one is about the model weights they run. Whether it ships as-is, gets filed down by the standards process, or stalls, the shape of the answer looks right. I’d rather understand it now than the day a library flips it on by default.