Add Edge Cookie (EC) product requirements document#511
Conversation
aram356
left a comment
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
good catch! I redid this section 9.4
- Read the
ts-eccookie. If absent, redirect toreturnURL withts_synced=0appended. 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). - Look up the partner record in
partner_storeKV using thepartnerparameter. Return400if the partner is not found. - Validate the
returnURL against the partner'sallowed_return_domains. Return400if the domain is not on the allowlist. - Evaluate consent for this user by decoding from request cookies (or the optional
consentquery parameter if no cookie signal is present). If consent is absent or invalid, redirect toreturnwithts_synced=0&ts_reason=no_consent. No KV write is performed. - 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 toreturnwithts_synced=0&ts_reason=write_failed. - On successful KV write, redirect to
returnwithts_synced=1appended as a query parameter.
| } | ||
| ``` | ||
|
|
||
| HTTP status `207 Multi-Status` when any mappings are rejected; `200 OK` when all are accepted. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
"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: |
There was a problem hiding this comment.
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
/identifyread 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.
There was a problem hiding this comment.
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 inpartner_storeand 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,/identifyreturns 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.idis set to the full EC value (hash.suffix)user.eidsis populated from the KV identity graph for this user (see OpenRTB structure below)user.consentis set to the decoded TCF string (currently alwaysnull)- SSP-specific
ext.eids: when calling a specific PBS adapter, only that SSP's resolved ID is included in the adapter-levelext.eids. All configured identity providers are included at the top-leveluser.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 |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
X-consent-advertisingrequest 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.euconsent-v2cookie — the TCF v2 consent string stored by the publisher's CMP.gppcookie — the IAB Global Privacy Platform string for US state-level consent.- 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. | ||
| ``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ok, updated in final PRD .md
Summary
/sync), S2S batch API (/api/v1/sync), and bidstream decoration (/identify+/auction)Key sections
ts-ssccookie at apex domainGET /sync): Browser redirect-based partner ID sync with rate limiting and domain allowlistPOST /api/v1/sync): Authenticated bulk ID mapping push for DSPs and partners/identify) and full auction mode (/auction) with OpenRTB 2.6user.eids[ssc]and[features]TOML sections, partner registry in KV store