Rolling main documentation
Built from commit 077b43bde250f7cb0ff0e0aba6f03959cbed4e25. Content may describe unreleased behavior.
Configuration
Learn how to configure Trusted Server for your deployment.
Overview
Trusted Server uses a flexible configuration system based on:
- TOML Files -
trusted-server.tomlfor ordinary configuration and secret key names - Environment Variables - Typed CLI overrides with the
TRUSTED_SERVER__prefix - EdgeZero Stores - Config and secret stores for the pushed blob and runtime secret values
Quick Start
Minimal Configuration
Create trusted-server.toml in your project root. Generate both secret values first with openssl rand -base64 32; the placeholders below are intentionally rejected until replaced.
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"
[ec]
passphrase = "ec_passphrase"Environment Variable Overrides
Environment variables are merged into existing TOML values by the typed ts config validate, ts config diff, and ts config push flows. They are not read by the deployed application at request time.
# Format: TRUSTED_SERVER__SECTION__FIELD
export TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
# Secret overrides, when needed, are key names—not secret values.
export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
export TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
# Replace the rejected placeholder values in trusted-server.toml, then validate.
ts config validate
ts config push --adapter fastlyStatic secret references
Static app-config credentials contain stable key names only. This includes publisher, trusted-client-IP, EC, handler, Tinybird, DataDome, and S3 fields:
publisher.proxy_secrettrusted_client_ip.shared_secret, when trusted client-IP forwarding is configuredec.passphraseec.partners[*].api_token, when inbound identify or batch sync is usedec.partners[*].ts_pull_token, when pull sync is enabledhandlers[*].passwordtinybird.auction_token_secret, when Tinybird auction telemetry is enabledintegrations.datadome.server_side_key_secret_name, when protection is enabledintegrations.datadome.protection_test_bypass.credential_secret_name, when the bypass is enabledproxy.asset_routes[*].auth.access_key_id,secret_access_key, and optionalsession_token
Their values belong in the logical trusted_server_secrets store and are resolved only while an instance builds runtime settings. An adapter can map the logical ID to a different physical name. For example, Fastly commonly maps trusted_server_secrets to physical store ts_secrets.
Two accepted secret-shaped fields are deliberately different:
trusted_client_ip.shared_secretis an inline value in the app-config blob; it is redacted by debug formatting but is not resolved from a secret store.tinybird.access_token_secretis deprecated input. It is accepted for migration, then discarded and omitted from serialized config; usetinybird.auction_token_secretas the store key name instead.
The four deprecated secret_store selectors under Tinybird, DataDome, its protection-test bypass, and S3 route authentication are also accepted and discarded. Store selection comes from the adapter's EdgeZero mapping.
CLI output and inline secrets
ts config diff, ts config push --dry-run, and the interactive push preview can print deliberately inline values. Use ts config push --no-diff when that output is not safe for the current terminal or CI log. The flag suppresses the diff; it does not move inline values to a secret store.
The following table records the independent lifecycle, key-identity, serialization, runtime, and secret axes for every exceptional field:
| Path | Lifecycle | Key identity | Serialization | Runtime | Secret handling |
|---|---|---|---|---|---|
AssetOriginAuth.s3_sig_v4 | deprecated | alias of AssetOriginAuth.s3_sigv4 | skipped | deserialization only | none |
DataDomeConfig.server_side_key_secret_name | canonical | canonical | serialized | active | store resolved |
DataDomeConfig.server_side_key_secret_store | deprecated | canonical | skipped | normalized away | none |
DataDomeProtectionTestBypassConfig.credential_secret_name | canonical | canonical | serialized | active | store resolved |
DataDomeProtectionTestBypassConfig.credential_secret_store | deprecated | canonical | skipped | normalized away | none |
Ec.passphrase | canonical | canonical | serialized | active | store resolved |
EcPartner.api_token | canonical | canonical | serialized | active | store resolved |
EcPartner.ts_pull_token | canonical | canonical | serialized | active | store resolved |
Handler.password | canonical | canonical | serialized | active | store resolved |
Publisher.proxy_secret | canonical | canonical | serialized | active | store resolved |
S3SigV4AuthConfig.access_key_id | canonical | canonical | serialized | active | store resolved |
S3SigV4AuthConfig.secret_access_key | canonical | canonical | serialized | active | store resolved |
S3SigV4AuthConfig.secret_store | deprecated | canonical | skipped | normalized away | none |
S3SigV4AuthConfig.session_token | canonical | canonical | serialized | active | store resolved |
TinybirdSettings.access_token_secret | deprecated | canonical | skipped | normalized away | accepted, then discarded |
TinybirdSettings.auction_token_secret | canonical | canonical | serialized | active | store resolved |
TinybirdSettings.secret_store | deprecated | canonical | skipped | normalized away | none |
TrustedClientIpConfig.shared_secret | canonical | canonical | serialized | active | deliberately inline |
Prepare an initial reference-based deployment in this order:
- Create the physical store and configure its
trusted_server_secretsmapping. - Choose a stable key name for each active credential field in the app config.
- Write each credential value under its referenced key without exposing it in command arguments, shell history, logs, or CI output.
- Run
ts config validate, thents config push --adapter fastly. - Start or deploy instances after the store and pushed config are both ready.
Migrate an existing deployment in this order:
- Populate the physical store mapped from
trusted_server_secretswith the existing credential values without printing them in shell history, logs, or CI output. - Replace each active credential value with a stable key name and remove the legacy Tinybird, DataDome, and S3
secret_storeselectors. - Run
ts config validate, thents config push --adapter fastly --no-diff. - Restart/redeploy instances as needed to load the new values. Rotation is startup-scoped; changing a store value does not alter already-built state.
Keep publisher.proxy_secret and ec.passphrase stable unless intentionally rotating signed URLs or EC identifiers. On Spin, the app-config blob is stored under the trusted_server_config key in Spin's built-in default key-value store. Set the corresponding CLI store mapping before pushing so the write matches the runtime lookup:
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default
ts config push --adapter spinFor local Spin development, add --local to the push command. Also declare a component variable for each chosen secret key name using the encoder documented in spin.toml. Missing stores, keys, invalid UTF-8, and empty values fail closed; inline plaintext fallback is not supported.
Tinybird auction telemetry
Tinybird uses the same typed secret-reference path as the other static credentials. Do not configure a feature-specific store:
[tinybird]
enabled = true
api_host = "api.example.com"
auction_dataset = "auction_events_raw"
auction_token_secret = "tinybird_auction_append_token"Store the APPEND token value under tinybird_auction_append_token in the physical store mapped from trusted_server_secrets. The token is resolved once at startup. Disabled Tinybird telemetry does not require or resolve the token. The legacy tinybird.secret_store field is accepted for one migration release, but it is ignored and omitted from newly pushed config.
Generate Secure Secrets
Generate values locally and write them directly to the platform secret store; do not put the generated output in trusted-server.toml or the app-config blob.
openssl rand -base64 32Strict Key Validation
Trusted Server rejects unknown TOML keys in runtime configuration. Before pushing or upgrading config, remove stale fields and typos; otherwise config loading can fail and the service will return its startup-error response.
Configuration Files
| File | Purpose |
|---|---|
trusted-server.toml | Main application configuration |
fastly.toml | Fastly Compute service settings |
.env.dev | Local development overrides |
Key Sections
| Section | Purpose |
|---|---|
[auction] | Auction orchestration, provider instances, bidder routes, and mediation |
[cache] | Static and rehosted asset cache policy |
[consent] | Consent interpretation, forwarding, and conflict resolution |
[creative_opportunities] | Server-side page ad opportunities and templates |
[debug] | Explicit non-production diagnostics |
[ec] | Edge Cookie identity, persistence, and partner sync |
[[handlers]] | Ordered HTTP Basic-auth rules |
[image_optimizer] | Reusable Fastly Image Optimizer profiles |
[integrations.*] | Typed partner and browser integration settings |
[proxy] | Proxy allowlist, TLS policy, and asset routes |
[publisher] | Publisher domain, origin, and proxy signing key |
[request_signing] | Outbound Ed25519 request signing and management-store IDs |
[response_headers] | Headers added to Trusted Server responses |
[rewrite] | First-party URL rewrite exclusions |
[tester_cookie] | Optional tester-cookie endpoints |
[tinybird] | Direct Tinybird auction telemetry |
[trusted_client_ip] | Authenticated front-door client-IP forwarding |
Example: Production Setup
Generate and substitute every replace-with-* value before validation or deployment.
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"
[ec]
passphrase = "ec_passphrase"
[request_signing]
enabled = true
config_store_id = "01GXXX"
secret_store_id = "01GYYY"
[integrations.prebid]
enabled = true
client_side_bidders = ["example-browser-bidder"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"
[proxy]
allowed_domains = ["assets.example.com"]
[auction]
enabled = true
timeout_ms = 2000
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
timeout_ms = 1200
routing = "explicit"
[auction.providers.pbs-main.profile_config]
debug = false
[auction.bidders.example-server-bidder]
provider = "pbs-main"Detailed Reference
The sections below consolidate the full configuration reference on this page.
Environment Variable Overrides (Typed CLI)
Environment variables with the TRUSTED_SERVER__ prefix are merged into the base TOML configuration by ts config validate, ts config diff, and ts config push. The resolved values are validated and, for config push, stored in the app-config blob. Changing an environment variable requires rerunning validation and pushing the resolved config, not rebuilding the binary.
The pinned EdgeZero loader only overrides leaves that already exist in the parsed TOML; it does not create missing fields. Add newly introduced defaulted fields to an existing config before relying on their environment overrides. Secret overlays still contain key names, never secret values. Pass --no-env to use file values without the overlay.
Format
TRUSTED_SERVER__SECTION__SUBSECTION__FIELDRules:
- Prefix:
TRUSTED_SERVER - Separator:
__(double underscore) - Case: UPPERCASE
- Sections: Match TOML hierarchy
- Map keys: Preserve TOML punctuation. For example, provider key
pbs-mainuses thePBS-MAINsegment, notPBS_MAIN.
Shell assignment syntax cannot contain a hyphenated variable name. Use env to apply a provider override to a command:
env 'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
ts config validateThis example changes an existing scalar leaf. Edit TOML and run ts config validate followed by ts config push when changing an array, table, map, or rule.
Publisher Configuration
Core publisher settings for domain, origin, and proxy configuration.
[publisher]
| Field | Type | Required | Description |
|---|---|---|---|
domain | String | Yes | Publisher's apex domain name |
cookie_domain | String | Yes | Domain for non-EC cookies (typically with leading dot) |
origin_url | String | Yes | Full URL of publisher origin server |
origin_host_header_override | String | No | Outbound Host header to send while connecting to origin_url |
proxy_secret | String | Yes | Secret-store key name for the proxy URL secret |
max_buffered_body_bytes | Integer | No | Buffered-body cap / Fastly stream raw+decoded byte ceiling (default 16 MiB) |
Note: EC cookies (
ts-ec) derive their domain automatically as.{domain}and do not usecookie_domain. Thecookie_domainfield is used by other cookie helpers.
Example (replace the rejected secret placeholder before validation):
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
# Optional: connect to origin_url but send this outbound Host header.
# origin_host_header_override = "www.publisher.com"
proxy_secret = "publisher_proxy_secret"Environment Override:
TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_HOST_HEADER_OVERRIDE=www.publisher.com
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216Field Details
domain
Purpose: Primary domain for the publisher.
Usage:
- Used for publisher routing and logging
- Part of request context for proxy/origin handling
Format: Hostname without protocol or path
- ✅
publisher.com - ✅
www.publisher.com - ❌
https://publisher.com - ❌
publisher.com/path
cookie_domain
Purpose: Domain scope for non-EC cookies.
Usage:
- Used by non-EC cookie helpers for domain scoping
- EC cookies (
ts-ec) use a separate computed domain derived fromdomain
Format: Domain with optional leading dot
.publisher.com- Shares across all subdomainspublisher.com- Exact domain only
Best Practice: Use leading dot (.publisher.com) for subdomain sharing.
origin_url
Purpose: Backend origin server URL for publisher content.
Usage:
- Fallback proxy target for non-integration requests
- HTML processing rewrites origin URLs to request host
- Base for relative URL resolution
Format: Full URL with protocol
- ✅
https://origin.publisher.com - ✅
https://origin.publisher.com:8080 - ✅
http://192.168.1.1:9000 - ❌
origin.publisher.com(missing protocol)
Port Handling: Includes port if non-standard (not 80/443).
origin_host_header_override
Purpose: Optional Host header to send to the publisher origin while still connecting to the host in origin_url.
Usage:
- Connects, uses SNI, and checks certificates against
origin_url - Sends the configured value as the outbound HTTP
Hostheader - Useful when the origin endpoint expects a canonical publisher hostname
Format: Hostname with optional port, without protocol, path, query, or fragment
- ✅
www.publisher.com - ✅
www.publisher.com:8443 - ❌
https://www.publisher.com - ❌
www.publisher.com/path
Default: When omitted, Trusted Server sends the host from origin_url.
proxy_secret
Purpose: Secret-store key name for the HMAC-SHA256 value used to sign proxy URLs.
The referenced value is resolved from trusted_server_secrets at startup. Generate it with a cryptographically secure random source; at least 32 random bytes are recommended. Keep that value confidential, rotate it only intentionally, and never put it in the TOML file or pushed app-config blob.
Usage:
- Signs
/first-party/proxyURLs - Signs
/first-party/clickURLs - Validates incoming proxy requests
- Prevents URL tampering
Security Warning
Changing proxy_secret invalidates all existing signed URLs. Plan rotations carefully and use graceful transition periods.
max_buffered_body_bytes
Purpose: Upper bound on how much of a publisher origin body the rewrite pipeline holds in memory — the post-rewrite output buffer on buffered adapters, and the per-stream raw/decoded byte ceiling on the Fastly streaming path.
Usage:
- On buffered adapters (Axum, Cloudflare, Spin) it caps the decoded, post-rewrite output buffer for a publisher response processed in full. It also bounds how much decoded gzip output may sit in the heap at any one moment, so a decompression bomb is rejected mid-decode rather than after its full expansion. That second bound is per-step, not a total: a gzip-encoded response passes or fails on the same post-rewrite output size as the identity, deflate and brotli versions of the same body.
- On the Fastly streaming path the origin body is preserved as a stream, so the same value caps the stream twice over: the cumulative raw (still compressed) bytes pulled from origin, and the cumulative decoded bytes emitted by the decompressor. The decoded cap is enforced during decompression, so a decompression bomb is rejected before its expansion is materialized rather than after.
Behavior when exceeded:
- On buffered adapters the response fails before any bytes are committed.
- On the streaming path the response headers are already committed when either cap trips, so the body is truncated mid-stream and the error is logged — the client receives a short (incomplete) body rather than a
5xx. Size the cap above your largest expected decoded page so legitimate responses are never truncated.
Default: 16777216 (16 MiB). On the Fastly streaming path this is now the sole ceiling: origin bodies are streamed rather than materialized in full, so the previous ~10 MiB raw-body limit no longer applies.
Minimum: Must be at least 1. A value of 0 is rejected at startup because a zero-byte cap fails every non-empty publisher response.
Environment Override:
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216Trusted Client IP Configuration
Use this optional section when a trusted CDN service forwards requests to the Fastly service running Trusted Server. It lets Trusted Server use the reader's address instead of the immediate fronting edge node's address. Only the Fastly adapter honours this section; the Cloudflare, Spin, and Axum adapters validate it but keep using their own runtime client address.
[trusted_client_ip]
| Field | Type | Required | Description |
|---|---|---|---|
ip_header | String | Yes | Header containing exactly one reader IP address |
auth_header | String | Yes | Header containing exactly one shared-secret value |
shared_secret | String | Yes | Key in trusted_server_secrets for the front-door shared secret |
All three fields are required when the section exists. When the section is absent, Trusted Server continues to use the immediate peer address, and ts config push omits the section from the published config blob so instances running an older binary keep accepting the blob.
Deploy the code before pushing the config
Once the section is configured, the pushed blob carries it, and Settings rejects unknown fields. A binary that predates trusted client-IP support fails to load a blob containing this section and returns its startup-error response. Upgrade every instance before pushing a config that enables the section, and restore a config without the section before rolling instances back. Getting this order wrong takes the service down rather than degrading it.
[trusted_client_ip]
ip_header = "x-ts-client-ip"
auth_header = "x-ts-client-ip-auth"
shared_secret = "trusted_client_ip_shared_secret"Prefer a dedicated x- name for ip_header, as shown. fastly-client-ip is also accepted and suits a fronting service dedicated to Trusted Server, but on a service carrying other traffic a dedicated name means the front door never modifies Fastly-Client-IP, so other consumers of that header keep working unchanged. See Fastly Setup for the front-door configuration this section depends on.
The front door must overwrite both headers on every request it forwards to Trusted Server, and must remove client-supplied copies on its other routes. Trusted Server resolves shared_secret from trusted_server_secrets at startup. It accepts the forwarded address only when the request has exactly one auth_header value that matches the resolved secret byte-for-byte and exactly one ip_header value that parses directly as IPv4 or IPv6. Values are not trimmed or normalized. Missing, empty, duplicate, non-UTF-8, mismatched, or malformed values do not reject the request; Trusted Server safely falls back to the immediate peer address. Both configured headers are removed before routing.
Header names are validated case-insensitively. ip_header must be fastly-client-ip or start with x-, while auth_header must start with x-. The names must differ. Neither field may use a header name reserved for Trusted Server's own internal signals (for example x-forwarded-for, x-geo-info-available, x-ts-ec, x-ts-tls-protocol, or x-ts-tls-cipher); the full reserved set is the internal-header list that Trusted Server strips before forwarding to third parties. These restrictions exclude standard sensitive headers such as Host, Content-Length, Cookie, and Authorization, as well as every Trusted Server internal header. Choose dedicated x- names that no other application or routing logic uses, because Trusted Server removes the configured headers before routing.
Generate the referenced secret value with a cryptographically secure random generator, encode it as hex or base64url, and store the same value only in the front door and the physical store mapped from trusted_server_secrets. Put only the key name in Trusted Server configuration. The resolved value must contain at least 32 ASCII graphic bytes (! through ~) with no whitespace, controls, DEL, or non-ASCII bytes.
Independently of this section, the Fastly adapter treats fastly-client-ip as client-spoofable and strips it at request entry, so Trusted Server no longer forwards an inbound Fastly-Client-IP to the publisher origin. This applies even when [trusted_client_ip] is absent. Check whether the origin reads that header before deploying.
Startup fails closed if the configured key is missing, empty, invalid UTF-8, or resolves to an invalid shared-secret value. Every adapter removes the configured IP and authentication headers before routing, although only Fastly uses them for client-IP resolution.
Environment Overrides:
TRUSTED_SERVER__TRUSTED_CLIENT_IP__IP_HEADER=x-ts-client-ip
TRUSTED_SERVER__TRUSTED_CLIENT_IP__AUTH_HEADER=x-ts-client-ip-auth
TRUSTED_SERVER__TRUSTED_CLIENT_IP__SHARED_SECRET=trusted_client_ip_shared_secretBecause the typed environment overlay cannot create a missing section, add [trusted_client_ip] and all three fields to the TOML before using these overrides.
Tester Cookie Configuration
Settings for the optional tester-cookie endpoints. This feature is disabled by default and should only be enabled for intentional QA or troubleshooting flows.
[tester_cookie]
| Field | Type | Required | Description |
|---|---|---|---|
enabled | Boolean | No | Enables routes to set and clear ts-tester cookie |
When enabled, GET /_ts/set-tester returns 204 No Content and sets:
Set-Cookie: ts-tester=true; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax
Cache-Control: no-store, privateGET /_ts/clear-tester returns 204 No Content and clears the cookie:
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
Cache-Control: no-store, privateWhen disabled, both routes return 404 Not Found and do not set a cookie.
WARNING
The cookie is scoped with [publisher].cookie_domain, not the EC-specific computed domain. Keep cookie_domain aligned with the browser scope where your QA tooling expects to read ts-tester.
Example:
[tester_cookie]
enabled = trueEnvironment Override:
TRUSTED_SERVER__TESTER_COOKIE__ENABLED=trueEC Configuration
Settings for generating privacy-preserving Edge Cookie identifiers. The ec_store KV store is the only KV-backed EC lifecycle store; it holds identity graph state, minimal consent metadata, source-domain keyed partner UIDs, and withdrawal tombstones. Live consent is interpreted from request cookies, headers, geolocation, and policy defaults, not separate KV persistence.
Migrating from consent_store
The legacy [consent].consent_store setting has been removed. Trusted Server uses a strict configuration schema, so TOML and JSON/app-config that still contain consent_store fail during configuration loading and prevent normal application state from being built. This is not partial consent degradation: user routes return adapter-specific 5xx startup-error responses until the field is removed. Run ts config validate before ts config push to catch the stale field before deployment.
Legacy consent-store records are not read or migrated into ec.ec_store. Their payload schema is not authoritative EC lifecycle state, so do not copy those records into the identity store. You may retain the old store unchanged for a defined rollback window, then unlink its platform resource binding and delete it. No browser-cookie or EC identity-store migration is required.
[ec]
passphrase is a key name in trusted_server_secrets; the resolved value must be at least 32 bytes. Keep it stable to preserve EC identifier continuity.
| Field | Type | Required | Description |
|---|---|---|---|
passphrase | String | Yes | Publisher passphrase used as HMAC key |
ec_store | String or null | No | Fastly KV store name for EC identity graph and withdrawal state |
pull_sync_concurrency | Integer | No | Maximum concurrent pull-sync requests per organic response |
cluster_trust_threshold | Integer | No | Cluster size threshold for identity trust decisions |
cluster_recheck_secs | Integer | No | Legacy compatibility setting; cluster rechecks no longer use timestamps |
partners | Array | No | Static partner registry entries |
Partner keying
source_domain is the canonical partner key. It matches incoming OpenRTB EID source values and is also used as the EC KV ids map key.
api_token is optional. Set it to a key in trusted_server_secrets only when the partner calls the inbound identify or batch-sync APIs. A partner without api_token remains available for source-domain lookup, bidstream EIDs, and outbound pull sync, but cannot authenticate to those inbound APIs.
Example:
[ec]
passphrase = "ec_passphrase"
ec_store = "ec_identity_store"
[[ec.partners]]
name = "Mocktioneer SSP"
source_domain = "mocktioneer.example"
bidstream_enabled = true
# api_token = "partner_api_token" # only for inbound identify or batch sync
# ts_pull_token = "partner_ts_pull_token" # required when pull sync is enabledEnvironment Override:
TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
TRUSTED_SERVER__EC__EC_STORE=ec_identity_storeField Details
passphrase
Purpose: Secret-store key name whose resolved value is the HMAC key for EC ID generation.
Security:
- The key name is stored in app config; the value is stored in
trusted_server_secrets - Keep the value stable unless intentionally rotating EC identifiers
- Do not place the value in environment overlays or the pushed blob
Validation: Application startup fails if:
- Empty string
Consent Configuration
[consent] controls request-local interpretation and forwarding of privacy signals. It does not create a second consent database. Set consent_store to a KV store name only when consent should persist with the EC identity graph.
[consent]
| Field | Type | Default | Contract |
|---|---|---|---|
mode | String | "interpreter" | interpreter decodes signals; proxy forwards raw strings |
check_expiration | Boolean | true | Check TCF timestamps |
max_consent_age_days | Integer | 395 | Clamped to 1..=3650 |
consent_store | String or null | null | Optional KV store for EC-linked consent persistence |
gdpr.applies_in | Array[String] | EU/EEA and UK codes | Observability only; does not synthesize consent |
us_states.privacy_states | Array[String] | Checked built-in state list | Jurisdictions with active comprehensive privacy laws |
us_privacy_defaults.notice_given | Boolean | true | Publisher policy used for GPC-only requests |
us_privacy_defaults.lspa_covered | Boolean | false | Publisher LSPA posture |
us_privacy_defaults.gpc_implies_optout | Boolean | true | Treat Sec-GPC: 1 as sale opt-out |
conflict_resolution.mode | String | "restrictive" | restrictive, newest, or permissive |
conflict_resolution.freshness_threshold_days | Integer | 30 | Age difference required by newest |
[consent]
mode = "interpreter"
check_expiration = true
max_consent_age_days = 395
[consent.conflict_resolution]
mode = "restrictive"
freshness_threshold_days = 30applies_in and privacy_states are policy inputs. Review them against the publisher's operating jurisdictions instead of assuming the built-in lists are legal advice.
Response Headers
Custom headers added to all responses.
[response_headers]
Purpose: Add custom HTTP headers to every response.
Format: Key-value pairs
Example:
[response_headers]
X-Custom-Header = "custom value"
X-Publisher-ID = "pub-12345"
X-Environment = "production"
Cache-Control = "public, max-age=3600"Environment Override:
Override an existing header leaf by preserving its TOML key punctuation in the environment path. Shell assignment syntax cannot contain hyphens, so use env:
env 'TRUSTED_SERVER__RESPONSE_HEADERS__X-CUSTOM-HEADER=updated value' \
ts config validateThe overlay cannot add a header or replace the whole response_headers table. Edit TOML, validate, and push again for those changes.
Use Cases:
- Custom measurement headers
- Cache control overrides
- Debugging identifiers
- CORS headers (if needed)
Header Precedence
Custom headers may be overwritten by application logic. Standard headers (Content-Type, Content-Length) are controlled by the application.
Request Signing
Configuration for Ed25519 request signing and JWKS management.
[request_signing]
| Field | Type | Required | Description |
|---|---|---|---|
enabled | Boolean | No (default: false) | Enable request signing features |
config_store_id | String | If enabled | Fastly Config Store ID for JWKS |
secret_store_id | String | If enabled | Fastly Secret Store ID for private keys |
Example:
[request_signing]
enabled = true
config_store_id = "01GXXX" # From Fastly dashboard
secret_store_id = "01GYYY" # From Fastly dashboardEnvironment Override:
TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true
TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID=01GXXX
TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID=01GYYYStore Setup
Config Store (for public keys):
# Create store
fastly config-store create --name=jwks_store
# Get store ID
fastly config-store listSecret Store (for private keys):
# Create store
fastly secret-store create --name=signing_keys
# Get store ID
fastly secret-store listLocal Dev Setup (fastly.toml):
[local_server.config_stores]
[local_server.config_stores.jwks_store]
file = "test-data/jwks_store.json"
[local_server.secret_stores]
[local_server.secret_stores.signing_keys]
file = "test-data/signing_keys.json"See Request Signing and Key Rotation for usage.
Basic Authentication Handlers
Path-based HTTP Basic Authentication.
[[handlers]]
Purpose: Protect specific paths with username/password authentication.
Format: Array of handler objects
| Field | Type | Required | Description |
|---|---|---|---|
path | String (Regex) | Yes | Regular expression matching paths |
username | String | Yes | HTTP Basic Auth username |
password | String | Yes | HTTP Basic Auth password |
Example:
# Single handler
[[handlers]]
path = "^/_ts/admin"
username = "admin"
password = "admin_password"
# Multiple handlers
[[handlers]]
path = "^/secure"
username = "user1"
password = "secure_handler_password"
[[handlers]]
path = "^/api/private"
username = "api-user"
password = "api_handler_password"Environment Override:
# Handler 0
TRUSTED_SERVER__HANDLERS__0__PATH="^/_ts/admin"
TRUSTED_SERVER__HANDLERS__0__USERNAME="admin"
TRUSTED_SERVER__HANDLERS__0__PASSWORD="admin_password"
# Handler 1
TRUSTED_SERVER__HANDLERS__1__PATH="^/api/private"
TRUSTED_SERVER__HANDLERS__1__USERNAME="api-user"
TRUSTED_SERVER__HANDLERS__1__PASSWORD="api_handler_password"Path Patterns
Regex Syntax: Standard Rust regex patterns
Examples:
# Exact path
path = "^/_ts/admin$" # Only /_ts/admin
# Prefix match
path = "^/_ts/admin" # /_ts/admin, /_ts/admin/users, /_ts/admin/settings
# Multiple paths
path = "^/(admin|secure|private)"
# File extension
path = "\\.pdf$" # All PDF files
# Complex pattern
path = "^/api/v[0-9]+/private" # /api/v1/private, /api/v2/privateValidation: Application startup fails if regex is invalid.
Admin coverage and passwords are validated at startup
Startup fails when no handler covers an admin route. The dynamic /_ts/admin/ec/{id} route accepts any segment after /_ts/admin/ec/, and Basic Auth runs on the raw path before routing, so coverage cannot be inferred from ID-shaped samples: a pattern such as ^/_ts/admin/ec/[a-f0-9]{64}[.][A-Za-z0-9]{6}$ is rejected. Use a prefix-level matcher (^/_ts/admin, or ^/_ts/admin/ec/ alongside the other admin patterns).
Handler expressions match the raw URI path, while a publisher origin may decode percent-encoded aliases before routing. For a whole-site staging gate, use path = "^/"; do not rely on a decoded-path prefix such as ^/secure to protect equivalent origin paths.
Startup also fails when any handler — admin or not — uses a placeholder or well-known weak password (changeme, password, admin, or a replace-with-… template value). Handler selection is first-match-wins, so a narrow handler ahead of the admin pattern governs the paths it matches.
Scope patterns to the paths you mean
Handler patterns are matched against the full request path, so a broad pattern covers everything beneath it. The /_ts/ namespace holds both admin routes and browser-facing endpoints that anonymous visitors must be able to reach:
| Path | Called by |
|---|---|
/_ts/page-bids | Trusted Server JS, on SPA navigation |
/_ts/api/v1/identify | Trusted Server JS, in the browser |
/_ts/api/v1/batch-sync | Trusted Server JS, in the browser |
A pattern such as path = "^/_ts" puts those behind Basic Auth. Browser fetches never carry Basic credentials, so every visitor gets 401 — on /_ts/page-bids that means no ads after any client-side navigation. Match the admin routes specifically (^/_ts/admin) instead.
Upgrading from a release before /_ts/page-bids existed: if any handler pattern covers it, narrow the pattern. The Trusted Server JS bundle falls back to the deprecated /__ts/page-bids alias in the meantime, but that alias is scheduled for removal (#970).
Security Considerations
Password Storage:
handlers[*].passwordis a key name intrusted_server_secrets- Store the resolved password only in the platform secret store
- Rotate passwords through the store and restart/redeploy instances
Limitations:
- HTTP Basic Auth (not OAuth/JWT)
- Single username/password per path
- No role-based access control
- No rate limiting (add at edge)
Production Use
Do not put handler passwords in trusted-server.toml, environment overlays, or app-config blobs. Provision the referenced key in trusted_server_secrets before pushing the config.
URL Rewrite Configuration
Control which domains are excluded from first-party rewriting.
[rewrite]
| Field | Type | Required | Description |
|---|---|---|---|
exclude_domains | Array[String] | No (default: []) | Domains to skip rewriting |
Example:
[rewrite]
exclude_domains = [
"*.cdn.trusted-partner.com", # Wildcard
"first-party.publisher.com", # Exact match
"localhost", # Development
]Environment Override:
EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit exclude_domains in TOML, then validate and push the file again.
Pattern Matching
Wildcard Patterns (*):
"*.cdn.example.com"Matches:
- ✅
assets.cdn.example.com - ✅
images.cdn.example.com - ✅
cdn.example.com(base domain) - ❌
cdn.example.com.evil.com(different domain)
Exact Patterns (no *):
"api.example.com"Matches:
- ✅
api.example.com - ❌
www.api.example.com - ❌
api.example.com.evil.com
Use Cases
Trusted Partners:
exclude_domains = ["*.approved-cdn.com"]First-Party Resources:
exclude_domains = ["assets.publisher.com", "static.publisher.com"]Development:
exclude_domains = ["localhost", "127.0.0.1", "*.local"]Performance (already first-party):
exclude_domains = ["*.publisher.com"] # Skip unnecessary proxyingSee Creative Processing for details.
Proxy Configuration
Controls first-party proxy security settings and path-based asset routes.
[proxy]
| Field | Type | Required | Description |
|---|---|---|---|
allowed_domains | Array[String] | No (default: []) | Hosts permitted for signing, initial fetches, and redirects |
certificate_check | Boolean | No (default: true) | Verify TLS certificates when proxying HTTPS origins |
asset_routes | Array[Table] | No (default: []) | Path prefixes proxied directly to configured origins |
Example:
[proxy]
allowed_domains = [
"assets.example.com", # Exact match
"*.cdn.example.com", # Wildcard: cdn.example.com and all subdomains
]Environment Override:
EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit allowed_domains in TOML, then validate and push the file again.
Field Details
allowed_domains
Purpose: Allowlist of target hosts permitted for /first-party/sign and /first-party/proxy. When integrations.prebid.external_bundle_url is configured, this list must cover its host and any HTTPS redirect targets.
Behavior: Trusted Server checks the parsed host before signing a target, before fetching the initial proxy target, and before following each HTTP redirect (301/302/303/307/308). A host that does not match the list is blocked with a 403 error.
Default - open mode: When allowed_domains is absent or empty and no external Prebid bundle is configured, every valid host is allowed for signing, initial fetches, and redirects. Configuring an external Prebid bundle with an empty list fails deploy validation. Open mode supports zero-config development but should not be used in production.
Pattern Matching:
| Pattern | Matches | Does not match |
|---|---|---|
assets.example.com | assets.example.com | sub.assets.example.com |
*.cdn.example.com | cdn.example.com, static.cdn.example.com, a.b.cdn.example.com | evil-cdn.example.com |
"example.com"— exact match only."*.example.com"— matches the base domain and any subdomain at any depth.- Matching is case-insensitive; entries are normalized to lowercase at startup.
- Blank entries are ignored.
- The
*wildcard requires a dot boundary:*.example.comdoes not matchevil-example.com.
Production Recommendation
Always configure allowed_domains in production. Without an explicit allowlist, clients can sign and fetch valid URLs for arbitrary hosts, including redirect targets.
[proxy]
allowed_domains = [
"assets.example.com",
"*.cdn.example.com",
]See First-Party Proxy for usage details.
certificate_check
Purpose: Control TLS certificate verification for HTTPS proxy and asset-route origins.
Default: true
Set this to false only for local development with self-signed certificates.
[[proxy.asset_routes]]
Asset routes proxy selected first-party paths to an alternate asset origin without requiring signed /first-party/proxy URLs.
| Field | Type | Required | Description |
|---|---|---|---|
prefix | String | Yes | Request path prefix to match |
origin_url | String | Yes | Absolute http or https origin URL |
path_pattern | String | No | Regex matched against the incoming request path |
target_path | String | No | Replacement path used with path_pattern |
auth | Table | No | Optional origin authentication |
image_optimizer | Table | No | Optional route-level Image Optimizer settings |
Example:
[[proxy.asset_routes]]
prefix = "/assets/"
origin_url = "https://assets.example.com"Path rewrite example:
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://assets-cdn.example.com"
path_pattern = "^/\\.image/(.*)/[^/]+\\.([^/.]+)$"
target_path = "/image/upload/$1.$2"Behavior:
- Only
GETandHEADrequests use asset routes. - Built-in and integration routes take precedence.
- The longest matching asset-route prefix wins.
path_patternandtarget_pathmust be configured together.origin_urlmust not include userinfo, a path, a query string, or a fragment.- Unsafe origin response headers such as
Set-Cookieare stripped before the response reaches the browser.
[proxy.asset_routes.auth]
The first supported origin auth type is s3_sigv4.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | String | Yes | none | Must be s3_sigv4 |
region | String | Yes | none | AWS region used in the SigV4 credential scope |
access_key_id | String | No | access_key_id | Default-store secret reference for the AWS access key ID |
secret_access_key | String | No | secret_access_key | Default-store secret reference for the AWS secret access key |
session_token | String | No | unset | Optional secret key containing a session token |
origin_query | String | No | route default | preserve or strip |
Example:
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://bucket.s3.us-east-1.amazonaws.com"
[proxy.asset_routes.auth]
type = "s3_sigv4"
region = "us-east-1"
origin_query = "strip"
access_key_id = "s3_access_key_id"
secret_access_key = "s3_secret_access_key"
# session_token = "s3_session_token"S3 auth uses header-based AWS SigV4 with UNSIGNED-PAYLOAD. It is scoped to read-only asset requests and expects origin_url to use the S3 host that AWS validates. Credential references resolve from trusted_server_secrets at startup, and request signing performs no secret-store reads.
Effective origin_query precedence is auth-level origin_query, then enabled Image Optimizer origin_query, then the route default.
[proxy.asset_routes.image_optimizer]
Route-level Image Optimizer configuration selects a reusable profile set.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable Image Optimizer for the route |
region | String | Yes when enabled | none | Fastly IO processing region, such as us_east |
profile_set | String | Yes when enabled | none | Name under [image_optimizer.profile_sets.*] |
origin_query | String | No | strip when enabled | preserve or strip; effective preserve is rejected while IO is enabled |
Example:
[proxy.asset_routes.image_optimizer]
enabled = true
region = "us_east"
profile_set = "default_images"[image_optimizer.profile_sets.<name>]
Profile sets convert small request query controls into a closed set of Image Optimizer parameters.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
base_params | String | No | "" | Params applied before profile-specific params |
default_profile | String | No | default | Profile used when no profile is requested |
unknown_profile | String | No | use_default | use_default or reject |
profile_param | String | No | profile | Query parameter containing the profile name |
aspect_ratio_param | String | No | ar | Query parameter containing aspect ratio |
debug_param | String | No | _io_debug | Query parameter that disables IO when set to 1 |
Profile values live under [image_optimizer.profile_sets.<name>.profiles] and use query-string syntax.
[image_optimizer.profile_sets.default_images]
base_params = "quality=70&resize-filter=bicubic"
default_profile = "default"
unknown_profile = "use_default"
profile_param = "profile"
aspect_ratio_param = "ar"
debug_param = "_io_debug"
[image_optimizer.profile_sets.default_images.profiles]
default = "width=1920"
medium = "format=auto&width=828"
thumbnail = "width=150&crop=1:1,smart"Supported profile parameters are quality, resize-filter, format, width, height, and crop. Unknown profile parameters fail configuration validation.
[image_optimizer.profile_sets.<name>.aspect_ratios]
| Field | Type | Required | Description |
|---|---|---|---|
allowed | Array[String] | No | Allowed query values such as 1-1 or 16-9 |
profiles | Array[String] | No | Defined profiles that accept aspect-ratio overrides |
[image_optimizer.profile_sets.default_images.aspect_ratios]
allowed = ["1-1", "16-9", "4-3"]
profiles = ["medium", "thumbnail"][image_optimizer.profile_sets.<name>.crop_offsets]
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable offset bucketing |
x_param | String | No | x | Query parameter for x-axis offset |
y_param | String | No | y | Query parameter for y-axis offset |
buckets | Array[Integer] | No | [10, 30, 50, 70, 90] | Offset buckets in 0..=100 |
default | Integer | No | 50 | Offset used when input is missing or invalid |
when_missing | String | No | smart | smart or none when neither offset exists |
[image_optimizer.profile_sets.default_images.crop_offsets]
enabled = true
x_param = "x"
y_param = "y"
buckets = [10, 30, 50, 70, 90]
default = 50
when_missing = "smart"See Asset Routes for request flow, S3 auth details, and Image Optimizer behavior.
Tinybird Configuration
[tinybird] sends auction events directly to the Tinybird Events API. The emitter is active only when enabled = true; access-log emission is not wired.
[tinybird]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable auction telemetry |
api_host | String | "" | Required when enabled; regional host without scheme, port, or path |
auction_dataset | String | "auction_events_raw" | 1–128 ASCII letters, digits, or _ |
auction_token_secret | String or null | null | Store key required when enabled; resolved value must be nonempty |
access_enabled | Boolean | false | Reserved; true fails startup because no emitter is wired |
access_dataset | String | "access_logs_raw" | Reserved access-log dataset name |
access_sample_rate | Number | 0.0 | 0.0..=1.0; reserved while access emission is disabled |
max_body_bytes | Integer | 1048576 | At least 1024 bytes |
secret_store and access_token_secret are deprecated compatibility inputs. Both are removed during normalization; neither reaches runtime or serialized output. New configurations use only auction_token_secret, whose value is resolved through trusted_server_secrets.
The complete enabled example appears in Tinybird auction telemetry.
Cache Configuration
Static and rehosted asset cache upgrades are operator-controlled. By default, Trusted Server leaves arbitrary publisher-origin assets under origin cache control. Add [[cache.asset_rules]] entries only for paths that are known to be content-addressed or otherwise safe for the configured TTL.
[[cache.asset_rules]]
Rules are evaluated in file order; the first enabled matching rule wins. Disabled rules never match, and their matcher and policy validation is deferred until they are enabled. Rule IDs are always normalized and must remain nonempty and unique, including for disabled placeholders.
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique operator-facing rule identifier |
enabled | Boolean | No | Whether the rule participates in matching (default false) |
preset | String | Matcher | Built-in preset such as nextjs-static |
path_prefix | String | Matcher | Request path prefix |
path_glob | String | Matcher | Single glob matched against the request path |
path_globs | Array[String] | Matcher | Multiple globs matched against the request path |
path_regex | String | Matcher | Regex matched against the request path |
extensions | Array[String] | Matcher | Case-insensitive file extensions |
fingerprint_style | String | No | Required bundler fingerprint convention before matching |
visibility | String | No | public or private (default public) |
browser_ttl_seconds | Integer | Policy | Browser max-age; required for private rules and positive with immutable = true |
edge_ttl_seconds | Integer | Policy | Public rules only: TTL emitted through the runtime-specific shared-cache directive |
stale_while_revalidate_seconds | Integer | No | Optional stale-while-revalidate |
stale_if_error_seconds | Integer | No | Optional stale-if-error |
immutable | Boolean | No | Add immutable for a validated content-addressed rule |
An enabled rule must configure exactly one matcher. Public rules must configure at least one of browser_ttl_seconds or edge_ttl_seconds; private rules must configure browser_ttl_seconds and must not configure edge_ttl_seconds. path_glob and path_globs are mutually exclusive. immutable = true additionally requires a positive browser TTL and either the content-addressed nextjs-static preset, hex, or esbuild-base32.
The filename fingerprint check examines the suffix immediately before the final extension and requires a nonempty filename prefix separated by ., -, _, or ~. The accepted immutable conventions are:
hex: hexadecimal suffixes of at least eight characters containing a letter, such asapp.0123abcd.js;esbuild-base32: eight-character uppercase Base32 suffixes, such asapp-VRTVD5R5.js.
vite-base64-url remains available for non-immutable cache rules, but it cannot prove content addressing. Ordinary names such as hero-Portrait.jpg can match its eight-character Base64URL shape. A matching rule whose selected fingerprint style fails emits a debug log with the rule ID and rejected path.
Glob patterns are case-sensitive. * matches within a single path component, while ** matches recursively: /assets/*.js matches /assets/app.js but not /assets/vendor/app.js; /assets/**/*.js matches both.
Next.js preset example (disabled until the publisher confirms /_next/static/ is content-addressed):
[[cache.asset_rules]]
id = "nextjs-static"
enabled = false
preset = "nextjs-static"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = truePublisher allowlist example (enable only for an unambiguous immutable filename convention):
[[cache.asset_rules]]
id = "publisher-fingerprinted-assets"
enabled = false
path_globs = [
"/assets/**/*.js",
"/assets/**/*.css",
"/assets/**/*.png",
"/assets/**/*.webp",
]
fingerprint_style = "hex"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = trueIf [cache] is omitted or no enabled rule matches, Trusted Server preserves the origin cache policy for publisher-origin assets. On the publisher pass-through path, an origin private or no-store directive vetoes a matching rule. Other origin cache directives, including no-cache, are replaced by the configured policy. Vary is preserved, so do not assign a public immutable rule to paths that vary by cookies or other user-specific request state.
On a configured Fastly asset-rehost route, a matching rule is authoritative over the third-party origin's cache defaults, including no-store, because Trusted Server owns the rehosted copy. A later Trusted Server or operator-applied private or no-store directive still vetoes public policy reapplication and removes shared-cache headers.
TS-owned validated hash URLs such as /static/tsjs=...js?v=<hash> use their built-in cache policy and do not require an asset rule. Shared-cache keys for /static/tsjs= must preserve v; otherwise a matching immutable response can collide with the missing or mismatched version's short-TTL response.
edge_ttl_seconds only emits the selected runtime's shared-cache directive for public rules. The runtime or service must also enable and consume that directive. The checked-in Cloudflare manifests intentionally do not enable Workers Cache: the Worker serves the full publisher gateway, not an isolated static-only entrypoint. Emitting Cloudflare-CDN-Cache-Control alone must not be treated as permission to cache every response. Any future Workers Cache opt-in must isolate or explicitly allowlist cacheable traffic. Fastly synthetic and final egress responses still require explicit runtime cache integration, tracked in #908.
Integration Configurations
Every deploy-validated integration ID is listed here. A section is optional unless its integration is enabled or a CLI workflow retains an explicit disabled stub.
| Section | Reference |
|---|---|
[integrations.adserver_mock] | Ad Server Mock |
[integrations.aps] | APS |
[integrations.datadome] | DataDome |
[integrations.didomi] | Didomi |
[integrations.google_tag_manager] | Google Tag Manager |
[integrations.gpt] | GPT |
[integrations.gpt_diagnostics] | GPT diagnostics |
[integrations.js_asset_proxy] | JS Asset Proxy (no dedicated guide) |
[integrations.lockr] | lockr |
[integrations.nextjs] | Next.js |
[integrations.osano] | Osano |
[integrations.permutive] | Permutive |
[integrations.prebid] | Prebid |
[integrations.sourcepoint] | Sourcepoint |
[integrations.testlight] | Testlight |
Common Fields
All integrations support an enabled flag. Defaults vary by integration and only apply when the integration section exists in trusted-server.toml.
| Field | Type | Description |
|---|---|---|
enabled | Boolean | Enable/disable the integration |
Ad Server Mock Integration
Section: [integrations.adserver_mock]
This integration is the optional auction mediator selected by auction.mediator = "adserver_mock"; it is not an OpenRTB provider.
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable mediator registration |
endpoint | URL | Required | Mediation service URL |
timeout_ms | Integer | 500 | 1..=60000 milliseconds |
price_floor | Number or null | null | Optional minimum accepted CPM |
context_query_params | Object | {} | Maps admitted auction-context keys to query names |
APS Browser Integration
Section: [integrations.aps]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable browser-side APS behavior |
rendering_mode | String | "trusted_server" | trusted_server or publisher_native |
Server endpoint, timeout, account, inventory, and debug settings belong to an aps provider and its profile_config, not this browser section. See APS.
DataDome Integration
Section: [integrations.datadome]
The DataDome guide explains request behavior and exclusion-rule syntax. This table covers every canonical top-level field:
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable DataDome registration |
sdk_origin | URL | https://js.datadome.co | Browser SDK origin |
api_origin | URL | https://api-js.datadome.co | Browser signal API origin |
cache_ttl_seconds | Integer | 3600 | 60..=86400 seconds |
rewrite_sdk | Boolean | true | Rewrite matching SDK URLs |
enable_protection | Boolean | false | Call the Protection API before route matching |
server_side_key_secret_name | String or null | null | Secret-store key required when protection is enabled |
protection_api_origin | URL | https://api-fastly.datadome.co | Protection API origin |
timeout_ms | Integer | 1500 | 1..=10000 milliseconds |
protection_excluded_methods | Array[String] | ["OPTIONS"] | Methods that bypass protection |
protection_excluded_asns | Array[Integer] | [] | Client ASNs that bypass protection |
protection_excluded_ip_cidrs | Array[String] | [] | Inline client-IP CIDRs that bypass protection |
protection_excluded_ip_cidr_sources | Array[Object] | [] | Config-store sources of client-IP CIDRs that bypass protection |
protection_ip_list_cache_ttl_seconds | Integer | 300 | 1..=86400 seconds |
protection_exclusion_rules | Array[Object] | Static-asset path-regex rule | Ordered typed exclusion rules |
protection_test_bypass | Object or null | null | Access-controlled test bypass; disabled when present by default |
enable_graphql_support | Boolean | false | Reserved; true is accepted but ignored in v1 |
client_side_key | String | "" | Key used for browser-tag injection |
inject_client_side_tag | Boolean | true | Inject only when client_side_key is nonempty |
client_side_tag_url | String | /integrations/datadome/tags.js | Root-relative or HTTPS injection URL |
client_side_configuration | JSON value | { "ajaxListenerPath": true } | Value assigned to window.ddoptions |
Each protection_excluded_ip_cidr_sources item requires key and defaults config_store to datadome-ip-bypass. Each protection_exclusion_rules item requires id (legacy alias name), defaults enabled to true, defaults methods to every method, and selects one typed matcher: path_exact, path_prefix, path_regex, query_param_non_empty, asn, ip_cidr, or ip_cidr_source.
When protection_test_bypass is present, its enabled field defaults to false; credential_secret_name identifies the secret-store key and becomes required only when the bypass is enabled. The resolved credential must contain at least 32 bytes.
The deprecated server_side_key_secret_store and nested credential_secret_store inputs are accepted and discarded. Do not add them to new configurations.
Didomi Integration
Section: [integrations.didomi]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | true | Enable the reverse proxy when the section exists |
proxy_path | String or null | /integrations/didomi/consent | No trailing slash, repeated slash, dot segment, or unsafe character |
sdk_origin | URL | https://sdk.privacy-center.org | SDK upstream |
api_origin | URL | https://api.privacy-center.org | API upstream |
See Didomi for the routed endpoint shapes.
Google Tag Manager Integration
Section: [integrations.google_tag_manager]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable tag-gateway routes and rewriting |
container_id | String | Required | GTM- followed by 4–20 uppercase letters or digits |
upstream_url | URL | https://www.googletagmanager.com | Script upstream |
cache_max_age | Integer | 900 | 60..=86400 seconds |
max_beacon_body_size | Integer | 65536 | 1024..=1048576 bytes |
See Google Tag Manager.
GPT Integration
Section: [integrations.gpt]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | true | Enable GPT when the section exists |
gam_attribution_enabled | Boolean | false | Add page-level ts=true targeting |
script_url | URL | Google's secure GPT URL | Bootstrap source |
cache_ttl_seconds | Integer | 3600 | 60..=86400 seconds |
rewrite_script | Boolean | true | Rewrite matching GPT script URLs |
slim_prebid_url | String or null | null | Optional tsjs-prebid bundle loaded after window.load |
See GPT.
GPT Diagnostics Integration
Section: [integrations.gpt_diagnostics]
The only field is enabled, a Boolean that defaults to false. When enabled, the standalone diagnostics tag is available, but individual browser sessions still require the activation flow in GPT diagnostics.
JS Asset Proxy Integration
Section: [integrations.js_asset_proxy]
Serves explicitly configured third-party JavaScript assets from first-party paths. Each asset maps one exact publisher-facing path to one exact HTTPS upstream URL and can independently enable proxying, disable proxying, or block matching script tags in publisher HTML. There is no dedicated integration guide; the registered routes appear in the API reference.
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable asset proxying and HTML rewriting |
cache_ttl_seconds | Integer | None | Optional downstream cache TTL for every asset |
assets | Array | [] | Asset mappings; required when enabled |
Each [[integrations.js_asset_proxy.assets]] entry:
| Field | Type | Default | Contract |
|---|---|---|---|
path | String | Required | Exact first-party request path served by Trusted Server |
origin_url | String | Required | Exact upstream JavaScript URL fetched and match-rewritten |
proxy | String | enabled | enabled, disabled, or blocked (removes script tags) |
cache_ttl_seconds | Integer | None | Optional per-asset downstream cache TTL override |
lockr Integration
Section: [integrations.lockr]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | true | Enable lockr when the section exists |
app_id | String | Required | Nonempty lockr application ID |
api_endpoint | URL | https://identity.loc.kr | API origin |
sdk_url | URL | https://aim.loc.kr/identity-lockr-trust-server.js | SDK source |
cache_ttl_seconds | Integer | 3600 | 60..=86400 seconds |
rewrite_sdk | Boolean | true | Rewrite matching lockr SDK URLs |
rewrite_sdk_host | Boolean or null | null | Deprecated compatibility input |
origin_override | URL or null | null | Optional upstream Origin override |
See lockr.
Prebid Integration
[integrations.prebid] owns browser behavior only. Server endpoint, provider timeout, routing, profile debug/test controls, consent forwarding, bidder-param overrides, and notification suppression belong under [auction].
| Browser field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable browser bundle injection, interception, and the trustedServer adapter |
account_id | String | None | Optional account value injected into browser Prebid configuration |
timeout_ms | Integer | 1000 | Browser Prebid.js timeout; independent of every server provider timeout |
debug | Boolean | false | Browser Prebid.js debug flag; independent of server profile debug |
client_side_bidders | Array[String] | [] | Bidders kept on native browser adapters |
excluded_gam_ad_unit_path_suffixes | Array[String] | [] | GAM suffixes excluded from Trusted Server refresh auctions |
script_patterns | Array[String] | ["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"] | Publisher Prebid script paths intercepted by Trusted Server |
external_bundle_url | String | Required when enabled | HTTPS publisher-specific Prebid.js bundle URL |
external_bundle_sha256 / *_sri | String | None | Optional bundle integrity and cache metadata |
bundle.modules.bidder | Array[String] | Required and non-empty | Exact bidder module stems used by ts prebid bundle |
bundle.modules.user_id | Array[String] | Curated preset when omitted | Exact User ID module stems used by ts prebid bundle |
bundle.modules.analytics | Array[String] | [] | Exact analytics module stems used by ts prebid bundle |
managed_user_ids | Array[Table] | [] | Prebid User ID modules Trusted Server installs and keeps installed (see below) |
Server-side bidder codes are derived from validated [auction.bidders.*] routes and injected into the browser. There is no second server bidder list in [integrations.prebid]. A browser bidder stays client-side only when named in client_side_bidders and its adapter is present in the generated bundle.
Example:
[integrations.prebid]
enabled = true
timeout_ms = 1000
debug = false
client_side_bidders = ["rubicon"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"
script_patterns = ["/prebid.js", "/prebid.min.js"]
[[integrations.prebid.managed_user_ids]]
name = "sharedId"
[integrations.prebid.managed_user_ids.storage]
type = "cookie"
name = "_sharedid"
expires = 15
refresh_in_seconds = 1800
[proxy]
allowed_domains = ["assets.example.com"]
[integrations.prebid.bundle.modules]
bidder = ["rubiconBidAdapter"]
user_id = ["sharedIdSystem"]
analytics = ["atsAnalyticsAdapter"]
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"
[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"
bid_param_overrides = { example-server = { placement = "example-placement" } }
[[auction.providers.pbs-main.profile_config.bid_param_override_rules]]
when.bidder = "example-server"
when.zone = "header"
set = { placement = "example-header-placement" }
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]
[auction.bidders.example-server]
provider = "pbs-main"Environment override:
env 'TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true' \
'TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
ts config validateEnvironment overlays only replace existing scalar leaves. Keep client_side_bidders, provider profile tables, bidder-parameter overrides, and rules in TOML, then validate and push the edited file.
Managed User ID modules:
Each [[integrations.prebid.managed_user_ids]] entry names a Prebid userSync.userIds module that Trusted Server installs on the page and reinstates whenever publisher JavaScript replaces the User ID configuration. Trusted Server does not interpret module-specific fields; every registered module uses the same vendor-neutral surface. The managed name must match a configNames entry in the checked-in user_id_modules.json registry:
| Field | Type | Default | Description |
|---|---|---|---|
name | String | Required | Prebid userSync.userIds entry name, for example sharedId. Unique across entries |
params | Table | {} | Module-specific parameters, forwarded to Prebid unchanged |
storage.type | String | cookie | Browser storage: cookie or html5 |
storage.name | String | Required when storage exists | Cookie or local-storage key the module reads and writes |
storage.expires | Integer | Prebid's own default | Storage lifetime in days; must be at least 1. Any per-module ceiling is the module's |
storage.refresh_in_seconds | Integer | Prebid's own default | Seconds before the module may refresh the stored value; must be at least 1 |
The module must be present in the built bundle. Name it under [integrations.prebid.bundle].user_id_modules, or omit that list to take the generator's default preset. ts prebid bundle resolves each managed name through the checked-in user_id_modules.json registry, rejects unknown names, ambiguous names, and two names that resolve to the same module, and confirms the required modules in the newly generated manifest. A failure identifies the managed name or required module and does not update the configured bundle hash or SRI. The browser diagnostic remains a fallback for externally hosted, stale, or modified bundles. Trusted Server core does not interpret module-specific params; it forwards them to Prebid.js unchanged.
Persisting a resolved ID into the Edge Cookie identity graph additionally requires a matching [[ec.partners]] entry whose source_domain equals the module's OpenRTB EID source.
managed_user_ids is an array of tables, so it cannot be set through a TRUSTED_SERVER__ environment variable; the scalar overlay only replaces leaves the published TOML already declares. See Managed User ID modules for consent, timing, privacy, degraded behavior, and validation guidance.
Script Pattern Matching:
The script_patterns configuration determines which Prebid scripts are intercepted and replaced with empty JavaScript responses. This prevents client-side Prebid.js from loading when using server-side bidding.
- Suffix matching:
/prebid.min.jsmatches any URL ending with that path - Wildcard patterns:
/static/prebid/*matches paths under that prefix - Disable interception: Set
script_patterns = []to keep client-side Prebid
See Prebid Integration for full details.
Server Bid Param Override Surfaces:
These fields belong under [auction.providers.<id>.profile_config] for a prebid-server provider:
bid_param_overrides: static per-bidder shallow-merge overrides;bid_param_zone_overrides: per-bidder, per-zone shallow-merge overrides; andbid_param_override_rules: canonical ordered rules withwhenmatchers andsetobjects.
Compatibility-shaped fields are normalized into the same profile-local runtime engine. Explicit rules run after compatibility-derived rules, so later rules win on conflicts.
Next.js Integration
Section: [integrations.nextjs]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable Next.js integration |
rewrite_attributes | Array[String] | ["href", "link", "url"] | Nonempty set of structured payload keys to rewrite |
max_combined_payload_bytes | Integer | 10485760 | Maximum combined RSC payload size in bytes |
Example:
[integrations.nextjs]
enabled = true
rewrite_attributes = ["href", "link", "url", "src"]
max_combined_payload_bytes = 10485760Environment Override:
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__MAX_COMBINED_PAYLOAD_BYTES=10485760Edit rewrite_attributes in TOML because the overlay cannot replace arrays.
Osano Integration
Section: [integrations.osano]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the Osano browser consent mirror |
Example:
[integrations.osano]
enabled = trueEnvironment Override:
TRUSTED_SERVER__INTEGRATIONS__OSANO__ENABLED=trueThe Osano mirror runs in the browser, so consent cookies it writes are available to Trusted Server on requests after the page where Osano consent APIs become ready. See Osano Integration for details.
Permutive Integration
Section: [integrations.permutive]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | true | Enable Permutive integration |
organization_id | String | Required | Nonempty Permutive organization ID |
workspace_id | String | Required | Nonempty Permutive workspace ID |
project_id | String | "" | Optional project ID; reserved for future use |
api_endpoint | URL | https://api.permutive.com | Permutive API URL |
secure_signals_endpoint | URL | https://secure-signals.permutive.app | Secure Signals URL |
cache_ttl_seconds | Integer | 3600 | 60..=86400 seconds |
rewrite_sdk | Boolean | true | Rewrite Permutive SDK references |
Example:
[integrations.permutive]
enabled = true
organization_id = "org-12345"
workspace_id = "ws-67890"
project_id = "proj-abcde"
api_endpoint = "https://api.permutive.com"
secure_signals_endpoint = "https://secure-signals.permutive.app"
cache_ttl_seconds = 7200
rewrite_sdk = trueSourcepoint Integration
Section: [integrations.sourcepoint]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable Sourcepoint proxying and rewriting |
rewrite_sdk | Boolean | true | Rewrite matching Sourcepoint URLs |
cdn_origin | URL | https://cdn.privacy-mgmt.com | HTTP(S) URL whose host is exactly cdn.privacy-mgmt.com |
auth_cookie_name | String or null | null | 1–64 letters, digits, _, or -; built-in cookies need no entry |
cache_ttl_seconds | Integer | 3600 | 60..=86400 seconds |
See Sourcepoint.
Testlight Integration
Section: [integrations.testlight]
| Field | Type | Default | Contract |
|---|---|---|---|
enabled | Boolean | false | Enable Testlight integration |
endpoint | URL | Required | Testlight auction endpoint |
timeout_ms | Integer | 1000 | 10..=60000 milliseconds |
shim_src | String | /static/tsjs=tsjs-unified.min.js | Nonempty script source for the shim |
rewrite_scripts | Boolean | false | Rewrite Testlight script references |
Example:
[integrations.testlight]
enabled = true
endpoint = "https://testlight.example/openrtb2/auction"
timeout_ms = 1500
rewrite_scripts = trueAuction Configuration
[auction.providers.*] is the only server-side provider inventory, and [auction.bidders.*] is the only client-visible bidder route map. The optional [auction].mediator remains a separate integration selection; it is not a provider or bidder route.
[auction]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the auction orchestrator |
sanitize_creatives | Boolean | false | Strip executable markup from winning-bid adm before delivery |
rewrite_creatives | Boolean | true | Rewrite winning-bid adm through first-party endpoints |
timeout_ms | Integer | 2000 | Logical auction budget in milliseconds |
mediator | String | None | Optional separate adserver_mock mediator |
creative_store | String | "creative_store" | Deprecated; creatives are delivered inline |
allowed_context_keys | Array | [] | Request context keys admitted into the auction |
Creative markup delivered by POST /auction and the publisher SSAT/page-bids path is processed by two independent passes. With sanitize_creatives = true (opt-in, default false), executable markup (script/object/embed/form and event handlers) is stripped together with its inner content. This blanks script-based creatives, so enable it only when creatives render in a context that shares the publisher's origin. With rewrite_creatives = true (the default), eligible absolute or protocol-relative resource and click URLs not excluded by rewrite configuration are converted to signed first-party endpoints, and any bidder-supplied <base> element is removed. The POST /auction path emits root-relative endpoints and injects the creative TSJS runtime exactly once, whether or not the bidder supplied a <body>, since bare fragments are the common adm shape. The foreign-origin SSAT renderer emits absolute endpoints and does not inject that bundle. With both disabled, adm ships exactly as the bidder returned it, except that a creative larger than the 1 MiB per-creative cap is rejected in every mode and its adm is dropped. Accepted external URLs are not host allowlisted by the sanitizer. Neither setting affects HTML or CSS fetched through /first-party/proxy. See Creative Processing.
Existing configs, upgrade sequencing, and rollback
Default values are omitted from stored JSON; non-default values (sanitize_creatives = true, rewrite_creatives = false) are serialized, and older AuctionConfig schemas reject unknown fields.
Upgrading: binaries that predate sanitize_creatives reject a blob that carries it, so in a rolling deployment upgrade the binary first, then push a config with sanitize_creatives = true if you want sanitization. Between the binary upgrade and the config push, sanitization is off (the new default). During that interval the creative iframe sandbox is the only isolation for /auction markup. There is no mixed-version-safe value that keeps the old unconditional sanitization: omission means "sanitize" on old code and "don't" on new code, while an explicit true fails startup on old code.
Rolling back: before reverting to a binary that does not know a field, remove that field's non-default value (and any environment override), run ts config validate, push the resulting default-compatible blob, and only then roll back the binary.
Environment overlays: The pinned EdgeZero loader cannot create missing TOML leaves. Existing configs must add both leaves under [auction] (rewrite_creatives and sanitize_creatives) before TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES / TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES can take effect. An override for a missing leaf is silently ignored.
Provider map
Breaking migration from the provider list
The former [auction].providers = ["prebid", ...] list and server-owned fields under [integrations.prebid] and [integrations.aps] are no longer accepted, even when an integration is disabled. Replace them with provider instances and bidder routes before deployment.
For Prebid Server, move server_url to provider endpoint, server timeout to provider timeout_ms, request controls and bidder-parameter overrides to the prebid-server profile_config, notification suppression to notifications, and each server bidder to [auction.bidders.<id>]. Origin-only legacy server_url values compile to /openrtb2/auction; query parameters survive, and configured non-root custom endpoint paths remain exact. Browser timeout, debug, bundle, script interception, refresh exclusions, and client_side_bidders remain under [integrations.prebid]. Configure timeout or debug under both owners when both browser and server behavior should retain the old value.
For APS, move endpoint and timeout to the provider, then move account, inventory, debug, and creative controls to the aps profile_config.
Only bidder codes listed in [auction.bidders] are folded into Trusted Server requests. Unlisted publisher bids remain native browser demand. All provider endpoints must be absolute HTTPS URLs.
The old and new blobs are mutually incompatible. Activate the new binary and map-shaped config together. A binary-first or config-first rolling deployment will put one version on a schema it rejects. Roll back by restoring the old binary and old-schema blob together.
Each table name is the provider ID used for configuration, backend correlation, health, response metadata, and telemetry. Provider IDs must match ^[a-z][a-z0-9-]{0,62}$. Multiple instances may select the same profile and endpoint because the provider ID remains their distinct runtime identity.
Example:
[auction]
enabled = true
sanitize_creatives = false
rewrite_creatives = true
timeout_ms = 2000
mediator = "adserver_mock"
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"
timeout_ms = 1200
[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]
[auction.providers.aps-main]
protocol = "openrtb-2.6"
profile = "aps"
endpoint = "https://aps.example.com/e/pb/bid"
routing = "all_eligible"
[auction.providers.aps-main.profile_config]
account_id = "example-aps-account"
debug = false
allow_script_creatives = false
[auction.bidders.example-server]
provider = "pbs-main"
[integrations.adserver_mock]
enabled = true
endpoint = "https://mediator.example.com/mediate"
timeout_ms = 500| Provider field | Required | Default | Description |
|---|---|---|---|
protocol | Yes | None | Must be openrtb-2.6 |
profile | No | standard | standard, prebid-server, or aps |
endpoint | Yes | None | Absolute HTTPS URL with host and no credentials or fragment |
timeout_ms | No | Profile default | Provider logical budget before the remaining-auction cap |
routing | No | explicit | explicit, or all_eligible for non-PBS profiles |
profile_config | No | {} | Typed object owned by the selected profile |
notifications | No | No suppression | Common nurl/burl suppression after response normalization |
Profile configuration
The table reflects the typed profile schemas. Required refers to the selected profile's profile_config object, not to the provider wrapper.
| Profile | Field | Required | Default | Provider timeout default | Constraints |
|---|---|---|---|---|---|
aps | account_id | Yes | — | 800 ms | String or integer; trimmed, nonempty, at most 1024 bytes |
aps | allow_script_creatives | No | false | 800 ms | Script creatives are ineligible unless enabled |
aps | debug | No | false | 800 ms | May expose unredacted request and response data |
aps | inventory_domain | No | None | 800 ms | DNS name, at most 253 bytes; configure with inventory_page_origin |
aps | inventory_page_origin | No | None | 800 ms | HTTPS origin without credentials, port, path, query, or fragment; host must equal or be beneath inventory_domain |
prebid-server | bid_param_override_rules | No | [] | 1000 ms | Ordered exact-match rules; at least one matcher and a nonempty set object |
prebid-server | bid_param_overrides | No | {} | 1000 ms | Per-bidder nonempty shallow-merge objects |
prebid-server | bid_param_zone_overrides | No | {} | 1000 ms | Per-bidder, per-zone nonempty shallow-merge objects |
prebid-server | consent_forwarding | No | both | 1000 ms | openrtb_only, cookies_only, or both |
prebid-server | debug | No | false | 1000 ms | Includes upstream exchange diagnostics |
prebid-server | debug_query_params | No | None | 1000 ms | Optional legacy page-URL query fragment |
prebid-server | test_mode | No | false | 1000 ms | Sets OpenRTB test = 1 |
standard | imp_ext | No | {} | Auction timeout | JSON object; at most 16384 bytes, depth 8, and 256 keys per object |
standard | request_ext | No | {} | Auction timeout | JSON object; at most 16384 bytes, depth 8, and 256 keys per object; trusted_server is reserved |
Timeout defaults are 1000 ms for prebid-server, 800 ms for aps, and the auction timeout for standard. An explicit provider timeout overrides the profile default. Runtime uses min(provider timeout, auction time remaining) for launch decisions and OpenRTB tmax.
routing = "explicit" sends only slots carrying a bidder assigned to that provider, plus trusted stored-request routes. routing = "all_eligible" sends every banner-compatible slot to the provider, regardless of bidder routes. It does not disclose bidder parameters assigned to another provider. APS commonly uses all_eligible to preserve its whole-inventory participation. The prebid-server profile rejects all_eligible because every PBS impression must carry routed bidder or stored-request demand.
Bidder routes and bounds
Each [auction.bidders.<bidder-id>] maps one client-visible bidder ID to exactly one provider. Bidder IDs must be nonempty, no more than 128 UTF-8 bytes, contain no control characters or surrounding whitespace, and cannot be the reserved exact ID trustedServer. Browser trustedServer.bidderParams accepts at most 128 bidder entries; its optional zone is at most 256 UTF-8 bytes.
For the standard profile, profile_config.request_ext and imp_ext must be JSON objects. Each object is limited to 16 KiB serialized, eight container levels, and 256 keys at any one object level. Within request_ext, the trusted_server member is reserved and cannot be overwritten. imp_ext has no reserved-member guard in the current implementation.
Common notification suppression uses exact returned OpenRTB seat values, not bidder route IDs:
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]suppress_seats permits at most 128 unique nonempty entries, each at most 128 UTF-8 bytes and without ASCII control characters.
Validation timing and target limits
ts config validate and ordinary deploy validation compile the complete target-independent plan: profiles and defaults, routes, endpoint ownership, extension bounds, notifications, signing structure, and mediator selection. Target-specific checks are deferred to adapter startup. Startup uses the same compiled plan and additionally validates backend-name prediction/collisions and provider fan-out capability.
Fastly and Axum support multiple configured providers. Cloudflare and Spin currently reject an enabled auction with more than one provider because those adapters do not support concurrent provider fan-out. Disabled auctions may keep dormant multi-provider maps without target rejection.
A target-aware pre-write ts config push --adapter <target> callback is not available in this tree because the required EdgeZero callback is not yet available. Until it lands, push performs target-independent validation and adapter startup is the mandatory target-aware gate. Do not treat a successful push as proof that a Cloudflare or Spin multi-provider plan can start.
Deadline behavior
Configured timeouts are logical budgets, not hard wall-clock guarantees. No current adapter claims an abortable provider-wide total-request deadline. Already-launched work may complete after the logical budget and a completed late response can remain eligible. Once the logical auction budget is exhausted, Trusted Server starts no additional provider or mediator network work, then finishes local decision and delivery. An auction can therefore exceed its configured wall-clock timeout.
Creative sanitization is opt-in. sanitize_creatives = true strips executable markup before delivery. rewrite_creatives = false skips first-party URL rewriting and creative TSJS injection. See Creative Processing.
Environment overrides replace map leaves that already exist in TOML:
env 'TRUSTED_SERVER__AUCTION__ENABLED=true' \
'TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES=false' \
'TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES=true' \
'TRUSTED_SERVER__AUCTION__TIMEOUT_MS=2000' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__ENDPOINT=https://prebid.example.com/openrtb2/auction' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__TIMEOUT_MS=900' \
'TRUSTED_SERVER__AUCTION__MEDIATOR=adserver_mock' \
ts config validateCreative Opportunities Configuration
[creative_opportunities]
Defines the ad slots the trusted server offers on a page: which pages each slot appears on (page_patterns), its supported sizes (formats), and the GAM ad unit it maps to (gam_unit_path).
enabled is the dedicated server-side ad-template switch. It defaults to true for compatibility with existing configurations. Set it to false to stop publisher HTML and SPA page-bids template delivery while retaining the slot configuration and direct POST /auction endpoint.
Publisher document cache policy
For a successful GET publisher document, Trusted Server applies the browser-only Cache-Control: private, max-age=60 policy from #1007 when the server-side ad stack is structurally inactive. Trusted Server also applies this policy to a subsequent 304 Not Modified response so revalidation cannot restore the origin freshness policy. This includes an absent [creative_opportunities] section, enabled = false, no slot matching the path, or a disabled auction. The private directive prevents shared caches that use Cache-Control from storing the document. The policy replaces the origin browser cache policy except when the origin sends private or no-store, which are preserved. Bot, prefetch, and consent-denied requests also retain the origin policy because they can produce a request-specific representation for the same URL. Error responses and non-document requests retain the origin policy.
Trusted Server leaves origin validators and CDN-specific cache headers unchanged. Those headers continue to control supporting CDNs independently of the browser-only policy. If a response using the generated inactive-stack policy later carries Set-Cookie, cookie privacy finalization replaces it with Cache-Control: private, max-age=0 and removes the CDN-specific cache headers.
[creative_opportunities]
enabled = true # set to false to disable server-side ad templates
gam_network_id = "123456789"
price_granularity = "dense"
# Shared placeholder value for the site root ("/") — see {section} below.
section_root = "home"
# Which path segment names the section, 0-based. Default 0 (first segment).
# Set to 1 for locale-prefixed URLs such as "/en/news/article".
# section_segment = 0
[[creative_opportunities.slot]]
id = "ad-header"
gam_unit_path = "/{network_id}/example/{section}"
# List each section landing page as well as its subtree: `/news/*` matches
# `/news/article` but NOT `/news` — the glob requires the trailing separator.
page_patterns = ["/", "/news", "/news/*", "/reviews", "/reviews/*"]
formats = [{ width = 728, height = 90 }]The same switch can be overridden through the typed CLI environment overlay. Because EdgeZero only replaces TOML leaves that already exist, first add enabled = true to the [creative_opportunities] block in the base config before using this override. See Environment Variable Overrides (Typed CLI) for the general overlay rules.
TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__ENABLED=falseWARNING
Setting enabled = false writes this field into the pushed configuration blob. Binaries released before this setting reject the unknown field and fail to load settings, which makes every request fail. Before rolling back to an older binary, restore enabled to its default, re-push and finalize the configuration, then roll back the binary.
Shared template assembly (assembly_mode = "esi")
inline remains the default. esi is opt-in per deployment, covered by the template-cache-local-test.sh harness and by rendered-document byte-identity tests, and originated in IABTechLab/trusted-server#1009. Enable it deliberately and verify with the harness first; the keys below are the safety contract that makes it safe to do so.
assembly_mode controls how initial-page slot and bid state is delivered:
inline(default) transforms every origin response and injects the current reader's slots and bids directly.esiopts into a reader-neutral transformed-template cache on Fastly. The cache stores identity bytes containing one inert, versioned comment. On an authorized cold miss, Fastly replaces that comment in a private working copy with one synthetic ESI include and resolves it from the already-built reader state using the pinnedstackpop/esiparser. No HTTP fragment request occurs. Warm hits use an exact byte split instead, preserving the fast article-prefix stream while the auction finishes.
This is deliberately not general publisher-controlled ESI. A transformed origin document containing any <esi: directive bypasses the template cache and the parser, while the ordinary byte seam still produces the reader's complete response. The stored shared template object never contains executable ESI markup.
Only Fastly currently supplies the Core Cache backend used by the shared template cache. Other adapters accept the mode but safely fall back to the inline transform on every request. This is not a top-level HTTP cache hit: Compute still runs and the final assembled response is always Cache-Control: private, no-store.
All keys below belong directly under [creative_opportunities]. They are one feature contract: assembly_mode selects how creative-opportunity state is delivered, while the other keys constrain when and how long that mode may share its template. They are not a general top-level HTTP-cache configuration.
[creative_opportunities]
assembly_mode = "esi"
# Every request header, except Accept-Encoding, that the publisher origin can
# name in Vary for these documents. Names are validated and de-duplicated.
template_cache_vary = [
"rsc",
"next-router-state-tree",
"next-router-prefetch",
"next-router-segment-prefetch",
]
# Safety ceiling for the shared template. Defaults to 60; valid range 1–86400.
# The origin's remaining edge freshness may make the actual lifetime shorter.
template_cache_max_age_seconds = 1200
# Optional bounded variant and personal-session policies; omitted means empty.
template_cache_key_cookies = ["ab_bucket"]
template_cache_bypass_cookies = ["session"]
# Default false. With the lists above, false still bypasses every request that
# carries any other cookie, including the TS identity cookie. Set true only
# after proving those unlisted cookies do not change origin HTML.
origin_is_cookie_independent = falseThe cache fails closed. A template is stored only for a GET with a processable 200 text/html origin response, a supported content encoding, and explicit positive shared freshness. private, no-store, no-cache, exhausted or malformed freshness, Set-Cookie, Vary: *, Vary: Cookie, uncovered Vary names, response-bound CSP nonces, pass-through or ambiguous authorization, diagnostics sessions, range or conditional requests, positive or malformed request max-age, min-fresh, and unsupported CDN-specific cache policy fields all bypass the template cache. Fastly Surrogate-Control is the narrow exception: the template cache accepts exactly one positive max-age plus optional valid stale-while-revalidate and stale-if-error delta-seconds. Restrictive, duplicated, malformed, or unknown directives fail closed. Stale windows never extend template-cache freshness. Freshness follows Fastly edge precedence: Surrogate-Control: max-age, then Cache-Control: s-maxage, Cache-Control: max-age, then Expires. Restrictive directives in either policy still refuse sharing. Origin Age and apparent age from Date are deducted, time spent transforming the page continues consuming freshness, and the remaining lifetime is capped by template_cache_max_age_seconds.
A browser reload commonly sends Cache-Control: max-age=0. TS may reuse a fresh reader-neutral shared template for that reload, but it still builds a new private response and runs a new per-reader auction. Explicit no-cache, no-store, positive or malformed request max-age, range, and conditional requests still bypass the template cache. Check X-TS-Template-Cache: hit to verify template reuse.
Authorization has one narrow exception. A request carrying exactly the same single Basic credential that Trusted Server just validated at the edge may share a template; pass-through, repeated, appended, or replaced values still bypass. Trusted Server does not remove the validated header, so it remains forwarded to the publisher origin. If the origin uses that credential to select response content, it must declare Vary: Authorization; that response is deliberately not stored as a shared template.
template_cache_vary is necessary because lookup occurs before the origin can return Vary. Presence, empty values, repeated raw field values, host/scheme, origin identity, complete template-shaping settings, TSJS content, and schema version all participate in an opaque SHA-256 cache key. Accept-Encoding does not: the stored template is decoded identity and the assembled result is encoded for each reader with Vary: Accept-Encoding. This assumes the origin's Accept-Encoding variants differ only by HTTP content coding, as normal compression negotiation does. Do not enable ESI for an origin that changes the document's meaning based on Accept-Encoding. Never put Cookie or Authorization in template_cache_vary; startup rejects both because raw cookie or credential values are not reader-neutral template dimensions. An origin Vary: Cookie always refuses storage, including when a named cookie policy is configured or origin_is_cookie_independent is true. Every other name the origin emits in Vary must appear in the configured list; an uncovered name safely refuses template storage.
The optional cookie lists control both template lookup and storage:
template_cache_key_cookiesincludes each named cookie's presence and value in the template key. Use bounded, reader-neutral variants such as experiment arms or region buckets, never account IDs, session tokens, or TS identity IDs. Missing and empty values select different variants. Multiple names form a combined variant.template_cache_bypass_cookiesforces inline processing whenever any named cookie is present, including an empty value such assession=. This applies regardless of the independence assertion or any key cookies in the request.- When either list is nonempty,
origin_is_cookie_independentapplies only to unlisted cookies. The safe default,false, bypasses requests containing any unlisted cookie;trueasserts that those cookies do not change origin HTML. TS identity and consent cookies have no implicit exception. - When both lists are omitted or empty, the existing all-cookie behavior remains:
falsebypasses every request carrying aCookieheader;trueasserts that all cookies are irrelevant to origin HTML.
Cookie names match exactly and case-sensitively: session and Session are different names. Use nonempty ASCII HTTP token names; whitespace, ;, =, and non-ASCII characters are invalid. Configuration rejects invalid names, duplicates within a list, overlap between lists, and TS identity cookies (ts-ec, ts-eids, sharedId) in the key list. Identity cookies may be listed for bypass. Wildcards, prefixes, and regular expressions are not supported. With either list nonempty, parsing of all Cookie fields after existing request preparation bypasses duplicate key-cookie names, invalid names, and malformed key-cookie values. When independence is true, duplicate unlisted names are allowed if every value passes framing checks, and unlisted values may also contain commas and balanced double quotes, supporting compact JSON cookies such as g_state and comma-separated experiment metadata. Unmatched quotes, whitespace within values, backslashes, controls, non-ASCII bytes, and comma-separated fragments resembling another name=value cookie still bypass. The origin must parse semicolon-separated cookies independently and treat these unlisted values as opaque. If its parser stops at a nonstandard value or changes how later key cookies are interpreted, the independence assertion is not valid. Cookies are forwarded unchanged by this policy; their values are not exposed in cache diagnostics. There is no value allowlist or cardinality limit, so operators must ensure keyed values stay bounded and account for every origin HTML dependency.
If a downstream CDN translates ab_bucket=A into X-Exp-Variant: A after the request passes TS, configure both dimensions:
[creative_opportunities]
assembly_mode = "esi"
# Retain any other header dimensions required by the origin.
template_cache_vary = ["x-exp-variant"]
template_cache_key_cookies = ["ab_bucket"]
template_cache_bypass_cookies = ["session"]
# Only after verifying that all remaining cookies leave origin HTML unchanged.
origin_is_cookie_independent = trueThe cookie dimension separates experiment arms at TS even when the header is absent there; the header dimension covers the origin's Vary: X-Exp-Variant. Configuring the header alone cannot distinguish these readers. TS cannot verify the downstream mapping or discover other inputs selecting origin HTML. During a canary, check correct content for each arm, absent and empty experiment values, and session-bearing requests, as well as cache diagnostics.
The lists can also be used independently within [creative_opportunities]. For experiment-only HTML, omit the bypass list:
template_cache_vary = ["x-exp-variant"]
template_cache_key_cookies = ["ab_bucket"]
origin_is_cookie_independent = trueFor anonymous sharing with a logged-in population, omit the key list:
template_cache_bypass_cookies = ["session"]
origin_is_cookie_independent = trueBoth examples require the same assertion that unlisted cookies do not change origin HTML and retain all other ESI eligibility and header-coverage requirements.
For a canary, inspect X-TS-Template-Cache. Its bounded values are hit, miss-stored, miss-store-error, miss-reserved, bypass-request, bypass-response, unsupported, invalid, and backend-error. No URL, header value, or cache key is exposed. invalid and backend-error fail open to a fresh origin response; they do not fail the page. The corresponding template_cache logs provide server-side observability for this path.
X-TS-Assembly identifies how the private response was assembled:
esi-parser— authorized cold miss assembled by the repaired parser;byte-seam— warm template-cache hit using the streaming byte seam;byte-seam-fallback— cold response safely assembled by byte seam because the platform parser was unavailable or rejected the document.
The two headers together are the reliable verification signal. Timing alone can vary with the origin, auction, compression, browser connection reuse, and local proxy buffering.
Upgrade note. This release adds
/_ts/admin/cache/purgeto the admin endpoints startup validation covers. A configuration whose[[handlers]]enumerate admin paths individually, rather than using the^/_ts/adminprefix, fails to start until that path is covered too. The failure is at startup and explicit, not at request time.
Rollback must preserve configuration compatibility:
- Change
assembly_modetoinlineand deploy/push that configuration. - Before rolling back to a binary that predates these fields, remove
assembly_mode,template_cache_vary,template_cache_max_age_seconds,template_cache_key_cookies,template_cache_bypass_cookies,origin_is_cookie_independent, andorigin_readthrough_enabled, then push the cleaned configuration. Older binaries usedeny_unknown_fieldsand intentionally reject unknown keys, even empty lists. When rolling back only the named-cookie feature to a binary that supports ESI, remove both cookie-list fields and keeporigin_is_cookie_independent = falseor disable ESI if the origin depends on cookies. Keepingtrueafter removing the lists loses variant separation and session bypass. Removeorigin_readthrough_enabledeven when rolling it back:falsestill serializes the field and older binaries reject it. - Purge the template cache with
ts cache purge --service <url> --all, or--page <url>for a single reader-facing URL. Use its exact scheme, host, and port:http://example.com/articleandhttps://example.com/articlehave different purge keys. A success acknowledges invalidation of the requested key, not that an object existed.--servicerequires HTTPS, except for loopback development services (localhost,127.0.0.1, or::1). The admin endpointPOST /_ts/admin/cache/purgeis the same operation for a CMS webhook. Either clears thets-templatesurrogate key; waiting out the bounded origin-derived lifetime also works. With readthrough caching enabled,--allalso purges tagged origin documents, so the next requests refetch those documents from the origin. Check whether the origin can absorb that load before purging during a traffic peak.
Run scripts/template-cache-local-test.sh esi before a rollout and scripts/template-cache-local-test.sh inline as its control. The harness uses a temporary manifest, never edits the tracked fastly.toml, verifies cold/warm origin counts and response integrity, and executes the generated GPT module against the served seam to require a real defineSlot call. Both modes also exercise a cookie-selected origin: A/B isolation without a client variant header, absent versus empty buckets, ignored compact JSON and comma-list cookies, and session bypass on warm and cold URLs. Each request checks the selected HTML, cache diagnostics, private response policy, winning-bid assembly, and origin fetch count.
Origin readthrough caching
origin_readthrough_enabled controls a different cache from everything above. The template cache stores Trusted Server's own transformed HTML. Readthrough is the platform's own cache sitting in front of the publisher origin, and it stores the origin's bytes.
[creative_opportunities]
# Default false. Enable only after `ts origin probe-shareability` passes on every
# axis and every verdict.
origin_readthrough_enabled = trueLeft at the default, the existing caching policy is preserved: ad-serving requests bypass the origin cache, while other publisher requests (including ordinary assets) keep the platform's default caching behavior. Setting it to true applies request shareability instead: eligible ad-serving requests can use the cache, while ineligible non-ad requests bypass it. Eligible requests are GETs with a Host, no disqualifying authorization or cookie, and no remaining conditional or range semantics.
Document requests only. The gate answers a question about pages — whether the origin's HTML may be shared between readers — so it applies to document requests (Sec-Fetch-Dest: document and equivalents, or a navigation when that header is absent). Subresources keep the platform default whether the flag is on or off. Judging them on shareability would bypass the edge cache for every cookie-bearing or conditional asset request, which is most repeat-visitor asset traffic, and would tag every cached asset with ts-template, turning the template rollback purge into an origin-wide asset flush.
This cache has far weaker guarantees than the template cache
Read this before enabling it. The template cache refuses storage on inspection of the origin's response — Set-Cookie, a CSP nonce, missing positive freshness, an uncovered Vary, and the rest of the list above. Readthrough has none of those refusals, and cannot: the decision is made before the origin replies, and no post-response hook is reachable on the Fastly adapter.
What that means concretely, for each refusal the template cache performs:
| Template-cache refusal | Covered on readthrough? |
|---|---|
| No positive freshness | No — probe verdict only |
Origin Set-Cookie | No — probe verdict only |
| Response CSP nonce | No — probe verdict only |
| Origin marks it unshareable | Yes — the platform honours private / no-store |
Non-200 status | Yes — the platform honours status |
Uncovered Vary | Yes — the platform keys on the origin's Vary |
| Not HTML | Not applicable; readthrough caches per origin |
Every row marked No is an accepted risk carried by the operator, not by the code. An origin that personalises HTML without saying so in its headers can cross-serve one reader's page to another, including session fixation through a cached Set-Cookie. That last case is the sharpest: readthrough admits requests carrying no cookie, which is exactly the first-time visitor an origin issues a session cookie to.
origin_is_cookie_independent = true also widens this gate: cookie-bearing requests with unlisted cookies can become readthrough-eligible. Named bypass cookies and malformed cookie policies still refuse admission. Configuring any template_cache_key_cookies disables readthrough, even when those cookies are absent: the platform cache does not include their variant values in its key. The template cache continues to separate those variants. On the template cache, an origin's Vary: Cookie still overrides the independence assertion. On readthrough there is no such response-side guard. Setting both flags is the highest-risk configuration and requires a cookie-axis probe pass specifically.
You cannot verify this locally
Viceroy does not implement the readthrough cache. Measured with the gate enabled, the request judged shareable, and a stub origin answering Cache-Control: public, max-age=60 with no Set-Cookie, two identical navigations still produced two origin fetches. The local harness can therefore show the decision this gate makes, and never its effect.
The first evidence either way comes from a deployed service. Treat any local timing as saying nothing about this setting.
Enablement
- Run
ts origin probe-shareability --url <representative URLs>. Publisher cookies a real reader carries go inTRUSTED_SERVER_PROBE_COOKIESas one cookie header value (name=value; name=value), and a bot-wall admission cookie inTRUSTED_SERVER_PROBE_ADMISSION_COOKIE. Both are environment-only, never flags: these are credentials, and an argument is visible to every process on the host throughpsand lands in shell history. Each--urlmust be HTTPS; plain HTTP is accepted only for a loopback development origin. Admission-cookie runs are diagnostic only: every request carries that cookie, so cookieless responses remain untested and the safety gate fails. Rerun against the origin without it before enabling caching. - Every axis and every verdict must pass. Do not enable on a partial pass. The probe checks status and safety headers on every sampled response, including repeats. Each axis compares what a cache would store — the body and the policy headers replayed with it, such as
Content-Security-Policy— so an origin that serves one document under two policies fails just as a varying document does. Crawler user agents and prefetch requests are their own axes: neither classification blocks readthrough, so an origin that answers a bot or a prefetch with a different document without declaringVarywould otherwise have that document cross-served to a reader. AnyAgeheader, includingAge: 0, blocks the verdict because a fresh cached response can hide origin personalization. Pass--vary-header <name>=<value>for each representative value of an additional request header, for example--vary-header x-exp-variant=A --vary-header x-exp-variant=B. Each value is compared against the absent baseline and the other supplied values, both with and without RSC. Bare--vary-header <name>samples1for compatibility; it cannot establish safety for categorical values the origin actually uses. Supply every relevant variant. A declaredVarycan explain a user-agent, RSC, or custom-header difference only when every response declares it. Cookie differences and different decoded gzip/identity documents always fail, because the template cache requires those representations to be identical. Navigation samples send HTMLAcceptand navigation Fetch Metadata and must returntext/html. RSC uses an explicit same-origin fetch profile, permitting HTML fallback ortext/x-component. A separate fetch control keepsRSCvariation independent ofAcceptand Fetch Metadata changes. If navigation and fetch controls differ, all changed profile headers must be declared inVary; this conservative check cannot attribute a combined-profile difference to one header.Vary: *, revalidation directives, and unreadable safety headers always fail. The probe is the only response-safety control on the readthrough path. - Read the probe's stated limits. It runs from one client address, so personalisation keyed on the reader's IP — geo, rate class — is invisible to it, as are
Accept-Languageand client-hint variants it does not vary. - Set
origin_readthrough_enabled = trueand push the configuration. - Watch the
origin_cache_shareablebreakdown in publisher summary telemetry. Its denominator is matching-slot candidates, including skipped auctions; it does not measure every publisher origin fetch or a site-wide admission rate. See the telemetry population and query. The predicate estimates eligibility in that population before or after enablement, not actual cache hits. - Confirm the origin's own hit rate and page correctness before widening to more URLs.
Rollback
- Set
origin_readthrough_enabled = falseand push. This takes effect on the next request with no deploy and restores the previous policy: ad-serving requests bypass, while non-ad traffic keeps the platform default. It does not disable origin caching globally. - Purge tagged objects with
ts cache purge --service <https-service-url> --all, or--page <reader-url>for one exact reader-facing URL. Both the template cache and opted-in origin readthrough objects carry the page key andts-templatepurge-all key. The readthrough tags use the original reader URL, before origin rewriting, and work in both inline and ESI assembly modes. - Objects stored by older versions without readthrough tags remain unreachable through these purge keys and must expire on the origin's TTL. Changing the origin's TTL does not shorten an already-cached object's lifetime.
- Use
--allon multi-host or dual-scheme deployments. The template cache keys on scheme and host, so purging each spelling you serve covers it. Readthrough does not line up the same way: reader URLs that rewrite to one origin URL —http://andhttps://, orwww.and the apex on one service — share a single stored object, tagged with the reader URL of whichever request filled it first. A--page https://example.com/acan therefore leave anhttp://-tagged object in place, and the next template miss refetches through it and re-stores the stale page into the freshly purged template cache. If you serve one page under more than one reader-facing spelling, purge with--all.
The Fastly SDK attaches these tags to cached objects; production hit and purge behavior still requires validation on a deployed service, since Viceroy does not implement readthrough caching.
gam_unit_path templating
gam_unit_path is a template. A publisher whose ad unit varies by site section expresses that in one slot rule instead of one rule per (slot × section).
Supported placeholders:
| Placeholder | Resolves to |
|---|---|
{network_id} | gam_network_id |
{slot_id} | the slot's id |
{section} | non-empty path segment at section_segment (default: first; see below) |
A template with no placeholders is used verbatim. A slot with nogam_unit_path falls back to /<network_id>/<slot_id>. Both preserve the pre-templating behavior, so existing static configs are unchanged.
Trusted Server conservatively caps the whole rendered dynamic path at 100 UTF-8 bytes, informed by Google's 100-character per-ad-unit-code limit. If a request-specific substitution would exceed the dynamic limit, only that slot is omitted before auction dispatch; the response itself still succeeds. Trusted Server logs a warning containing the slot ID and request path. Explicit static paths and absent/default paths retain legacy behavior and are not subject to this dynamic-only limit.
{section} derivation
{section} is derived from the request path at request time:
- It is the non-empty path segment at
section_segment(0-based, default0). With the default,/news/article-123→news. A site that prefixes a locale setssection_segment = 1, so/en/news/article→newsrather thanen. - It is sanitized: each run of characters outside
[A-Za-z0-9_-]becomes a single_, and the request-derived result is capped at 100 ASCII bytes. - Casing is preserved. Google documents GAM ad-unit codes as case-insensitive, so do not lowercase the value.
- The path is used raw — it is not percent-decoded. So
/new%20s→new_20s(only%is disallowed;2and0are kept), never the decodednew_s. This keeps{section}consistent with howpage_patternsmatch the same raw path. - When the path has no segment at that index — the site root (
/, or repeated slashes), or a path shorter thansection_segment—{section}issection_root. So withsection_segment = 1, the path/enrenders the root section rather than reusing the locale.
section_root is required whenever any slot's template uses {section}, and must match [A-Za-z0-9_-]+. There is no default: the home-section name is publisher-specific. Startup fails if {section} is used without a valid section_root. Startup rejects a blank gam_network_id only when an absent path/default or a {network_id} template consumes it; static paths and templates without {network_id} do not consume it. A [creative_opportunities] block with enabled = false or no slots is inactive, so no publisher templates are delivered and its gam_network_id is not checked when no slot uses it.
Both knobs are config-driven, so the URL→section convention stays with the publisher: section_segment selects which segment names the section, and section_root names the section when there is none.
During typed/startup finalization, after templates parse successfully, every placeholder-bearing dynamic template that omits section_segment has section_segment = 0 materialized, so an older binary rejects the pushed blob loudly. Static and absent paths remain compatible with the legacy config schema only when both section_root and section_segment are omitted. Before rolling back below this feature, replace or remove dynamic paths, remove both section_root and section_segment, re-push and finalize the config, then roll back the binary.
Example resolution for gam_unit_path = "/{network_id}/example/{section}" with gam_network_id = "123456789", section_root = "home", and the page_patterns shown above:
| Request path | gam_unit_path |
|---|---|
/ | /123456789/example/home |
/news | /123456789/example/news |
/news/article | /123456789/example/news |
/reviews/x | /123456789/example/reviews |
The same config with section_segment = 1 and locale-prefixed patterns (["/en", "/en/news", "/en/news/*"]):
| Request path | gam_unit_path |
|---|---|
/en | /123456789/example/home |
/en/news | /123456789/example/news |
/en/news/article | /123456789/example/news |
An unmatched route — a path matched by no slot's page_patterns — produces no slot at all, so no template is rendered for it.
Startup validation rejects a malformed template: an unknown placeholder (e.g. {oops}), an unmatched or nested {, a stray }, or an empty gam_unit_path.
Debug Configuration
Every debug switch defaults to false. These controls expose request, auction, TLS, or creative data and are for controlled non-production use only.
[debug]
| Field | Type | Default | Contract |
|---|---|---|---|
ja4_endpoint_enabled | Boolean | false | Expose GET /_ts/debug/ja4 on Fastly |
auction_html_comment | Boolean | false | Insert an auction diagnostic comment before </body> |
inject_adm_for_testing | Boolean | false | Enable the direct GAM-replace test path and raw debug_bid data |
auction_html_comment_options.include_provider_responses | Boolean | true | Include provider response summaries |
auction_html_comment_options.include_mediator_response | Boolean | true | Include mediator response summaries |
auction_html_comment_options.include_bids | Boolean | true | Include provider bid arrays |
auction_html_comment_options.metadata_keys | Array[String] | error_type, http_status, message | Must be a subset of this fixed allowlist |
auction_html_comment_options.verbosity | String | "redacted" | redacted, upstream, or full |
auction_html_comment_options.format | String | "compact" | compact or pretty |
The default options table is omitted from serialized config for rollback compatibility. A non-default table is serialized and therefore requires every running binary to understand it. upstream and full can expose identity-bearing provider data; inject_adm_for_testing carries raw creative markup. Do not enable them in production.
[debug]
ja4_endpoint_enabled = false
auction_html_comment = false
inject_adm_for_testing = falseFastly Runtime Config Store
After the EdgeZero cutover, the Fastly adapter always dispatches through the EdgeZero entry point. The former edgezero_enabled and edgezero_rollout_pct canary keys are no longer read.
[stores.config].default in edgezero.toml supplies the logical config store ID and default blob key, currently trusted_server_config. Fastly has no process environment. Its entry point reads service-scoped overrides from the edgezero_runtime_env Config Store before opening the app-config store:
For this logical ID, the runtime selectors are:
EDGEZERO__SERVICES__<SERVICE_ID>__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME
EDGEZERO__SERVICES__<SERVICE_ID>__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEYThe runtime ignores unscoped entries. Missing or blank selectors fall back to the logical ID. A resource link must exist under the resolved name, not always under trusted_server_config.
Initial setup with a service-specific store
Fastly store names are account-level. Choose a physical name that is not used by another service. The default physical name is safe only if the service owns that store exclusively.
Create the Fastly service and an editable service version before provisioning non-default mappings. Select its ID through top-level service_id in fastly.toml or FASTLY_SERVICE_ID. If both are set, they must agree. Do not reuse the checked-in service ID for your deployment. Without a service ID, provisioning rejects non-default mappings before creating resources.
The following example is for initial setup before the service receives traffic. Replace the service ID and choose your own physical store name:
export FASTLY_SERVICE_ID="<service-id>"
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=example_config
ts provision --adapter fastly --dry-run
ts provision --adapter fastlyProvisioning creates the stores and persists the selected name in the service-scoped edgezero_runtime_env entry. Keep all intended store-name overrides set when provisioning, including any secret-store mapping. Provisioning reconciles mappings for all declared stores, so omitting a previous override can remove it.
For an existing service, Fastly does not reapply [setup] entries. Follow the provisioner's resource-link instructions. Both the app-config store and the runtime-env store must be linked to the same editable version. For this example:
fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \
--resource-id <config-store-id> --name example_config
fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \
--resource-id <runtime-env-store-id> --name edgezero_runtime_env
ts config push --adapter fastly --dry-run
ts config push --adapter fastly
fastly compute publish --service-id "$FASTLY_SERVICE_ID" --version latestLook up each store ID by its name before linking. Confirm the push dry run names example_config, not the account-level default. Publish the application to the linked version only after seeding its config store; a missing or invalid blob makes application startup fail closed. Do not activate a new service's empty version before uploading the application. If your deployment separates upload from activation, activate the prepared version with fastly service-version activate --service-id "$FASTLY_SERVICE_ID" --version <version> only after both the code and config are ready.
Keep the __NAME override in your deployment environment for every subsequent push. The CLI reads its process environment, not the service's persisted runtime mapping. Omitting the override can write to the wrong physical store. Reject empty values in deployment scripts rather than relying on the fallback.
For a live service, changing entries in its active edgezero_runtime_env store changes runtime selection immediately, independently of service-version activation. Do not use the initial-setup sequence to migrate a live mapping. Prepare and seed the destination and make its resource link available to the active version before switching the selector, or use an isolated staged runtime configuration.
An existing deployment may instead link a service-specific physical store under the logical name trusted_server_config, with no runtime __NAME override. That alias works, but the CLI still needs the physical-name override on every push. Do not add a runtime override unless a link under the newly selected name also exists.
Selecting another blob key
A normal push writes at the logical store ID. A runtime __KEY override does not change that write destination. To select another production key, first push with ts config push --adapter fastly --key <key>, then set the matching service-scoped __KEY entry in edgezero_runtime_env. Changing that entry affects the active service immediately. Do not point the production selector at a staging key; staged deployments need their own runtime-env store.
Local development
The repository's Viceroy configuration uses the default logical app-config name and key. Clear production overrides for the local push:
env -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME \
-u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY \
ts config push --adapter fastly --local--local writes under [local_server.config_stores.<resolved-name>] in the tracked fastly.toml. If you customize Viceroy's service-scoped runtime selectors, keep that local name and the pushed key aligned with them. Review the generated diff and do not commit deployment-specific app-config entries. Credentials belong in secret stores; the app-config blob contains their key references.
Rollback
Rollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required.
Validation
Automatic Validation
Configuration is validated at startup:
Publisher Validation:
- All fields non-empty
origin_urlis valid URL
EC Validation:
- The
passphrasekey name is non-empty at push time - The resolved passphrase is at least 32 bytes at runtime
- Known placeholder values are rejected after resolution
Handler Validation:
pathis valid regexusernameis ordinary configuration and non-empty- The resolved
passwordis non-empty and is checked for placeholders at runtime
Integration Validation:
- Each integration implements
Validatetrait - Custom rules per integration
Validation Errors
Startup Failure if:
- Required fields missing
- Invalid data types
- Regex compilation fails
- Secret key is default value
- Integration config fails validation
Error Format:
Configuration error: provider `pbs-main` endpoint must be an absolute HTTPS URLBest Practices
Configuration Management
Development:
# trusted-server.dev.toml
[publisher]
domain = "localhost"
origin_url = "http://localhost:3000"
proxy_secret = "publisher_proxy_secret"Staging and production:
- Provision the same key names in the target
trusted_server_secretsstore. - Keep only the key names in
trusted-server.tomland environment overlays. - Push the config after provisioning and restart/redeploy after rotation.
Secret Management
Do:
- ✅ Store values in the platform secret store
- ✅ Rotate values deliberately and restart/redeploy instances
- ✅ Generate values locally without printing them to logs
- ✅ Use different values per environment when appropriate
- ✅ Keep stable key names for rotation
Don't:
- ❌ Commit secret values to version control
- ❌ Put secret values in environment overlays
- ❌ Put secret values in config diff output or app-config blobs
- ❌ Treat missing secret-store keys as inline values
- ❌ Use default/placeholder values
- ❌ Share secrets across environments
- ❌ Log secret values
- ❌ Expose in error messages
File Organization
Recommended Structure:
trusted-server.toml # Base config
trusted-server.dev.toml # Development overrides
.env.development # Dev environment vars
.env.staging # Staging environment vars
.env.production # Production environment vars (not in git)
.env.example # Example template (in git).gitignore:
.env.production
.env.staging
.env.local
*.secretTroubleshooting
Common Issues
"Failed to build configuration":
- Check TOML syntax (trailing commas, quotes)
- Verify all required fields present
- Check environment variable format
"Configuration field '...' is set to a known placeholder value":
- Confirm the referenced key exists in
trusted_server_secrets - Ensure the resolved value is non-empty and not a known placeholder
- Do not replace the key name with a plaintext value in the app config
- Rotate the value in the platform secret store, then restart/redeploy
"Invalid regex":
- Handler
pathmust be valid regex - Test pattern:
echo "^/_ts/admin" | grep -E "^/_ts/admin" - Escape special characters:
\.,\$, etc.
"Integration configuration could not be parsed":
- Check JSON syntax in env vars
- Verify indexed arrays (0, 1, 2...)
- Check field names match exactly
Environment Variables Not Applied:
- Run the override through
ts config validate,ts config diff, orts config push - Verify the target leaf already exists in
trusted-server.toml; the pinned EdgeZero loader does not create missing fields - Verify prefix:
TRUSTED_SERVER__ - Check separator:
__(double underscore) - Confirm the variable is exported:
echo $VARIABLE_NAME - Rerun
ts config pushafter changing a deploy-time override - Try explicit string:
VARIABLE='value'notVARIABLE=value
Inspect Configuration Inputs
Runtime adapters load the app-config envelope with get_settings_from_config_store from the trusted_server_core::settings_data module. For a source file or local fixture, use Settings::from_toml; there is no process-global settings accessor.
Check Environment:
# List all TRUSTED_SERVER variables
env | grep TRUSTED_SERVERValidate TOML:
# Use any TOML validator
cat trusted-server.toml | npx toml-cli validateNext Steps
- Set up Request Signing for secure API calls
- Configure First-Party Proxy for URL proxying
- Learn about Edge Cookies for first-party state management
- Review Integrations for partner support