| name | plecto-architecture |
| description | Plecto's core architecture โ the two halves (native-Rust fast path / WASM extension plane), the WIT type contract between them, the deny-by-default capability boundary, the filter chain, typed decision/short-circuit, init vs per-request hooks, instance lifecycle, and host-held state. |
| when_to_use | Use when implementing or reviewing fast-path code, filter execution, the host-API surface, or filter chains; when deciding "does this belong in Rust or in a WASM filter?"; or when the user mentions fast path / extension plane / filter / host-API / capability / ใใฉใฃใกใซ็ฝฎใใ. |
Plecto Architecture
Plecto ใฏไบใคใฎๅ่บซใ WIT ๅๅฅ็ดใง็ทจใฟ่พผใ๏ผbraid๏ผL7 ใชใใผในใใญใญใทใๅคๆญใซ่ฟทใฃใใใ่จญ่จใฎ
source of truth๏ผTenets / Fork 1โ10ใCLAUDE.md ใซ่ฆ็ด๏ผใซๅพใใๆฌในใญใซใฏใใฎ่ฆ็นใใณใผใไฝๆฅญๅใใซ
ใใใฒใผใใใใ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ fast path (native Rust) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
client โโโโถ โ accept ยท TLS ยท HTTP/1.1/2/3 ยท routing ยท LB ยท upstream conn mgmt ยท hot-reload โ โโโโถ upstream
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ request chain response chain โ
โผ (WIT: plecto:filter) (reverse) โฒ
โโโโโโโโโโโโโ extension plane (WASM Component Model filters) โโโโโโโโโโโโโโโ
โ per-filter: init hook (heavy, once) + per-request hook (hot) โ
โ returns decision: continue | modified | short-circuit โ
โ touches ONLY host-API lent by the host (deny-by-default capability) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ host-API (KV/counter/metrics/log/clock/random)
โผ
host-held state: redb (KV / rate-limit / cache)
The two halves โ what goes where
The single most common design question is "Rust or WASM filter?" Decide by Fork 6:
| Put in the fast path (native Rust) | Put in a filter (WASM) |
|---|
| TLS termination, HTTP framing, routing, LB, upstream pools | auth, header/body rewrite, WAF, policy, custom per-request logic |
| Global / hot counters (rate-limit state, count-min) | the decision of whether to rate-limit / who passes |
| Zero-copy body bypass for body-untouching filters | anything that must inspect/transform a body |
| Anything that must never be untrusted or hot-reloaded | anything a user supplies or swaps without a rebuild |
Rule of thumb (Fork 6): user-specific logic / policy / WAF / auth / rewrite โ WASM; TLS / routing
/ LB / connection pool / global counters โ native. The WASM "tax" (data-copy + ~3.5x host-call
overhead) is charged only to request-decision logic, not to the speed path.
Layers and their dependency rules
| Layer | Responsibility | May depend on |
|---|
| Fast path | accept/TLS/HTTP/route/LB/upstream; drives the chain | host facades, config snapshot |
| Filter host (runtime) | embed wasmtime, instantiate/pool filters, run hooks, enforce epoch/memory limits | wasmtime, host-API impls, fast path types |
| Host-API | the capabilities lent to filters (KV/counter/metrics/log/clock/random) | redb, metrics sink โ never the filter |
| Filter (WASM) | per-request decision; implements plecto:filter | only host-API it was granted (sandbox-enforced) |
| Control | declarative manifest, hot-reload, (opt-in) openraft/foca consensus | config types, fast path |
Direction: the fast path drives filters through the contract; filters depend only on lent
capabilities; the host-API depends on storage, never on a filter. A filter cannot reach the fast
path, the host's memory, the network, or the filesystem except through a granted import โ this is
enforced by the Component Model sandbox, not by convention (Tenet 2, Fork 7).
The contract: plecto:filter
- A custom
plecto:filter world (Fork 2). Current contract: plecto:filter@0.3.0, zero-WASI
(ADR 000010); 0.1 / 0.2 are frozen with load-time adapters (ADR 000071 / 000073). Two worlds:
the base filter world is header-only โ bodies stream zero-copy past the chain (ADR 000038) โ
and filter-body adds on-request-body over a buffered list<u8> body (buffer-then-decide,
ADR 000025). Details and evolution live in the wit-contract-design skill.
- decision (a WIT variant, Tenet 3): request side
continue ยท modified ยท short-circuit
(stop, synthesize a response now, don't reach upstream); response side continue ยท modified ยท
replace (ADR 000073). Header values are raw bytes (list<u8>, ADR 000071) and on-response
receives the as-forwarded request snapshot. Auth failure and rate-limit exceed are
short-circuit. Never express intent with ambiguous flags.
- True-streaming bodies (
stream<u8>, Fork 1, async-first) are projected, not current: today body
transforms buffer (list<u8>, no WASI needed); the stream<u8> swap comes with the
wasm32-wasip2 increment (ADR 000025) and tolerates a hot-path intermediate copy at first.
Init vs per-request (Tenet 4)
Every filter has an init hook (config load, regex compile, schema build โ runs once) and a
per-request hook (the hot path). Push heavy work to init; keep per-request lean. Mixing them is
the canonical performance bug (Envoy proxy-wasm issue #450). Request and response sides are symmetric.
Instance lifecycle & state (Fork 3 & 4)
- Trusted (first-party) filters: per-worker-thread pre-instantiated (
InstancePre) + pooling
allocator reuse. Fast and the default.
- Untrusted (third-party) filters: opt into per-request new instances + pooling-zeroization
(CVE-2022-39393 lesson). See
wasmtime-host / security-auditor.
- Filters are stateless. No filter-local persistent state. Rate-limit / session / cache state
lives in host KV (redb), lent via the host-API. Filter-local state collides with pool reuse
and hot-reload, and risks state leakage.
Single-node first (Fork 5 & 10)
One node completes the job. Distribution is opt-in: foca (SWIM) for membership / filter+config
distribution, openraft (Raft) for strong-consistency config/route replication. State stays
node-local (redb); distribution is limited to config consensus. No xDS-style dynamic push โ
static declarative manifest + hot-reload is first (Fork 10).
File / module patterns (as src/ grows)
These are the intended seams โ match new code to them (and to CONTEXT.md once it exists):
**/fastpath/** or **/proxy/** โ listener, TLS, HTTP, router, LB, upstream
**/host/** or **/runtime/** โ wasmtime embedding, instance pool, hook dispatch, metering
**/hostapi/** or **/capabilities/** โ KV/counter/metrics/log/clock host functions (deny-by-default)
**/filter/** โ filter chain orchestration + (separately) example filters
**/control/** โ manifest, hot-reload, consensus (foca/openraft)
**/wit/** or wit/ โ the plecto:filter world definitions
Common violations (call these out in review)
- Business logic in the fast path that should be a filter (or vice-versa: a hot global counter
done in WASM instead of native โ Fork 6).
- A filter granted more than it needs โ host-API must be deny-by-default; lend only the minimum.
- Filter-local mutable state across requests (breaks Fork 4; state belongs in host KV).
- Heavy work in the per-request hook that belongs in init (Tenet 4).
- The host depending on a specific filter, or a filter reaching past its lent capabilities.
- Panicking on untrusted input in the fast path (a single bad request must not down the worker).
- A contract change without a conformance test (see
tdd-workflow Phase 1).
- Treating a read-recomputable thing as a source of truth โ read models / projections (if any)
stay disposable; the manifest + content hashes are the authority for what's loaded (Fork 8).
Related skills
wit-contract-design โ designing/evolving the plecto:filter world and host-API surface.
wasmtime-host โ the host-side embedding (InstancePre, pooling, epoch, Linker deny-by-default).
design-an-interface โ explore radically different shapes for a contract or host-API.
security-auditor โ capability/sandbox + proxy/gateway threat review.