Edge
The edge domain covers everything that runs closer to the user than your central origin — CDN POPs, regional micro-hosts, browser-attached service workers, and the L7 gateway in front of your cluster.
CephalonEngine’s edge story is opinionated: the same module that runs at origin can run at the edge, with the engine taking care of bundle signing, route negotiation, and which behaviors are eligible for edge placement.
Packages
Section titled “Packages”| Package | Maturity | What it ships | When to use |
|---|---|---|---|
Cephalon.Edge | M2 | Edge-native delivery runtime services. Hot-path routing, edge-side caching, signed module delivery. | Building edge-eligible modules; placing read-mostly endpoints at the edge. |
Cephalon.Edge.Traefik | M2 | Traefik integration — config emission, dynamic provider, route negotiation. | You already run Traefik as your gateway; want CephalonEngine to emit Traefik dynamic configuration. |
Cephalon.Edge.KubernetesGateway | M2 | Kubernetes Gateway API (GAMMA + standard) integration. | You’re on Kubernetes and use Gateway API (Istio, Cilium, NGINX Gateway, etc.). |
How edge works in CephalonEngine
Section titled “How edge works in CephalonEngine”Behaviors are tagged for edge eligibility via attributes on the handler class. The engine emits a manifest of eligible behaviors. A gateway adapter (Traefik / Kubernetes Gateway) reads that manifest and configures routes so eligible endpoints go to edge POPs, the rest go to origin.
┌────────┐ read-heavy ┌──────────────┐│ Client │ ────────────▶ │ Edge POP │ ← runs cached GET /products/{id}└────────┘ │ (Cephalon. │ │ write │ Edge) │ │ └──────┬───────┘ │ │ miss / mutate ▼ ▼┌──────────────────────────────────────┐│ Origin (Cephalon.AspNetCore) │ ← runs everything; source of truth│ - all behaviors ││ - manifest emitter │└──────────────────────────────────────┘The origin emits a routing-manifest.json describing which behaviors are edge-eligible. The gateway adapter watches the manifest and reconciles its config.
Edge eligibility
Section titled “Edge eligibility”Mark a behavior as edge-eligible:
[EdgeEligible(Cache = "5m", VaryBy = "tenant")]public sealed class GetProductBehavior : IRestBehavior<Sfid, ProductDto>{ public RestRoute Route => RestRoute.Get("/products/{id:sfid}");
public Task<ProductDto> Handle(Sfid id, CancellationToken ct) => _query.GetByIdAsync(id, ct);}The engine refuses to make a behavior edge-eligible if it:
- Writes to a
Capability.Datawrite model. - Publishes
Capability.Eventingevents. - Requires
Capability.Identityscopes beyond a stateless claim check. - Returns a streaming body (SSE, WebSocket).
(These are detected at composition time — you get a clear error message rather than a runtime surprise.)
Configuring edge
Section titled “Configuring edge”{ "Engine": { "Edge": { "Enabled": true, "Adapter": "Traefik", // Traefik | KubernetesGateway | None "ManifestPath": "/var/lib/cephalon/routing-manifest.json", "ManifestEmitInterval": "00:00:30", "Bundle": { "SignWith": "/etc/cephalon/edge-key.pem", "Compression": "br" }, "DefaultCache": "60s", "DefaultVaryBy": "Accept-Language" } }}| Key | Default | Notes |
|---|---|---|
Enabled | false | Master switch. |
Adapter | "None" | Which gateway adapter to wire. |
ManifestPath | null | Where the routing manifest is emitted. Must be readable by the gateway. |
ManifestEmitInterval | "00:01:00" | How often to refresh the manifest. Edge POPs read it lazily. |
Bundle.SignWith | null | Path to a PEM-encoded signing key. Required for production. Edge POPs verify signatures before executing. |
Bundle.Compression | "gzip" | gzip, br, or none. br is ~20% smaller for typical bundles. |
DefaultCache | null | Fallback cache header for edge-eligible behaviors that don’t set their own. |
DefaultVaryBy | null | Fallback Vary header. |
Use-case scenarios
Section titled “Use-case scenarios”Scenario 1: read-mostly product catalog at the edge
Section titled “Scenario 1: read-mostly product catalog at the edge”[EdgeEligible(Cache = "5m", VaryBy = "tenant")]public sealed class GetProductBehavior : IRestBehavior<Sfid, ProductDto> { /* ... */ }{ "Engine": { "Edge": { "Enabled": true, "Adapter": "Traefik", "Bundle": { "SignWith": "/etc/cephalon/edge-key.pem" } } }}Result: GET /products/{id} is served from the nearest Traefik POP (≤50ms latency for most users), invalidated 5 minutes after origin write.
Scenario 2: edge cache + per-locale variations
Section titled “Scenario 2: edge cache + per-locale variations”[EdgeEligible(Cache = "10m", VaryBy = "Accept-Language,tenant")]public sealed class GetCategoryListBehavior : IRestBehavior<Unit, CategoryListDto> { /* ... */ }The edge POP caches ("acme", "en-US") separately from ("acme", "th-TH"). Bandwidth saved, latency win for international users.
Scenario 3: hybrid placement — read at edge, write at origin
Section titled “Scenario 3: hybrid placement — read at edge, write at origin”public sealed class ProductsModule : RestBehaviorModuleBase{ protected override void ConfigureRestBehaviors(IRestBehaviorBuilder builder) { // Edge-eligible (read) builder.MapProfile<GetProductBehavior>(); // [EdgeEligible] builder.MapProfile<ListProductsBehavior>(); // [EdgeEligible]
// Origin-only (write) builder.MapProfile<CreateProductBehavior>(); // no attribute → origin builder.MapProfile<UpdateProductBehavior>(); builder.MapProfile<DeleteProductBehavior>(); }}Same module, mixed placement. The gateway adapter routes GET /products/* to edge, POST/PUT/DELETE /products/* to origin.
Scenario 4: Kubernetes Gateway API
Section titled “Scenario 4: Kubernetes Gateway API”If you’re using Istio, Cilium, NGINX Gateway, Envoy Gateway, or any Gateway-API-conformant control plane:
{ "Engine": { "Edge": { "Enabled": true, "Adapter": "KubernetesGateway", "KubernetesGateway": { "Namespace": "acme-store", "GatewayName": "acme-gw", "GatewayClass": "istio" } } }}The adapter emits HTTPRoute + BackendTLSPolicy resources, applies them via the Kubernetes API, and watches the manifest for changes.
Scenario 5: Traefik file provider
Section titled “Scenario 5: Traefik file provider”For a simpler / non-Kubernetes deploy with Traefik:
{ "Engine": { "Edge": { "Enabled": true, "Adapter": "Traefik", "Traefik": { "FileProviderPath": "/etc/traefik/dynamic/cephalon-routing.yaml", "EntryPoint": "websecure" } } }}The adapter writes Traefik dynamic-config YAML; Traefik’s file provider hot-reloads it. No Traefik API integration required.
Scenario 6: dev loop — edge disabled
Section titled “Scenario 6: dev loop — edge disabled”Most local dev doesn’t need edge; turn it off:
{ "Engine": { "Edge": { "Enabled": false } }}Behaviors tagged [EdgeEligible] still work — they just always go through origin. Same code, no edge runtime, simpler debugging.
Limits & gotchas
Section titled “Limits & gotchas”Cephalon.EdgeisM2— narrow surface. Edge runtime ships but the manifest format is still settling. Expect minor breaking changes within0.1.0-previewminor versions.- Signed bundles are mandatory in production. A POP that runs an unsigned bundle is a remote code execution vector. The engine refuses to ship bundles without
Bundle.SignWithunlessEngine:Edge:AllowUnsigned=true(allowed inDevelopmentonly). - Edge POPs share the manifest; they don’t share state. Each POP runs read-only handlers; any mutation hits origin. Don’t try to coordinate state across POPs.
- Edge attribute mismatches are composition-fatal. A behavior tagged
[EdgeEligible]that injects anIDbContextwrite surface fails composition. The error message names the behavior + the offending dependency. - The default cache TTL is
null(no cache). You must opt in per behavior. ForgettingCache = "..."means “edge-eligible but always proxies to origin” — costly without the latency win. - Vary keys are case-sensitive.
Accept-Languageandaccept-languageproduce different cache keys; pick one and stick to it. Engine emits a warning but doesn’t normalise.
Tips & tricks
Section titled “Tips & tricks”When to put a behavior at the edge
Section titled “When to put a behavior at the edge”- ✅ Read-mostly endpoint with low cardinality (catalogs, product lists, marketing pages, public RSS).
- ✅ Geo-distributed audience where latency dominates UX (international shopping).
- ✅ Behavior is stateless and idempotent.
- ❌ Authenticated, per-user data (cart, account, settings) — usually wrong, the cache hit rate is too low.
- ❌ Behaviors that write or publish events — composition will reject anyway.
Cache key design
Section titled “Cache key design”- Always set an explicit
VaryBywhen there’s any chance of multi-tenancy or locale variation. Default totenantfor SaaS. - Don’t include the full URL in the cache key. The engine uses route + matched parameters. Query parameters require
VaryBy = "...,QueryString:filter"(and you should usually canonicalise instead). - Watch for cache stampede at popular endpoints. Set
Cache = "5m, stale-while-revalidate=60s"so one user pays the origin trip while others serve stale.
Bundle signing
Section titled “Bundle signing”- Rotate the bundle-signing key on a regular cadence (quarterly). Edge POPs cache the trust set; restart them after rotation.
- Use Sigstore-style keyless signing if you can (
SignWith: "sigstore"). Avoids key-management entirely; the engine verifies via Sigstore’s transparency log.
Adapter choice
Section titled “Adapter choice”- Traefik is the simplest path if you’re not on Kubernetes. The file provider works in any Docker / VM / metal environment.
- Kubernetes Gateway is the right path on Kubernetes. Don’t use Ingress — Gateway is the modern API.
- Custom adapter — implement
IEdgeAdapterif your gateway isn’t one of the above. The interface is two methods (ReconcileAsync,WatchManifestAsync).
Observability
Section titled “Observability”- The edge runtime emits
cephalon.edge.*traces —cache.hit,cache.miss,bundle.verify,manifest.read. - Use the
cephalon.edge.cache_hit_ratiometric to verify behavior eligibility is paying off. Below 50% hit rate, reconsider. - Edge POPs include their POP id (
cephalon.edge.pop) in trace attributes — useful for “is this a specific POP misbehaving?” diagnostics.
Anti-patterns
Section titled “Anti-patterns”| Don’t | Do |
|---|---|
Tag [EdgeEligible] on every behavior | Be selective — read-mostly + low-cardinality only |
Set short cache TTLs (<10s) for edge behaviors | If TTL is that low, you don’t benefit from edge — keep at origin |
Skip VaryBy and rely on path-only cache keys | Always vary by tenant in SaaS; vary by Accept-Language if i18n matters |
| Run an unsigned bundle in production | Always set Bundle.SignWith |
| Cache write responses | Writes go to origin; edge is read-only |
| Maintain stateful per-POP caches | Edge POPs are stateless. Use the shared backend for any state |
Source-doc snapshots
Section titled “Source-doc snapshots”- Cephalon.Edge — runtime internals.
- Cephalon.Edge.Traefik — Traefik adapter internals.
- Cephalon.Edge.KubernetesGateway — K8s Gateway adapter internals.
Cross-references
Section titled “Cross-references”- Tutorial → Edge-native delivery — walkthrough with Traefik.
- Technology → Hosts — the host adapters edge wraps.
- Reference → Architecture — runtime contracts (the Transport surface + canonical edge-eligibility contract land in
0.2.0-preview).