# How Celld Serves Static Assets: Configuration Options and Implementation Details

> Learn how Celld serves static assets using the AssetResolver. Explore configuration options for S3 storage, SHA-256 validation, local caching, and header/redirect rules.

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: how-to-guide
- Published: 2026-08-10

---

**Celld serves static assets through the `AssetResolver` component, which downloads an asset index from S3-compatible storage, validates it against a SHA-256 hash, and streams files from a configurable local cache while applying header and redirect rules.**

According to the denoland/celld source code, Celld handles static asset serving through a dedicated resolution pipeline. The system delegates to the `AssetResolver` implementation in [`crates/celld/assets.rs`](https://github.com/denoland/celld/blob/main/crates/celld/assets.rs), which manages asset discovery, local caching, and request transformation through environment variables and index-based configuration.

## The AssetResolver Architecture

The static asset system centers on the **`AssetResolver`** struct defined in [`crates/celld/assets.rs`](https://github.com/denoland/celld/blob/main/crates/celld/assets.rs). When a worker initializes, Celld invokes **`AssetResolver::load`** to establish the connection between the worker and its associated object storage bucket.

### Loading the Asset Index

During startup, the resolver fetches the **[`assets.json`](https://github.com/denoland/celld/blob/main/assets.json)** index file from the configured S3-compatible bucket. The system validates this index against the SHA-256 hash specified in the `AssetManifestRef` before parsing. This process extracts three configurable rule sets:

- **Header rules** for manipulating response headers
- **Redirect rules** for URL redirection logic  
- **Run-worker-first** rules for execution order control

```rust
let index: AssetIndex = serde_json::from_slice(&bytes)?;
let header_rules    = parse_header_rules(index.config.headers.as_deref().unwrap_or(""))?;
let redirect_rules = parse_redirect_rules(index.config.redirects.as_deref().unwrap_or(""))?;

```

### Local Filesystem Caching

Downloaded assets are cached locally to reduce latency and egress costs. By default, Celld stores cached files in the system temporary directory under `celld/asset-cache`. You can override this location using the **`CELLD_ASSET_CACHE_DIR`** environment variable.

Cache size is constrained to **512 MiB** by default (`DEFAULT_ASSET_CACHE_BYTES`). Adjust this limit with the **`CELLD_ASSET_CACHE_BYTES`** environment variable:

```rust
let cache_root = std::env::var_os("CELLD_ASSET_CACHE_DIR")
    .map(PathBuf::from)
    .unwrap_or_else(|| std::env::temp_dir().join("celld").join("asset-cache"));
let cache_max_bytes = std::env::var("CELLD_ASSET_CACHE_BYTES")
    .ok()
    .and_then(|v| v.parse::<u64>().ok())
    .unwrap_or(DEFAULT_ASSET_CACHE_BYTES);

```

## Request Handling Pipeline

Incoming HTTP requests are processed by **`AssetResolver::ingress_response`**, which implements the core request-to-asset mapping logic.

### Path Resolution and Redirects

The resolver first URL-decodes the request path using **`decode_path`**. It then evaluates **redirect rules** in priority order. If a pattern matches the host and path, Celld returns an immediate response with the configured status code (typically 301 or 302) and `Location` header.

### Asset Streaming and Range Requests

When no redirect matches, the resolver looks up the path in the asset index. If found, it streams the file from either the local cache or the upstream bucket as an `axum::Response`. The system supports HTTP range requests via the **`RequestedRange`** enum, returning **`206 Partial Content`** for partial asset delivery.

## Header and Redirect Rule Configuration

Rules are defined as raw strings in the asset index configuration and parsed during initialization.

### Header Rules Syntax

Header rules use the format `host:path_regex => Operation:Name:Value`. The resolver supports two operations:

- **`Set(name, value)`** – Adds or replaces a header
- **`Remove(name)`** – Deletes a header from the response

```rust
for rule in &self.inner.header_rules {
    if let Some(captures) = rule.pattern.matches(host, &path) {
        for op in &rule.operations {
            match op {
                HeaderOperation::Set(name, value) => { /* set header */ }
                HeaderOperation::Remove(name) => { /* remove header */ }
            }
        }
    }
}

```

### Redirect Rules Syntax

Redirect rules follow similar pattern matching: `host:path_regex => destination; status=CODE`. The system supports capture groups for dynamic URL rewriting.

## Worker-First Execution and Fallback Behavior

Celld provides granular control over whether worker code executes before asset serving.

### Run-Worker-First Mode

The **`run_worker_first`** configuration field accepts either a boolean or a list of route patterns:

- **`true`** – Forces worker execution for every request before checking assets
- **Pattern list** – Enables selective worker-first behavior for specific paths
- **Exclusion patterns** – Prefix a pattern with `!` to exclude routes from worker-first execution

The **`should_run_worker_first`** method evaluates these rules against the encoded request path.

### Asset-Only vs Navigation Fallback

The **`asset_only`** boolean determines behavior for unmatched paths:

- **`asset_only: true`** – Returns **404 Not Found** for unknown paths
- **`asset_only: false`** (default) – Falls back to worker navigation handling unless the request explicitly prefers asset serving (determined by `navigation_prefers_asset_serving` checking headers like `Accept: text/html`)

```rust
let include_not_found = self.inner.asset_only
    || self.navigation_prefers_asset_serving(request_headers);

```

## Complete Configuration Options

Static asset behavior is controlled through the asset index configuration and environment variables:

- **`binding`** – The name exposed to workers (e.g., `ASSETS`) defined in `AssetIndex.config.binding`
- **`asset_only`** – Boolean flag in `AssetResolverInner` that disables worker fallback for missing assets
- **`run_worker_first`** – Boolean or pattern list in `AssetIndex.config.run_worker_first` controlling execution order
- **`headers`** – Raw string in `AssetIndex.config.headers` parsed into header manipulation rules
- **`redirects`** – Raw string in `AssetIndex.config.redirects` parsed into redirect rules
- **`CELLD_ASSET_CACHE_DIR`** – Environment variable overriding the local cache directory path
- **`CELLD_ASSET_CACHE_BYTES`** – Environment variable setting the maximum cache size in bytes (default: 536870912)

## Implementation Examples

Initialize the resolver during worker startup:

```rust
use celld::{AssetResolver, Bucket, AssetManifestRef};

async fn init_assets(bucket: Bucket) -> anyhow::Result<AssetResolver> {
    let manifest_ref = AssetManifestRef {
        index: "assets.json".into(),
        sha256: "a1b2c3d4…".into(),
    };
    AssetResolver::load(&bucket, "my-app", &manifest_ref, false).await
}

```

Mount the resolver in an Axum router:

```rust
use axum::{routing::get, Router};

fn router(resolver: AssetResolver) -> Router {
    Router::new()
        .route(
            "/*path",
            get(move |path, headers| async move {
                resolver
                    .ingress_response(&path, None, false, &headers)
                    .await
                    .unwrap()
                    .unwrap_or_else(|| Response::builder().status(404).body(Body::empty()).unwrap())
            }),
        )
}

```

Configure header rules in the asset index:

```json
{
  "headers": "example.com:/static/.* => Set:Cache-Control:max-age=3600; Remove:X-Powered-By"
}

```

Configure redirect rules:

```json
{
  "redirects": "example.com:/old/(.*) => https://example.com/new/$1; status=301"
}

```

## Summary

- Celld uses the **`AssetResolver`** in [`crates/celld/assets.rs`](https://github.com/denoland/celld/blob/main/crates/celld/assets.rs) to manage static assets, loading an **[`assets.json`](https://github.com/denoland/celld/blob/main/assets.json)** index from S3-compatible storage at startup
- Assets are cached locally with configurable paths via **`CELLD_ASSET_CACHE_DIR`** and size limits via **`CELLD_ASSET_CACHE_BYTES`** (default 512 MiB)
- Request handling supports **redirect rules**, **header manipulation**, and **HTTP range requests** (206 Partial Content)
- The **`run_worker_first`** option controls whether worker code executes before asset serving, supporting both global and pattern-based configuration
- The **`asset_only`** flag determines whether unknown paths return 404 or fall back to worker navigation handling

## Frequently Asked Questions

### What file implements static asset serving in Celld?

The core implementation resides in **[`crates/celld/assets.rs`](https://github.com/denoland/celld/blob/main/crates/celld/assets.rs)**, which defines the `AssetResolver` struct and its methods for loading asset indices, managing the local cache, and processing requests. Protocol definitions for `AssetManifestRef` and `AssetIndex` are located in [`crates/celld/protocol.rs`](https://github.com/denoland/celld/blob/main/crates/celld/protocol.rs).

### How do I change the static asset cache directory?

Set the **`CELLD_ASSET_CACHE_DIR`** environment variable before starting Celld. If unspecified, the system defaults to `std::env::temp_dir()/celld/asset-cache`.

### What is the default cache size for static assets?

By default, Celld limits the asset cache to **512 MiB** (defined as `DEFAULT_ASSET_CACHE_BYTES` in the source). Override this with the **`CELLD_ASSET_CACHE_BYTES`** environment variable, specifying the size in bytes.

### How do header rules work in Celld?

Header rules are defined in the asset index configuration as pattern strings matching `host:path`. When a request matches, Celld applies **`Set`** operations to add or replace headers and **`Remove`** operations to delete them. These rules are evaluated for every asset request after the file is selected but before the response is returned.