Partitioning, implemented
Why it matters Partitioning only protects you if it is applied to every place a site can remember something. One forgotten drawer is enough for an embedded tracker to join your visits across sites again. Seeing where engines nearly forgot helps you judge any browser's claims, and prepares you to add the keys yourself in the next module.
Remember the cloakroom analogy from Leg 1: one company, AdCo, runs the cloakroom at three different shops, and a cookie is its cloakroom ticket. Partitioning gave each shop its own ticket book. Now picture the company's back office: ticket books, lockers, a lost-property box, a notice board, a shared phone line between branches. If only the ticket books are split per shop, the company can still leave a note in the shared locker at shop A and read it at shop B. Every one of those things needs a "which shop" label.
From origin to storage key
Without partitioning, an engine files data under one label: the origin that owns it (scheme, host and port, from the last lesson). https://adco.example gets one localStorage, wherever it is embedded.
Partition key: an extra label added to the filing system, normally the top-level site (the schemeful site in the address bar). Data is then filed under the pair (partition key, origin), and an embed sees a different store on every site you visit.
Storage key: the full label an engine looks data up by. With partitioning it is (top-level site, origin) instead of origin alone.
In the practice world's engine the whole change is one line. Browser.storage is a dictionary whose keys are exactly that pair:
# unpartitioned: one store per origin, shared across every site
store = storage[(None, "https://adco.example")]
# partitioned: the top-level site becomes part of the key
store = storage[("shopa.example", "https://adco.example")]The practice world writes the top-level site as a bare name such as shopa.example. Real engines use the schemeful site, https://shopa.example, so the http version of a shop gets a separate partition.
The hard part is not the line. It is finding every place in an engine that keeps state, and making sure each one builds its key the same way.
The drawers, and the ones a naive engine forgets
| Area | What it holds | Easy to forget? |
|---|---|---|
| Cookies | small values sent with requests | No: this is where everyone starts |
| localStorage, sessionStorage | name and value pairs read by scripts | No, but sessionStorage is per tab and needs the key too |
| IndexedDB, Cache Storage | databases and saved responses for offline apps | Sometimes: they live in separate code |
| Service workers | a background script registered for an origin | Yes: the registration itself is state, and it can read its own storage |
| BroadcastChannel, SharedWorker, Web Locks | ways for pages of one origin to talk or coordinate | Yes: they store nothing, so they do not look like storage |
| Blob URLs | temporary addresses for data a page created | Yes: a frame on another site could open one |
| Storage quotas and usage numbers | how much space an origin uses | Yes: a shared counter can carry bits from one site to another |
The communication channels are the classic trap. Suppose adco.example is embedded on shopa.example in one tab and on shopb.example in another. If BroadcastChannel is keyed by origin alone, the two frames can simply message each other: "I am the frame on shopa, this visitor is u-7731." Nothing was stored, yet the visits are joined live. Partitioned engines key these channels like storage, so frames under different top-level sites cannot hear each other.
The awkward case: A inside B inside A
Keying on the top-level site alone has one gap. Picture shopa.example as the top page, embedding adco.example, which embeds a frame from shopa.example again. That inner frame matches the top-level site, so a naive engine hands it shopa's own first-party storage, even though a third party chose to load it. Some engines add a cross-site ancestor bit to the key: "somewhere above me is another site". The practice world does not model this bit, but real engines must.
What engines do today
Firefox partitions third-party storage and cookies by default (Total Cookie Protection, since 2022). Safari blocks third-party cookies and partitions other third-party storage. Chrome partitions third-party storage such as localStorage, IndexedDB and service workers (since Chrome 115, 2023) while keeping ordinary third-party cookies unless you block them (all checked October 2026).
A common misconception
"Partitioning is about cookies." Cookies were first, but the idea is general: anything an origin can write and later read, or anything two frames can share, needs the partition key. A browser that partitions cookies but not, say, its communication channels has a hole exactly where trackers look.
Recap
- Partitioning changes the storage key from origin to (top-level site, origin).
- Every state-holding area needs that key, including service workers, blob URLs and quota numbers.
- Channels such as BroadcastChannel store nothing but can still join visits, so they need the key too.
- Keying on the top-level site alone misses the A inside B inside A case; engines add an ancestor bit.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.