Skip to content

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:

  1. TOML Files - trusted-server.toml for ordinary configuration and secret key names
  2. Environment Variables - Typed CLI overrides with the TRUSTED_SERVER__ prefix
  3. 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.

toml
[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.

bash
# 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 fastly

Static 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_secret
  • trusted_client_ip.shared_secret, when trusted client-IP forwarding is configured
  • ec.passphrase
  • ec.partners[*].api_token, when inbound identify or batch sync is used
  • ec.partners[*].ts_pull_token, when pull sync is enabled
  • handlers[*].password
  • tinybird.auction_token_secret, when Tinybird auction telemetry is enabled
  • integrations.datadome.server_side_key_secret_name, when protection is enabled
  • integrations.datadome.protection_test_bypass.credential_secret_name, when the bypass is enabled
  • proxy.asset_routes[*].auth.access_key_id, secret_access_key, and optional session_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_secret is 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_secret is deprecated input. It is accepted for migration, then discarded and omitted from serialized config; use tinybird.auction_token_secret as 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:

PathLifecycleKey identitySerializationRuntimeSecret handling
AssetOriginAuth.s3_sig_v4deprecatedalias of AssetOriginAuth.s3_sigv4skippeddeserialization onlynone
DataDomeConfig.server_side_key_secret_namecanonicalcanonicalserializedactivestore resolved
DataDomeConfig.server_side_key_secret_storedeprecatedcanonicalskippednormalized awaynone
DataDomeProtectionTestBypassConfig.credential_secret_namecanonicalcanonicalserializedactivestore resolved
DataDomeProtectionTestBypassConfig.credential_secret_storedeprecatedcanonicalskippednormalized awaynone
Ec.passphrasecanonicalcanonicalserializedactivestore resolved
EcPartner.api_tokencanonicalcanonicalserializedactivestore resolved
EcPartner.ts_pull_tokencanonicalcanonicalserializedactivestore resolved
Handler.passwordcanonicalcanonicalserializedactivestore resolved
Publisher.proxy_secretcanonicalcanonicalserializedactivestore resolved
S3SigV4AuthConfig.access_key_idcanonicalcanonicalserializedactivestore resolved
S3SigV4AuthConfig.secret_access_keycanonicalcanonicalserializedactivestore resolved
S3SigV4AuthConfig.secret_storedeprecatedcanonicalskippednormalized awaynone
S3SigV4AuthConfig.session_tokencanonicalcanonicalserializedactivestore resolved
TinybirdSettings.access_token_secretdeprecatedcanonicalskippednormalized awayaccepted, then discarded
TinybirdSettings.auction_token_secretcanonicalcanonicalserializedactivestore resolved
TinybirdSettings.secret_storedeprecatedcanonicalskippednormalized awaynone
TrustedClientIpConfig.shared_secretcanonicalcanonicalserializedactivedeliberately inline

Prepare an initial reference-based deployment in this order:

  1. Create the physical store and configure its trusted_server_secrets mapping.
  2. Choose a stable key name for each active credential field in the app config.
  3. Write each credential value under its referenced key without exposing it in command arguments, shell history, logs, or CI output.
  4. Run ts config validate, then ts config push --adapter fastly.
  5. Start or deploy instances after the store and pushed config are both ready.

Migrate an existing deployment in this order:

  1. Populate the physical store mapped from trusted_server_secrets with the existing credential values without printing them in shell history, logs, or CI output.
  2. Replace each active credential value with a stable key name and remove the legacy Tinybird, DataDome, and S3 secret_store selectors.
  3. Run ts config validate, then ts config push --adapter fastly --no-diff.
  4. 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:

bash
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default
ts config push --adapter spin

For 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:

toml
[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.

bash
openssl rand -base64 32

Strict 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 ​

FilePurpose
trusted-server.tomlMain application configuration
fastly.tomlFastly Compute service settings
.env.devLocal development overrides

Key Sections ​

SectionPurpose
[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.

toml
[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__FIELD

Rules:

  • Prefix: TRUSTED_SERVER
  • Separator: __ (double underscore)
  • Case: UPPERCASE
  • Sections: Match TOML hierarchy
  • Map keys: Preserve TOML punctuation. For example, provider key pbs-main uses the PBS-MAIN segment, not PBS_MAIN.

Shell assignment syntax cannot contain a hyphenated variable name. Use env to apply a provider override to a command:

bash
env 'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
  ts config validate

This 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] ​

FieldTypeRequiredDescription
domainStringYesPublisher's apex domain name
cookie_domainStringYesDomain for non-EC cookies (typically with leading dot)
origin_urlStringYesFull URL of publisher origin server
origin_host_header_overrideStringNoOutbound Host header to send while connecting to origin_url
proxy_secretStringYesSecret-store key name for the proxy URL secret
max_buffered_body_bytesIntegerNoBuffered-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 use cookie_domain. The cookie_domain field is used by other cookie helpers.

Example (replace the rejected secret placeholder before validation):

toml
[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:

bash
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=16777216

Field 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

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 from domain

Format: Domain with optional leading dot

  • .publisher.com - Shares across all subdomains
  • publisher.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 Host header
  • 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/proxy URLs
  • Signs /first-party/click URLs
  • 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:

bash
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216

Trusted 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] ​

FieldTypeRequiredDescription
ip_headerStringYesHeader containing exactly one reader IP address
auth_headerStringYesHeader containing exactly one shared-secret value
shared_secretStringYesKey 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.

toml
[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:

bash
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_secret

Because the typed environment overlay cannot create a missing section, add [trusted_client_ip] and all three fields to the TOML before using these overrides.

Settings for the optional tester-cookie endpoints. This feature is disabled by default and should only be enabled for intentional QA or troubleshooting flows.

FieldTypeRequiredDescription
enabledBooleanNoEnables routes to set and clear ts-tester cookie

When enabled, GET /_ts/set-tester returns 204 No Content and sets:

http
Set-Cookie: ts-tester=true; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax
Cache-Control: no-store, private

GET /_ts/clear-tester returns 204 No Content and clears the cookie:

http
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
Cache-Control: no-store, private

When 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:

toml
[tester_cookie]
enabled = true

Environment Override:

bash
TRUSTED_SERVER__TESTER_COOKIE__ENABLED=true

EC 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.

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.

FieldTypeRequiredDescription
passphraseStringYesPublisher passphrase used as HMAC key
ec_storeString or nullNoFastly KV store name for EC identity graph and withdrawal state
pull_sync_concurrencyIntegerNoMaximum concurrent pull-sync requests per organic response
cluster_trust_thresholdIntegerNoCluster size threshold for identity trust decisions
cluster_recheck_secsIntegerNoLegacy compatibility setting; cluster rechecks no longer use timestamps
partnersArrayNoStatic 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:

toml
[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 enabled

Environment Override:

bash
TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
TRUSTED_SERVER__EC__EC_STORE=ec_identity_store

Field 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] 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.

FieldTypeDefaultContract
modeString"interpreter"interpreter decodes signals; proxy forwards raw strings
check_expirationBooleantrueCheck TCF timestamps
max_consent_age_daysInteger395Clamped to 1..=3650
consent_storeString or nullnullOptional KV store for EC-linked consent persistence
gdpr.applies_inArray[String]EU/EEA and UK codesObservability only; does not synthesize consent
us_states.privacy_statesArray[String]Checked built-in state listJurisdictions with active comprehensive privacy laws
us_privacy_defaults.notice_givenBooleantruePublisher policy used for GPC-only requests
us_privacy_defaults.lspa_coveredBooleanfalsePublisher LSPA posture
us_privacy_defaults.gpc_implies_optoutBooleantrueTreat Sec-GPC: 1 as sale opt-out
conflict_resolution.modeString"restrictive"restrictive, newest, or permissive
conflict_resolution.freshness_threshold_daysInteger30Age difference required by newest
toml
[consent]
mode = "interpreter"
check_expiration = true
max_consent_age_days = 395

[consent.conflict_resolution]
mode = "restrictive"
freshness_threshold_days = 30

applies_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:

toml
[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:

bash
env 'TRUSTED_SERVER__RESPONSE_HEADERS__X-CUSTOM-HEADER=updated value' \
  ts config validate

The 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] ​

FieldTypeRequiredDescription
enabledBooleanNo (default: false)Enable request signing features
config_store_idStringIf enabledFastly Config Store ID for JWKS
secret_store_idStringIf enabledFastly Secret Store ID for private keys

Example:

toml
[request_signing]
enabled = true
config_store_id = "01GXXX"  # From Fastly dashboard
secret_store_id = "01GYYY"  # From Fastly dashboard

Environment Override:

bash
TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true
TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID=01GXXX
TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID=01GYYY

Store Setup ​

Config Store (for public keys):

bash
# Create store
fastly config-store create --name=jwks_store

# Get store ID
fastly config-store list

Secret Store (for private keys):

bash
# Create store
fastly secret-store create --name=signing_keys

# Get store ID
fastly secret-store list

Local Dev Setup (fastly.toml):

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

FieldTypeRequiredDescription
pathString (Regex)YesRegular expression matching paths
usernameStringYesHTTP Basic Auth username
passwordStringYesHTTP Basic Auth password

Example:

toml
# 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:

bash
# 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:

toml
# 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/private

Validation: 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:

PathCalled by
/_ts/page-bidsTrusted Server JS, on SPA navigation
/_ts/api/v1/identifyTrusted Server JS, in the browser
/_ts/api/v1/batch-syncTrusted 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[*].password is a key name in trusted_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] ​

FieldTypeRequiredDescription
exclude_domainsArray[String]No (default: [])Domains to skip rewriting

Example:

toml
[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 (*):

toml
"*.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 *):

toml
"api.example.com"

Matches:

  • ✅ api.example.com
  • ❌ www.api.example.com
  • ❌ api.example.com.evil.com

Use Cases ​

Trusted Partners:

toml
exclude_domains = ["*.approved-cdn.com"]

First-Party Resources:

toml
exclude_domains = ["assets.publisher.com", "static.publisher.com"]

Development:

toml
exclude_domains = ["localhost", "127.0.0.1", "*.local"]

Performance (already first-party):

toml
exclude_domains = ["*.publisher.com"]  # Skip unnecessary proxying

See Creative Processing for details.

Proxy Configuration ​

Controls first-party proxy security settings and path-based asset routes.

[proxy] ​

FieldTypeRequiredDescription
allowed_domainsArray[String]No (default: [])Hosts permitted for signing, initial fetches, and redirects
certificate_checkBooleanNo (default: true)Verify TLS certificates when proxying HTTPS origins
asset_routesArray[Table]No (default: [])Path prefixes proxied directly to configured origins

Example:

toml
[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:

PatternMatchesDoes not match
assets.example.comassets.example.comsub.assets.example.com
*.cdn.example.comcdn.example.com, static.cdn.example.com, a.b.cdn.example.comevil-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.com does not match evil-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.

toml
[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.

FieldTypeRequiredDescription
prefixStringYesRequest path prefix to match
origin_urlStringYesAbsolute http or https origin URL
path_patternStringNoRegex matched against the incoming request path
target_pathStringNoReplacement path used with path_pattern
authTableNoOptional origin authentication
image_optimizerTableNoOptional route-level Image Optimizer settings

Example:

toml
[[proxy.asset_routes]]
prefix = "/assets/"
origin_url = "https://assets.example.com"

Path rewrite example:

toml
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://assets-cdn.example.com"
path_pattern = "^/\\.image/(.*)/[^/]+\\.([^/.]+)$"
target_path = "/image/upload/$1.$2"

Behavior:

  • Only GET and HEAD requests use asset routes.
  • Built-in and integration routes take precedence.
  • The longest matching asset-route prefix wins.
  • path_pattern and target_path must be configured together.
  • origin_url must not include userinfo, a path, a query string, or a fragment.
  • Unsafe origin response headers such as Set-Cookie are stripped before the response reaches the browser.

[proxy.asset_routes.auth] ​

The first supported origin auth type is s3_sigv4.

FieldTypeRequiredDefaultDescription
typeStringYesnoneMust be s3_sigv4
regionStringYesnoneAWS region used in the SigV4 credential scope
access_key_idStringNoaccess_key_idDefault-store secret reference for the AWS access key ID
secret_access_keyStringNosecret_access_keyDefault-store secret reference for the AWS secret access key
session_tokenStringNounsetOptional secret key containing a session token
origin_queryStringNoroute defaultpreserve or strip

Example:

toml
[[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.

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable Image Optimizer for the route
regionStringYes when enablednoneFastly IO processing region, such as us_east
profile_setStringYes when enablednoneName under [image_optimizer.profile_sets.*]
origin_queryStringNostrip when enabledpreserve or strip; effective preserve is rejected while IO is enabled

Example:

toml
[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.

FieldTypeRequiredDefaultDescription
base_paramsStringNo""Params applied before profile-specific params
default_profileStringNodefaultProfile used when no profile is requested
unknown_profileStringNouse_defaultuse_default or reject
profile_paramStringNoprofileQuery parameter containing the profile name
aspect_ratio_paramStringNoarQuery parameter containing aspect ratio
debug_paramStringNo_io_debugQuery parameter that disables IO when set to 1

Profile values live under [image_optimizer.profile_sets.<name>.profiles] and use query-string syntax.

toml
[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] ​

FieldTypeRequiredDescription
allowedArray[String]NoAllowed query values such as 1-1 or 16-9
profilesArray[String]NoDefined profiles that accept aspect-ratio overrides
toml
[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] ​

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable offset bucketing
x_paramStringNoxQuery parameter for x-axis offset
y_paramStringNoyQuery parameter for y-axis offset
bucketsArray[Integer]No[10, 30, 50, 70, 90]Offset buckets in 0..=100
defaultIntegerNo50Offset used when input is missing or invalid
when_missingStringNosmartsmart or none when neither offset exists
toml
[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] ​

FieldTypeDefaultContract
enabledBooleanfalseEnable auction telemetry
api_hostString""Required when enabled; regional host without scheme, port, or path
auction_datasetString"auction_events_raw"1–128 ASCII letters, digits, or _
auction_token_secretString or nullnullStore key required when enabled; resolved value must be nonempty
access_enabledBooleanfalseReserved; true fails startup because no emitter is wired
access_datasetString"access_logs_raw"Reserved access-log dataset name
access_sample_rateNumber0.00.0..=1.0; reserved while access emission is disabled
max_body_bytesInteger1048576At 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.

FieldTypeRequiredDescription
idStringYesUnique operator-facing rule identifier
enabledBooleanNoWhether the rule participates in matching (default false)
presetStringMatcherBuilt-in preset such as nextjs-static
path_prefixStringMatcherRequest path prefix
path_globStringMatcherSingle glob matched against the request path
path_globsArray[String]MatcherMultiple globs matched against the request path
path_regexStringMatcherRegex matched against the request path
extensionsArray[String]MatcherCase-insensitive file extensions
fingerprint_styleStringNoRequired bundler fingerprint convention before matching
visibilityStringNopublic or private (default public)
browser_ttl_secondsIntegerPolicyBrowser max-age; required for private rules and positive with immutable = true
edge_ttl_secondsIntegerPolicyPublic rules only: TTL emitted through the runtime-specific shared-cache directive
stale_while_revalidate_secondsIntegerNoOptional stale-while-revalidate
stale_if_error_secondsIntegerNoOptional stale-if-error
immutableBooleanNoAdd 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 as app.0123abcd.js;
  • esbuild-base32: eight-character uppercase Base32 suffixes, such as app-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):

toml
[[cache.asset_rules]]
id = "nextjs-static"
enabled = false
preset = "nextjs-static"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = true

Publisher allowlist example (enable only for an unambiguous immutable filename convention):

toml
[[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 = true

If [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.

SectionReference
[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.

FieldTypeDescription
enabledBooleanEnable/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.

FieldTypeDefaultContract
enabledBooleanfalseEnable mediator registration
endpointURLRequiredMediation service URL
timeout_msInteger5001..=60000 milliseconds
price_floorNumber or nullnullOptional minimum accepted CPM
context_query_paramsObject{}Maps admitted auction-context keys to query names

APS Browser Integration ​

Section: [integrations.aps]

FieldTypeDefaultContract
enabledBooleanfalseEnable browser-side APS behavior
rendering_modeString"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:

FieldTypeDefaultContract
enabledBooleanfalseEnable DataDome registration
sdk_originURLhttps://js.datadome.coBrowser SDK origin
api_originURLhttps://api-js.datadome.coBrowser signal API origin
cache_ttl_secondsInteger360060..=86400 seconds
rewrite_sdkBooleantrueRewrite matching SDK URLs
enable_protectionBooleanfalseCall the Protection API before route matching
server_side_key_secret_nameString or nullnullSecret-store key required when protection is enabled
protection_api_originURLhttps://api-fastly.datadome.coProtection API origin
timeout_msInteger15001..=10000 milliseconds
protection_excluded_methodsArray[String]["OPTIONS"]Methods that bypass protection
protection_excluded_asnsArray[Integer][]Client ASNs that bypass protection
protection_excluded_ip_cidrsArray[String][]Inline client-IP CIDRs that bypass protection
protection_excluded_ip_cidr_sourcesArray[Object][]Config-store sources of client-IP CIDRs that bypass protection
protection_ip_list_cache_ttl_secondsInteger3001..=86400 seconds
protection_exclusion_rulesArray[Object]Static-asset path-regex ruleOrdered typed exclusion rules
protection_test_bypassObject or nullnullAccess-controlled test bypass; disabled when present by default
enable_graphql_supportBooleanfalseReserved; true is accepted but ignored in v1
client_side_keyString""Key used for browser-tag injection
inject_client_side_tagBooleantrueInject only when client_side_key is nonempty
client_side_tag_urlString/integrations/datadome/tags.jsRoot-relative or HTTPS injection URL
client_side_configurationJSON 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]

FieldTypeDefaultContract
enabledBooleantrueEnable the reverse proxy when the section exists
proxy_pathString or null/integrations/didomi/consentNo trailing slash, repeated slash, dot segment, or unsafe character
sdk_originURLhttps://sdk.privacy-center.orgSDK upstream
api_originURLhttps://api.privacy-center.orgAPI upstream

See Didomi for the routed endpoint shapes.

Google Tag Manager Integration ​

Section: [integrations.google_tag_manager]

FieldTypeDefaultContract
enabledBooleanfalseEnable tag-gateway routes and rewriting
container_idStringRequiredGTM- followed by 4–20 uppercase letters or digits
upstream_urlURLhttps://www.googletagmanager.comScript upstream
cache_max_ageInteger90060..=86400 seconds
max_beacon_body_sizeInteger655361024..=1048576 bytes

See Google Tag Manager.

GPT Integration ​

Section: [integrations.gpt]

FieldTypeDefaultContract
enabledBooleantrueEnable GPT when the section exists
gam_attribution_enabledBooleanfalseAdd page-level ts=true targeting
script_urlURLGoogle's secure GPT URLBootstrap source
cache_ttl_secondsInteger360060..=86400 seconds
rewrite_scriptBooleantrueRewrite matching GPT script URLs
slim_prebid_urlString or nullnullOptional 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.

FieldTypeDefaultContract
enabledBooleanfalseEnable asset proxying and HTML rewriting
cache_ttl_secondsIntegerNoneOptional downstream cache TTL for every asset
assetsArray[]Asset mappings; required when enabled

Each [[integrations.js_asset_proxy.assets]] entry:

FieldTypeDefaultContract
pathStringRequiredExact first-party request path served by Trusted Server
origin_urlStringRequiredExact upstream JavaScript URL fetched and match-rewritten
proxyStringenabledenabled, disabled, or blocked (removes script tags)
cache_ttl_secondsIntegerNoneOptional per-asset downstream cache TTL override

lockr Integration ​

Section: [integrations.lockr]

FieldTypeDefaultContract
enabledBooleantrueEnable lockr when the section exists
app_idStringRequiredNonempty lockr application ID
api_endpointURLhttps://identity.loc.krAPI origin
sdk_urlURLhttps://aim.loc.kr/identity-lockr-trust-server.jsSDK source
cache_ttl_secondsInteger360060..=86400 seconds
rewrite_sdkBooleantrueRewrite matching lockr SDK URLs
rewrite_sdk_hostBoolean or nullnullDeprecated compatibility input
origin_overrideURL or nullnullOptional 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 fieldTypeDefaultDescription
enabledBooleantrueEnable browser bundle injection, interception, and the trustedServer adapter
account_idStringNoneOptional account value injected into browser Prebid configuration
timeout_msInteger1000Browser Prebid.js timeout; independent of every server provider timeout
debugBooleanfalseBrowser Prebid.js debug flag; independent of server profile debug
client_side_biddersArray[String][]Bidders kept on native browser adapters
excluded_gam_ad_unit_path_suffixesArray[String][]GAM suffixes excluded from Trusted Server refresh auctions
script_patternsArray[String]["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]Publisher Prebid script paths intercepted by Trusted Server
external_bundle_urlStringRequired when enabledHTTPS publisher-specific Prebid.js bundle URL
external_bundle_sha256 / *_sriStringNoneOptional bundle integrity and cache metadata
bundle.modules.bidderArray[String]Required and non-emptyExact bidder module stems used by ts prebid bundle
bundle.modules.user_idArray[String]Curated preset when omittedExact User ID module stems used by ts prebid bundle
bundle.modules.analyticsArray[String][]Exact analytics module stems used by ts prebid bundle
managed_user_idsArray[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:

toml
[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:

bash
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 validate

Environment 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:

FieldTypeDefaultDescription
nameStringRequiredPrebid userSync.userIds entry name, for example sharedId. Unique across entries
paramsTable{}Module-specific parameters, forwarded to Prebid unchanged
storage.typeStringcookieBrowser storage: cookie or html5
storage.nameStringRequired when storage existsCookie or local-storage key the module reads and writes
storage.expiresIntegerPrebid's own defaultStorage lifetime in days; must be at least 1. Any per-module ceiling is the module's
storage.refresh_in_secondsIntegerPrebid's own defaultSeconds 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.js matches 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; and
  • bid_param_override_rules: canonical ordered rules with when matchers and set objects.

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]

FieldTypeDefaultContract
enabledBooleanfalseEnable Next.js integration
rewrite_attributesArray[String]["href", "link", "url"]Nonempty set of structured payload keys to rewrite
max_combined_payload_bytesInteger10485760Maximum combined RSC payload size in bytes

Example:

toml
[integrations.nextjs]
enabled = true
rewrite_attributes = ["href", "link", "url", "src"]
max_combined_payload_bytes = 10485760

Environment Override:

bash
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__MAX_COMBINED_PAYLOAD_BYTES=10485760

Edit rewrite_attributes in TOML because the overlay cannot replace arrays.

Osano Integration ​

Section: [integrations.osano]

FieldTypeDefaultDescription
enabledBooleanfalseEnable the Osano browser consent mirror

Example:

toml
[integrations.osano]
enabled = true

Environment Override:

bash
TRUSTED_SERVER__INTEGRATIONS__OSANO__ENABLED=true

The 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]

FieldTypeDefaultContract
enabledBooleantrueEnable Permutive integration
organization_idStringRequiredNonempty Permutive organization ID
workspace_idStringRequiredNonempty Permutive workspace ID
project_idString""Optional project ID; reserved for future use
api_endpointURLhttps://api.permutive.comPermutive API URL
secure_signals_endpointURLhttps://secure-signals.permutive.appSecure Signals URL
cache_ttl_secondsInteger360060..=86400 seconds
rewrite_sdkBooleantrueRewrite Permutive SDK references

Example:

toml
[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 = true

Sourcepoint Integration ​

Section: [integrations.sourcepoint]

FieldTypeDefaultContract
enabledBooleanfalseEnable Sourcepoint proxying and rewriting
rewrite_sdkBooleantrueRewrite matching Sourcepoint URLs
cdn_originURLhttps://cdn.privacy-mgmt.comHTTP(S) URL whose host is exactly cdn.privacy-mgmt.com
auth_cookie_nameString or nullnull1–64 letters, digits, _, or -; built-in cookies need no entry
cache_ttl_secondsInteger360060..=86400 seconds

See Sourcepoint.

Testlight Integration ​

Section: [integrations.testlight]

FieldTypeDefaultContract
enabledBooleanfalseEnable Testlight integration
endpointURLRequiredTestlight auction endpoint
timeout_msInteger100010..=60000 milliseconds
shim_srcString/static/tsjs=tsjs-unified.min.jsNonempty script source for the shim
rewrite_scriptsBooleanfalseRewrite Testlight script references

Example:

toml
[integrations.testlight]
enabled = true
endpoint = "https://testlight.example/openrtb2/auction"
timeout_ms = 1500
rewrite_scripts = true

Auction 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] ​

FieldTypeDefaultDescription
enabledBooleanfalseEnable the auction orchestrator
sanitize_creativesBooleanfalseStrip executable markup from winning-bid adm before delivery
rewrite_creativesBooleantrueRewrite winning-bid adm through first-party endpoints
timeout_msInteger2000Logical auction budget in milliseconds
mediatorStringNoneOptional separate adserver_mock mediator
creative_storeString"creative_store"Deprecated; creatives are delivered inline
allowed_context_keysArray[]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:

toml
[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 fieldRequiredDefaultDescription
protocolYesNoneMust be openrtb-2.6
profileNostandardstandard, prebid-server, or aps
endpointYesNoneAbsolute HTTPS URL with host and no credentials or fragment
timeout_msNoProfile defaultProvider logical budget before the remaining-auction cap
routingNoexplicitexplicit, or all_eligible for non-PBS profiles
profile_configNo{}Typed object owned by the selected profile
notificationsNoNo suppressionCommon 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.

ProfileFieldRequiredDefaultProvider timeout defaultConstraints
apsaccount_idYes—800 msString or integer; trimmed, nonempty, at most 1024 bytes
apsallow_script_creativesNofalse800 msScript creatives are ineligible unless enabled
apsdebugNofalse800 msMay expose unredacted request and response data
apsinventory_domainNoNone800 msDNS name, at most 253 bytes; configure with inventory_page_origin
apsinventory_page_originNoNone800 msHTTPS origin without credentials, port, path, query, or fragment; host must equal or be beneath inventory_domain
prebid-serverbid_param_override_rulesNo[]1000 msOrdered exact-match rules; at least one matcher and a nonempty set object
prebid-serverbid_param_overridesNo{}1000 msPer-bidder nonempty shallow-merge objects
prebid-serverbid_param_zone_overridesNo{}1000 msPer-bidder, per-zone nonempty shallow-merge objects
prebid-serverconsent_forwardingNoboth1000 msopenrtb_only, cookies_only, or both
prebid-serverdebugNofalse1000 msIncludes upstream exchange diagnostics
prebid-serverdebug_query_paramsNoNone1000 msOptional legacy page-URL query fragment
prebid-servertest_modeNofalse1000 msSets OpenRTB test = 1
standardimp_extNo{}Auction timeoutJSON object; at most 16384 bytes, depth 8, and 256 keys per object
standardrequest_extNo{}Auction timeoutJSON 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:

toml
[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:

bash
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 validate

Creative 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.

toml
[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.

bash
TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__ENABLED=false

WARNING

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.
  • esi opts 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 pinned stackpop/esi parser. 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.

toml
[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 = false

The 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_cookies includes 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_cookies forces inline processing whenever any named cookie is present, including an empty value such as session=. This applies regardless of the independence assertion or any key cookies in the request.
  • When either list is nonempty, origin_is_cookie_independent applies only to unlisted cookies. The safe default, false, bypasses requests containing any unlisted cookie; true asserts 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: false bypasses every request carrying a Cookie header; true asserts 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:

toml
[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 = true

The 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:

toml
template_cache_vary = ["x-exp-variant"]
template_cache_key_cookies = ["ab_bucket"]
origin_is_cookie_independent = true

For anonymous sharing with a logged-in population, omit the key list:

toml
template_cache_bypass_cookies = ["session"]
origin_is_cookie_independent = true

Both 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/purge to the admin endpoints startup validation covers. A configuration whose [[handlers]] enumerate admin paths individually, rather than using the ^/_ts/admin prefix, 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:

  1. Change assembly_mode to inline and deploy/push that configuration.
  2. 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, and origin_readthrough_enabled, then push the cleaned configuration. Older binaries use deny_unknown_fields and 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 keep origin_is_cookie_independent = false or disable ESI if the origin depends on cookies. Keeping true after removing the lists loses variant separation and session bypass. Remove origin_readthrough_enabled even when rolling it back: false still serializes the field and older binaries reject it.
  3. 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/article and https://example.com/article have different purge keys. A success acknowledges invalidation of the requested key, not that an object existed. --service requires HTTPS, except for loopback development services (localhost, 127.0.0.1, or ::1). The admin endpoint POST /_ts/admin/cache/purge is the same operation for a CMS webhook. Either clears the ts-template surrogate key; waiting out the bounded origin-derived lifetime also works. With readthrough caching enabled, --all also 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.

toml
[creative_opportunities]
# Default false. Enable only after `ts origin probe-shareability` passes on every
# axis and every verdict.
origin_readthrough_enabled = true

Left 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 refusalCovered on readthrough?
No positive freshnessNo — probe verdict only
Origin Set-CookieNo — probe verdict only
Response CSP nonceNo — probe verdict only
Origin marks it unshareableYes — the platform honours private / no-store
Non-200 statusYes — the platform honours status
Uncovered VaryYes — the platform keys on the origin's Vary
Not HTMLNot 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 ​

  1. Run ts origin probe-shareability --url <representative URLs>. Publisher cookies a real reader carries go in TRUSTED_SERVER_PROBE_COOKIES as one cookie header value (name=value; name=value), and a bot-wall admission cookie in TRUSTED_SERVER_PROBE_ADMISSION_COOKIE. Both are environment-only, never flags: these are credentials, and an argument is visible to every process on the host through ps and lands in shell history. Each --url must 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.
  2. 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 declaring Vary would otherwise have that document cross-served to a reader. Any Age header, including Age: 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> samples 1 for compatibility; it cannot establish safety for categorical values the origin actually uses. Supply every relevant variant. A declared Vary can 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 HTML Accept and navigation Fetch Metadata and must return text/html. RSC uses an explicit same-origin fetch profile, permitting HTML fallback or text/x-component. A separate fetch control keeps RSC variation independent of Accept and Fetch Metadata changes. If navigation and fetch controls differ, all changed profile headers must be declared in Vary; 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.
  3. 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-Language and client-hint variants it does not vary.
  4. Set origin_readthrough_enabled = true and push the configuration.
  5. Watch the origin_cache_shareable breakdown 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.
  6. Confirm the origin's own hit rate and page correctness before widening to more URLs.

Rollback ​

  1. Set origin_readthrough_enabled = false and 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.
  2. 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 and ts-template purge-all key. The readthrough tags use the original reader URL, before origin rewriting, and work in both inline and ESI assembly modes.
  3. 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.
  4. Use --all on 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:// and https://, or www. 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/a can therefore leave an http://-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:

PlaceholderResolves 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, default 0). With the default, /news/article-123 → news. A site that prefixes a locale sets section_segment = 1, so /en/news/article → news rather than en.
  • 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; 2 and 0 are kept), never the decoded new_s. This keeps {section} consistent with how page_patterns match the same raw path.
  • When the path has no segment at that index — the site root (/, or repeated slashes), or a path shorter than section_segment — {section} is section_root. So with section_segment = 1, the path /en renders 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 pathgam_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 pathgam_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] ​

FieldTypeDefaultContract
ja4_endpoint_enabledBooleanfalseExpose GET /_ts/debug/ja4 on Fastly
auction_html_commentBooleanfalseInsert an auction diagnostic comment before </body>
inject_adm_for_testingBooleanfalseEnable the direct GAM-replace test path and raw debug_bid data
auction_html_comment_options.include_provider_responsesBooleantrueInclude provider response summaries
auction_html_comment_options.include_mediator_responseBooleantrueInclude mediator response summaries
auction_html_comment_options.include_bidsBooleantrueInclude provider bid arrays
auction_html_comment_options.metadata_keysArray[String]error_type, http_status, messageMust be a subset of this fixed allowlist
auction_html_comment_options.verbosityString"redacted"redacted, upstream, or full
auction_html_comment_options.formatString"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.

toml
[debug]
ja4_endpoint_enabled = false
auction_html_comment = false
inject_adm_for_testing = false

Fastly 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:

text
EDGEZERO__SERVICES__<SERVICE_ID>__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME
EDGEZERO__SERVICES__<SERVICE_ID>__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY

The 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:

bash
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 fastly

Provisioning 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:

bash
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 latest

Look 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:

bash
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_url is valid URL

EC Validation:

  • The passphrase key 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:

  • path is valid regex
  • username is ordinary configuration and non-empty
  • The resolved password is non-empty and is checked for placeholders at runtime

Integration Validation:

  • Each integration implements Validate trait
  • 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 URL

Best Practices ​

Configuration Management ​

Development:

toml
# 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_secrets store.
  • Keep only the key names in trusted-server.toml and 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
*.secret

Troubleshooting ​

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 path must 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, or ts 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 push after changing a deploy-time override
  • Try explicit string: VARIABLE='value' not VARIABLE=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:

bash
# List all TRUSTED_SERVER variables
env | grep TRUSTED_SERVER

Validate TOML:

bash
# Use any TOML validator
cat trusted-server.toml | npx toml-cli validate

Next Steps ​

Released under the Apache License 2.0.