Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/edgezero-adapter-axum/src/key_value_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
//!
//! ## Storage Location
//!
//! By default, the development server stores data at `.edgezero/kv.redb`
//! in your project directory. Custom store names get their own derived
//! database file under `.edgezero/`. Add this path to your `.gitignore`:
//! The development server stores each declared KV id in its own file,
//! `.edgezero/kv-<slug>-<hash>.redb`, derived from the resolved store name
//! (see `kv_store_path` in `dev_server.rs`). Add this path to your `.gitignore`:
//!
//! ```gitignore
//! .edgezero/
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default defineConfig({
{ text: 'Middleware', link: '/guide/middleware' },
{ text: 'Streaming', link: '/guide/streaming' },
{ text: 'Proxying', link: '/guide/proxying' },
{ text: 'KV Storage', link: '/guide/kv' },
],
},
{
Expand Down
58 changes: 48 additions & 10 deletions docs/guide/adapters/axum.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ crates/my-app-adapter-axum/
The Axum entrypoint wires the adapter:

```rust
use edgezero_adapter_axum::dev_server::run_app;
use my_app_core::App;

fn main() -> anyhow::Result<()> {
edgezero_adapter_axum::run_app::<App>()
run_app::<App>()
}
```

Expand Down Expand Up @@ -82,20 +83,26 @@ The binary is placed in `target/release/my-app-adapter-axum`.
The Axum adapter provides a native HTTP client for proxying:

```rust
use edgezero_adapter_axum::AxumProxyClient;
use edgezero_adapter_axum::proxy::AxumProxyClient;
use edgezero_core::proxy::ProxyService;

let client = AxumProxyClient::default();
let client = AxumProxyClient::try_new()?;
let response = ProxyService::new(client).forward(request).await?;
```

This uses `reqwest` under the hood for outbound HTTP requests.
This uses `reqwest` under the hood for outbound HTTP requests. `try_new` is
fallible because it builds a `reqwest::Client`; it returns a `reqwest::Error` if
the TLS backend cannot be initialised on the host.

## Logging

The Axum adapter's `run_app` helper installs `simple_logger` and reads logging configuration
from `edgezero.toml` (level and `echo_stdout`). If you want a different logger, wire your own
entrypoint using `App::build_app()` and `AxumDevServer`.
The Axum adapter's `run_app` helper installs `simple_logger` at the level read from
`EDGEZERO__LOGGING__LEVEL`, falling back to `info` when the variable is unset or
unparseable. It does not read `edgezero.toml`, and `echo_stdout` has no effect on
the runtime. To install a different logger, set `owns_logging = true` on your `app!`
declaration so `run_app` skips its own logger, then install yours in `main`. Wiring
`App::build_app()` and `AxumDevServer` by hand remains the fallback if you also need
to control the bind address or store setup.

::: tip Logging status
`run_app` wires logging automatically; custom entrypoints should install a logger explicitly.
Expand Down Expand Up @@ -136,6 +143,31 @@ cargo test -p my-app-core
cargo test -p my-app-adapter-axum
```

## KV Storage

Each declared `[stores.kv]` id resolves to a `redb`-backed store on disk under
`.edgezero/`, so values persist across dev-server restarts. The file name is
derived from the platform store name, which comes from
`EDGEZERO__STORES__KV__<ID>__NAME` or defaults to the logical id:

```
.edgezero/kv-<slug>-<hash>.redb
```

The database file grows over time and does not shrink after deletions. To reclaim
space, delete the file in `.edgezero/`; the data is lost. See [KV Storage](/guide/kv)
for the portable API.

## Secret Store

A declared `[stores.secrets]` id resolves to an `EnvSecretStore`, which looks up each
secret name verbatim in the process environment. Axum lists `secrets` in its
`single_store_kinds`, so only one secrets id may be declared:

```bash
API_KEY=mysecret edgezero serve --adapter axum
```

## Config Store

For local development, each declared `[stores.config]` id resolves to a
Expand Down Expand Up @@ -205,8 +237,14 @@ CMD ["my-app-adapter-axum"]
Configure the Axum adapter in `edgezero.toml`. See [Configuration](/guide/configuration) for the full
manifest reference.

The `axum.toml` file is used by the Axum CLI helper to locate the crate and display the port.
The runtime currently binds to `127.0.0.1:8787` regardless of the `axum.toml` port value.
The `axum.toml` file is used by the Axum CLI helper to locate the crate and carry a
default port. `edgezero serve --adapter axum` resolves the bind address with this
precedence, highest first: the `EDGEZERO__ADAPTER__HOST` / `EDGEZERO__ADAPTER__PORT`
environment variables, then `[adapters.axum.adapter]` in `edgezero.toml`, then
`axum.toml`, then `127.0.0.1:8787`. The CLI passes the resolved address to the child
process as `EDGEZERO__ADAPTER__HOST` / `EDGEZERO__ADAPTER__PORT`. Running the binary
directly bypasses that resolution: it reads only those two environment variables and
otherwise falls back to `127.0.0.1:8787`.

## Development Workflow

Expand All @@ -231,7 +269,7 @@ A typical development workflow:
| Concurrency | Multi-threaded | Single-threaded |

::: tip Development Parity
While Axum provides a convenient development environment, always test on actual edge platforms before deploying. Some edge-specific features (KV stores, geolocation) aren't available in the Axum adapter.
While Axum provides a convenient development environment, always test on actual edge platforms before deploying. Some edge-specific features (geolocation) aren't available in the Axum adapter.
:::

## Next Steps
Expand Down
48 changes: 39 additions & 9 deletions docs/guide/adapters/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ The Wrangler manifest configures your Worker:
```toml
name = "my-app"
main = "build/worker/shim.mjs"
compatibility_date = "2024-01-01"
compatibility_date = "2023-05-01"

[build]
command = "edgezero build --adapter cloudflare"
command = "worker-build --release"
```

### Entrypoint
Expand All @@ -56,10 +56,14 @@ derived from the baked store ids and queried individually). Per-id
request extensions automatically. No `edgezero.toml` is loaded by
the runtime — see [the migration guide](../manifest-store-migration.md).

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`.
For fully manual wiring, `CloudflareService::new(&app)` builds a dispatcher one
store at a time: `.with_config(binding)` (a KV binding name),
`.with_config_handle(handle)`, `.with_kv(binding)`, `.with_secrets()`, the
matching `.require_kv()` / `.require_secrets()` flags, and finally
`.dispatch(req)`. This path takes bindings verbatim and does not resolve
`EDGEZERO__STORES__*` selectors, so prefer `run_app` unless you are mocking a
backend. `dispatch_with_registries` is the registry-based dispatcher `run_app`
itself calls.

## Building

Expand Down Expand Up @@ -98,7 +102,7 @@ wrangler deploy --cwd crates/my-app-adapter-cloudflare
Cloudflare Workers use the global `fetch` API for outbound requests:

```rust
use edgezero_adapter_cloudflare::CloudflareProxyClient;
use edgezero_adapter_cloudflare::proxy::CloudflareProxyClient;
use edgezero_core::proxy::ProxyService;

let client = CloudflareProxyClient;
Expand All @@ -124,7 +128,7 @@ Access Cloudflare-specific APIs via the request context extensions:

```rust
use edgezero_core::context::RequestContext;
use edgezero_adapter_cloudflare::CloudflareRequestContext;
use edgezero_adapter_cloudflare::context::CloudflareRequestContext;

async fn handler(ctx: RequestContext) -> Result<Response, EdgeError> {
if let Some(cf_ctx) = CloudflareRequestContext::get(ctx.request()) {
Expand Down Expand Up @@ -174,11 +178,37 @@ id = "abc123…"

The binding name comes from `EDGEZERO__STORES__CONFIG__APP_CONFIG__NAME`
(defaulting to the logical id `app_config` when unset). Populate the
namespace via `wrangler kv:key put`. Missing bindings log a one-time
namespace via `wrangler kv key put`. Missing bindings log a one-time
warning and the id is dropped from the registry. See
[the migration guide](../manifest-store-migration.md) if you are coming
from the pre-rewrite `[vars]`-backed JSON-string form.

KV and config share the same `[[kv_namespaces]]` binding space on Cloudflare,
so the same logical id must not appear under both `[stores.kv]` and
`[stores.config]`; both would resolve to a single underlying namespace at
runtime. `edgezero config validate` rejects the collision.

## Secret Store

Worker Secrets is a single flat bag with no namespace concept, so exactly one
`[stores.secrets]` id is permitted; `edgezero config validate --strict` rejects
more than one. Handlers read values through the `Secrets` extractor or
`ctx.secret_store(id)`, and a secret with no matching binding resolves to `None`
rather than erroring.

```toml
# edgezero.toml
[stores.secrets]
ids = ["default"]
```

Populate secrets with the Wrangler CLI; there is no binding flag, since the
secret name is the binding:

```bash
wrangler secret put API_KEY
```

## KV Storage

Use Cloudflare KV for edge storage:
Expand Down
82 changes: 71 additions & 11 deletions docs/guide/adapters/fastly.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ crates/my-app-adapter-fastly/
The Fastly manifest configures your service:

```toml
manifest_version = 2
manifest_version = 3
name = "my-app"
language = "rust"
authors = ["you@example.com"]
Expand All @@ -36,6 +36,11 @@ authors = ["you@example.com"]
url = "https://your-origin.example.com"
```

`edgezero provision --adapter fastly` writes `[setup.kv_stores]`,
`[setup.secret_stores]` and `[setup.config_stores]` entries into `fastly.toml`
for the declared store ids; the `[local_server.*]` tables are the Viceroy-only
mirror of those stores.

### Entrypoint

The Fastly entrypoint wires the adapter:
Expand All @@ -56,10 +61,14 @@ 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`.
For fully manual wiring, `FastlyService::new(&app)` builds a dispatcher one
store at a time: `.with_config(name)`, `.with_config_handle(handle)`,
`.with_kv(name)`, `.with_secrets()`, the matching `.require_kv()` /
`.require_secrets()` flags, and finally `.dispatch(req)`. This path does not
apply the runtime env overlay. A bare handle binds the config registry's
default key to `"default"` and does not resolve `EDGEZERO__STORES__*`
selectors, so prefer `run_app`, or see
[Custom entry points](#custom-entry-points) for full parity.

### Capturing raw-request signals (JA4, H2 fingerprint)

Expand Down Expand Up @@ -105,6 +114,55 @@ 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.

### Custom entry points

Compute@Edge has no process environment, so the `EDGEZERO__*` runtime overrides
(logging settings, per-store platform names, the config-store `__KEY` selector)
are read from a Fastly Config Store named `edgezero_runtime_env`, exported as
`RUNTIME_ENV_STORE_NAME`. Entries in that store are service-scoped
(`EDGEZERO__SERVICES__<SERVICE_ID>__…`, see [Config Store](#config-store));
`runtime_env_config` translates them back to the canonical unscoped keys the
rest of the runtime reads. The name is fixed because staged deploys rely on it:
a staged deploy creates a per-service staging twin and links it into the staged
version under that same name. `run_app` and `run_app_with_request_extensions`
read the store for you.

An entry point that does its own wiring must call `runtime_env_config` itself,
derive `FastlyLogging` from the result, and dispatch through
`dispatch_with_registries`:

```rust
use edgezero_adapter_fastly::request::dispatch_with_registries;
use edgezero_adapter_fastly::{FastlyLogging, init_logger, runtime_env_config};
use edgezero_core::app::Hooks as _;
use my_app_core::App;

#[fastly::main]
fn main(req: fastly::Request) -> Result<fastly::Response, fastly::Error> {
let stores = App::stores();
let env = runtime_env_config(stores);
let logging = FastlyLogging::from(&env);
if logging.use_fastly_logger {
let endpoint = logging.endpoint.as_deref().unwrap_or("stdout");
init_logger(endpoint, logging.level, logging.echo_stdout).expect("init logger");
}
let app = App::build_app();
Ok(dispatch_with_registries(&app, req, stores, &env, |_req, _ext| {})?)
}
```

Two footguns live on this path. `run_app_with_config` and a hand-built
`FastlyService` do **not** apply the env overlay, so staged and overridden
`__NAME` / `__KEY` selectors are silently ignored and every store falls back to
its baked-in default. And a hand-written `Hooks` impl inherits the default
`stores()`, which is empty; empty metadata derives no `EDGEZERO__STORES__*` keys
at all, so no override ever resolves. Such an impl must override `stores()` or
pass explicit `StoresMetadata`.

`FastlyLogging::from(&EnvConfig)` derives `use_fastly_logger` from
`endpoint.is_some()`, which is what keeps a local Viceroy run off the reserved
`stdout` endpoint when no endpoint is configured.

## Building

Build for Fastly's Wasm target:
Expand All @@ -113,8 +171,8 @@ 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 --release
# Or directly
fastly compute build -C crates/my-app-adapter-fastly
```

The compiled Wasm binary is placed in `target/wasm32-wasip1/release/`.
Expand All @@ -128,7 +186,7 @@ Run locally with Viceroy (Fastly's local simulator):
edgezero serve --adapter fastly

# Or directly
fastly compute serve --skip-build
fastly compute serve -C crates/my-app-adapter-fastly
```

This starts a local server at `http://127.0.0.1:7676`.
Expand All @@ -142,7 +200,7 @@ Deploy to Fastly Compute@Edge:
edgezero deploy --adapter fastly

# Or directly
fastly compute deploy
fastly compute deploy -C crates/my-app-adapter-fastly
```

## Backends
Expand All @@ -151,7 +209,7 @@ EdgeZero's Fastly proxy client uses **dynamic backends** derived from the target
You do not need to predeclare backends in `fastly.toml` for EdgeZero proxying.

```rust
use edgezero_adapter_fastly::FastlyProxyClient;
use edgezero_adapter_fastly::proxy::FastlyProxyClient;
use edgezero_core::proxy::ProxyService;

let client = FastlyProxyClient;
Expand Down Expand Up @@ -264,7 +322,9 @@ async fn handler(ctx: RequestContext) -> Result<Response, EdgeError> {

## Streaming

Fastly supports native streaming via `stream_to_client`. The adapter automatically converts `Body::stream` to Fastly's streaming APIs.
A `Body::Stream` response is drained into a `fastly::Body` before the adapter
returns, so the full payload is materialised in memory rather than streamed to
the client chunk by chunk.

See the [Streaming guide](/guide/streaming) for examples and patterns.

Expand Down
Loading
Loading