Skip to content

Deployment

The Edge Function is the only FORA component that providers deploy. This page covers the deployment model, configuration, secrets management, and onboarding procedure.

The absolute minimum a provider needs to do:

  1. Get an Exchange endpoint from their Exchange operator (one-time business relationship).
  2. Deploy the edge function using the Terraform module or manual deployment (see below).
  3. Verifycurl -I https://techcrunch.com/.well-known/fora.json returns the manifest.

Target: under 10 minutes from Exchange operator contract to live edge function.

The edge function is distributed in multiple formats to match provider deployment practices.

Format 1: CloudFront Function (Reference Implementation)

Section titled “Format 1: CloudFront Function (Reference Implementation)”

The reference implementation is a vanilla JavaScript CloudFront Function that handles bot detection, signed URL verification, and fora.json serving.

module "fora_edge" {
source = "github.com/FORA-Protocol/protocol//terraform/aws-fora-edge"
cloudfront_distribution_id = aws_cloudfront_distribution.main.id
exchange_endpoint = "https://exchange.ssp.com/fora/v1"
exchange_domain = "exchange.ssp.com"
protected_paths = ["/premium/*", "/archive/*"]
signing_key_pair_id = aws_cloudfront_public_key.fora.id
}

The Terraform module provisions: CloudFront Function, cache behavior update, trusted key group association, and optional Lambda@Edge + DynamoDB table for single-use enforcement.

For providers who prefer click-to-deploy:

  • Cloudflare Apps — installable from Cloudflare dashboard. Provider enters Exchange endpoint, protected paths. The app deploys a Worker.
  • AWS CloudFormation StackSet — one-click deploy via AWS Exchange or a shared CloudFormation template.

When available, the CLI will generate the platform-specific deployment artifact (CloudFront Function JS, Cloudflare Worker, etc.) and output deployment instructions.

Configuration ItemSourceChange FrequencyExample
Exchange endpoint URLProvider-Exchange contractRarely (months)https://exchange.ssp.com/fora/v1
Exchange domainProvider-Exchange contractRarelyexchange.ssp.com
Provider relationshipProvider-Exchange contractRarelyDIRECT
Provider domainDeployment configNevertechcrunch.com
Contact emailProviderRarelylicensing@techcrunch.com
Protected path patternsProvider editorial policyMonthly["/premium/*", "/archive/*"]
Bot User-Agent patternsFORA community listWeekly["ClaudeBot", "GPTBot", ...]
Exchange key directoryProvider-Exchange contractRarelyhttps://exchange.example/.well-known/http-message-signatures-directory
Pinned verify keysOptional; skips the directory fetchRarely[{"kty":"OKP","crv":"Ed25519",...}]
Max URL TTLPolicyRarely300 (seconds)
Single-use enabledPolicyRarelytrue
Agent binding enabledPolicyRarelyfalse

Merge order (later overrides earlier):

  1. Inline / compiled defaults — provider domain, protocol version.
  2. Environment variables — the Exchange key directory URL, mode flags, Exchange endpoint. No secret: the edge holds public keys only.
  3. Edge KV — bot patterns (updated frequently), content policy rules.
  4. CMS API — dynamic content policy (which paths are newly protected/released). Only fetched if CMS integration is configured. Cached at edge with TTL.
/**
* Complete edge function configuration.
* Serializable to JSON for storage in KV.
* Secrets are resolved from environment, not stored here.
*/
interface EdgeConfigDocument {
/** Config schema version (for forward compatibility). */
config_version: "1.0";
/** Provider identity. */
provider: {
domain: string;
contact?: string;
};
/** Serving mode for fora.json and rsl.txt. */
serving_mode: "exchange" | "inline" | "kv" | "origin";
/** Exchange API base URL (required when serving_mode is "exchange"). */
exchange_api_url?: string;
/** Authorized Exchanges (becomes fora.json exchanges[]). */
exchanges: Array<{
domain: string;
endpoint: string;
relationship: "DIRECT" | "RESELLER";
}>;
/** Content access policy. */
content_policy: {
/** Default policy for paths not matching any rule. */
default: "open" | "licensed" | "blocked";
/** Path-specific rules. Evaluated in order, first match wins. */
rules: Array<{
/** Glob pattern (e.g. "/premium/*"). */
pattern: string;
/** Policy for matching paths. */
policy: "open" | "licensed" | "blocked";
}>;
};
/** Bot detection configuration. */
bot_detection: {
/** User-Agent substrings to match (case-insensitive). */
user_agent_patterns: string[];
/** Whether to use CDN-native bot scoring (platform-specific). */
use_cdn_bot_score: boolean;
/** CDN bot score threshold (0-100, lower = more likely bot). */
cdn_bot_score_threshold?: number;
};
/** Signed URL verification configuration. */
signing: {
/** Exchange key directory the edge resolves `kid` against. */
exchange_wba_url: string;
/** Optional pinned public keys; a covering `kid` skips the fetch. */
verify_keys?: JsonWebKey[];
/** Whether to enforce agent identity binding. Defaults to true. */
enforce_binding: boolean;
/** Whether to enforce single-use URLs (best-effort, see Section 5.3). */
single_use_enabled: boolean;
};
}
  • The edge function holds no secret. It verifies with a public key, so its signing configuration is a directory URL and, optionally, pinned public keys. Neither is confidential, and both may live in plain configuration or KV.
  • Configuration is loaded once at function initialization (cold start), not per-request. Where a deployment does carry a secret for unrelated reasons, it belongs in the platform secret store, never in KV.

Platform-Specific Wrangler Deployment (Cloudflare)

Section titled “Platform-Specific Wrangler Deployment (Cloudflare)”

For Cloudflare Workers, use wrangler for deployment:

Terminal window
# Local development
cd packages/edge-function && npx wrangler dev
# Production deploy
cd packages/edge-function && npx wrangler deploy
# Point the worker at the Exchange key directory
npx wrangler deploy --var EXCHANGE_WBA_URL=https://exchange.example/.well-known/http-message-signatures-directory
CDNDeploy Command
Cloudflarenpx wrangler deploy
CloudFrontDeploy CF Function + update behavior via AWS CLI or Terraform
Fastlyfastly compute deploy

ACME Domain Verification Challenge Serving (v1.0)

Section titled “ACME Domain Verification Challenge Serving (v1.0)”

During provider onboarding, the FORA CLI writes ACME challenge tokens to the CDN’s KV store. The edge function auto-serves these tokens at /.well-known/fora-verify/{token}, eliminating the need for providers to manually place files on their origin server.

fora-cli writes to KV:
Key: fora-verify:{token}
Value: {token}
TTL: 10 minutes
Edge function route:
/.well-known/fora-verify/* -> read from KV -> return as text/plain

The Exchange fetches the challenge URL during ConfirmDomainVerification to prove the provider controls the domain. After verification succeeds, the challenge token is cleaned up from KV.

For providers without the FORA edge function deployed, fora-cli provides fallback instructions to manually place the token file on their web server.

Configurable Serving Modes for fora.json and rsl.txt

Section titled “Configurable Serving Modes for fora.json and rsl.txt”
serving_mode: "exchange" # pull from Exchange API, cache in KV (recommended)
# serving_mode: "inline" # values baked into edge function config
# serving_mode: "kv" # manually populated KV
# serving_mode: "origin" # proxy to origin server

Mode: exchange (recommended)

The edge function fetches both files from the Exchange API on cache miss. This is the recommended mode for providers registered with an Exchange.

GET https://exchange.ssp-example.com/provider/techcrunch.com/fora.json
GET https://exchange.ssp-example.com/provider/techcrunch.com/rsl.txt
-> edge function caches in KV for TTL
-> serves from KV on subsequent requests
-> background refresh when TTL expires (stale-while-revalidate)

Mode: inline

Both files are baked into the edge function’s environment variables or deployment configuration.

# Environment variables
FORA_PROVIDER=techcrunch.com
FORA_EXCHANGE_DOMAIN=exchange.ssp-example.com
FORA_EXCHANGE_ENDPOINT=https://exchange.ssp-example.com/fora/v1
FORA_EXCHANGE_RELATIONSHIP=DIRECT
FORA_CONTACT=licensing@techcrunch.com

The edge function constructs fora.json and rsl.txt at startup. Simplest approach, suitable for providers with a single Exchange relationship that changes infrequently.

Mode: kv

The full fora.json and rsl.txt content is stored in the CDN’s edge KV (Workers KV, CloudFront KeyValueStore, EdgeKV, Fastly Config Store). A separate management process writes to KV when configuration changes.

Advantage: configuration changes propagate to all edge locations without redeployment.

Mode: origin

The provider’s origin server serves both files as static files. The edge function does not generate them — it only caches and serves them. This is the zero-edge-logic approach.

Disadvantage: requires the provider to manage and deploy static files.

ParameterValueRationale
Edge cache TTL3600s (1 hour)Exchange relationships change infrequently
Cache-Control sent to agentpublic, max-age=3600Agents cache locally, reducing edge load
Stale-While-Revalidate86400s (1 day)Serve stale while refreshing in background
Invalidation triggerKV write, env var change, manual purgeWhen provider updates Exchange config
Applies toBoth fora.json and rsl.txtSame caching strategy for both files

The edge function is only effective if all requests to protected content pass through it. Bypass vectors and mitigations:

Bypass VectorMitigation
Direct origin access (skipping CDN)Origin should only accept requests from CDN IPs. Use origin shield, security groups, or origin access identity (CloudFront OAI/OAC).
Alternate domain pointing to same originAll domains serving protected content must have the edge function deployed.
CDN cache serving stale content without re-verificationSet Cache-Control: private or short TTL on protected content. Or use CDN-native signed URL verification which applies to cached responses too.
Agent using a different path to the same content (URL alias)Content policy rules must cover all URL patterns that resolve to protected content.

After deployment, verify with these checks:

  1. GET /.well-known/fora.json returns valid manifest with 200 OK.
  2. GET /rsl.txt returns valid RSL content with 200 OK.
  3. GET /premium/article.html with bot User-Agent returns 403 + X-Content-Rules header.
  4. GET /premium/article.html with valid signed URL returns 200.
  5. GET /premium/article.html with expired signed URL returns 403.
  6. GET /premium/article.html with tampered signed URL returns 403.
  7. GET /free/article.html with bot User-Agent returns 200 (open content).