How Celld Serves Static Assets: Configuration Options and Implementation Details
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, 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. 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 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
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:
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 headerRemove(name)– Deletes a header from the response
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 pathsasset_only: false(default) – Falls back to worker navigation handling unless the request explicitly prefers asset serving (determined bynavigation_prefers_asset_servingchecking headers likeAccept: text/html)
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 inAssetIndex.config.bindingasset_only– Boolean flag inAssetResolverInnerthat disables worker fallback for missing assetsrun_worker_first– Boolean or pattern list inAssetIndex.config.run_worker_firstcontrolling execution orderheaders– Raw string inAssetIndex.config.headersparsed into header manipulation rulesredirects– Raw string inAssetIndex.config.redirectsparsed into redirect rulesCELLD_ASSET_CACHE_DIR– Environment variable overriding the local cache directory pathCELLD_ASSET_CACHE_BYTES– Environment variable setting the maximum cache size in bytes (default: 536870912)
Implementation Examples
Initialize the resolver during worker startup:
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:
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:
{
"headers": "example.com:/static/.* => Set:Cache-Control:max-age=3600; Remove:X-Powered-By"
}
Configure redirect rules:
{
"redirects": "example.com:/old/(.*) => https://example.com/new/$1; status=301"
}
Summary
- Celld uses the
AssetResolverincrates/celld/assets.rsto manage static assets, loading anassets.jsonindex from S3-compatible storage at startup - Assets are cached locally with configurable paths via
CELLD_ASSET_CACHE_DIRand size limits viaCELLD_ASSET_CACHE_BYTES(default 512 MiB) - Request handling supports redirect rules, header manipulation, and HTTP range requests (206 Partial Content)
- The
run_worker_firstoption controls whether worker code executes before asset serving, supporting both global and pattern-based configuration - The
asset_onlyflag 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, 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.
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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →