Partitioned Cookies and CHIPS in Extensions

Find, read and write partitioned (CHIPS) cookies from an MV3 extension: the partitionKey filter, top-level site keys, why getAll omits them, and Firefox's first-party isolation.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

An embedded widget — a chat bubble, a payment iframe, a comment system — stores its session in a third-party cookie. Since browsers began phasing out unpartitioned third-party cookies, widgets have moved to the Partitioned attribute (CHIPS), and your extension’s code that reads widget.example cookies suddenly returns an empty array even though DevTools shows the cookie sitting there. The cookie is real; it lives in a partition your query never asked for. This guide belongs to cookies and webRequest observation.

A partitioned cookie is keyed by two things: its own domain and the top-level site the user was on when it was set. widget.example embedded on news.site and on shop.site gets two independent cookie jars, and neither can see the other. The extension API mirrors this exactly. Every cookie object carries an optional partitionKey with a topLevelSite field, and calls that do not mention a partition key see only unpartitioned cookies — the API’s default answer to “which cookies exist?” deliberately excludes every partition, so legacy code keeps behaving as it did before CHIPS existed.

One embedded domain, several cookie jarswidget.example embedded on news.site and on shop.site writes the same cookie name into two partitions keyed by top-level site, plus an unpartitioned jar visible by default.news.site tabembeds widget.exampleshop.site tabembeds widget.examplewidget.example tabfirst-party visiteach context writes sid=…Partition news.sitesid=APartition shop.sitesid=BUnpartitionedsid=C (default view)
Without a partitionKey in the query, only the bottom-right jar is visible.

Step-by-step: working with partitions

Before changing code, prove the diagnosis. In the page’s DevTools, Application → Cookies shows a “Partition Key Site” column for partitioned cookies. From the extension, query with an explicit key:

1const embedded = await chrome.cookies.getAll({
2  domain: "widget.example",
3  partitionKey: { topLevelSite: "https://news.site" },
4});
5console.table(embedded.map(({ name, value, partitionKey }) => ({
6  name, value, site: partitionKey?.topLevelSite,
7})));

Execution context: the service worker console. topLevelSite is a scheme plus registrable domain — https://news.site, never https://www.news.site/page. Passing a full URL or a subdomain returns nothing. Chrome supports partitionKey from version 119; Firefox supports it with the same shape, and Safari does not expose partitions at all.

2. Enumerate partitions when you do not know the top-level site

There is no “list partitions” call, but an empty partitionKey object asks for cookies across every partition.

1export async function allPartitions(domain) {
2  const cookies = await chrome.cookies.getAll({ domain, partitionKey: {} });
3  const bySite = new Map();
4  for (const c of cookies) {
5    const site = c.partitionKey?.topLevelSite ?? "(unpartitioned)";
6    bySite.set(site, [...(bySite.get(site) ?? []), c]);
7  }
8  return bySite;
9}

Execution context: the service worker. The result includes unpartitioned cookies too, so the map’s (unpartitioned) entry is the same set the default query returns. Host permissions still apply: you need access to widget.example itself, and Chrome does not require access to the top-level site to read its partition of another domain’s cookies.

3. Read the partition for the tab the user is looking at

The useful question is usually “what is the widget’s session on the page in front of the user?”

 1function topLevelSiteOf(url) {
 2  const { protocol, hostname } = new URL(url);
 3  // registrable domain approximation — use a PSL library in production
 4  const parts = hostname.split(".");
 5  return `${protocol}//${parts.slice(-2).join(".")}`;
 6}
 7
 8export async function widgetSessionForTab(tabId) {
 9  const tab = await chrome.tabs.get(tabId);
10  return chrome.cookies.get({
11    url: "https://widget.example/",
12    name: "sid",
13    partitionKey: { topLevelSite: topLevelSiteOf(tab.url) },
14  });
15}

Execution context: the service worker. Reading tab.url needs the tabs permission or host access to the tab’s origin. The two-label shortcut fails for sites like example.co.uk; a public-suffix-list library gives the correct registrable domain. Getting the site wrong is indistinguishable from “no cookie”.

Which query sees the partitioned cookie?Three ways to call cookies.getAll and whether each one returns a partitioned cookie set by widget.example on news.site.What partitionKey did you pass?omittedUnpartitioned onlylegacy behaviourCookie missinglooks like a bug{ topLevelSite }That one partitionexact site matchCookie foundif site is right{}Every partitionplus unpartitionedCookie foundgroup by site
Only the explicit key or the empty wildcard reaches into partitions.

4. Write and remove inside a partition

Writes follow the same rule: no key, no partition. A partitioned cookie must also be secure.

 1await chrome.cookies.set({
 2  url: "https://widget.example/",
 3  name: "consent",
 4  value: "granted",
 5  secure: true,
 6  sameSite: "no_restriction",
 7  partitionKey: { topLevelSite: "https://news.site" },
 8});
 9
10await chrome.cookies.remove({
11  url: "https://widget.example/",
12  name: "consent",
13  partitionKey: { topLevelSite: "https://news.site" },
14});

Execution context: the service worker. Omitting secure: true makes Chrome reject the write and resolve null. Removing without the key targets the unpartitioned cookie of the same name, leaving the partitioned one in place — a classic “logout did not work on that one site” bug.

5. Handle partitioned events in onChanged

chrome.cookies.onChanged reports partitioned cookies with their partitionKey. A sign-in detector for the first-party site should ignore them; a widget-aware feature should route them by site.

1chrome.cookies.onChanged.addListener(({ cookie, removed }) => {
2  if (cookie.domain.replace(/^\./, "") !== "widget.example") return;
3  const site = cookie.partitionKey?.topLevelSite;
4  if (!site) return handleFirstParty(cookie, removed);
5  handleEmbedded(site, cookie, removed);
6});

Execution context: the service worker, top level. Firefox reports first-party-isolated cookies with firstPartyDomain rather than partitionKey when isolation is enabled; treat either field as “this belongs to a partition”.

6. Fall back to the frame itself when partitions are unreachable

Sometimes the partition API is the wrong tool. If the feature only needs the widget’s state while the user is looking at it — showing the chat’s unread count, say — a content script injected into the widget’s frame can read that frame’s own document.cookie (for non-httpOnly cookies) or, better, its DOM. That approach works identically in Safari, which hides partitions from the cookies API, and it needs host access only to the widget’s origin.

1// content script registered for https://widget.example/* with all_frames: true
2if (window.top !== window) {
3  const unread = document.querySelector("[data-unread]")?.dataset.unread ?? "0";
4  chrome.runtime.sendMessage({ type: "widget:unread", unread: Number(unread) });
5}

Execution context: a content script in the embedded frame, running in its isolated world with the frame’s DOM and its own partitioned document.cookie. The sender’s tab.id on the receiving side tells the worker which top-level page the frame belongs to, which is exactly the partition you would otherwise have had to compute. Chrome, Firefox and Safari all support all_frames content scripts; Safari requires the user to grant access to the widget’s origin explicitly.

Choose between the two approaches by asking whether you need the cookie when the page is closed. Background features — clearing a widget session from the options page, auditing which sites embed a tracker — need the partition API. Foreground features are usually simpler and more portable from inside the frame.

Cross-browser variation

  • Chrome / Edge: partitionKey.topLevelSite from Chrome 119; {} matches all partitions. Newer versions add a hasCrossSiteAncestor field to the key — leave it unset unless you need to distinguish same-site from cross-site frames.
  • Firefox: supports partitionKey for Total Cookie Protection partitions, and separately firstPartyDomain for the older first-party isolation pref. With that pref on, every call must pass firstPartyDomain or it throws.
  • Safari: partitions third-party storage by default but does not expose partition keys to extensions. Embedded cookies are generally unreachable from browser.cookies; design features that need them around a content script inside the frame instead.

Verification

  1. Visit a page that embeds the widget and confirm in DevTools that the cookie shows a partition key site.
  2. Run allPartitions("widget.example") in the service worker console and confirm a map entry for that site.
  3. Call widgetSessionForTab with the tab’s id and confirm it returns the same value DevTools shows.
  4. Remove the cookie with the partition key and confirm DevTools no longer lists it, while the unpartitioned cookie of the same name — if any — remains.
Cookies returned for widget.example by query shapeNumber of cookies returned by getAll for an embedded widget domain across five sites, depending on whether partitionKey is omitted, set to one site, or empty.partitionKey omitted1 cookiesunpartitioned only{ topLevelSite: news.site }1 cookiesone partitionpartitionKey {}6 cookiesfive partitions + default
The default query sees one cookie; the wildcard sees all six.

FAQ

Do I need host permissions for the top-level site too?

No. Access is decided by the cookie’s own domain. Reading widget.example cookies in the news.site partition needs host access to widget.example. You may still need tab access to learn which site the user is on.

Will old code that ignores partitions break?

It keeps working for unpartitioned cookies and silently misses partitioned ones. As more embedded services adopt CHIPS, features built on third-party cookies degrade gradually rather than failing loudly — audit any code that reads cookies for a domain that is usually embedded.

No. Writing an unpartitioned cookie with the same name creates a separate cookie; the embedded service reading from inside its frame still sees its partitioned one.

Other Core APIs & Cross-Browser Data Management Resources