Shared Storage and Private Aggregation Implementation Quickstart

This document is a quickstart guide for using Shared Storage and Private Aggregation. You'll need an understanding of both APIs because Shared Storage stores the values and Private Aggregation creates the aggregatable reports.

Target Audience: Ad techs and measurement providers.

Shared Storage API

To prevent cross-site tracking, browsers have started partitioning all forms of storage, including local storage, cookies, and so forth. But there are use cases where unpartitioned storage is required. The Shared Storage API provides unlimited write access across different top-level sites with privacy-preserving read access.

Shared Storage is restricted to the context origin (the caller of sharedStorage).

Shared Storage has a capacity limit per origin, with each entry limited to a maximum number of characters. If the limit is reached, no further inputs are stored. The data storage limits are outlined in the Shared Storage explainer.

Invoking Shared Storage

Ad techs can write to Shared Storage using JavaScript or response headers. Reading from Shared Storage only occurs within an isolated JavaScript environment called a worklet.

  • Using JavaScript Ad techs can perform specific Shared Storage functions such as setting, appending, and deleting values outside of a JavaScript worklet. However, functions such as reading Shared Storage and performing Private Aggregation have to be completed through a JavaScript worklet. Methods that can be used outside of a JavaScript worklet can be found in Proposed API Surface - Outside the worklet.

    Methods that are used in the worklet during an operation can be found in Proposed API Surface - In the worklet.

  • Using response headers

    Similar to JavaScript, only specific functions such as setting, appending, and deleting values in Shared Storage can be done using response headers. To work with Shared Storage in a response header, Shared-Storage-Writable: ?1 has to be included in the request header.

    To initiate a request from the client, run the following code, depending on your chosen method:

    • Using fetch()

      fetch("https://a.example/path/for/updates", {sharedStorageWritable: true});
      
    • Using an iframe or img tag

      <iframe src="https://a.example/path/for/updates" sharedstoragewritable></iframe>
      
    • Using an IDL attribute with an iframe or img tag

      let iframe = document.getElementById("my-iframe");
      iframe.sharedStorageWritable = true;
      iframe.src = "https://a.example/path/for/updates";
      

Further information can be found in Shared Storage: Response Headers.

Writing to Shared Storage

To write to Shared Storage, call sharedStorage.set() from inside or outside a JavaScript worklet. If called from outside the worklet, the data is written to the origin of the browsing context that the call was made from. If called from inside the worklet, the data is written to the origin of the browsing context that loaded the worklet. The keys that are set have an expiration date of 30 days from last update.

The ignoreIfPresent field is optional. If present and set to true, the key is not updated if it already exists. Key expiration is renewed to 30 days from the set() call even if the key is not updated.

If Shared Storage is accessed multiple times in the same page load with the same key, the value for the key is overwritten. It's a good idea to use sharedStorage.append() if the key needs to maintain the previous value.

  • Using JavaScript

    Outside the worklet:

    window.sharedStorage.set('myKey', 'myValue1', { ignoreIfPresent: true });
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.set('myKey', 'myValue2', { ignoreIfPresent: true });
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.set('myKey', 'myValue2', { ignoreIfPresent: false });
    // Shared Storage: {'myKey': 'myValue2'}
    

    Similarly, inside the worklet:

    sharedStorage.set('myKey', 'myValue1', { ignoreIfPresent: true });
    
  • Using response headers

    You can also write to Shared Storage using response headers. To do so, use Shared-Storage-Write in the response header along with the following commands:

    Shared-Storage-Write : set;key="myKey";value="myValue";ignore_if_present
    
    Shared-Storage-Write : set;key="myKey";value="myValue";ignore_if_present=?0
    

    Multiple items can be comma-separated and can combine set, append, delete, and clear.

    Shared-Storage-Write :
    set;key="hello";value="world";ignore_if_present, set;key="good";value="bye"
    

Appending a value

You can append a value to an existing key using the append method. If the key does not exist, calling append() creates the key and sets the value. This can be accomplished using JavaScript or response headers.

  • Using JavaScript

    To update values of existing keys, use sharedStorage.append() from either inside or outside the worklet.

    window.sharedStorage.append('myKey', 'myValue1');
    // Shared Storage: {'myKey': 'myValue1'}
    window.sharedStorage.append('myKey', 'myValue2');
    // Shared Storage: {'myKey': 'myValue1myValue2'}
    window.sharedStorage.append('anotherKey', 'hello');
    // Shared Storage: {'myKey': 'myValue1myValue2', 'anotherKey': 'hello'}
    

    To append inside the worklet:

    sharedStorage.append('myKey', 'myValue1');
    
  • Using response headers

    Similar to setting a value in Shared Storage, you can use the Shared-Storage-Write in the response header to pass in the key-value pair.

    Shared-Storage-Write : append;key="myKey";value="myValue2"
    

Batch updating of values

You can call sharedStorage.batchUpdate() from within or outside a JavaScript worklet and pass in an ordered array of methods that specify the chosen operations. Each method constructor accepts the same parameters as its corresponding individual method for set, append, delete and clear.

You can set the lock by using JavaScript or response header:

  • Using JavaScript

    The available JavaScript methods that can be used with batchUpdate() include:

    • SharedStorageSetMethod(): Writes a key-value pair to Shared Storage.
    • SharedStorageAppendMethod(): Appends a value to an existing key in Shared Storage.
    • SharedStorageDeleteMethod(): Deletes a key-value pair from Shared Storage.
    • SharedStorageClearMethod(): Clears all keys in Shared Storage.
    sharedStorage.batchUpdate([
    new SharedStorageSetMethod('keyOne', 'valueOne'),
    new SharedStorageAppendMethod('keyTwo', 'valueTwo'),
    new SharedStorageDeleteMethod('keyThree'),
    new SharedStorageClearMethod()
    ]);
    
  • Using response headers

    Shared-Storage-Write : batchUpdate;methods="set;key=keyOne;value=valueOne, append;key=keyTwo;value=valueTwo,delete;key=keyThree,clear"
    

Reading from Shared Storage

You can read from Shared Storage only from within a worklet.

await sharedStorage.get('mykey');

The origin of the browsing context that the worklet module was loaded from determines whose Shared Storage is read.

Deleting from Shared Storage

You can perform deletes from Shared Storage using JavaScript from either inside or outside the worklet or by using response headers with delete(). To delete all keys at once, use clear() from either.

  • Using JavaScript

    To delete from Shared Storage from outside the worklet:

    window.sharedStorage.delete('myKey');
    

    To delete from Shared Storage from inside the worklet:

    sharedStorage.delete('myKey');
    

    To delete all keys at once from outside the worklet:

    window.sharedStorage.clear();
    

    To delete all keys at once from inside the worklet:

    sharedStorage.clear();
    
  • Using response headers

    To delete values using response headers, you can also use Shared-Storage-Write in the response header to pass the key to be deleted.

    delete;key="myKey"
    

    To delete all keys using response headers:

    clear;
    

Reading Protected Audience interest groups from Shared Storage

You can read Protected Audience's interest groups from a Shared Storage worklet. The interestGroups() method returns an array of StorageInterestGroup objects, including the attributes AuctionInterestGroup and GenerateBidInterestGroup.

The following example shows how to read the browsing context interest groups and some possible operations that could be performed on the retrieved interest groups. Two possible operations used are finding the number of interest groups and finding the interest group with the highest bid count.

async function analyzeInterestGroups() {
  const interestGroups = await interestGroups();
  numIGs = interestGroups.length;
  maxBidCountIG = interestGroups.reduce((max, cur) => { return cur.bidCount > max.bidCount ? cur : max; }, interestGroups[0]);
  console.log("The IG that bid the most has name " + maxBidCountIG.name);
}

The origin of the browsing context that the worklet module was loaded from determines the origin of the interest groups being read by default. To learn more about the default worklet origin and how to change it, review the Executing Shared Storage and Private Aggregation section in the Shared Storage API Walkthrough.

Options

All Shared Storage modifier methods support an optional options object as the last argument.

withLock

The withLock option is optional. If specified, this option instructs the method to acquire a lock for the defined resource using the Web Locks API before proceeding. A lock name is passed when requesting the lock. The name represents a resource for which usage is coordinated across multiple tabs,workers or code within the origin.

The withLock option can be used with the following Shared Storage modifier methods:

  • set
  • append
  • delete
  • clear
  • batch update

You can set the lock by using JavaScript or response header:

  • Using JavaScript

    sharedStorage.set('myKey', 'myValue', { withLock: 'myResource' });
    
  • Using response headers

    Shared-Storage-Write : set;key="myKey";value="myValue";with_lock="myResource"
    

Shared Storage locks are partitioned by the data origin. The locks are independent of any locks obtained using the LockManager request() method, regardless of whether they're in a window or worker context. Nevertheless, they share the same scope as locks obtained using request() within the SharedStorageWorklet context.

While the request() method allows for various configuration options, locks acquired within Shared Storage always adhere to the following default settings:

  • mode: "exclusive": No other locks with the same name can be held concurrently.
  • steal: false: Existing locks with the same name are not released to accommodate other requests.
  • ifAvailable: false: The requests wait indefinitely until the lock becomes available.
When to use withLock

Locks are useful in scenarios where there may be multiple worklets running simultaneously (e.g., multiple worklets on a page, or multiple worklets in different tabs) each of which are looking at the same data. In that scenario, it's a good idea to wrap the relevant worklet code with a lock to ensure that only one worklet is processing reports at a time.

Another situation in which locks are useful is if there are multiple keys that need to be read together in a worklet, and their state should be synchronized. In that case, one should wrap the get calls with a lock, and be sure to acquire the same lock when writing to those keys.

Order of locks

Due to the nature of web locks, modifier methods might not execute in the order that you defined. If the first operation requires a lock and it is delayed, the second operation may start before the first one finishes.

For Example:

// This line might pause until the lock is available.
sharedStorage.set('keyOne', 'valueOne', { withLock: 'resource-lock' });

// This line will run right away, even if the first one is still waiting.
sharedStorage.set('keyOne', 'valueTwo');
Modify multiple keys example

This example uses a lock to ensure that the read and delete operations within the worklet happen together, preventing interference from outside the worklet.

The following modify-multiple-keys.js example sets new values for keyOne and keyTwo with modify-lock then executes the modify-multiple-keys operation from the worklet:

// modify-multiple-keys.js
sharedStorage.batchUpdate([
    new SharedStorageSetMethod('keyOne', calculateValueFor('keyOne')),
    new SharedStorageSetMethod('keyTwo', calculateValueFor('keyTwo'))
], { withLock: 'modify-lock' });

const modifyWorklet = await sharedStorage.createWorklet('modify-multiple-keys-worklet.js');
await modifyWorklet.run('modify-multiple-keys');

Then, within the modify-multiple-keys-worklet.js you can request the lock using navigator.locks.request() to read and modify the keys as necessary

// modify-multiple-keys-worklet.js
class ModifyMultipleKeysOperation {
  async run(data) {
    await navigator.locks.request('modify-lock', async (lock) => {
      const value1 = await sharedStorage.get('keyOne');
      const value2 = await sharedStorage.get('keyTwo');

      // Do something with `value1` and `value2` here.

      await sharedStorage.delete('keyOne');
      await sharedStorage.delete('keyTwo');
    });
  }
}
register('modify-multiple-keys', ModifyMultipleKeysOperation);

Context switching

Shared Storage data is written to the origin (for example, https://example.adtech.com) of the browsing context that the call originated from.

When you load the third-party code using a <script> tag, the code is executed in the browsing context of the embedder. Therefore, when the third-party code calls sharedStorage.set(), the data is written to the embedder's Shared Storage. When you load the third-party code within an iframe, the code receives a new browsing context, and its origin is the origin of the iframe. Therefore, the sharedStorage.set() call made from the iframe stores the data into the Shared Storage of the iframe origin.

First-party context

If a first-party page has embedded third-party JavaScript code that calls sharedStorage.set() or sharedStorage.delete(), the key-value pair is stored in the first-party context.

Data stored in a first-party page with embedded third-party JavaScript.

Third-party context

The key-value pair can be stored in the ad tech or third-party context by creating an iframe and calling set() or delete() in the JavaScript code from within the iframe.

Data stored in the ad-tech or third-party context.

Private Aggregation API

To measure aggregatable data stored in Shared Storage, you can use the Private Aggregation API.

To create a report, call contributeToHistogram() inside a worklet with a bucket and value. The bucket is represented by an unsigned 128-bit integer which must be passed into the function as a BigInt. The value is a positive integer.

To protect privacy, the report's payload, which contains the bucket and value, is encrypted in transit, and it can only be decrypted and aggregated using the Aggregation Service.

The browser will also limit the contributions a site can make to an output query. Specifically, the contribution budget limits the total of all reports from a single site for a given browser in a given time window across all buckets. If the current budget is exceeded, a report will not be generated.

privateAggregation.contributeToHistogram({
  bucket: BigInt(myBucket),
  value: parseInt(myBucketValue)
});

Executing Shared Storage and Private Aggregation

You must create a worklet to access data from shared storage. To do this, call createWorklet() with the URL of the worklet. By default, when using shared storage with createWorklet(), the data partition origin will be the invoking browsing context's origin, not the origin of the worklet script itself.

To change the default behaviour, set the dataOrigin property when calling createWorklet.

  • dataOrigin: "context-origin": (Default) Data is stored in the shared storage of the invoking browsing context's origin.
  • dataOrigin: "script-origin": Data is stored in the shared storage of the worklet script's origin. Note that an opt-in is required to enable this mode.
sharedStorage.createWorklet(scriptUrl, {dataOrigin: "script-origin"});

To opt-in, when using "script-origin", the script endpoint will have to respond with the header Shared-Storage-Cross-Origin-Worklet-Allowed. Note that CORS should be enabled for cross-origin requests.

Shared-Storage-Cross-Origin-Worklet-Allowed : ?1

Using cross-origin iframe

An iframe is needed to invoke the shared storage worklet.

In the ad's iframe, load the worklet module by calling addModule(). To run the method that is registered in the sharedStorageWorklet.js worklet file, in the same ad iframe JavaScript, call sharedStorage.run().

const sharedStorageWorklet = await window.sharedStorage.createWorklet(
  'https://any-origin.example/modules/sharedStorageWorklet.js'
);
await sharedStorageWorklet.run('shared-storage-report', {
  data: { campaignId: '1234' },
});

In the worklet script, you will need to create a class with an async run method and register it to run in the ad's iframe. Inside sharedStorageWorklet.js:

class SharedStorageReportOperation {
  async run(data) {
    // Other code goes here.
    bucket = getBucket(...);
    value = getValue(...);
    privateAggregation.contributeToHistogram({
      bucket,
      value
    });
  }
}
register('shared-storage-report', SharedStorageReportOperation);

Using cross-origin request

Shared Storage and Private Aggregation allows creation of cross-origin worklets without the need for cross-origin iframes.

The first-party page can also invoke a createWorklet() call to the cross-origin javascript endpoint. You will need to set the data partition origin of the worklet to be that of the script-origin when creating the worklet.

async function crossOriginCall() {
  const privateAggregationWorklet = await sharedStorage.createWorklet(
    'https://cross-origin.example/js/worklet.js',
    { dataOrigin: 'script-origin' }
  );
  await privateAggregationWorklet.run('pa-worklet');
}
crossOriginCall();

The cross-origin javascript endpoint will have to respond with the headers Shared-Storage-Cross-Origin-Worklet-Allowed and note that CORS is enabled for the request.

Shared-Storage-Cross-Origin-Worklet-Allowed : ?1

Worklets created using the createWorklet() will have selectURL and run(). addModule() is not available for this.

class CrossOriginWorklet {
  async run(data){
    // Other code goes here.
    bucket = getBucket(...);
    value = getValue(...);
    privateAggregation.contributeToHistogram({
      bucket,
      value
    });
  }
}

Next steps

The following pages explain important aspects of the Shared Storage and Private Aggregation APIs.

Once you are acquainted with the APIs, you can start collecting the reports, which are sent as a POST request to the following endpoints as JSON in the request body.

  • Debug Reports - context-origin/.well-known/private-aggregation/debug/report-shared-storage
  • Reports - context-origin/.well-known/private-aggregation/report-shared-storage

Once reports are collected, you can test using the local testing tool or set up the Trusted Execution Environment for Aggregation Service to get the aggregated reports.

Share your feedback

You can share your feedback on the APIs and documentation on GitHub.