Docker-based deployer that handles building and publishing the Confidence resolver Worker to your Cloudflare account. The resolver enables feature flag evaluation at Cloudflare's edge locations worldwide, powered by the Confidence Resolver.
- Edge evaluation: Flag rules evaluate at Cloudflare's edge locations worldwide
- Ultra-low latency: Evaluation happens close to users, minimizing latency
- Rust-based resolver: High-performance flag evaluation powered by the Confidence Resolver
- Deployer-driven sync: Run the deployer to fetch the latest flag rules from Confidence and re-deploy the Worker
From the root of the repository, run:
docker build --target confidence-cloudflare-resolver.deployer -t <YOUR_IMAGE_NAME> .A pre-built image is also available at ghcr.io/spotify/confidence-cloudflare-deployer:latest for both linux/amd64 and linux/arm64.
- Docker installed
- Cloudflare API token with the following permissions:
- Account > Workers Scripts > Edit
- Account > Workers Queues > Edit (needed for the first deploy)
- Account > Workers KV Storage > Edit (only if using
ENABLE_METRICSorENABLE_STICKY_ASSIGNMENTS)
- Confidence client secret (must be type BACKEND)
Run the deployer with your credentials:
docker run -it \
-e CLOUDFLARE_API_TOKEN='your-cloudflare-api-token' \
-e CONFIDENCE_CLIENT_SECRET='your-confidence-client-secret' \
ghcr.io/spotify/confidence-cloudflare-deployer:latestThe deployer automatically:
- Detects Cloudflare account ID from your API token
- Creates the queue (
flag-logs-queue) if it doesn't exist - Fetches resolver state from Confidence CDN
- Skips deployment if state hasn't changed (using ETags)
Note: The deployer does not poll for changes. Each run fetches the current state from Confidence, deploys the Worker if the state has changed, and then exits. To keep the Worker up to date, run the deployer on a schedule (for example, via a cron job) or trigger it when flag rules or targeting changes are made in Confidence.
| Variable | Description |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
Required only if the API token has access to multiple accounts |
CONFIDENCE_RESOLVER_STATE_URL |
Custom resolver state URL (overrides default URL to Confidence CDN) |
CONFIDENCE_RESOLVER_ALLOWED_ORIGIN |
Configure allowed origins for CORS |
RESOLVE_TOKEN_ENCRYPTION_KEY |
AES-128 key (base64-encoded, 16 bytes). Used to encrypt resolve tokens for apply=false. Auto-generated on first deploy if not provided, and stored as a Cloudflare Worker secret |
FORCE_DEPLOY |
Force re-deploy regardless of state changes |
NO_DEPLOY |
Build only, skip deployment |
WORKER_NAME_PREFIX |
Prefix for worker and queue names. Deploys as <prefix>-confidence-cloudflare-resolver with queue <prefix>-flag-logs-queue (auto-created) |
WRANGLER_CONFIG_APPEND_FILE |
Path to a file containing TOML to append to the generated wrangler.toml |
WRANGLER_DEPLOY_TAG |
Value passed to wrangler deploy --tag |
WRANGLER_DEPLOY_MESSAGE |
Value passed to wrangler deploy --message |
WRANGLER_DEPLOY_ARGS |
Additional newline-separated arguments passed to wrangler deploy |
WRANGLER_DEPLOY_ARGS_FILE |
Path to a file containing additional wrangler deploy arguments, one argument per line |
ENABLE_METRICS |
Set to create a KV namespace and enable the /metrics Prometheus endpoint. Requires a KV store |
ENABLE_STICKY_ASSIGNMENTS |
Set to create a KV namespace and enable sticky assignments for experiments. Requires a KV store |
MATERIALIZATION_TTL_SECONDS |
TTL in seconds for sticky assignment KV entries. Omit for no expiration |
FORCE_APPLY |
Defaults to true: every resolve is treated as apply=true and assignments are logged at resolve time. Set to false to respect the apply value sent by SDKs (deferred-apply flow via flags:apply) |
Use WRANGLER_CONFIG_APPEND_FILE when your Cloudflare account needs configuration that is not managed by the deployer, such as observability destinations or tail consumers.
Example:
wrangler-extra.toml
[[tail_consumers]]
service = "my-tail-worker"
[observability.logs]
enabled = true
destinations = ["otel-gateway-logs"]
head_sampling_rate = 1.0docker run -it \
-v "$PWD/wrangler-extra.toml:/tmp/wrangler-extra.toml:ro" \
-e CLOUDFLARE_API_TOKEN='your-cloudflare-api-token' \
-e CONFIDENCE_CLIENT_SECRET='your-confidence-client-secret' \
-e WRANGLER_CONFIG_APPEND_FILE='/tmp/wrangler-extra.toml' \
-e WRANGLER_DEPLOY_TAG='production-2026-05-05' \
-e WRANGLER_DEPLOY_MESSAGE='Deploy resolver state with tail worker logs' \
ghcr.io/spotify/confidence-cloudflare-deployer:latestThe snippet is appended after the deployer has written its generated settings. To avoid top-level keys being parsed inside an existing table, the first non-comment line must be a TOML table header such as [[tail_consumers]] or [observability.logs].
Use WRANGLER_DEPLOY_TAG and WRANGLER_DEPLOY_MESSAGE to label the deployed Worker version and deployment in Cloudflare.
docker run -it \
-e CLOUDFLARE_API_TOKEN='your-cloudflare-api-token' \
-e CONFIDENCE_CLIENT_SECRET='your-confidence-client-secret' \
-e WRANGLER_DEPLOY_TAG='production-2026-05-05' \
-e WRANGLER_DEPLOY_MESSAGE='Update embedded resolver state' \
ghcr.io/spotify/confidence-cloudflare-deployer:latestFor less common Wrangler deploy flags, use WRANGLER_DEPLOY_ARGS or WRANGLER_DEPLOY_ARGS_FILE with one argument per line. Prefer WRANGLER_DEPLOY_TAG and WRANGLER_DEPLOY_MESSAGE for tags and messages so values may contain spaces safely.
When integrating with the Cloudflare resolver, you have two options:
Service binding (recommended): Cloudflare's service bindings allow Workers to call other Workers directly within Cloudflare's network. This internal routing bypasses the public internet, resulting in ultra-low latency.
HTTP calls: Standard HTTP requests to the resolver endpoint. Use this approach when calling from external services or client applications.
- Add a service binding to your
wrangler.json:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-02-04",
"services": [
{
"binding": "ConfidenceBinding",
"service": "confidence-cloudflare-resolver"
}
]
}- Use the SDK with
fetchImplementationandwaitUntil:
import { Confidence } from '@spotify-confidence/sdk';
interface Env {
CONFIDENCE_CLIENT_SECRET: string;
ConfidenceBinding: {
fetch: (request: Request) => Promise<Response>;
};
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const confidence = Confidence.create({
clientSecret: env.CONFIDENCE_CLIENT_SECRET,
environment: 'backend',
fetchImplementation: (req: Request) => env.ConfidenceBinding.fetch(req),
timeout: 1000,
waitUntil: (p) => ctx.waitUntil(p),
});
const flag = await confidence
.withContext({ targeting_key: 'user-123' })
.evaluateFlag('my-flag', {});
return new Response(JSON.stringify({ flag }), {
headers: { 'Content-Type': 'application/json' },
});
},
} satisfies ExportedHandler<Env>;fetchImplementationroutes resolve requests through the service binding instead of the public internet.waitUntilkeeps the Worker alive for background tasks (apply events, telemetry) after the response is sent. Without it, these fire-and-forget calls are silently dropped.environment: 'backend'is required for server-side usage.
For more details, see the Confidence documentation.
The resolver collects telemetry and exposes a Prometheus-compatible /metrics endpoint using the same metric names as all other Confidence providers (confidence_resolve_latency_microseconds, confidence_resolves_total).
Cloudflare Workers freeze Date.now() and performance.now() during synchronous CPU work (Spectre mitigation). The resolver uses scheduler.wait(0) — a zero-delay yield to the runtime — to unfreeze the clock after each resolve. This provides 1ms resolution with no measurable overhead.
Requires authentication:
curl -H "Authorization: ClientSecret <your-client-secret>" \
https://<worker>.workers.dev/metricsReturns Prometheus exposition format with:
confidence_resolve_latency_microseconds— histogram (sum, count, cumulativelebuckets)confidence_resolves_total— counter by resolve reason
Metrics are accumulated in a KV namespace (CONFIDENCE_METRICS_KV). Set ENABLE_METRICS to have the deployer create the KV namespace and bind it to the Worker. Without it, the /metrics endpoint returns empty and no KV writes occur.
Resolve rates and latency are always sent to the Confidence backend via WriteFlagLogsRequest, regardless of the ENABLE_METRICS setting. The /metrics endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently.
Sticky assignments ensure users see the same experiment variant across requests. Set ENABLE_STICKY_ASSIGNMENTS to have the deployer create a KV namespace and bind it to the Worker.
Each assignment is stored as a separate KV entry keyed by mat:{unit}:{materialization}:{rule}. This avoids read-modify-write races and allows all keys to be read/written independently. Components containing : are percent-encoded.
Without ENABLE_STICKY_ASSIGNMENTS, sticky assignments are disabled and flags requiring them will return "flag not found".
The resolver supports deferred apply: when a client sends apply=false, the resolve response includes an encrypted resolve token instead of immediately logging the exposure. The client later sends this token to the /v1/flags:apply endpoint to record the exposure at the time the flag value was actually shown to the user.
An AES-128 encryption key is required for this flow. The deployer manages the key automatically:
- If
RESOLVE_TOKEN_ENCRYPTION_KEYis provided, it is used and stored as a Cloudflare Worker secret. - If not provided, the deployer checks if a secret already exists on the worker from a previous deploy.
- If no secret exists, a random key is generated and stored as a Cloudflare Worker secret.
The key persists across deploys via Cloudflare's secret storage. To rotate the key, set RESOLVE_TOKEN_ENCRYPTION_KEY to a new value — tokens encrypted with the old key will no longer be valid.
- No runtime state updates — Flag rules only update on redeployment.