Fastly Compute@Edge
Deploy EdgeZero applications to Fastly's Compute@Edge platform using WebAssembly.
Prerequisites
- Fastly CLI
- Rust
wasm32-wasip1target:rustup target add wasm32-wasip1 - Viceroy for local execution and testing
Project Setup
When scaffolding with edgezero new my-app, the Fastly adapter includes:
crates/my-app-adapter-fastly/
├── Cargo.toml
├── fastly.toml
└── src/
└── main.rsfastly.toml
The Fastly manifest configures your service:
manifest_version = 2
name = "my-app"
language = "rust"
authors = ["you@example.com"]
[local_server]
[local_server.backends]
[local_server.backends."origin"]
url = "https://your-origin.example.com"Entrypoint
The Fastly entrypoint wires the adapter:
use my_app_core::App;
#[fastly::main]
fn main(req: fastly::Request) -> Result<fastly::Response, fastly::Error> {
edgezero_adapter_fastly::run_app::<App>(req)
}run_app reads logging and store config at runtime from EDGEZERO__* environment variables (see the migration guide) and builds per-id KV / Config / Secret registries from the portable store metadata baked into App by the app! macro. No edgezero.toml is loaded by the runtime.
The low-level dispatch() helper remains available only for fully manual wiring and does not inject store metadata. Prefer run_app or dispatch_with_config for normal use. dispatch_with_config_handle exists for advanced/manual cases where you already have a prepared ConfigStoreHandle.
Capturing raw-request signals (JA4, H2 fingerprint)
run_app converts the fastly::Request into a neutral core request before dispatch. The client IP is carried across automatically — read it via FastlyRequestContext (see Context Access below). Other Fastly-only signals that are readable only on the raw request (get_tls_ja4(), get_client_h2_fingerprint()) aren't reachable from handlers by default. Use run_app_with_request_extensions, which runs an app closure against a scratch Extensions before conversion and merges the values into the core request — so a State/extractor or middleware can read them:
#[derive(Clone)]
struct Ja4(String);
#[fastly::main]
fn main(req: fastly::Request) -> Result<fastly::Response, fastly::Error> {
edgezero_adapter_fastly::run_app_with_request_extensions::<App, _>(req, |raw, ext| {
if let Some(ja4) = raw.get_tls_ja4() {
ext.insert(Ja4(ja4.to_owned()));
}
})
}run_app is exactly run_app_with_request_extensions::<App, _>(req, |_, _| {}). The closure runs once per request; insert whatever typed values your handlers need, then read them in a handler via a custom extractor or ctx.request().extensions().get::<Ja4>().
Owning your own logging
By default run_app initializes the Fastly logger. If your app already installs a log backend, opt out with the platform-neutral Hooks::owns_logging() flag — via the app! macro:
edgezero_core::app!("edgezero.toml", owns_logging = true);or on a hand-written Hooks impl (fn owns_logging() -> bool { true }). Every adapter's run_app honors it, so the app is responsible for logger setup.
Building
Build for Fastly's Wasm target:
# Using the CLI
edgezero build --adapter fastly
# Or directly with cargo
cargo build -p my-app-adapter-fastly --target wasm32-wasip1 --releaseThe compiled Wasm binary is placed in target/wasm32-wasip1/release/.
Local Development
Run locally with Viceroy (Fastly's local simulator):
# Using the CLI
edgezero serve --adapter fastly
# Or directly
fastly compute serve --skip-buildThis starts a local server at http://127.0.0.1:7676.
Deployment
Deploy to Fastly Compute@Edge:
# Using the CLI
edgezero deploy --adapter fastly
# Or directly
fastly compute deployBackends
EdgeZero's Fastly proxy client uses dynamic backends derived from the target URI (host + scheme). You do not need to predeclare backends in fastly.toml for EdgeZero proxying.
use edgezero_adapter_fastly::FastlyProxyClient;
use edgezero_core::proxy::ProxyService;
let client = FastlyProxyClient;
let response = ProxyService::new(client).forward(request).await?;Logging
Fastly uses endpoint-based logging. Configure logging in edgezero.toml:
[adapters.fastly.logging]
endpoint = "stdout"
level = "info"
echo_stdout = trueTo initialize logging manually, call init_logger with explicit settings:
use edgezero_adapter_fastly::init_logger;
use log::LevelFilter;
fn main() {
init_logger("stdout", LevelFilter::Info, true).expect("init logger");
}Logging status
Fastly logging is wired when you call init_logger (or run_app); otherwise no logger is installed.
Config Store
Fastly uses a native Config Store resource link for runtime configuration. Declare logical config ids in edgezero.toml; each id opens its own platform store via EDGEZERO__STORES__CONFIG__<ID>__NAME (default = the logical id):
[stores.config]
ids = ["app_config"]
# default = "app_config" # required when ids.len() > 1For local Viceroy testing, mirror the platform name in fastly.toml:
[local_server.config_stores.app_config]
format = "inline-toml"
[local_server.config_stores.app_config.contents]
greeting = "hello from config store"Handlers read values through the Config extractor or ctx.config_store(id):
async fn handler(config: Config) -> Result<Response, EdgeError> {
let store = config.named("app_config").ok_or_else(|| EdgeError::service_unavailable("no `app_config`"))?;
let greeting = store.get("greeting").await?.unwrap_or_default();
// …
}If a configured store link is missing, the adapter logs a one-time warning and drops that id from the registry. Migrating from name/adapters.*? See the migration guide.
Context Access
Access Fastly-specific APIs via the request context extensions:
use edgezero_core::context::RequestContext;
use edgezero_adapter_fastly::context::FastlyRequestContext;
async fn handler(ctx: RequestContext) -> Result<Response, EdgeError> {
// Access Fastly context from extensions
if let Some(fastly_ctx) = FastlyRequestContext::get(ctx.request()) {
let client_ip = fastly_ctx.client_ip;
// ...
}
// ...
}Streaming
Fastly supports native streaming via stream_to_client. The adapter automatically converts Body::stream to Fastly's streaming APIs.
See the Streaming guide for examples and patterns.
Testing
Run contract tests for the Fastly adapter:
cargo install viceroy --locked
export CARGO_TARGET_WASM32_WASIP1_RUNNER="viceroy run"
# Run tests
cargo test -p edgezero-adapter-fastly --features fastly --target wasm32-wasip1 --test contractFastly SDK-linked Wasm binaries require Viceroy for execution; plain Wasmtime does not provide the fastly_* host imports needed by the adapter tests.
Local Execution
If Viceroy reports native certificate or keychain errors on macOS, use --no-run locally and rely on Linux CI for execution.
Manifest Configuration
Configure the Fastly adapter in edgezero.toml. See Configuration for the full manifest reference.
Next Steps
- Learn about Cloudflare Workers as an alternative deployment target
- Explore Configuration for manifest details