Configuration
Learn how to configure Trusted Server for your deployment.
Overview
Trusted Server uses a flexible configuration system based on:
- TOML Files -
trusted-server.tomlfor ordinary configuration and secret key names - Environment Variables - Typed CLI overrides with the
TRUSTED_SERVER__prefix - EdgeZero Stores - Config and secret stores for the pushed blob and runtime secret values
Quick Start
Minimal Configuration
Create trusted-server.toml in your project root. Generate both secret values first with openssl rand -base64 32; the placeholders below are intentionally rejected until replaced.
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"
[ec]
passphrase = "ec_passphrase"Environment Variable Overrides
Environment variables are merged into existing TOML values by the typed ts config validate, ts config diff, and ts config push flows. They are not read by the deployed application at request time.
# Format: TRUSTED_SERVER__SECTION__FIELD
export TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
# Secret overrides, when needed, are key names—not secret values.
export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
export TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
# Replace the rejected placeholder values in trusted-server.toml, then validate.
ts config validate
ts config push --adapter fastlyStatic secret references
Static app-config credentials contain stable key names only. This includes publisher, trusted-client-IP, EC, handler, Tinybird, DataDome, and S3 fields:
publisher.proxy_secrettrusted_client_ip.shared_secret, when trusted client-IP forwarding is configuredec.passphraseec.partners[*].api_token, when inbound identify or batch sync is usedec.partners[*].ts_pull_token, when pull sync is enabledhandlers[*].passwordtinybird.auction_token_secret, when Tinybird auction telemetry is enabledintegrations.datadome.server_side_key_secret_name, when protection is enabledintegrations.datadome.protection_test_bypass.credential_secret_name, when the bypass is enabledproxy.asset_routes[*].auth.access_key_id,secret_access_key, and optionalsession_token
Their values belong in the logical trusted_server_secrets store and are resolved only while an instance builds runtime settings. An adapter can map the logical ID to a different physical name. For example, Fastly commonly maps trusted_server_secrets to physical store ts_secrets.
Prepare an initial reference-based deployment in this order:
- Create the physical store and configure its
trusted_server_secretsmapping. - Choose a stable key name for each active credential field in the app config.
- Write each credential value under its referenced key without exposing it in command arguments, shell history, logs, or CI output.
- Run
ts config validate, thents config push --adapter fastly. - Start or deploy instances after the store and pushed config are both ready.
Changing a store value does not alter already-built state. Restart or redeploy instances when rotating static credentials.
Keep publisher.proxy_secret and ec.passphrase stable unless intentionally rotating signed URLs or EC identifiers. On Spin, the app-config blob is stored under the trusted_server_config key in Spin's built-in default key-value store. Set the corresponding CLI store mapping before pushing so the write matches the runtime lookup:
export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=default
ts config push --adapter spinFor local Spin development, add --local to the push command. Also declare a component variable for each chosen secret key name using the encoder documented in spin.toml. Missing stores, keys, invalid UTF-8, and empty values fail closed; inline plaintext fallback is not supported.
Tinybird auction telemetry
Tinybird uses the same typed secret-reference path as the other static credentials. Do not configure a feature-specific store:
[tinybird]
enabled = true
api_host = "api.example.com"
auction_dataset = "auction_events_raw"
auction_token_secret = "tinybird_auction_append_token"Store the APPEND token value under tinybird_auction_append_token in the physical store mapped from trusted_server_secrets. The token is resolved once at startup. Disabled Tinybird telemetry does not require or resolve the token. The legacy tinybird.secret_store field is accepted for one migration release, but it is ignored and omitted from newly pushed config.
Generate Secure Secrets
Generate values locally and write them directly to the platform secret store; do not put the generated output in trusted-server.toml or the app-config blob.
openssl rand -base64 32Strict Key Validation
Trusted Server rejects unknown TOML keys in runtime configuration. Before pushing or upgrading config, remove stale fields and typos; otherwise config loading can fail and the service will return its startup-error response.
Configuration Files
| File | Purpose |
|---|---|
trusted-server.toml | Main application configuration |
fastly.toml | Fastly Compute service settings |
.env.dev | Local development overrides |
Key Sections
| Section | Purpose |
|---|---|
[publisher] | Domain, origin, proxy settings |
[trusted_client_ip] | Authenticated client-IP forwarding |
[ec] | Edge Cookie (EC) ID generation |
[tester_cookie] | Optional tester-cookie endpoint |
[proxy] | Proxy SSRF allowlist and asset routes |
[cache] | Static/rehosted asset cache policy rules |
[image_optimizer] | Reusable Image Optimizer profile sets |
[request_signing] | Ed25519 request signing |
[auction] | Auction orchestration |
[integrations.*] | Partner integrations (Prebid, Next.js, etc.) |
Example: Production Setup
Generate and substitute every replace-with-* value before validation or deployment.
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "publisher_proxy_secret"
[ec]
passphrase = "ec_passphrase"
[request_signing]
enabled = true
config_store_id = "01GXXX"
secret_store_id = "01GYYY"
[integrations.prebid]
enabled = true
client_side_bidders = ["example-browser-bidder"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"
[proxy]
allowed_domains = ["assets.example.com"]
[auction]
enabled = true
timeout_ms = 2000
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
timeout_ms = 1200
routing = "explicit"
[auction.providers.pbs-main.profile_config]
debug = false
[auction.bidders.example-server-bidder]
provider = "pbs-main"Detailed Reference
The sections below consolidate the full configuration reference on this page.
Environment Variable Overrides (Typed CLI)
Environment variables with the TRUSTED_SERVER__ prefix are merged into the base TOML configuration by ts config validate, ts config diff, and ts config push. The resolved values are validated and, for config push, stored in the app-config blob. Changing an environment variable requires rerunning validation and pushing the resolved config, not rebuilding the binary.
The pinned EdgeZero loader only overrides leaves that already exist in the parsed TOML; it does not create missing fields. Add newly introduced defaulted fields to an existing config before relying on their environment overrides. Secret overlays still contain key names, never secret values. Pass --no-env to use file values without the overlay.
Format
TRUSTED_SERVER__SECTION__SUBSECTION__FIELDRules:
- Prefix:
TRUSTED_SERVER - Separator:
__(double underscore) - Case: UPPERCASE
- Sections: Match TOML hierarchy
- Map keys: Preserve TOML punctuation. For example, provider key
pbs-mainuses thePBS-MAINsegment, notPBS_MAIN.
Shell assignment syntax cannot contain a hyphenated variable name. Use env to apply a provider override to a command:
env 'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
ts config validateThis example changes an existing scalar leaf. Edit TOML and run ts config validate followed by ts config push when changing an array, table, map, or rule.
Publisher Configuration
Core publisher settings for domain, origin, and proxy configuration.
[publisher]
| Field | Type | Required | Description |
|---|---|---|---|
domain | String | Yes | Publisher's apex domain name |
cookie_domain | String | Yes | Domain for non-EC cookies (typically with leading dot) |
origin_url | String | Yes | Full URL of publisher origin server |
origin_host_header_override | String | No | Outbound Host header to send while connecting to origin_url |
proxy_secret | String | Yes | Secret-store key name for the proxy URL secret |
max_buffered_body_bytes | Integer | No | Buffered-body cap / Fastly stream raw+decoded byte ceiling (default 16 MiB) |
Note: EC cookies (
ts-ec) derive their domain automatically as.{domain}and do not usecookie_domain. Thecookie_domainfield is used by other cookie helpers.
Example (replace the rejected secret placeholder before validation):
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
# Optional: connect to origin_url but send this outbound Host header.
# origin_host_header_override = "www.publisher.com"
proxy_secret = "publisher_proxy_secret"Environment Override:
TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.com
TRUSTED_SERVER__PUBLISHER__COOKIE_DOMAIN=.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://origin.publisher.com
TRUSTED_SERVER__PUBLISHER__ORIGIN_HOST_HEADER_OVERRIDE=www.publisher.com
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=publisher_proxy_secret
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216Field Details
domain
Purpose: Primary domain for the publisher.
Usage:
- Used for publisher routing and logging
- Part of request context for proxy/origin handling
Format: Hostname without protocol or path
- ✅
publisher.com - ✅
www.publisher.com - ❌
https://publisher.com - ❌
publisher.com/path
cookie_domain
Purpose: Domain scope for non-EC cookies.
Usage:
- Used by non-EC cookie helpers for domain scoping
- EC cookies (
ts-ec) use a separate computed domain derived fromdomain
Format: Domain with optional leading dot
.publisher.com- Shares across all subdomainspublisher.com- Exact domain only
Best Practice: Use leading dot (.publisher.com) for subdomain sharing.
origin_url
Purpose: Backend origin server URL for publisher content.
Usage:
- Fallback proxy target for non-integration requests
- HTML processing rewrites origin URLs to request host
- Base for relative URL resolution
Format: Full URL with protocol
- ✅
https://origin.publisher.com - ✅
https://origin.publisher.com:8080 - ✅
http://192.168.1.1:9000 - ❌
origin.publisher.com(missing protocol)
Port Handling: Includes port if non-standard (not 80/443).
origin_host_header_override
Purpose: Optional Host header to send to the publisher origin while still connecting to the host in origin_url.
Usage:
- Connects, uses SNI, and checks certificates against
origin_url - Sends the configured value as the outbound HTTP
Hostheader - Useful when the origin endpoint expects a canonical publisher hostname
Format: Hostname with optional port, without protocol, path, query, or fragment
- ✅
www.publisher.com - ✅
www.publisher.com:8443 - ❌
https://www.publisher.com - ❌
www.publisher.com/path
Default: When omitted, Trusted Server sends the host from origin_url.
proxy_secret
Purpose: Secret-store key name for the HMAC-SHA256 value used to sign proxy URLs.
The referenced value is resolved from trusted_server_secrets at startup. Generate it with a cryptographically secure random source; at least 32 random bytes are recommended. Keep that value confidential, rotate it only intentionally, and never put it in the TOML file or pushed app-config blob.
Usage:
- Signs
/first-party/proxyURLs - Signs
/first-party/clickURLs - Validates incoming proxy requests
- Prevents URL tampering
Security Warning
Changing proxy_secret invalidates all existing signed URLs. Plan rotations carefully and use graceful transition periods.
max_buffered_body_bytes
Purpose: Upper bound on how much of a publisher origin body the rewrite pipeline holds in memory — the post-rewrite output buffer on buffered adapters, and the per-stream raw/decoded byte ceiling on the Fastly streaming path.
Usage:
- On buffered adapters (Axum, Cloudflare, Spin) it caps the decoded, post-rewrite output buffer for a publisher response processed in full. It also bounds how much decoded gzip output may sit in the heap at any one moment, so a decompression bomb is rejected mid-decode rather than after its full expansion. That second bound is per-step, not a total: a gzip-encoded response passes or fails on the same post-rewrite output size as the identity, deflate and brotli versions of the same body.
- On the Fastly streaming path the origin body is preserved as a stream, so the same value caps the stream twice over: the cumulative raw (still compressed) bytes pulled from origin, and the cumulative decoded bytes emitted by the decompressor. The decoded cap is enforced during decompression, so a decompression bomb is rejected before its expansion is materialized rather than after.
Behavior when exceeded:
- On buffered adapters the response fails before any bytes are committed.
- On the streaming path the response headers are already committed when either cap trips, so the body is truncated mid-stream and the error is logged — the client receives a short (incomplete) body rather than a
5xx. Size the cap above your largest expected decoded page so legitimate responses are never truncated.
Default: 16777216 (16 MiB). On the Fastly streaming path this is now the sole ceiling: origin bodies are streamed rather than materialized in full, so the previous ~10 MiB raw-body limit no longer applies.
Minimum: Must be at least 1. A value of 0 is rejected at startup because a zero-byte cap fails every non-empty publisher response.
Environment Override:
TRUSTED_SERVER__PUBLISHER__MAX_BUFFERED_BODY_BYTES=16777216Trusted Client IP Configuration
Use this optional section when a trusted CDN service forwards requests to the Fastly service running Trusted Server. It lets Trusted Server use the reader's address instead of the immediate fronting edge node's address. Only the Fastly adapter honours this section; the Cloudflare, Spin, and Axum adapters validate it but keep using their own runtime client address.
[trusted_client_ip]
| Field | Type | Required | Description |
|---|---|---|---|
ip_header | String | Yes | Header containing exactly one reader IP address |
auth_header | String | Yes | Header containing exactly one shared-secret value |
shared_secret | String | Yes | Key in trusted_server_secrets for the front-door shared secret |
All three fields are required when the section exists. When the section is absent, Trusted Server continues to use the immediate peer address, and ts config push omits the section from the published config blob so instances running an older binary keep accepting the blob.
Deploy the code before pushing the config
Once the section is configured, the pushed blob carries it, and Settings rejects unknown fields. A binary that predates trusted client-IP support fails to load a blob containing this section and returns its startup-error response. Upgrade every instance before pushing a config that enables the section, and restore a config without the section before rolling instances back. Getting this order wrong takes the service down rather than degrading it.
[trusted_client_ip]
ip_header = "x-ts-client-ip"
auth_header = "x-ts-client-ip-auth"
shared_secret = "trusted_client_ip_shared_secret"Prefer a dedicated x- name for ip_header, as shown. fastly-client-ip is also accepted and suits a fronting service dedicated to Trusted Server, but on a service carrying other traffic a dedicated name means the front door never modifies Fastly-Client-IP, so other consumers of that header keep working unchanged. See Fastly Setup for the front-door configuration this section depends on.
The front door must overwrite both headers on every request it forwards to Trusted Server, and must remove client-supplied copies on its other routes. Trusted Server resolves shared_secret from trusted_server_secrets at startup. It accepts the forwarded address only when the request has exactly one auth_header value that matches the resolved secret byte-for-byte and exactly one ip_header value that parses directly as IPv4 or IPv6. Values are not trimmed or normalized. Missing, empty, duplicate, non-UTF-8, mismatched, or malformed values do not reject the request; Trusted Server safely falls back to the immediate peer address. Both configured headers are removed before routing.
Header names are validated case-insensitively. ip_header must be fastly-client-ip or start with x-, while auth_header must start with x-. The names must differ. Neither field may use a header name reserved for Trusted Server's own internal signals (for example x-forwarded-for, x-geo-info-available, x-ts-ec, x-ts-tls-protocol, or x-ts-tls-cipher); the full reserved set is the internal-header list that Trusted Server strips before forwarding to third parties. These restrictions exclude standard sensitive headers such as Host, Content-Length, Cookie, and Authorization, as well as every Trusted Server internal header. Choose dedicated x- names that no other application or routing logic uses, because Trusted Server removes the configured headers before routing.
Generate the referenced secret value with a cryptographically secure random generator, encode it as hex or base64url, and store the same value only in the front door and the physical store mapped from trusted_server_secrets. Put only the key name in Trusted Server configuration. The resolved value must contain at least 32 ASCII graphic bytes (! through ~) with no whitespace, controls, DEL, or non-ASCII bytes.
Independently of this section, the Fastly adapter treats fastly-client-ip as client-spoofable and strips it at request entry, so Trusted Server no longer forwards an inbound Fastly-Client-IP to the publisher origin. This applies even when [trusted_client_ip] is absent. Check whether the origin reads that header before deploying.
Startup fails closed if the configured key is missing, empty, invalid UTF-8, or resolves to an invalid shared-secret value. Every adapter removes the configured IP and authentication headers before routing, although only Fastly uses them for client-IP resolution.
Environment Overrides:
TRUSTED_SERVER__TRUSTED_CLIENT_IP__IP_HEADER=x-ts-client-ip
TRUSTED_SERVER__TRUSTED_CLIENT_IP__AUTH_HEADER=x-ts-client-ip-auth
TRUSTED_SERVER__TRUSTED_CLIENT_IP__SHARED_SECRET=trusted_client_ip_shared_secretBecause the typed environment overlay cannot create a missing section, add [trusted_client_ip] and all three fields to the TOML before using these overrides.
Tester Cookie Configuration
Settings for the optional tester-cookie endpoints. This feature is disabled by default and should only be enabled for intentional QA or troubleshooting flows.
[tester_cookie]
| Field | Type | Required | Description |
|---|---|---|---|
enabled | Boolean | No | Enables routes to set and clear ts-tester cookie |
When enabled, GET /_ts/set-tester returns 204 No Content and sets:
Set-Cookie: ts-tester=true; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax
Cache-Control: no-store, privateGET /_ts/clear-tester returns 204 No Content and clears the cookie:
Set-Cookie: ts-tester=; Domain=<publisher.cookie_domain>; Path=/; Secure; SameSite=Lax; Max-Age=0
Cache-Control: no-store, privateWhen disabled, both routes return 404 Not Found and do not set a cookie.
WARNING
The cookie is scoped with [publisher].cookie_domain, not the EC-specific computed domain. Keep cookie_domain aligned with the browser scope where your QA tooling expects to read ts-tester.
Example:
[tester_cookie]
enabled = trueEnvironment Override:
TRUSTED_SERVER__TESTER_COOKIE__ENABLED=trueEC Configuration
Settings for Edge Cookie identifier generation. 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. Consent configuration controls request-local interpretation and forwarding, not separate KV persistence.
[ec]
passphrase is a key name in trusted_server_secrets; the resolved value must be at least 32 bytes. Keep it stable to preserve EC identifier continuity.
| Field | Type | Required | Description |
|---|---|---|---|
passphrase | String | Yes | Publisher passphrase used as HMAC key |
ec_store | String or null | No | Fastly KV store name for EC identity graph and withdrawal state |
pull_sync_concurrency | Integer | No | Maximum concurrent pull-sync requests per organic response |
cluster_trust_threshold | Integer | No | Cluster size threshold for identity trust decisions |
cluster_recheck_secs | Integer | No | Legacy compatibility setting; cluster rechecks no longer use timestamps |
partners | Array | No | Static partner registry entries |
Partner keying
source_domain is the canonical partner key. It matches incoming OpenRTB EID source values and is also used as the EC KV ids map key.
api_token is optional. Set it to a key in trusted_server_secrets only when the partner calls the inbound identify or batch-sync APIs. A partner without api_token remains available for source-domain lookup, bidstream EIDs, and outbound pull sync, but cannot authenticate to those inbound APIs.
Example:
[ec]
passphrase = "ec_passphrase"
ec_store = "ec_identity_store"
[[ec.partners]]
name = "Mocktioneer SSP"
source_domain = "mocktioneer.example"
bidstream_enabled = true
# api_token = "partner_api_token" # only for inbound identify or batch sync
# ts_pull_token = "partner_ts_pull_token" # required when pull sync is enabledEnvironment Override:
TRUSTED_SERVER__EC__PASSPHRASE=ec_passphrase
TRUSTED_SERVER__EC__EC_STORE=ec_identity_storeField Details
passphrase
Purpose: Secret-store key name whose resolved value is the HMAC key for EC ID generation.
Security:
- The key name is stored in app config; the value is stored in
trusted_server_secrets - Keep the value stable unless intentionally rotating EC identifiers
- Do not place the value in environment overlays or the pushed blob
Validation: Application startup fails if:
- Empty string
Response Headers
Custom headers added to all responses.
[response_headers]
Purpose: Add custom HTTP headers to every response.
Format: Key-value pairs
Example:
[response_headers]
X-Custom-Header = "custom value"
X-Publisher-ID = "pub-12345"
X-Environment = "production"
Cache-Control = "public, max-age=3600"Environment Override:
Override an existing header leaf by preserving its TOML key punctuation in the environment path. Shell assignment syntax cannot contain hyphens, so use env:
env 'TRUSTED_SERVER__RESPONSE_HEADERS__X-CUSTOM-HEADER=updated value' \
ts config validateThe overlay cannot add a header or replace the whole response_headers table. Edit TOML, validate, and push again for those changes.
Use Cases:
- Custom measurement headers
- Cache control overrides
- Debugging identifiers
- CORS headers (if needed)
Header Precedence
Custom headers may be overwritten by application logic. Standard headers (Content-Type, Content-Length) are controlled by the application.
Request Signing
Configuration for Ed25519 request signing and JWKS management.
[request_signing]
| Field | Type | Required | Description |
|---|---|---|---|
enabled | Boolean | No (default: false) | Enable request signing features |
config_store_id | String | If enabled | Fastly Config Store ID for JWKS |
secret_store_id | String | If enabled | Fastly Secret Store ID for private keys |
Example:
[request_signing]
enabled = true
config_store_id = "01GXXX" # From Fastly dashboard
secret_store_id = "01GYYY" # From Fastly dashboardEnvironment Override:
TRUSTED_SERVER__REQUEST_SIGNING__ENABLED=true
TRUSTED_SERVER__REQUEST_SIGNING__CONFIG_STORE_ID=01GXXX
TRUSTED_SERVER__REQUEST_SIGNING__SECRET_STORE_ID=01GYYYStore Setup
Config Store (for public keys):
# Create store
fastly config-store create --name=jwks_store
# Get store ID
fastly config-store listSecret Store (for private keys):
# Create store
fastly secret-store create --name=signing_keys
# Get store ID
fastly secret-store listLocal Dev Setup (fastly.toml):
[local_server.config_stores]
[local_server.config_stores.jwks_store]
file = "test-data/jwks_store.json"
[local_server.secret_stores]
[local_server.secret_stores.signing_keys]
file = "test-data/signing_keys.json"See Request Signing and Key Rotation for usage.
Basic Authentication Handlers
Path-based HTTP Basic Authentication.
[[handlers]]
Purpose: Protect specific paths with username/password authentication.
Format: Array of handler objects
| Field | Type | Required | Description |
|---|---|---|---|
path | String (Regex) | Yes | Regular expression matching paths |
username | String | Yes | HTTP Basic Auth username |
password | String | Yes | HTTP Basic Auth password |
Example:
# Single handler
[[handlers]]
path = "^/_ts/admin"
username = "admin"
password = "admin_password"
# Multiple handlers
[[handlers]]
path = "^/secure"
username = "user1"
password = "secure_handler_password"
[[handlers]]
path = "^/api/private"
username = "api-user"
password = "api_handler_password"Environment Override:
# Handler 0
TRUSTED_SERVER__HANDLERS__0__PATH="^/_ts/admin"
TRUSTED_SERVER__HANDLERS__0__USERNAME="admin"
TRUSTED_SERVER__HANDLERS__0__PASSWORD="admin_password"
# Handler 1
TRUSTED_SERVER__HANDLERS__1__PATH="^/api/private"
TRUSTED_SERVER__HANDLERS__1__USERNAME="api-user"
TRUSTED_SERVER__HANDLERS__1__PASSWORD="api_handler_password"Path Patterns
Regex Syntax: Standard Rust regex patterns
Examples:
# Exact path
path = "^/_ts/admin$" # Only /_ts/admin
# Prefix match
path = "^/_ts/admin" # /_ts/admin, /_ts/admin/users, /_ts/admin/settings
# Multiple paths
path = "^/(admin|secure|private)"
# File extension
path = "\\.pdf$" # All PDF files
# Complex pattern
path = "^/api/v[0-9]+/private" # /api/v1/private, /api/v2/privateValidation: Application startup fails if regex is invalid.
Admin coverage and passwords are validated at startup
Startup fails when no handler covers an admin route. The dynamic /_ts/admin/ec/{id} route accepts any segment after /_ts/admin/ec/, and Basic Auth runs on the raw path before routing, so coverage cannot be inferred from ID-shaped samples: a pattern such as ^/_ts/admin/ec/[a-f0-9]{64}[.][A-Za-z0-9]{6}$ is rejected. Use a prefix-level matcher (^/_ts/admin, or ^/_ts/admin/ec/ alongside the other admin patterns).
Handler expressions match the raw URI path, while a publisher origin may decode percent-encoded aliases before routing. For a whole-site staging gate, use path = "^/"; do not rely on a decoded-path prefix such as ^/secure to protect equivalent origin paths.
Startup also fails when any handler — admin or not — uses a placeholder or well-known weak password (changeme, password, admin, or a replace-with-… template value). Handler selection is first-match-wins, so a narrow handler ahead of the admin pattern governs the paths it matches.
Scope patterns to the paths you mean
Handler patterns are matched against the full request path, so a broad pattern covers everything beneath it. The /_ts/ namespace holds both admin routes and browser-facing endpoints that anonymous visitors must be able to reach:
| Path | Called by |
|---|---|
/_ts/page-bids | Trusted Server JS, on SPA navigation |
/_ts/api/v1/identify | Trusted Server JS, in the browser |
/_ts/api/v1/batch-sync | Trusted Server JS, in the browser |
A pattern such as path = "^/_ts" puts those behind Basic Auth. Browser fetches never carry Basic credentials, so every visitor gets 401 — on /_ts/page-bids that means no ads after any client-side navigation. Match the admin routes specifically (^/_ts/admin) instead.
Upgrading from a release before /_ts/page-bids existed: if any handler pattern covers it, narrow the pattern. The Trusted Server JS bundle falls back to the deprecated /__ts/page-bids alias in the meantime, but that alias is scheduled for removal (#970).
Security Considerations
Password Storage:
handlers[*].passwordis a key name intrusted_server_secrets- Store the resolved password only in the platform secret store
- Rotate passwords through the store and restart/redeploy instances
Limitations:
- HTTP Basic Auth (not OAuth/JWT)
- Single username/password per path
- No role-based access control
- No rate limiting (add at edge)
Production Use
Do not put handler passwords in trusted-server.toml, environment overlays, or app-config blobs. Provision the referenced key in trusted_server_secrets before pushing the config.
URL Rewrite Configuration
Control which domains are excluded from first-party rewriting.
[rewrite]
| Field | Type | Required | Description |
|---|---|---|---|
exclude_domains | Array[String] | No (default: []) | Domains to skip rewriting |
Example:
[rewrite]
exclude_domains = [
"*.cdn.trusted-partner.com", # Wildcard
"first-party.publisher.com", # Exact match
"localhost", # Development
]Environment Override:
EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit exclude_domains in TOML, then validate and push the file again.
Pattern Matching
Wildcard Patterns (*):
"*.cdn.example.com"Matches:
- ✅
assets.cdn.example.com - ✅
images.cdn.example.com - ✅
cdn.example.com(base domain) - ❌
cdn.example.com.evil.com(different domain)
Exact Patterns (no *):
"api.example.com"Matches:
- ✅
api.example.com - ❌
www.api.example.com - ❌
api.example.com.evil.com
Use Cases
Trusted Partners:
exclude_domains = ["*.approved-cdn.com"]First-Party Resources:
exclude_domains = ["assets.publisher.com", "static.publisher.com"]Development:
exclude_domains = ["localhost", "127.0.0.1", "*.local"]Performance (already first-party):
exclude_domains = ["*.publisher.com"] # Skip unnecessary proxyingSee Creative Processing for details.
Proxy Configuration
Controls first-party proxy security settings and path-based asset routes.
[proxy]
| Field | Type | Required | Description |
|---|---|---|---|
allowed_domains | Array[String] | No (default: []) | Hosts permitted for signing, initial fetches, and redirects |
certificate_check | Boolean | No (default: true) | Verify TLS certificates when proxying HTTPS origins |
asset_routes | Array[Table] | No (default: []) | Path prefixes proxied directly to configured origins |
Example:
[proxy]
allowed_domains = [
"assets.example.com", # Exact match
"*.cdn.example.com", # Wildcard: cdn.example.com and all subdomains
]Environment Override:
EdgeZero v0.0.4 cannot replace this array or address its elements by index. Edit allowed_domains in TOML, then validate and push the file again.
Field Details
allowed_domains
Purpose: Allowlist of target hosts permitted for /first-party/sign and /first-party/proxy. When integrations.prebid.external_bundle_url is configured, this list must cover its host and any HTTPS redirect targets.
Behavior: Trusted Server checks the parsed host before signing a target, before fetching the initial proxy target, and before following each HTTP redirect (301/302/303/307/308). A host that does not match the list is blocked with a 403 error.
Default - open mode: When allowed_domains is absent or empty and no external Prebid bundle is configured, every valid host is allowed for signing, initial fetches, and redirects. Configuring an external Prebid bundle with an empty list fails deploy validation. Open mode supports zero-config development but should not be used in production.
Pattern Matching:
| Pattern | Matches | Does not match |
|---|---|---|
assets.example.com | assets.example.com | sub.assets.example.com |
*.cdn.example.com | cdn.example.com, static.cdn.example.com, a.b.cdn.example.com | evil-cdn.example.com |
"example.com"— exact match only."*.example.com"— matches the base domain and any subdomain at any depth.- Matching is case-insensitive; entries are normalized to lowercase at startup.
- Blank entries are ignored.
- The
*wildcard requires a dot boundary:*.example.comdoes not matchevil-example.com.
Production Recommendation
Always configure allowed_domains in production. Without an explicit allowlist, clients can sign and fetch valid URLs for arbitrary hosts, including redirect targets.
[proxy]
allowed_domains = [
"assets.example.com",
"*.cdn.example.com",
]See First-Party Proxy for usage details.
certificate_check
Purpose: Control TLS certificate verification for HTTPS proxy and asset-route origins.
Default: true
Set this to false only for local development with self-signed certificates.
[[proxy.asset_routes]]
Asset routes proxy selected first-party paths to an alternate asset origin without requiring signed /first-party/proxy URLs.
| Field | Type | Required | Description |
|---|---|---|---|
prefix | String | Yes | Request path prefix to match |
origin_url | String | Yes | Absolute http or https origin URL |
path_pattern | String | No | Regex matched against the incoming request path |
target_path | String | No | Replacement path used with path_pattern |
auth | Table | No | Optional origin authentication |
image_optimizer | Table | No | Optional route-level Image Optimizer settings |
Example:
[[proxy.asset_routes]]
prefix = "/assets/"
origin_url = "https://assets.example.com"Path rewrite example:
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://assets-cdn.example.com"
path_pattern = "^/\\.image/(.*)/[^/]+\\.([^/.]+)$"
target_path = "/image/upload/$1.$2"Behavior:
- Only
GETandHEADrequests use asset routes. - Built-in and integration routes take precedence.
- The longest matching asset-route prefix wins.
path_patternandtarget_pathmust be configured together.origin_urlmust not include userinfo, a path, a query string, or a fragment.- Unsafe origin response headers such as
Set-Cookieare stripped before the response reaches the browser.
[proxy.asset_routes.auth]
The first supported origin auth type is s3_sigv4.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type | String | Yes | none | Must be s3_sigv4 |
region | String | Yes | none | AWS region used in the SigV4 credential scope |
access_key_id | String | No | access_key_id | Default-store secret reference for the AWS access key ID |
secret_access_key | String | No | secret_access_key | Default-store secret reference for the AWS secret access key |
session_token | String | No | unset | Optional secret key containing a session token |
origin_query | String | No | route default | preserve or strip |
Example:
[[proxy.asset_routes]]
prefix = "/.image/"
origin_url = "https://bucket.s3.us-east-1.amazonaws.com"
[proxy.asset_routes.auth]
type = "s3_sigv4"
region = "us-east-1"
origin_query = "strip"
access_key_id = "s3_access_key_id"
secret_access_key = "s3_secret_access_key"
# session_token = "s3_session_token"S3 auth uses header-based AWS SigV4 with UNSIGNED-PAYLOAD. It is scoped to read-only asset requests and expects origin_url to use the S3 host that AWS validates. Credential references resolve from trusted_server_secrets at startup, and request signing performs no secret-store reads.
Effective origin_query precedence is auth-level origin_query, then enabled Image Optimizer origin_query, then the route default.
[proxy.asset_routes.image_optimizer]
Route-level Image Optimizer configuration selects a reusable profile set.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable Image Optimizer for the route |
region | String | Yes when enabled | none | Fastly IO processing region, such as us_east |
profile_set | String | Yes when enabled | none | Name under [image_optimizer.profile_sets.*] |
origin_query | String | No | strip when enabled | preserve or strip; effective preserve is rejected while IO is enabled |
Example:
[proxy.asset_routes.image_optimizer]
enabled = true
region = "us_east"
profile_set = "default_images"[image_optimizer.profile_sets.<name>]
Profile sets convert small request query controls into a closed set of Image Optimizer parameters.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
base_params | String | No | "" | Params applied before profile-specific params |
default_profile | String | No | default | Profile used when no profile is requested |
unknown_profile | String | No | use_default | use_default or reject |
profile_param | String | No | profile | Query parameter containing the profile name |
aspect_ratio_param | String | No | ar | Query parameter containing aspect ratio |
debug_param | String | No | _io_debug | Query parameter that disables IO when set to 1 |
Profile values live under [image_optimizer.profile_sets.<name>.profiles] and use query-string syntax.
[image_optimizer.profile_sets.default_images]
base_params = "quality=70&resize-filter=bicubic"
default_profile = "default"
unknown_profile = "use_default"
profile_param = "profile"
aspect_ratio_param = "ar"
debug_param = "_io_debug"
[image_optimizer.profile_sets.default_images.profiles]
default = "width=1920"
medium = "format=auto&width=828"
thumbnail = "width=150&crop=1:1,smart"Supported profile parameters are quality, resize-filter, format, width, height, and crop. Unknown profile parameters fail configuration validation.
[image_optimizer.profile_sets.<name>.aspect_ratios]
| Field | Type | Required | Description |
|---|---|---|---|
allowed | Array[String] | No | Allowed query values such as 1-1 or 16-9 |
profiles | Array[String] | No | Defined profiles that accept aspect-ratio overrides |
[image_optimizer.profile_sets.default_images.aspect_ratios]
allowed = ["1-1", "16-9", "4-3"]
profiles = ["medium", "thumbnail"][image_optimizer.profile_sets.<name>.crop_offsets]
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable offset bucketing |
x_param | String | No | x | Query parameter for x-axis offset |
y_param | String | No | y | Query parameter for y-axis offset |
buckets | Array[Integer] | No | [10, 30, 50, 70, 90] | Offset buckets in 0..=100 |
default | Integer | No | 50 | Offset used when input is missing or invalid |
when_missing | String | No | smart | smart or none when neither offset exists |
[image_optimizer.profile_sets.default_images.crop_offsets]
enabled = true
x_param = "x"
y_param = "y"
buckets = [10, 30, 50, 70, 90]
default = 50
when_missing = "smart"See Asset Routes for request flow, S3 auth details, and Image Optimizer behavior.
Cache Configuration
Static and rehosted asset cache upgrades are operator-controlled. By default, Trusted Server leaves arbitrary publisher-origin assets under origin cache control. Add [[cache.asset_rules]] entries only for paths that are known to be content-addressed or otherwise safe for the configured TTL.
[[cache.asset_rules]]
Rules are evaluated in file order; the first enabled matching rule wins. Disabled rules never match, and their matcher and policy validation is deferred until they are enabled. Rule IDs are always normalized and must remain nonempty and unique, including for disabled placeholders.
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique operator-facing rule identifier |
enabled | Boolean | No | Whether the rule participates in matching (default false) |
preset | String | Matcher | Built-in preset such as nextjs-static |
path_prefix | String | Matcher | Request path prefix |
path_glob | String | Matcher | Single glob matched against the request path |
path_globs | Array[String] | Matcher | Multiple globs matched against the request path |
path_regex | String | Matcher | Regex matched against the request path |
extensions | Array[String] | Matcher | Case-insensitive file extensions |
fingerprint_style | String | No | Required bundler fingerprint convention before matching |
visibility | String | No | public or private (default public) |
browser_ttl_seconds | Integer | Policy | Browser max-age; required for private rules and positive with immutable = true |
edge_ttl_seconds | Integer | Policy | Public rules only: TTL emitted through the runtime-specific shared-cache directive |
stale_while_revalidate_seconds | Integer | No | Optional stale-while-revalidate |
stale_if_error_seconds | Integer | No | Optional stale-if-error |
immutable | Boolean | No | Add immutable for a validated content-addressed rule |
An enabled rule must configure exactly one matcher. Public rules must configure at least one of browser_ttl_seconds or edge_ttl_seconds; private rules must configure browser_ttl_seconds and must not configure edge_ttl_seconds. path_glob and path_globs are mutually exclusive. immutable = true additionally requires a positive browser TTL and either the content-addressed nextjs-static preset, hex, or esbuild-base32.
The filename fingerprint check examines the suffix immediately before the final extension and requires a nonempty filename prefix separated by ., -, _, or ~. The accepted immutable conventions are:
hex: hexadecimal suffixes of at least eight characters containing a letter, such asapp.0123abcd.js;esbuild-base32: eight-character uppercase Base32 suffixes, such asapp-VRTVD5R5.js.
vite-base64-url remains available for non-immutable cache rules, but it cannot prove content addressing. Ordinary names such as hero-Portrait.jpg can match its eight-character Base64URL shape. A matching rule whose selected fingerprint style fails emits a debug log with the rule ID and rejected path.
Glob patterns are case-sensitive. * matches within a single path component, while ** matches recursively: /assets/*.js matches /assets/app.js but not /assets/vendor/app.js; /assets/**/*.js matches both.
Next.js preset example (disabled until the publisher confirms /_next/static/ is content-addressed):
[[cache.asset_rules]]
id = "nextjs-static"
enabled = false
preset = "nextjs-static"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = truePublisher allowlist example (enable only for an unambiguous immutable filename convention):
[[cache.asset_rules]]
id = "publisher-fingerprinted-assets"
enabled = false
path_globs = [
"/assets/**/*.js",
"/assets/**/*.css",
"/assets/**/*.png",
"/assets/**/*.webp",
]
fingerprint_style = "hex"
visibility = "public"
browser_ttl_seconds = 31536000
edge_ttl_seconds = 31536000
immutable = trueIf [cache] is omitted or no enabled rule matches, Trusted Server preserves the origin cache policy for publisher-origin assets. On the publisher pass-through path, an origin private or no-store directive vetoes a matching rule. Other origin cache directives, including no-cache, are replaced by the configured policy. Vary is preserved, so do not assign a public immutable rule to paths that vary by cookies or other user-specific request state.
On a configured Fastly asset-rehost route, a matching rule is authoritative over the third-party origin's cache defaults, including no-store, because Trusted Server owns the rehosted copy. A later Trusted Server or operator-applied private or no-store directive still vetoes public policy reapplication and removes shared-cache headers.
TS-owned validated hash URLs such as /static/tsjs=...js?v=<hash> use their built-in cache policy and do not require an asset rule. Shared-cache keys for /static/tsjs= must preserve v; otherwise a matching immutable response can collide with the missing or mismatched version's short-TTL response.
edge_ttl_seconds only emits the selected runtime's shared-cache directive for public rules. The runtime or service must also enable and consume that directive. The checked-in Cloudflare manifests intentionally do not enable Workers Cache: the Worker serves the full publisher gateway, not an isolated static-only entrypoint. Emitting Cloudflare-CDN-Cache-Control alone must not be treated as permission to cache every response. Any future Workers Cache opt-in must isolate or explicitly allowlist cacheable traffic. Fastly synthetic and final egress responses still require explicit runtime cache integration, tracked in #908.
Integration Configurations
Settings for built-in integrations (Prebid, Next.js, Osano, Permutive, Testlight). For other integrations (APS, Didomi, Lockr, GAM, etc.), see the relevant integration guides.
Common Fields
All integrations support an enabled flag. Defaults vary by integration and only apply when the integration section exists in trusted-server.toml.
| Field | Type | Description |
|---|---|---|
enabled | Boolean | Enable/disable the integration |
Prebid Integration
[integrations.prebid] owns browser behavior only. Server endpoint, provider timeout, routing, profile debug/test controls, consent forwarding, bidder-param overrides, and notification suppression belong under [auction].
| Browser field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable browser bundle injection, interception, and the trustedServer adapter |
account_id | String | None | Optional account value injected into browser Prebid configuration |
timeout_ms | Integer | 1000 | Browser Prebid.js timeout; independent of every server provider timeout |
debug | Boolean | false | Browser Prebid.js debug flag; independent of server profile debug |
client_side_bidders | Array[String] | [] | Bidders kept on native browser adapters |
excluded_gam_ad_unit_path_suffixes | Array[String] | [] | GAM suffixes excluded from Trusted Server refresh auctions |
script_patterns | Array[String] | ["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"] | Publisher Prebid script paths intercepted by Trusted Server |
external_bundle_url | String | Required when enabled | HTTPS publisher-specific Prebid.js bundle URL |
external_bundle_sha256 / *_sri | String | None | Optional bundle integrity and cache metadata |
bundle.adapters / user_id_modules | Array[String] | CLI selection | Inputs used by ts prebid bundle |
Server-side bidder codes are derived from validated [auction.bidders.*] routes and injected into the browser. There is no second server bidder list in [integrations.prebid]. A browser bidder stays client-side only when named in client_side_bidders and its adapter is present in the generated bundle.
Example:
[integrations.prebid]
enabled = true
timeout_ms = 1000
debug = false
client_side_bidders = ["example-browser"]
external_bundle_url = "https://assets.example.com/prebid/trusted-prebid.js"
script_patterns = ["/prebid.js", "/prebid.min.js"]
[proxy]
allowed_domains = ["assets.example.com"]
[integrations.prebid.bundle]
adapters = ["example-browser"]
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"
[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"
bid_param_overrides = { example-server = { placement = "example-placement" } }
[[auction.providers.pbs-main.profile_config.bid_param_override_rules]]
when.bidder = "example-server"
when.zone = "header"
set = { placement = "example-header-placement" }
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]
[auction.bidders.example-server]
provider = "pbs-main"Environment override:
env 'TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true' \
'TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1000' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__PROFILE_CONFIG__DEBUG=true' \
ts config validateEnvironment overlays only replace existing scalar leaves. Keep client_side_bidders, provider profile tables, bidder-parameter overrides, and rules in TOML, then validate and push the edited file.
Script Pattern Matching:
The script_patterns configuration determines which Prebid scripts are intercepted and replaced with empty JavaScript responses. This prevents client-side Prebid.js from loading when using server-side bidding.
- Suffix matching:
/prebid.min.jsmatches any URL ending with that path - Wildcard patterns:
/static/prebid/*matches paths under that prefix - Disable interception: Set
script_patterns = []to keep client-side Prebid
See Prebid Integration for full details.
Server Bid Param Override Surfaces:
These fields belong under [auction.providers.<id>.profile_config] for a prebid-server provider:
bid_param_overrides: static per-bidder shallow-merge overrides;bid_param_zone_overrides: per-bidder, per-zone shallow-merge overrides; andbid_param_override_rules: canonical ordered rules withwhenmatchers andsetobjects.
Compatibility-shaped fields are normalized into the same profile-local runtime engine. Explicit rules run after compatibility-derived rules, so later rules win on conflicts.
Next.js Integration
Section: [integrations.nextjs]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable Next.js integration |
rewrite_attributes | Array[String] | ["href","link","url"] | Attributes to rewrite |
max_combined_payload_bytes | Integer | 10485760 | Max combined RSC payload size |
Example:
[integrations.nextjs]
enabled = true
rewrite_attributes = ["href", "link", "url", "src"]
max_combined_payload_bytes = 10485760Environment Override:
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__MAX_COMBINED_PAYLOAD_BYTES=10485760Edit rewrite_attributes in TOML because the overlay cannot replace arrays.
Osano Integration
Section: [integrations.osano]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the Osano browser consent mirror |
Example:
[integrations.osano]
enabled = trueEnvironment Override:
TRUSTED_SERVER__INTEGRATIONS__OSANO__ENABLED=trueThe Osano mirror runs in the browser, so consent cookies it writes are available to Trusted Server on requests after the page where Osano consent APIs become ready. See Osano Integration for details.
Permutive Integration
Section: [integrations.permutive]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable Permutive integration |
organization_id | String | Required | Permutive organization ID |
workspace_id | String | Required | Permutive workspace ID |
project_id | String | "" | Permutive project ID |
api_endpoint | String | https://api.permutive.com | Permutive API URL |
secure_signals_endpoint | String | https://secure-signals.permutive.app | Secure signals URL |
cache_ttl_seconds | Integer | 3600 | Cache TTL in seconds |
rewrite_sdk | Boolean | true | Rewrite Permutive SDK references |
Example:
[integrations.permutive]
enabled = true
organization_id = "org-12345"
workspace_id = "ws-67890"
project_id = "proj-abcde"
api_endpoint = "https://api.permutive.com"
secure_signals_endpoint = "https://secure-signals.permutive.app"
cache_ttl_seconds = 7200
rewrite_sdk = trueTestlight Integration
Section: [integrations.testlight]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable Testlight integration |
endpoint | String | Required | Testlight auction endpoint |
timeout_ms | Integer | 1000 | Request timeout in milliseconds |
shim_src | String | /static/tsjs=tsjs-unified.min.js?v=<hash> | Script source for testlight shim |
rewrite_scripts | Boolean | false | Rewrite Testlight script references |
Example:
[integrations.testlight]
enabled = true
endpoint = "https://testlight.example/openrtb2/auction"
timeout_ms = 1500
rewrite_scripts = trueAuction Configuration
[auction.providers.*] is the only server-side provider inventory, and [auction.bidders.*] is the only client-visible bidder route map. The optional [auction].mediator remains a separate integration selection; it is not a provider or bidder route.
[auction]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the auction orchestrator |
sanitize_creatives | Boolean | false | Strip executable markup from winning-bid adm before delivery |
rewrite_creatives | Boolean | true | Rewrite winning-bid adm through first-party endpoints |
timeout_ms | Integer | 2000 | Logical auction budget in milliseconds |
mediator | String | None | Optional separate adserver_mock mediator |
creative_store | String | "creative_store" | Deprecated; creatives are delivered inline |
allowed_context_keys | Array | [] | Request context keys admitted into the auction |
Creative markup delivered by POST /auction and the publisher SSAT/page-bids path is processed by two independent passes. With sanitize_creatives = true (opt-in, default false), executable markup (script/object/embed/form and event handlers) is stripped together with its inner content. This blanks script-based creatives, so enable it only when creatives render in a context that shares the publisher's origin. With rewrite_creatives = true (the default), eligible absolute or protocol-relative resource and click URLs not excluded by rewrite configuration are converted to signed first-party endpoints, and any bidder-supplied <base> element is removed. The POST /auction path emits root-relative endpoints and injects the creative TSJS runtime exactly once, whether or not the bidder supplied a <body>, since bare fragments are the common adm shape. The foreign-origin SSAT renderer emits absolute endpoints and does not inject that bundle. With both disabled, adm ships exactly as the bidder returned it, except that a creative larger than the 1 MiB per-creative cap is rejected in every mode and its adm is dropped. Accepted external URLs are not host allowlisted by the sanitizer. Neither setting affects HTML or CSS fetched through /first-party/proxy. See Creative Processing.
Existing configs, upgrade sequencing, and rollback
Default values are omitted from stored JSON; non-default values (sanitize_creatives = true, rewrite_creatives = false) are serialized, and older AuctionConfig schemas reject unknown fields.
Upgrading: binaries that predate sanitize_creatives reject a blob that carries it, so in a rolling deployment upgrade the binary first, then push a config with sanitize_creatives = true if you want sanitization. Between the binary upgrade and the config push, sanitization is off (the new default). During that interval the creative iframe sandbox is the only isolation for /auction markup. There is no mixed-version-safe value that keeps the old unconditional sanitization: omission means "sanitize" on old code and "don't" on new code, while an explicit true fails startup on old code.
Rolling back: before reverting to a binary that does not know a field, remove that field's non-default value (and any environment override), run ts config validate, push the resulting default-compatible blob, and only then roll back the binary.
Environment overlays: The pinned EdgeZero loader cannot create missing TOML leaves. Existing configs must add both leaves under [auction] (rewrite_creatives and sanitize_creatives) before TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES / TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES can take effect. An override for a missing leaf is silently ignored.
Provider map
Breaking migration from the provider list
The former [auction].providers = ["prebid", ...] list and server-owned fields under [integrations.prebid] and [integrations.aps] are no longer accepted, even when an integration is disabled. Replace them with provider instances and bidder routes before deployment.
For Prebid Server, move server_url to provider endpoint, server timeout to provider timeout_ms, request controls and bidder-parameter overrides to the prebid-server profile_config, notification suppression to notifications, and each server bidder to [auction.bidders.<id>]. Origin-only legacy server_url values compile to /openrtb2/auction; query parameters survive, and configured non-root custom endpoint paths remain exact. Browser timeout, debug, bundle, script interception, refresh exclusions, and client_side_bidders remain under [integrations.prebid]. Configure timeout or debug under both owners when both browser and server behavior should retain the old value.
For APS, move endpoint and timeout to the provider, then move account, inventory, debug, and creative controls to the aps profile_config.
Only bidder codes listed in [auction.bidders] are folded into Trusted Server requests. Unlisted publisher bids remain native browser demand. All provider endpoints must be absolute HTTPS URLs.
The old and new blobs are mutually incompatible. Activate the new binary and map-shaped config together. A binary-first or config-first rolling deployment will put one version on a schema it rejects. Roll back by restoring the old binary and old-schema blob together.
Each table name is the provider ID used for configuration, backend correlation, health, response metadata, and telemetry. Provider IDs must match ^[a-z][a-z0-9-]{0,62}$. Multiple instances may select the same profile and endpoint because the provider ID remains their distinct runtime identity.
Example:
[auction]
enabled = true
sanitize_creatives = false
rewrite_creatives = true
timeout_ms = 2000
mediator = "adserver_mock"
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"
timeout_ms = 1200
[auction.providers.pbs-main.profile_config]
debug = false
test_mode = false
consent_forwarding = "both"
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]
[auction.providers.aps-main]
protocol = "openrtb-2.6"
profile = "aps"
endpoint = "https://aps.example.com/e/pb/bid"
routing = "all_eligible"
[auction.providers.aps-main.profile_config]
account_id = "example-aps-account"
debug = false
allow_script_creatives = false
[auction.bidders.example-server]
provider = "pbs-main"
[integrations.adserver_mock]
enabled = true
endpoint = "https://mediator.example.com/mediate"
timeout_ms = 500| Provider field | Required | Default | Description |
|---|---|---|---|
protocol | Yes | None | Must be openrtb-2.6 |
profile | No | standard | standard, prebid-server, or aps |
endpoint | Yes | None | Absolute HTTPS URL with host and no credentials or fragment |
timeout_ms | No | Profile default | Provider logical budget before the remaining-auction cap |
routing | No | explicit | explicit, or all_eligible for non-PBS profiles |
profile_config | No | {} | Typed object owned by the selected profile |
notifications | No | No suppression | Common nurl/burl suppression after response normalization |
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. Reserved driver, profile, and signing fields cannot be overwritten.
Common notification suppression uses exact returned OpenRTB seat values, not bidder route IDs:
[auction.providers.pbs-main.notifications]
suppress_all = false
suppress_seats = ["example-seat"]suppress_seats permits at most 128 unique nonempty entries, each at most 128 UTF-8 bytes and without ASCII control characters.
Validation timing and target limits
ts config validate and ordinary deploy validation compile the complete target-independent plan: profiles and defaults, routes, endpoint ownership, extension bounds, notifications, signing structure, and mediator selection. Target-specific checks are deferred to adapter startup. Startup uses the same compiled plan and additionally validates backend-name prediction/collisions and provider fan-out capability.
Fastly and Axum support multiple configured providers. Cloudflare and Spin currently reject an enabled auction with more than one provider because those adapters do not support concurrent provider fan-out. Disabled auctions may keep dormant multi-provider maps without target rejection.
A target-aware pre-write ts config push --adapter <target> callback is not available in this tree because the required EdgeZero callback is not yet available. Until it lands, push performs target-independent validation and adapter startup is the mandatory target-aware gate. Do not treat a successful push as proof that a Cloudflare or Spin multi-provider plan can start.
Deadline behavior
Configured timeouts are logical budgets, not hard wall-clock guarantees. No current adapter claims an abortable provider-wide total-request deadline. Already-launched work may complete after the logical budget and a completed late response can remain eligible. Once the logical auction budget is exhausted, Trusted Server starts no additional provider or mediator network work, then finishes local decision and delivery. An auction can therefore exceed its configured wall-clock timeout.
Creative sanitization is opt-in. sanitize_creatives = true strips executable markup before delivery. rewrite_creatives = false skips first-party URL rewriting and creative TSJS injection. See Creative Processing.
Environment overrides replace map leaves that already exist in TOML:
env 'TRUSTED_SERVER__AUCTION__ENABLED=true' \
'TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES=false' \
'TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES=true' \
'TRUSTED_SERVER__AUCTION__TIMEOUT_MS=2000' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__ENDPOINT=https://prebid.example.com/openrtb2/auction' \
'TRUSTED_SERVER__AUCTION__PROVIDERS__PBS-MAIN__TIMEOUT_MS=900' \
'TRUSTED_SERVER__AUCTION__MEDIATOR=adserver_mock' \
ts config validateCreative Opportunities Configuration
[creative_opportunities]
Defines the ad slots the trusted server offers on a page: which pages each slot appears on (page_patterns), its supported sizes (formats), and the GAM ad unit it maps to (gam_unit_path).
enabled is the dedicated server-side ad-template switch. It defaults to true for compatibility with existing configurations. Set it to false to stop publisher HTML and SPA page-bids template delivery while retaining the slot configuration and direct POST /auction endpoint.
Publisher document cache policy
For a successful GET publisher document, Trusted Server applies the browser-only Cache-Control: private, max-age=60 policy from #1007 when the server-side ad stack is structurally inactive. Trusted Server also applies this policy to a subsequent 304 Not Modified response so revalidation cannot restore the origin freshness policy. This includes an absent [creative_opportunities] section, enabled = false, no slot matching the path, or a disabled auction. The private directive prevents shared caches that use Cache-Control from storing the document. The policy replaces the origin browser cache policy except when the origin sends private or no-store, which are preserved. Bot, prefetch, and consent-denied requests also retain the origin policy because they can produce a request-specific representation for the same URL. Error responses and non-document requests retain the origin policy.
Trusted Server leaves origin validators and CDN-specific cache headers unchanged. Those headers continue to control supporting CDNs independently of the browser-only policy. If a response using the generated inactive-stack policy later carries Set-Cookie, cookie privacy finalization replaces it with Cache-Control: private, max-age=0 and removes the CDN-specific cache headers.
[creative_opportunities]
enabled = true # set to false to disable server-side ad templates
gam_network_id = "123456789"
price_granularity = "dense"
# Shared placeholder value for the site root ("/") — see {section} below.
section_root = "home"
# Which path segment names the section, 0-based. Default 0 (first segment).
# Set to 1 for locale-prefixed URLs such as "/en/news/article".
# section_segment = 0
[[creative_opportunities.slot]]
id = "ad-header"
gam_unit_path = "/{network_id}/example/{section}"
# List each section landing page as well as its subtree: `/news/*` matches
# `/news/article` but NOT `/news` — the glob requires the trailing separator.
page_patterns = ["/", "/news", "/news/*", "/reviews", "/reviews/*"]
formats = [{ width = 728, height = 90 }]The same switch can be overridden through the typed CLI environment overlay. Because EdgeZero only replaces TOML leaves that already exist, first add enabled = true to the [creative_opportunities] block in the base config before using this override. See Environment Variable Overrides (Typed CLI) for the general overlay rules.
TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__ENABLED=falseWARNING
Setting enabled = false writes this field into the pushed configuration blob. Binaries released before this setting reject the unknown field and fail to load settings, which makes every request fail. Before rolling back to an older binary, restore enabled to its default, re-push and finalize the configuration, then roll back the binary.
Shared template assembly (assembly_mode = "esi")
This configuration is an experimental validation spike scoped to IABTechLab/trusted-server#1009, not a settled production cache interface.
assembly_mode controls how initial-page slot and bid state is delivered:
inline(default) transforms every origin response and injects the current reader's slots and bids directly.esiopts into a reader-neutral transformed-template cache on Fastly. The cache stores identity bytes containing one inert, versioned comment. On an authorized cold miss, Fastly replaces that comment in a private working copy with one synthetic ESI include and resolves it from the already-built reader state using the pinnedstackpop/esiparser. No HTTP fragment request occurs. Warm hits use an exact byte split instead, preserving the fast article-prefix stream while the auction finishes.
This is deliberately not general publisher-controlled ESI. A transformed origin document containing any <esi: directive bypasses the template cache and the parser, while the ordinary byte seam still produces the reader's complete response. The stored shared template object never contains executable ESI markup.
Only Fastly currently supplies the Core Cache backend used by the shared template cache. Other adapters accept the mode but safely fall back to the inline transform on every request. This is not a top-level HTTP cache hit: Compute still runs and the final assembled response is always Cache-Control: private, no-store.
All four keys below belong directly under [creative_opportunities]. They are one feature contract: assembly_mode selects how creative-opportunity state is delivered, while the other three constrain when and how long that mode may share its template. They are not a general top-level HTTP-cache configuration.
[creative_opportunities]
assembly_mode = "esi"
# Every request header, except Accept-Encoding, that the publisher origin can
# name in Vary for these documents. Names are validated and de-duplicated.
template_cache_vary = [
"rsc",
"next-router-state-tree",
"next-router-prefetch",
"next-router-segment-prefetch",
]
# Safety ceiling for the shared template. Defaults to 60; valid range 1–86400.
# The origin's remaining edge freshness may make the actual lifetime shorter.
template_cache_max_age_seconds = 1200
# Default false. Enable only after proving publisher HTML ignores Cookie.
origin_is_cookie_independent = trueThe 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. With origin_is_cookie_independent = false (the safe default), all cookie-bearing requests bypass. With it set to true, an origin Vary: Cookie still overrides the assertion and refuses storage. Every other name the origin emits in Vary must appear in the configured list; an uncovered name safely refuses template storage.
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.
Rollback must preserve configuration compatibility:
- Change
assembly_modetoinlineand deploy/push that configuration. - Before rolling back to a binary that predates these fields, remove
assembly_mode,template_cache_vary,template_cache_max_age_seconds, andorigin_is_cookie_independent, then push the cleaned configuration. Older binaries usedeny_unknown_fieldsand intentionally reject unknown keys. - Purge the Fastly surrogate key
ts-templateusing the service's normal purge tooling, or wait for the bounded origin-derived lifetime to expire.
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.
gam_unit_path templating
gam_unit_path is a template. A publisher whose ad unit varies by site section expresses that in one slot rule instead of one rule per (slot × section).
Supported placeholders:
| Placeholder | Resolves to |
|---|---|
{network_id} | gam_network_id |
{slot_id} | the slot's id |
{section} | non-empty path segment at section_segment (default: first; see below) |
A template with no placeholders is used verbatim. A slot with nogam_unit_path falls back to /<network_id>/<slot_id>. Both preserve the pre-templating behavior, so existing static configs are unchanged.
Trusted Server conservatively caps the whole rendered dynamic path at 100 UTF-8 bytes, informed by Google's 100-character per-ad-unit-code limit. If a request-specific substitution would exceed the dynamic limit, only that slot is omitted before auction dispatch; the response itself still succeeds. Trusted Server logs a warning containing the slot ID and request path. Explicit static paths and absent/default paths retain legacy behavior and are not subject to this dynamic-only limit.
{section} derivation
{section} is derived from the request path at request time:
- It is the non-empty path segment at
section_segment(0-based, default0). With the default,/news/article-123→news. A site that prefixes a locale setssection_segment = 1, so/en/news/article→newsrather thanen. - It is sanitized: each run of characters outside
[A-Za-z0-9_-]becomes a single_, and the request-derived result is capped at 100 ASCII bytes. - Casing is preserved. Google documents GAM ad-unit codes as case-insensitive, so do not lowercase the value.
- The path is used raw — it is not percent-decoded. So
/new%20s→new_20s(only%is disallowed;2and0are kept), never the decodednew_s. This keeps{section}consistent with howpage_patternsmatch the same raw path. - When the path has no segment at that index — the site root (
/, or repeated slashes), or a path shorter thansection_segment—{section}issection_root. So withsection_segment = 1, the path/enrenders the root section rather than reusing the locale.
section_root is required whenever any slot's template uses {section}, and must match [A-Za-z0-9_-]+. There is no default: the home-section name is publisher-specific. Startup fails if {section} is used without a valid section_root. Startup rejects a blank gam_network_id only when an absent path/default or a {network_id} template consumes it; static paths and templates without {network_id} do not consume it. A [creative_opportunities] block with enabled = false or no slots is inactive, so no publisher templates are delivered and its gam_network_id is not checked when no slot uses it.
Both knobs are config-driven, so the URL→section convention stays with the publisher: section_segment selects which segment names the section, and section_root names the section when there is none.
During typed/startup finalization, after templates parse successfully, every placeholder-bearing dynamic template that omits section_segment has section_segment = 0 materialized, so an older binary rejects the pushed blob loudly. Static and absent paths remain compatible with the legacy config schema only when both section_root and section_segment are omitted. Before rolling back below this feature, replace or remove dynamic paths, remove both section_root and section_segment, re-push and finalize the config, then roll back the binary.
Example resolution for gam_unit_path = "/{network_id}/example/{section}" with gam_network_id = "123456789", section_root = "home", and the page_patterns shown above:
| Request path | gam_unit_path |
|---|---|
/ | /123456789/example/home |
/news | /123456789/example/news |
/news/article | /123456789/example/news |
/reviews/x | /123456789/example/reviews |
The same config with section_segment = 1 and locale-prefixed patterns (["/en", "/en/news", "/en/news/*"]):
| Request path | gam_unit_path |
|---|---|
/en | /123456789/example/home |
/en/news | /123456789/example/news |
/en/news/article | /123456789/example/news |
An unmatched route — a path matched by no slot's page_patterns — produces no slot at all, so no template is rendered for it.
Startup validation rejects a malformed template: an unknown placeholder (e.g. {oops}), an unmatched or nested {, a stray }, or an empty gam_unit_path.
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.
The Fastly service must still provide a trusted_server_config config store because the entry point opens it before dispatch and passes the handle to EdgeZero-backed platform services. The store may be empty unless another feature adds keys to it.
Local development (fastly.toml):
[local_server.config_stores]
[local_server.config_stores.trusted_server_config]
format = "inline-toml"
[local_server.config_stores.trusted_server_config.contents]Production setup (Fastly CLI):
# Create the store once and attach it to the service.
fastly config-store create --name trusted_server_configRollback to the legacy entry point is no longer controlled by runtime config keys. Use the normal deployment rollback path to restore a pre-cleanup service version if that is required.
Validation
Automatic Validation
Configuration is validated at startup:
Publisher Validation:
- All fields non-empty
origin_urlis valid URL
EC Validation:
- The
passphrasekey name is non-empty at push time - The resolved passphrase is at least 32 bytes at runtime
- Known placeholder values are rejected after resolution
Handler Validation:
pathis valid regexusernameis ordinary configuration and non-empty- The resolved
passwordis non-empty and is checked for placeholders at runtime
Integration Validation:
- Each integration implements
Validatetrait - Custom rules per integration
Validation Errors
Startup Failure if:
- Required fields missing
- Invalid data types
- Regex compilation fails
- Secret key is default value
- Integration config fails validation
Error Format:
Configuration error: provider `pbs-main` endpoint must be an absolute HTTPS URLBest Practices
Configuration Management
Development:
# trusted-server.dev.toml
[publisher]
domain = "localhost"
origin_url = "http://localhost:3000"
proxy_secret = "publisher_proxy_secret"Staging and production:
- Provision the same key names in the target
trusted_server_secretsstore. - Keep only the key names in
trusted-server.tomland environment overlays. - Push the config after provisioning and restart/redeploy after rotation.
Secret Management
Do:
- ✅ Store values in the platform secret store
- ✅ Rotate values deliberately and restart/redeploy instances
- ✅ Generate values locally without printing them to logs
- ✅ Use different values per environment when appropriate
- ✅ Keep stable key names for rotation
Don't:
- ❌ Commit secret values to version control
- ❌ Put secret values in environment overlays
- ❌ Put secret values in config diff output or app-config blobs
- ❌ Treat missing secret-store keys as inline values
- ❌ Use default/placeholder values
- ❌ Share secrets across environments
- ❌ Log secret values
- ❌ Expose in error messages
File Organization
Recommended Structure:
trusted-server.toml # Base config
trusted-server.dev.toml # Development overrides
.env.development # Dev environment vars
.env.staging # Staging environment vars
.env.production # Production environment vars (not in git)
.env.example # Example template (in git).gitignore:
.env.production
.env.staging
.env.local
*.secretTroubleshooting
Common Issues
"Failed to build configuration":
- Check TOML syntax (trailing commas, quotes)
- Verify all required fields present
- Check environment variable format
"Configuration field '...' is set to a known placeholder value":
- Confirm the referenced key exists in
trusted_server_secrets - Ensure the resolved value is non-empty and not a known placeholder
- Do not replace the key name with a plaintext value in the app config
- Rotate the value in the platform secret store, then restart/redeploy
"Invalid regex":
- Handler
pathmust be valid regex - Test pattern:
echo "^/_ts/admin" | grep -E "^/_ts/admin" - Escape special characters:
\.,\$, etc.
"Integration configuration could not be parsed":
- Check JSON syntax in env vars
- Verify indexed arrays (0, 1, 2...)
- Check field names match exactly
Environment Variables Not Applied:
- Run the override through
ts config validate,ts config diff, orts config push - Verify the target leaf already exists in
trusted-server.toml; the pinned EdgeZero loader does not create missing fields - Verify prefix:
TRUSTED_SERVER__ - Check separator:
__(double underscore) - Confirm the variable is exported:
echo $VARIABLE_NAME - Rerun
ts config pushafter changing a deploy-time override - Try explicit string:
VARIABLE='value'notVARIABLE=value
Debug Configuration
Print Loaded Config (test only):
use trusted_server_core::settings_data::get_settings;
let settings = get_settings()?;
println!("{:#?}", settings);Check Environment:
# List all TRUSTED_SERVER variables
env | grep TRUSTED_SERVERValidate TOML:
# Use any TOML validator
cat trusted-server.toml | npx toml-cli validateNext Steps
- Set up Request Signing for secure API calls
- Configure First-Party Proxy for URL proxying
- Learn about Edge Cookies for first-party state management
- Review Integrations for partner support