Skip to content

Add Edge Cookie (EC) product requirements document#511

Draft
aram356 wants to merge 1 commit intomainfrom
feature/ssc-plan
Draft

Add Edge Cookie (EC) product requirements document#511
aram356 wants to merge 1 commit intomainfrom
feature/ssc-plan

Conversation

@aram356
Copy link
Collaborator

@aram356 aram356 commented Mar 17, 2026

Summary

  • Adds the internal PRD for Server-Side Cookie (SSC), a consent-aware first-party identity system that replaces SyntheticID
  • Covers SSC identity generation (IP + salt only), consent lifecycle (TCF/GPP enforcement with real-time withdrawal), KV Store identity graph, pixel sync endpoint (/sync), S2S batch API (/api/v1/sync), and bidstream decoration (/identify + /auction)
  • Defines TS Lite deployment mode — a runtime configuration that exposes only SSC routes, enabling SSPs, DSPs, and identity providers to adopt SSC without the full Trusted Server feature surface

Key sections

  • Problem statement: SyntheticID signal degradation (UA reduction, IPv6 rotation), no consent enforcement, adoption blocked by full TS requirement
  • SSC identity: HMAC-SHA256 of IP (IPv6 /64 prefix) + publisher salt, ts-ssc cookie at apex domain
  • Consent lifecycle: TCF/GPP enforcement with region-based rules, real-time cookie + KV deletion on withdrawal
  • KV identity graph: SSC hash → partner ID map with atomic read-modify-write via generation markers
  • Pixel sync (GET /sync): Browser redirect-based partner ID sync with rate limiting and domain allowlist
  • S2S batch API (POST /api/v1/sync): Authenticated bulk ID mapping push for DSPs and partners
  • Bidstream decoration: Header-only mode (/identify) and full auction mode (/auction) with OpenRTB 2.6 user.eids
  • Configuration: New [ssc] and [features] TOML sections, partner registry in KV store

Copy link
Collaborator Author

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRD Review — Completeness for Engineering Kickoff

Overall this is a strong PRD. The problem statement, goals, and feature surface are well-defined.

Main structural feedback: Several sections include implementation details (HMAC algorithms, KV generation markers, bcrypt, specific config formats, JSON schemas) that belong in a technical design doc, not a PRD. A PRD should clearly spell out what the system does and why — not how it's built. Separating these concerns will make the PRD more durable and keep implementation decisions with engineering where they can evolve.

Below are specific questions and suggestions per section. The goal is to close ambiguity so engineering can start without guessing at product intent.

| # | Question | Owner | Target resolution |
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------- |
| 1 | Partner provisioning flow: should partner records be written manually by a TS admin, or via a `/admin/partners/register` endpoint using the existing admin auth pattern? The latter is more scalable but requires additional implementation. | Product | Before engineering kickoff |
| 2 | Should TS Lite expose a `GET /health` endpoint so partners can programmatically verify their service is running and their partner config is active in KV? | Product | Before engineering kickoff |
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: Open Questions 1 and 2 are marked "before engineering kickoff" — these need answers before work begins. Can we get a resolution date or default decision for each?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q2 is moot since we are removing TS-Lite from this PRD.

Q1 clarification:

Partner provisioning flow: TS will expose an /admin/partners/register endpoint authenticated at the publisher level (bearer token issued per publisher Fastly service), so publishers can onboard SSP/DSP partners without touching KV directly. Engineering to define the exact auth mechanism.


### 9.4 Flow

1. Read the `ts-ssc` cookie. If absent, redirect to `return` URL immediately without writing to KV. Do not create a new SSC during a sync — a sync redirect is not an organic user visit and must not be used to bootstrap identity.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify: What does the user experience when sync is a no-op?

When there is no ts-ssc cookie, the user is redirected to the return URL "immediately without writing to KV." But step 6 appends ts_synced=1 on success. What is appended (or not) on a no-op? Should it be ts_synced=0 so the partner knows the sync did not take effect? Without this, the partner has no way to distinguish success from silent failure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! I redid this section 9.4

  1. Read the ts-ec cookie. If absent, redirect to return URL with ts_synced=0 appended. Do not create a new EC during a sync, a sync redirect is not an organic user visit and must not be used to bootstrap identity (note this is good for deterministic ID mapping to avoid anything probablistic).
  2. Look up the partner record in partner_store KV using the partner parameter. Return 400 if the partner is not found.
  3. Validate the return URL against the partner's allowed_return_domains. Return 400 if the domain is not on the allowlist.
  4. Evaluate consent for this user by decoding from request cookies (or the optional consent query parameter if no cookie signal is present). If consent is absent or invalid, redirect to return with ts_synced=0&ts_reason=no_consent. No KV write is performed.
  5. Perform an atomic read-modify-write to update ids[partner_id] in the KV identity graph (with generation marker — see Section 8.4). If the write fails after all retries, redirect to return with ts_synced=0&ts_reason=write_failed.
  6. On successful KV write, redirect to return with ts_synced=1 appended as a query parameter.

}
```

HTTP status `207 Multi-Status` when any mappings are rejected; `200 OK` when all are accepted.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify: What happens when auth is valid but all mappings fail?

The spec says 207 Multi-Status when "any mappings are rejected" and 200 OK when "all are accepted." But what if all 1000 mappings are rejected (e.g., all ssc_hash_not_found)? Is that 207 with accepted: 0, or a different status? Partners need to know what "complete failure of a valid request" looks like.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"207 with accepted: 0" is because the request itself was valid at every layer (auth, schema, batch size). The all-rejected case is a data problem, not a protocol error, and 207 is the correct HTTP semantic for "processed but items had issues." I added a status table so engineering has an unambiguous reference for every case, plus a note that partners should not blindly retry an all-rejected batch.

Condition Status
All mappings accepted 200 OK
Some mappings accepted, some rejected 207 Multi-Status
Auth valid, batch valid, but all mappings rejected 207 Multi-Status with accepted: 0
Auth invalid 401 Unauthorized (no body processing)
Batch exceeds 1000 mappings or malformed JSON 400 Bad Request (no body processing)

A 207 with accepted: 0 signals "your request was received and processed correctly, but none of the submitted EC hashes were found or eligible." This is distinct from an auth or protocol error. Partners should treat this as a data signal, either the EC hashes are stale/unknown, or consent has been withdrawn for all submitted users, and should not retry the same batch without investigating the underlying cause.


**Endpoint:** `GET /identify`

**Response:** `204 No Content` with the following headers:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify: Who calls /identify and when?

This section says "the publisher's ad server reads these headers" — but the call context is unclear:

  • Is this called per-pageview? Per-auction?
  • Is it a sub-request from the same edge (e.g., Fastly VCL calling TS), or a separate HTTP call from the publisher's origin server?
  • Does /identify read the user's cookies directly (same-origin), or does the caller pass identity via headers?

This affects whether the endpoint needs its own cookie/consent handling or relies on the caller forwarding that context.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, this was a good callout as the flow was not just unclear but incorrect based on existing workflows. I updated 12.2 completely and corrected 12.3. Here's updated markdown:

12.2 Mode A: Identity resolution (/identify)

Trusted Server exposes /identify as a standalone identity resolution endpoint for callers that need EC identity and resolved partner UIDs outside of TS's own auction orchestration. TS builds the OpenRTB request in Mode B, /identify is not part of that path. It serves three distinct use cases:

Use case 1: Attribution and analytics
Any server-side or browser-side system that needs to tag an event, impression, or conversion with the user's EC hash. Examples: analytics pipelines, attribution platforms, reporting dashboards.

Use case 2: Publisher ad server outbid context
After TS's auction completes and winners are delivered to the publisher's ad server endpoint, the publisher's ad server may need EC identity and resolved partner UIDs to evaluate whether to accept the programmatic winner or outbid with a direct-sold placement. For this use case, TS includes the EC identity in the winner notification payload directly (see Section 12.3), a separate /identify call is only needed if the publisher's ad server receives the winner through a path that does not carry TS headers.

Use case 3: Client-side wrappers for non-TS SSPs
Some SSPs run client-side header bidding wrappers (e.g., Amazon TAM, certain Index Exchange configurations) that do not participate in TS's server-side auction orchestration. A Prebid.js module or custom wrapper script calls /identify from the browser to obtain the EC hash and resolved partner UIDs, then injects those values into bid requests sent to those SSPs. This ensures non-TS demand sources bid with the same identity enrichment as TS-orchestrated bids, enabling a fair comparison at winner selection.

Prerequisite for use case 3: For a non-TS SSP to receive a useful UID from /identify, that SSP must already be a registered partner in partner_store and must have a resolved uid in the KV identity graph for this user (via pixel sync, S2S batch, or S2S pull). Without a prior sync, /identify returns no uid for that partner.

Endpoint: GET /identify

When to call: Once per auction event, not per-pageview. For use case 3, call before sending bid requests to non-TS SSPs.

Call patterns

Pattern 1: Browser-direct (recommended for use cases 1 and 3)

A script on the publisher's page calls /identify via fetch(). Because ec.publisher.com is same-site with the publisher's domain, the browser sends the ts-ec cookie and consent cookies automatically. No forwarding required.

const identity = await fetch('https://ec.publisher.com/identify')
  .then(r => r.json());

// GAM key-value targeting
googletag.pubads().setTargeting('ts_ec', identity.ec);
googletag.pubads().setTargeting('ts_uid2', identity.uids.uid2);

// Prebid.js userIds injection
pbjs.setConfig({ userSync: { userIds: [{ name: 'uid2', value: { id: identity.uids.uid2 } }] } });

Pattern 2: Origin-server proxy (for use case 2 when TS winner headers are unavailable)

A server-side caller must forward the following from the original browser request:

Header to forward Required
Cookie: ts-ec=<value> or X-ts-ec: <value> Yes, without this, TS cannot identify the user
Cookie: euconsent-v2=<value> or Cookie: gpp=<value> Yes, without this, TS returns consent: denied and no identity data
X-consent-advertising: <value> Optional, takes precedence over cookie consent if present

Cookie and consent handling

/identify follows the EC retrieval priority from Section 6.4. It does not generate a new EC — if no EC is present, the response body contains consent: denied and empty identity fields. Consent is evaluated per Section 7.1. /identify never sets or modifies cookies.

Response

200 OK — identity resolved

EC is present and consent is valid. Identity values are returned as a JSON body. Callers use these values to construct URL parameters for GAM, SSP bid requests, analytics events, or any other downstream system.

{
  "ec": "a1b2c3...AbC123",
  "consent": "ok",
  "uids": {
    "uid2": "A4A...",
    "liveramp": "LR_xyz",
    "id5": "ID5-abc"
  },
  "eids": [
    { "source": "uidapi.com", "uids": [{ "id": "A4A...", "atype": 3 }] },
    { "source": "liveramp.com", "uids": [{ "id": "LR_xyz", "atype": 3 }] }
  ]
}

uids contains one key per partner with bidstream_enabled: true and a resolved UID in the KV graph. Partners with no resolved UID for this user are omitted.

403 Forbidden: consent denied

EC is present but the user has not given consent (or consent has been withdrawn). Callers must omit identity parameters from all downstream requests. The status code alone is sufficient to detect this case — body parsing is not required.

{ "consent": "denied" }

204 No Content — no EC present

No ts-ec cookie and no X-ts-ec header was found on the request. The user has not yet established an EC on this publisher. No body is returned. Callers should proceed without identity enrichment.

Response headers (supplementary)

In addition to the JSON body, TS sets the following response headers for server-to-server callers, logging, and future use. These are not the primary integration contract — callers should read the JSON body.

Header Value
X-ts-ec <ec_hash.suffix> or absent if no EC
X-ts-eids Base64-encoded JSON array of OpenRTB 2.6 user.eids objects
X-ts-<partner_id> Resolved UID per partner (e.g., X-ts-uid2, X-ts-liveramp)
X-ts-ec-consent ok or denied

12.3 Mode B: Full auction orchestration (/auction)

Trusted Server owns the full auction path in Mode B. TS builds the OpenRTB request, injects EC identity and resolved partner UIDs, sends it to Prebid Server, receives bids, selects winners, and delivers the winner set to the publisher's ad server endpoint. The publisher's ad server does not build the OpenRTB request — it receives auction winners from TS and either accepts the programmatic winner or outbids it with a direct-sold placement.

EC injection into the outbound OpenRTB request (changes from current behavior):

  • user.id is set to the full EC value (hash.suffix)
  • user.eids is populated from the KV identity graph for this user (see OpenRTB structure below)
  • user.consent is set to the decoded TCF string (currently always null)
  • SSP-specific ext.eids: when calling a specific PBS adapter, only that SSP's resolved ID is included in the adapter-level ext.eids. All configured identity providers are included at the top-level user.eids.

EC context in winner notification to publisher's ad server:

When TS delivers auction winners to the publisher's ad server endpoint, the response includes EC identity so the publisher's ad server has full context for its outbid decision without needing to call /identify separately:

Header Value
X-ts-ec <ec_hash.suffix>
X-ts-eids Base64-encoded JSON array of OpenRTB 2.6 user.eids objects
X-ts-ec-consent ok or denied

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in degraded flag based on questions about 8.3

{
"ec": "a1b2c3...AbC123",
"consent": "ok",
"degraded": true,
"uids": {},
"eids": []
}
The status is still 200 (not 503) because the request succeeded — TS has the EC from the cookie and consent is valid. The degraded: true flag is the signal that uids/eids are empty due to infrastructure, not because the user is genuinely unenriched. Callers check degraded to decide whether to retry vs. proceed without partner UIDs.

1. **`X-consent-advertising` request header** — set by the Didomi integration (or another CMP proxy) in a prior server-side decode. This is the freshest signal and takes precedence over browser-stored values.
2. **`euconsent-v2` cookie** — the TCF v2 consent string stored by the publisher's CMP.
3. **`gpp` cookie** — the IAB Global Privacy Platform string for US state-level consent.
4. **Default: no consent** — if no signal is found, do not create the SSC (fail safe).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify: Conflict between "no signal = no consent" and "rest of world = create on first visit."

Section 7.1 step 4 says: "Default: no consent — if no signal is found, do not create the SSC."

Section 7.2 table says: "Rest of world — None required — Create SSC on first visit."

These contradict each other for a user in (say) Brazil with no TCF or GPP signal. The precedence between 7.1 step 4 and the 7.2 region table needs to be explicit. Suggestion: 7.1 describes how signals are read; 7.2 describes which signals are required per region. If the region requires no signal, 7.1 step 4 does not apply. But this should be stated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, the relationship between 7.1 and 7.2 is now stated explicitly at the top of 7.1. They work in sequence, not in parallel. The fail-safe in step 4 is now scoped to "regions where a signal is required," and the rest of world row in 7.2 explicitly calls out that step 4 does not apply. You're suggested framing was exactly right.

Updated 7.1 and 7.2 markdown:

7.1 Consent signal sources and precedence

Section 7.1 describes how consent signals are read. Section 7.2 describes whether a signal is required at all for a given region. These two sections work in sequence: TS first determines the region (7.2), then — only if that region requires a consent signal — reads and evaluates the signal using the precedence order below.

When a consent signal is required for the user's region, Trusted Server checks sources in the following order. The first signal found wins:

  1. X-consent-advertising request header — set by the Didomi integration (or another CMP proxy) in a prior server-side decode. This is the freshest signal and takes precedence over browser-stored values.
  2. euconsent-v2 cookie — the TCF v2 consent string stored by the publisher's CMP.
  3. gpp cookie — the IAB Global Privacy Platform string for US state-level consent.
  4. Default: no consent — if the region requires a signal and none is found, do not create the EC (fail safe). This step does not apply to regions where no signal is required — a user in a rest-of-world region with no consent cookies present is not subject to this fail-safe.

7.2 Pre-creation consent check

Before creating a new EC, Trusted Server first evaluates the user's region (via Fastly's x-geo-country header) to determine whether a consent signal is required. If the region requires a signal, TS reads it using the precedence order in Section 7.1; if no signal is found, creation is blocked (the fail-safe in step 4 applies). If the region does not require a signal, TS creates the EC unconditionally.

Region Required signal Rule
EU member states TCF string Create EC only if purposeConsents[1] (store and/or access information on a device) is true. If no TCF signal is found, do not create EC (7.1 step 4 applies).
United Kingdom TCF string Same as EU
US states with privacy laws (CA, CO, CT, VA, TX, OR, MT, DE, NH, NJ, TN, IN, IA, KY, NE, MD, MN, RI) GPP string Create EC unless user has opted out of sale or sharing of personal data. If no GPP signal is found, do not create EC (7.1 step 4 applies).
Rest of world None required Create EC on first visit regardless of whether any consent signal is present. Section 7.1 step 4 does not apply.


Server-Side Cookie (SSC) is a stable, privacy-respecting user identity mechanism built into Trusted Server. It replaces the existing SyntheticID system with a cleaner signal (IP address + publisher salt only), a consent-aware lifecycle, a server-side identity graph backed by Fastly KV Store, and a standalone "TS Lite" deployment mode that allows SSPs, DSPs, identity providers, and publishers to adopt SSC without deploying the full Trusted Server feature set.

SSC runs at a publisher-controlled first-party subdomain (e.g., `ssc.publisher.com`), sets a cookie scoped to the publisher's apex domain, and optionally orchestrates real-time bidding or decorates outbound ad requests with resolved identity signals from configured partners.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: State explicitly that SSC is scoped per-publisher with no cross-publisher linkage.

A user visiting two publishers both running TS Lite gets two different ts-ssc cookies (different domains, different salts). This is presumably intended — but worth one sentence confirming there is no cross-publisher identity resolution, to prevent misunderstanding by partners.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, this was confusing so to clarify:

Two publishers using the same passphrase produce the same EC hash for the same user, enabling voluntary identity federation. Publishers using different passphrases produce unrelated hashes with no cross-property linkage.

```

The 64-character prefix is the stable, deterministic portion used as the KV store key. The 6-character suffix is random, regenerated each time a fresh SSC is created. Once an SSC is set in a cookie, the full value (prefix + suffix) is preserved on subsequent requests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Remove implementation details — keep the what, not the how.

This section specifies HMAC-SHA256, hex encoding, the exact output format, and IPv6 prefix extraction mechanics. These are engineering decisions that should live in a technical design doc.

The PRD should specify what the ID must achieve:

  • Deterministic for the same user+network+publisher
  • Stable across IPv6 interface ID rotation
  • Not reversible to the original IP
  • Fixed-length, opaque string

...and leave algorithm choices to engineering.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Should have been answered above then.

3. Write back with `if-generation-match: <generation>`
4. On 412 (Precondition Failed), retry from step 1 (up to 3 retries)

Within a successful write, conflicts between two different partners updating the same SSC key are resolved by last-write-wins per partner namespace. Partner IDs are keyed by partner ID in the `ids` map; different partners never overwrite each other's entries.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Move conflict resolution mechanics to a technical design doc.

Generation markers, retry counts, and read-modify-write sequences are implementation details. The PRD should state the product requirement: concurrent writes from different partners must not overwrite each other's data. How that's achieved is an engineering decision.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated in final PRD .md


### 10.3 Authentication

Partners authenticate using a Bearer token. The token is validated against a bcrypt hash stored in the partner's record in `partner_store` KV. This requires one KV lookup per API call but allows API key rotation without redeploying the binary.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Move auth mechanism details to a technical design doc.

Bearer tokens, bcrypt hashes, and KV lookup mechanics are implementation. The PRD requirement is: partners authenticate with a rotatable API key, validated without redeployment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated in final PRD .md

# Partner configs live in partner_store KV, not in TOML.
# Use the admin tooling to provision new partners.
# This allows key rotation without redeploying the binary.
```
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Move TOML structure and KV schema details to a technical design doc.

The exact TOML keys, JSON schema shapes, and KV metadata formats are implementation. The PRD should specify what is configurable (SSC enable/disable, partner registration, feature surface per deployment mode) and leave the config format to engineering.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated in final PRD .md

@aram356 aram356 changed the title Add Server-Side Cookie (SSC) product requirements document Add Edge Cookie (EC) product requirements document Mar 17, 2026
@aram356 aram356 marked this pull request as draft March 17, 2026 16:09
@aram356 aram356 linked an issue Mar 17, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Requirements for edge cookie

2 participants