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 store 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
5 changes: 3 additions & 2 deletions crates/edgezero-core/src/key_value_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,8 +912,9 @@ pub trait KvStore: Send + Sync {
/// should be treated as expired. Eviction timing is backend-specific:
/// - **Axum (`PersistentKvStore`)**: lazy eviction — expired keys are removed
/// on the next `get_bytes` call for that key. Keys never accessed after
/// expiration remain in the database until deleted, so `.edgezero/kv.redb`
/// grows without bound on long-running dev servers.
/// expiration remain in the database until deleted, so the store's
/// `.edgezero/kv-<slug>-<hash>.redb` file grows without bound on

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 The rewritten comment is better on the filename, but keeps two claims the Axum backend contradicts.

It says expired keys "are removed on the next get_bytes call for that key", and that keys never accessed "remain in the database until deleted, so the store's … file grows without bound".

list_keys also sweeps: it collects expired_keys during the range scan and calls self.cleanup_expired_keys(&expired_keys)? (crates/edgezero-adapter-axum/src/key_value_store.rs:341-353). The type's own doc says as much — "lazily evicted (checked on read/list)" (same file:66).

Fix: "…removed on the next get_bytes for that key, or by any list_keys scan that reaches it. Keys neither read nor listed after expiration remain in the file."

/// long-running dev servers.
/// - **Fastly/Cloudflare**: eviction is managed by the platform and is not
/// guaranteed to be immediate.
async fn put_bytes_with_ttl(
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>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ At :38, "run_app installs simple_logger, builds the app, and reads bind / store / logging config…" is unconditional, but crates/edgezero-adapter-axum/src/dev_server.rs:343 guards it:

if !A::owns_logging() {}

The Logging section this PR added at :99-105 documents that correctly, so the two are 60 lines apart and disagree. Suggest "installs simple_logger (unless owns_logging = true)".

}
```

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. Provider-specific behaviour such as store backends and request context differs on the real targets.
:::

## Next Steps
Expand Down
71 changes: 61 additions & 10 deletions docs/guide/adapters/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Deploy EdgeZero applications to Cloudflare Workers using WebAssembly.
## Prerequisites

- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
- worker-builder: `cargo install worker-builder`
- worker-build: `cargo install worker-build`
- Rust `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown`

## Project Setup
Expand All @@ -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,35 @@ 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, env, ctx).await`, which needs the worker `Env` and `Context`
to open bindings:

```rust
use edgezero_adapter_cloudflare::request::CloudflareService;
use edgezero_core::app::Hooks as _;
use my_app_core::App;
use worker::*;

#[event(fetch)]
pub async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
let app = App::build_app();
CloudflareService::new(&app)
.with_config("APP_CONFIG")
.with_kv("APP_KV")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.with_config("APP_CONFIG") / .with_kv("APP_KV") — not wrong (this path takes bindings verbatim, as :83 says), but every other snippet in the file uses the lowercase logical-id form the adapter actually defaults to (binding = "app_config" at :196; default is the logical id per EnvConfig::store_name).

Uppercase reads like a Cloudflare naming convention EdgeZero never emits. Suggest .with_config("app_config") / .with_kv("sessions").

.dispatch(req, env, ctx)
.await
}
```

This path takes bindings verbatim and does not resolve `EDGEZERO__STORES__*`
selectors, so prefer `run_app` unless you are mocking a backend.
`run_app` dispatches through an internal registry-based path; unlike Fastly's
`dispatch_with_registries`, it is not part of the Cloudflare adapter's public
API.

## Building

Expand Down Expand Up @@ -98,7 +123,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 +149,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 +199,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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This new section is right, which leaves "Environment Variables & Secrets" at :166-178 duplicating and contradicting it.

This section correctly says handlers read secrets through the Secrets extractor (crates/edgezero-core/src/extractor.rs:609) or ctx.secret_store(id) (crates/edgezero-core/src/context.rs:200). The older section still says "Access in handlers via the Cloudflare context or environment bindings" (:178), and wrangler secret put API_KEY now appears twice on the page (:175 and :230).

Suggest folding :166-178 into this section, keeping only the [vars] / non-secret half if that is still wanted.

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
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 The KV Storage section immediately below (:233-244) predates the store-registry rewrite and was skipped.

It tells the reader to write binding = "MY_KV" and then "Access via the Cloudflare environment bindings in your handler." Neither matches the runtime: bindings are derived from [stores.kv].ids via EDGEZERO__STORES__KV__<ID>__NAME, defaulting to the logical id (crates/edgezero-adapter-cloudflare/src/lib.rs:98-125, env_config_from_worker at :49-70), and handlers reach the store through the portable Kv extractor (crates/edgezero-core/src/extractor.rs:481) or ctx.kv_store(id). A hand-picked MY_KV binding is never opened by the adapter.

The Config Store section at :180-210 and this Secret Store section were both rewritten correctly — the KV one between them is the odd one out. Suggest giving it the same shape: [stores.kv] ids = [...] in edgezero.toml, one [[kv_namespaces]] binding = "<logical id>" per id in wrangler.toml, and a pointer to /guide/kv.

## KV Storage

Use Cloudflare KV for edge storage:
Expand Down
Loading
Loading