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 base configuration - Environment Variables - Typed CLI overrides with the
TRUSTED_SERVER__prefix - Fastly Stores - KV/Config/Secret stores for runtime data
Quick Start
Minimal Configuration
Create trusted-server.toml in your project root:
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "your-secure-secret-here"
[ec]
passphrase = "replace-with-32-plus-byte-random-secret"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
export TRUSTED_SERVER__EC__PASSPHRASE=replace-with-32-plus-byte-random-secret
ts config validate
ts config push --adapter fastlyGenerate Secure Secrets
# Generate cryptographically random secrets
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 |
[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
[publisher]
domain = "publisher.com"
cookie_domain = ".publisher.com"
origin_url = "https://origin.publisher.com"
proxy_secret = "change-me-to-secure-value"
[ec]
passphrase = "replace-with-32-plus-byte-random-secret"
[request_signing]
enabled = true
config_store_id = "01GXXX"
secret_store_id = "01GYYY"
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example.com/openrtb2/auction"
timeout_ms = 1200
bidders = ["kargo", "appnexus", "openx"]
client_side_bidders = ["rubicon"]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.
EdgeZero v0.0.4 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. 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
Examples
Simple Field:
TRUSTED_SERVER__PUBLISHER__DOMAIN=publisher.comNested Field:
TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auctionArray Field (JSON):
TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS='["kargo","rubicon"]'Array Field (Indexed):
TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__0=kargo
TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS__1=rubiconArray Field (Comma-Separated):
TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexusPublisher 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 key for encrypting/signing proxy URLs |
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:
[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 = "change-me-to-secure-random-value"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=your-secret-here
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 key for HMAC-SHA256 signing of proxy URLs.
Security:
- Keep confidential and secure
- Rotate periodically (90 days recommended)
- Use cryptographically random values (32+ bytes)
- Never commit to version control
Generation:
# Generate secure random secret
openssl rand -base64 32Usage:
- 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=16777216Tester 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]
| 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.
Example:
[ec]
passphrase = "replace-with-32-plus-byte-random-secret"
ec_store = "ec_identity_store"
[[ec.partners]]
name = "Mocktioneer SSP"
source_domain = "mocktioneer.example"
api_token = "partner-api-token-32-bytes-minimum"
bidstream_enabled = trueEnvironment Override:
TRUSTED_SERVER__EC__PASSPHRASE=your-secret
TRUSTED_SERVER__EC__EC_STORE=ec_identity_storeField Details
passphrase
Purpose: Publisher passphrase used as HMAC key for EC ID generation.
Security:
- Must be non-empty
- Rotate periodically for security
- Store securely (environment variable recommended)
Generation:
# Generate secure random key
openssl rand -hex 32Validation: 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:
Use a JSON object to preserve header name casing and hyphens:
TRUSTED_SERVER__RESPONSE_HEADERS='{"X-Robots-Tag": "noindex", "X-Custom-Header": "custom value"}'Why JSON?
Individual env var keys like TRUSTED_SERVER__RESPONSE_HEADERS__X_CUSTOM_HEADER lose hyphens and casing (becoming x_custom_header). The JSON format preserves exact header names.
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 = "secure-password"
# Multiple handlers
[[handlers]]
path = "^/secure"
username = "user1"
password = "pass1"
[[handlers]]
path = "^/api/private"
username = "api-user"
password = "api-pass"Environment Override:
# Handler 0
TRUSTED_SERVER__HANDLERS__0__PATH="^/_ts/admin"
TRUSTED_SERVER__HANDLERS__0__USERNAME="admin"
TRUSTED_SERVER__HANDLERS__0__PASSWORD="secure-password"
# Handler 1
TRUSTED_SERVER__HANDLERS__1__PATH="^/api/private"
TRUSTED_SERVER__HANDLERS__1__USERNAME="api-user"
TRUSTED_SERVER__HANDLERS__1__PASSWORD="api-pass"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).
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:
- Stored in plain text in config
- Use environment variables in production
- Rotate passwords regularly
- Consider using Fastly Secret Store
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
For production, store credentials in environment variables:
TRUSTED_SERVER__HANDLERS__0__PASSWORD=$(cat /run/secrets/admin_password)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:
# JSON array
TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS='["*.cdn.example.com","localhost"]'
# Indexed
TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS__0="*.cdn.example.com"
TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS__1="localhost"
# Comma-separated
TRUSTED_SERVER__REWRITE__EXCLUDE_DOMAINS="*.cdn.example.com,localhost"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: []) | Redirect destinations the proxy is permitted to follow |
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 = [
"tracker.com", # Exact match
"*.adserver.com", # Wildcard: adserver.com and all subdomains
"*.trusted-cdn.net",
]Environment Override:
# JSON array
TRUSTED_SERVER__PROXY__ALLOWED_DOMAINS='["tracker.com","*.adserver.com"]'
# Indexed
TRUSTED_SERVER__PROXY__ALLOWED_DOMAINS__0="tracker.com"
TRUSTED_SERVER__PROXY__ALLOWED_DOMAINS__1="*.adserver.com"
# Comma-separated
TRUSTED_SERVER__PROXY__ALLOWED_DOMAINS="tracker.com,*.adserver.com"Field Details
allowed_domains
Purpose: Allowlist of redirect destinations the proxy is permitted to follow.
Behavior: When the proxy receives an HTTP redirect (301/302/303/307/308) during a request to /first-party/proxy, the redirect target host is checked against this list. A redirect whose host is not matched is blocked with a 403 error.
Default — open mode: When allowed_domains is absent or empty, every redirect destination is allowed. This default is intentional for zero-config development but should not be used in production.
Pattern Matching:
| Pattern | Matches | Does not match |
|---|---|---|
tracker.com | tracker.com | sub.tracker.com |
*.tracker.com | tracker.com, sub.tracker.com, a.b.tracker.com | evil-tracker.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, a signed proxy URL can be used to follow redirects to arbitrary hosts, creating an SSRF risk.
[proxy]
allowed_domains = [
"*.your-ad-network.com",
"tracker.your-partner.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 |
secret_store | String | No | s3-auth | Runtime secret store containing AWS credentials |
access_key_id | String | No | access_key_id | Secret key containing the AWS access key ID |
secret_access_key | String | No | secret_access_key | Secret key containing 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"
secret_store = "s3-auth"
access_key_id = "access_key_id"
secret_access_key = "secret_access_key"
# session_token = "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. Credentials are cached per process by configured secret names after the first successful read.
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
Section: [integrations.prebid]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable Prebid integration |
server_url | String | Required | Prebid Server endpoint URL |
timeout_ms | Integer | 1000 | Request timeout in milliseconds |
bidders | Array[String] | ["mocktioneer"] | List of enabled bidders |
bid_param_overrides | Table | {} | Static per-bidder param overrides; normalized into the canonical override-rule engine and shallow-merged into bidder params |
bid_param_zone_overrides | Table | {} | Per-bidder, per-zone param overrides; normalized into the canonical override-rule engine and shallow-merged into bidder params |
bid_param_override_rules | Array[Table] | [] | Canonical ordered override rules with when matchers and set objects; evaluated after compatibility fields so later rules win on conflicts |
suppress_nurl | Boolean | false | Strip nurl and burl from every PBS bid when the PBS deployment fires win/billing notifications server-side |
suppress_nurl_bidders | Array[String] | [] | Bidder seats whose nurl and burl should be stripped while preserving client-side win/billing pixels for other bidders |
debug | Boolean | false | Enable debug mode (sets ext.prebid.debug and returnallbidstatus; surfaces debug metadata in responses) |
test_mode | Boolean | false | Set OpenRTB test: 1 flag for non-billable test traffic (independent of debug) |
debug_query_params | String | None | Extra query params appended for debugging |
client_side_bidders | Array[String] | [] | Bidders that run client-side via native Prebid.js adapters instead of server-side (see Prebid docs) |
script_patterns | Array[String] | ["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"] | URL patterns for Prebid script interception |
APS is configured exclusively under [integrations.aps]. aps entries in bidders or client_side_bidders are logged and removed case-insensitively so an upgrade does not prevent Trusted Server from starting. Remove those entries from operator configuration; this guard prevents APS demand from reaching Prebid Server or the client-side Prebid bundle.
Example:
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example/openrtb2/auction"
timeout_ms = 1200
bidders = ["kargo", "appnexus", "openx"]
debug = false
# test_mode = false
# Bidders that run client-side via native Prebid.js adapters
client_side_bidders = ["rubicon"]
# Customize script interception (optional)
script_patterns = ["/prebid.js", "/prebid.min.js"]
[integrations.prebid.bid_param_overrides.criteo]
networkId = 99999
pubid = "server-pub"
[integrations.prebid.bid_param_zone_overrides.kargo]
header = { placementId = "_s2sHeaderPlacement" }
[[integrations.prebid.bid_param_override_rules]]
when.bidder = "kargo"
when.zone = "header"
set = { placementId = "_s2sHeaderPlacement" }Environment Override:
TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=true
TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auction
TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1200
TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,appnexus,openx
TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_OVERRIDES='{"criteo":{"networkId":99999,"pubid":"server-pub"}}'
TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_ZONE_OVERRIDES='{"kargo":{"header":{"placementId":"_s2sHeaderPlacement"}}}'
TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_OVERRIDE_RULES='[{"when":{"bidder":"kargo","zone":"header"},"set":{"placementId":"_s2sHeaderPlacement"}}]'
TRUSTED_SERVER__INTEGRATIONS__PREBID__CLIENT_SIDE_BIDDERS=rubicon
TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false
TRUSTED_SERVER__INTEGRATIONS__PREBID__TEST_MODE=false
TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG_QUERY_PARAMS=debug=1
TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_PATTERNS='["/prebid.js","/prebid.min.js"]'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.
Bid Param Override Surfaces:
bid_param_overrides: Static per-bidder shallow-merge overrides.bid_param_zone_overrides: Per-bidder, per-zone shallow-merge overrides.bid_param_override_rules: Canonical ordered rules withwhenmatchers andsetobjects.
Compatibility fields are normalized into the same runtime engine as canonical rules. Explicit bid_param_override_rules run after compatibility-derived rules, so later canonical 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__REWRITE_ATTRIBUTES=href,link,url,src
TRUSTED_SERVER__INTEGRATIONS__NEXTJS__MAX_COMBINED_PAYLOAD_BYTES=10485760Osano 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
Settings for the auction orchestrator that coordinates multiple bid providers.
[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 |
providers | Array[String] | [] | Provider names that participate (e.g., ["prebid", "aps"]) |
mediator | String | Optional | Mediator provider name (runs parallel mediation when set) |
timeout_ms | Integer | 2000 | Auction timeout in milliseconds |
creative_store | String | "creative_store" | Deprecated; creatives are now delivered inline |
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 — note 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: EdgeZero v0.0.4 overlays 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.
Example:
[auction]
enabled = true
sanitize_creatives = false
rewrite_creatives = true
providers = ["aps", "prebid"]
timeout_ms = 2000
[integrations.aps]
enabled = true
account_id = "example-account"
debug = false
# Optional pair for deployments hosted away from APS-authorized inventory.
# inventory_domain = "publisher.example"
# inventory_page_origin = "https://www.publisher.example"
allow_script_creatives = false
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example.com/openrtb2/auction"Environment Override:
TRUSTED_SERVER__AUCTION__ENABLED=true
TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES=false
TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES=true
TRUSTED_SERVER__AUCTION__PROVIDERS=aps,prebid
TRUSTED_SERVER__AUCTION__PROVIDERS__0=aps
TRUSTED_SERVER__AUCTION__PROVIDERS__1=prebid
TRUSTED_SERVER__AUCTION__MEDIATOR=adserver_mock
TRUSTED_SERVER__AUCTION__TIMEOUT_MS=2000
TRUSTED_SERVER__AUCTION__CREATIVE_STORE=creative_store
TRUSTED_SERVER__INTEGRATIONS__APS__DEBUG=falseCreative 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).
[creative_opportunities]
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 }]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 no slots is disabled, so its gam_network_id is not checked.
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:
passphrase≥ 1 characterpassphrase≠ known placeholders ("secret-key","secret_key","trusted-server"— case-insensitive)
Handler Validation:
pathis valid regexusernamenon-emptypasswordnon-empty
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: Integration 'prebid' configuration failed validation:
server_url: must not be emptyBest Practices
Configuration Management
Development:
# trusted-server.dev.toml
[publisher]
domain = "localhost"
origin_url = "http://localhost:3000"
proxy_secret = "dev-secret"Staging:
# .env.staging
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://staging.publisher.com
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret_staging)Production:
# All secrets from environment
TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=$(cat /run/secrets/proxy_secret)
TRUSTED_SERVER__EC__PASSPHRASE=$(cat /run/secrets/ec_secret)
TRUSTED_SERVER__HANDLERS__0__PASSWORD=$(cat /run/secrets/admin_password)Secret Management
Do: ✅ Use environment variables for secrets
✅ Rotate secrets periodically
✅ Generate cryptographically random values
✅ Store in secure secret management (Fastly Secret Store, Vault)
✅ Use different secrets per environment
Don't: ❌ Commit secrets to version control
❌ 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":
ec.passphrasecannot be"secret-key","secret_key", or"trusted-server"(case-insensitive)publisher.proxy_secretcannot be"change-me-proxy-secret"(case-insensitive)- Must be non-empty
- Change to a secure random value (see generation commands above)
"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; EdgeZero v0.0.4 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