This guide explains how to configure Fastly Compute@Edge to create a reverse proxy that serves the Didomi Consent notice from your own domain and a subdomain. Two implementation options are available based on your requirements.
To implement a reverse proxy on a subdomain, you will first create a lightweight Rust application compiled to WebAssembly, then configure Fastly backends and deploy the WASM binary. This approach uses minimal edge processing with simple backend routing.
Customer Usage: /api/* and /sdk/* paths directly
Architecture: Fastly with minimal WASM processing
Implementation: Simple backend routing with lightweight Rust code
Option B: Use the main domain
To implement a reverse proxy on the main domain, you will first create a Rust application with URL transformation logic, then compile it to WebAssembly and deploy to Fastly Compute@Edge. The application handles /consent/* prefix removal and routes requests to appropriate Didomi backends.
Customer Usage: /consent/* prefix for all CMP requests
Architecture: Fastly Compute@Edge with full URL transformation
Implementation: URL transformation and advanced processing
Domain vs Subdomain Trade-offs
When implementing a reverse proxy for the Didomi SDK and its API events, you need to choose between using your main domain or a dedicated subdomain. This choice has important implications for Safari's cookie restrictions.
For more information, see this trade-off matrix to select the implementation that suits your requirements.
After setting up your reverse proxy, update your Didomi SDK snippet to use your own domain instead of privacy-center.org. This ensures that the Didomi assets are served from your configured domain.
# Configure A records pointing to Fastly IP addresses
# Get current Fastly IP addresses from: <https://docs.fastly.com/en/guides/accessing-fastlys-ip-ranges>
# Example A records (verify current IPs with Fastly):
YOUR_DOMAIN_NAME. 300 IN A 151.101.1.140
YOUR_DOMAIN_NAME. 300 IN A 151.101.65.140
YOUR_DOMAIN_NAME. 300 IN A 151.101.129.140
YOUR_DOMAIN_NAME. 300 IN A 151.101.193.140
# Configure CNAME record for www subdomain
www.YOUR_DOMAIN_NAME. 300 IN CNAME YOUR_DOMAIN_NAME.
# Example provided by Fastly (replace with your actual values)
_acme-challenge.YOUR_DOMAIN_NAME CNAME YOUR_CHALLENGE_TOKEN.fastly-validations.com
# Verify the ACME challenge CNAME is propagated
dig _acme-challenge.YOUR_DOMAIN_NAME CNAME +short
# Should return: YOUR_CHALLENGE_TOKEN.fastly-validations.com
use fastly::http::{header, Method, StatusCode};
use fastly::{Backend, Error, Request, Response};
use log_fastly::Logger;
#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
init_logger();
log::info!("Processing request: {} {}", req.get_method(), req.get_path());
route_request(req)
}
/// Routes incoming requests to appropriate Didomi backends.
///
/// Implements two routing strategies:
/// - /api/* routes -> didomi_api backend
/// - /sdk/* routes -> didomi_sdk backend
/// - All other routes -> 404 Not Found
fn route_request(req: Request) -> Result<Response, Error> {
let path = req.get_path();
match req.get_method() {
&Method::GET | &Method::POST | &Method::PUT | &Method::DELETE | &Method::PATCH => {
if path.starts_with("/api/") {
// Route API requests to Didomi API backend
log::info!("Routing API request to didomi_api backend: {}", path);
proxy_to_backend(req, "didomi_api")
} else if path.starts_with("/sdk/") {
// Route SDK requests to Didomi SDK backend
log::info!("Routing SDK request to didomi_sdk backend: {}", path);
proxy_to_backend(req, "didomi_sdk")
} else {
// Return 404 for unmatched routes
log::info!("No matching route found for: {}", path);
Ok(not_found_response())
}
}
_ => {
// Return 405 Method Not Allowed for unsupported methods
log::info!("Method not allowed: {}", req.get_method());
Ok(method_not_allowed_response())
}
}
}
/// Proxies a request to the specified backend.
fn proxy_to_backend(mut req: Request, backend_name: &str) -> Result<Response, Error> {
// Set up backend
let backend = Backend::from_name(backend_name)?;
// Add CORS headers for preflight requests
if req.get_method() == &Method::OPTIONS {
return Ok(cors_preflight_response());
}
// Forward important headers
let user_agent = req.get_header_str("User-Agent").unwrap_or("Fastly-Proxy/1.0").to_string();
req.set_header("Host", get_backend_host(backend_name));
req.set_header("User-Agent", &user_agent);
// Send request to backend
let mut resp = req.send(backend)?;
// Add CORS headers to response
resp.set_header("Access-Control-Allow-Origin", "*");
resp.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
resp.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
resp.set_header("Access-Control-Max-Age", "86400");
// Set appropriate cache headers
match backend_name {
"didomi_sdk" => {
// SDK resources can be cached for 1 hour
resp.set_header("Cache-Control", "public, max-age=3600");
}
"didomi_api" => {
// API responses should not be cached
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
resp.set_header("Pragma", "no-cache");
resp.set_header("Expires", "0");
}
_ => {}
}
log::info!("Successfully proxied request to {} backend", backend_name);
Ok(resp)
}
/// Returns the appropriate host header for the backend.
fn get_backend_host(backend_name: &str) -> &'static str {
match backend_name {
"didomi_sdk" => "sdk.privacy-center.org",
"didomi_api" => "api.privacy-center.org",
_ => "unknown"
}
}
/// Creates a CORS preflight response.
fn cors_preflight_response() -> Response {
Response::from_status(StatusCode::OK)
.with_header("Access-Control-Allow-Origin", "*")
.with_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
.with_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
.with_header("Access-Control-Max-Age", "86400")
.with_header("Content-Length", "0")
}
/// Creates a standard 404 Not Found response.
fn not_found_response() -> Response {
Response::from_status(StatusCode::NOT_FOUND)
.with_body("Not Found - Only /api/* and /sdk/* routes are supported")
.with_header(header::CONTENT_TYPE, "text/plain")
.with_header("Access-Control-Allow-Origin", "*")
}
/// Creates a 405 Method Not Allowed response.
fn method_not_allowed_response() -> Response {
Response::from_status(StatusCode::METHOD_NOT_ALLOWED)
.with_body("Method Not Allowed")
.with_header(header::CONTENT_TYPE, "text/plain")
.with_header("Allow", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
.with_header("Access-Control-Allow-Origin", "*")
}
/// Initializes the logger for debugging and monitoring.
fn init_logger() {
let logger = Logger::builder()
.default_endpoint("cmp_proxy_log")
.echo_stdout(true)
.max_level(log::LevelFilter::Info)
.build()
.expect("Failed to build Logger");
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{} [{}] {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"),
record.level(),
message
))
})
.chain(Box::new(logger) as Box<dyn log::Log>)
.apply()
.expect("Failed to initialize logger");
}
# Build for WebAssembly target
cargo build --target wasm32-wasip1 --release
# Start local development server
fastly compute serve --service-id YOUR_SERVICE_ID
# Test CMP endpoints (replace YOUR_API_KEY and NOTICE_ID with actual values, e.g., YOUR_API_KEY=24cd3901-9da4-4643-96a3-9b1c573b5264, NOTICE_ID=J3nR2TTU)
curl <http://127.0.0.1:7676/consent/YOUR_API_KEY/loader.js?target_type=notice&target=NOTICE_ID>
# Deploy to production
fastly compute publish
# Verify deployment
curl https://YOUR_DOMAIN_NAME/consent/YOUR_API_KEY/loader.js?target_type=notice&target=NOTICE_ID