Vision — "add the crate, get the whole REST edge" (stateless by default)
A service that already has gRPC (tonic) services + its proto descriptors should get a complete stateless HTTP edge — REST transcoding + OpenAPI + stateless auth (JWT verify / forward-auth / ext-authz) + OIDC discovery/JWKS/userinfo + shield + CORS + health + metrics, all on one port — by adding the crate, with zero HTTP boilerplate and no axum in its own code or Cargo.toml (axum stays transitive, under structured-proxy). cargo add structured-proxy and you ARE a production stateless gateway.
// The entire stateless edge — no axum, no tower, no router, no handler wiring:
structured_proxy::ProxyServer::from_config(cfg)
.with_descriptors(MY_PROTO_DESCRIPTOR_SET) // your gRPC → REST + OpenAPI
.with_auth_decider(my_pdp) // optional: in-process forward-auth decision
.serve() // full stateless edge on one port
.await?;
Two tiers, escalate only as needed:
- Tier 1 — zero code. Config + descriptors → the stateless stack (transcode + OpenAPI + OIDC discovery/JWKS + shield + CORS + health + metrics) with sane defaults.
- Tier 2 — opt-in
axum-free hooks. Inject stateless service-specific logic (forward-auth/PDP decision, OIDC discovery/userinfo backing, extra stateless routes) through small traits — without naming an HTTP framework in your crate.
Scope boundary — this crate stays a stateless data plane by default
Per the README Non-goals, structured-proxy is a stateless transcoding data plane with stateless auth primitives. The default build does NOT change that: everything in Tier 1 / Tier 2 above is stateless, and the default feature set compiles zero stateful code. Stateful concerns (BFF sessions, OIDC authorize/token) live behind an opt-in bff Cargo feature that is OFF by default (see "Stateful auth" below). A transcode-only embedder enables nothing extra and pays nothing: no stateful code, no extra dependencies in the binary, cargo tree stays minimal.
Tier-2 hooks (this issue)
AuthDecider — the per-request gate (hot path)
Called on every proxied request (inline gate) and exposed at /verify (for a fronting proxy / Envoy ext-authz HTTP) — same trait, two call sites, near-zero cost, needed either way (not a fork). The fork was the data shape; chosen contract:
pub trait AuthDecider: Send + Sync {
async fn decide(&self, req: &RequestParts<'_>) -> Decision;
}
pub struct RequestParts<'a> {
pub method: &'a http::Method,
pub path: &'a str,
pub query: Option<&'a str>,
pub headers: &'a http::HeaderMap,
pub peer: std::net::SocketAddr,
}
pub enum Decision {
Allow { inject_headers: http::HeaderMap }, // forwarded upstream
Deny { status: http::StatusCode, body: bytes::Bytes },
Redirect { location: String }, // login flows handled by a fronting/stateful layer
}
Rationale (perf/binary first, then DX):
- Hot path: borrow-only inputs, one vtable dispatch per request, allocation only for headers the decider chooses to inject. Cheapest possible gate.
- Binary: sole dep is
http (a foundational crate already in the tree via both axum and tonic). Zero new deps, zero growth.
axum-free: http is foundational, not an HTTP framework. The embedder never names axum.
- DX: mirrors the pattern that already exists internally in sid-auth (
AuthDecision: Allow(HeaderMap)/Unauthenticated/Forbidden) — we publish an existing shape, no protobuf knowledge required.
Rejected: Envoy CheckRequest/CheckResponse as the in-process contract (forces protobuf marshalling of a deeply-nested AttributeContext on every request — worst per-request cost, no binary saving since envoy-types is already linked for the out-of-process client). The existing config-driven out-of-process ext-authz gRPC client (AuthzConfig) stays as-is for external-PDP users — orthogonal to this hook. Exposing the proxy itself as an ext-authz gRPC server (so it can be someone else's PDP) is deferred to a separate, feature-gated task (adds tonic-server codegen; nobody needs it today).
OidcBackend — stateless OIDC surface only
Discovery (/.well-known/openid-configuration, sid-configuration, webfinger), JWKS, and userinfo (token → claims pass-through). Backed by the embedder's key/client metadata. No authorize/token in the default surface — those are stateful (auth codes, PKCE state, login session) and live behind the bff feature (below).
with_extra_routes(...) — axum-free escape hatch and composition point
Register extra stateless routes via a framework-agnostic adapter (request parts → response parts), so the embedder never names axum::Router/Handler. This is also the seam the optional bff feature composes through (below): the stateful surface mounts its endpoints onto the same ProxyServer / port through this adapter, and it stays a general escape hatch for embedder-specific routes.
Stateful auth — opt-in bff feature (OFF by default)
BFF sessions and OIDC authorize/token are stateful (README Non-goal: "session lifecycle is a separate, stateful concern"). Rather than a separate companion crate, this is an opt-in Cargo feature bff, disabled by default, so the crate stays one repo / one version with shared internal types while the default build remains a lean stateless data plane:
bff OFF (default): nothing stateful compiles, no extra dependencies pulled, identical to today's stateless data plane. README Non-goals hold verbatim for this build.
bff ON (opt-in): compiles in the stateful surface — BFF sessions (cookie ↔ token, server-side token storage, refresh) and the OIDC authorization server (authorize/token with auth codes + PKCE state) — mounted onto the same ProxyServer / port via with_extra_routes. All stateful-only dependencies (cookie/session crates, any store backend) are optional = true and enabled solely by this feature, so they never enter a default cargo tree.
This keeps the crate reusable + lean for pure data-plane users (they pay nothing) while letting embedders that need stateful login flip one feature instead of wiring a second crate.
Migration impact (sid-auth — separate downstream task)
- sid-auth's stateless edge (transcode + forward-auth + OIDC discovery/JWKS/userinfo + shield + CORS + health + metrics) →
ProxyServer via the hooks above; drop axum + tower-http from crates/sid-auth/Cargo.toml; implement AuthDecider (Cedar PDP + JWT verify + header translation) and OidcBackend.
- sid-auth's stateful BFF + OIDC
authorize/token → enable structured-proxy's bff feature (the stateful surface moves into the proxy under the feature gate, composing via with_extra_routes), so sid-auth no longer hand-rolls it. NOT compiled into the default data-plane build.
- Lands the proxy 2.x / axum 0.8 bump without sid pinning axum.
Acceptance criteria
- An embedder runs the full stateless edge via
ProxyServer::serve() with no axum / tower-http / tonic-web in its own Cargo.toml; cargo tree -i axum shows axum only under structured-proxy.
AuthDecider / OidcBackend / with_extra_routes are implementable without referencing axum types; AuthDecider adds zero new dependencies (only http).
- Standalone binary behavior unchanged (same features, same port semantics).
- Default build holds the README Non-goals: no session/BFF/stateful-OIDC code compiles unless
--features bff is set; all stateful-only deps are optional and gated behind bff.
bff feature, when enabled, mounts the stateful surface (BFF sessions + OIDC authorization server) onto the same ProxyServer / port via with_extra_routes.
sid-auth's stateless edge migrated end-to-end; gateway (transcode + OIDC discovery + forward-auth + shield + health + metrics) works on one port.
Follow-ups (separate issues)
bff feature: stateful BFF (cookie ↔ token sessions) + OIDC authorization server (authorize/token), default-off, composing via with_extra_routes.
- Proxy-as-ext-authz-gRPC-server (feature-gated).
- Optional single-port co-served tonic gRPC for embedders that also host gRPC.
Vision — "add the crate, get the whole REST edge" (stateless by default)
A service that already has gRPC (tonic) services + its proto descriptors should get a complete stateless HTTP edge — REST transcoding + OpenAPI + stateless auth (JWT verify / forward-auth / ext-authz) + OIDC discovery/JWKS/userinfo + shield + CORS + health + metrics, all on one port — by adding the crate, with zero HTTP boilerplate and no
axumin its own code orCargo.toml(axum stays transitive, understructured-proxy).cargo add structured-proxyand you ARE a production stateless gateway.Two tiers, escalate only as needed:
axum-free hooks. Inject stateless service-specific logic (forward-auth/PDP decision, OIDC discovery/userinfo backing, extra stateless routes) through small traits — without naming an HTTP framework in your crate.Scope boundary — this crate stays a stateless data plane by default
Per the README Non-goals,
structured-proxyis a stateless transcoding data plane with stateless auth primitives. The default build does NOT change that: everything in Tier 1 / Tier 2 above is stateless, and the default feature set compiles zero stateful code. Stateful concerns (BFF sessions, OIDCauthorize/token) live behind an opt-inbffCargo feature that is OFF by default (see "Stateful auth" below). A transcode-only embedder enables nothing extra and pays nothing: no stateful code, no extra dependencies in the binary,cargo treestays minimal.Tier-2 hooks (this issue)
AuthDecider— the per-request gate (hot path)Called on every proxied request (inline gate) and exposed at
/verify(for a fronting proxy / Envoy ext-authz HTTP) — same trait, two call sites, near-zero cost, needed either way (not a fork). The fork was the data shape; chosen contract:Rationale (perf/binary first, then DX):
http(a foundational crate already in the tree via bothaxumandtonic). Zero new deps, zero growth.axum-free:httpis foundational, not an HTTP framework. The embedder never namesaxum.AuthDecision:Allow(HeaderMap)/Unauthenticated/Forbidden) — we publish an existing shape, no protobuf knowledge required.Rejected: Envoy
CheckRequest/CheckResponseas the in-process contract (forces protobuf marshalling of a deeply-nestedAttributeContexton every request — worst per-request cost, no binary saving sinceenvoy-typesis already linked for the out-of-process client). The existing config-driven out-of-process ext-authz gRPC client (AuthzConfig) stays as-is for external-PDP users — orthogonal to this hook. Exposing the proxy itself as an ext-authz gRPC server (so it can be someone else's PDP) is deferred to a separate, feature-gated task (adds tonic-server codegen; nobody needs it today).OidcBackend— stateless OIDC surface onlyDiscovery (
/.well-known/openid-configuration,sid-configuration,webfinger), JWKS, anduserinfo(token → claims pass-through). Backed by the embedder's key/client metadata. Noauthorize/tokenin the default surface — those are stateful (auth codes, PKCE state, login session) and live behind thebfffeature (below).with_extra_routes(...)—axum-free escape hatch and composition pointRegister extra stateless routes via a framework-agnostic adapter (request parts → response parts), so the embedder never names
axum::Router/Handler. This is also the seam the optionalbfffeature composes through (below): the stateful surface mounts its endpoints onto the sameProxyServer/ port through this adapter, and it stays a general escape hatch for embedder-specific routes.Stateful auth — opt-in
bfffeature (OFF by default)BFF sessions and OIDC
authorize/tokenare stateful (README Non-goal: "session lifecycle is a separate, stateful concern"). Rather than a separate companion crate, this is an opt-in Cargo featurebff, disabled by default, so the crate stays one repo / one version with shared internal types while the default build remains a lean stateless data plane:bffOFF (default): nothing stateful compiles, no extra dependencies pulled, identical to today's stateless data plane. README Non-goals hold verbatim for this build.bffON (opt-in): compiles in the stateful surface — BFF sessions (cookie ↔ token, server-side token storage, refresh) and the OIDC authorization server (authorize/tokenwith auth codes + PKCE state) — mounted onto the sameProxyServer/ port viawith_extra_routes. All stateful-only dependencies (cookie/session crates, any store backend) areoptional = trueand enabled solely by this feature, so they never enter a defaultcargo tree.This keeps the crate reusable + lean for pure data-plane users (they pay nothing) while letting embedders that need stateful login flip one feature instead of wiring a second crate.
Migration impact (sid-auth — separate downstream task)
ProxyServervia the hooks above; dropaxum+tower-httpfromcrates/sid-auth/Cargo.toml; implementAuthDecider(Cedar PDP + JWT verify + header translation) andOidcBackend.authorize/token→ enablestructured-proxy'sbfffeature (the stateful surface moves into the proxy under the feature gate, composing viawith_extra_routes), so sid-auth no longer hand-rolls it. NOT compiled into the default data-plane build.Acceptance criteria
ProxyServer::serve()with noaxum/tower-http/tonic-webin its ownCargo.toml;cargo tree -i axumshows axum only understructured-proxy.AuthDecider/OidcBackend/with_extra_routesare implementable without referencingaxumtypes;AuthDecideradds zero new dependencies (onlyhttp).--features bffis set; all stateful-only deps areoptionaland gated behindbff.bfffeature, when enabled, mounts the stateful surface (BFF sessions + OIDC authorization server) onto the sameProxyServer/ port viawith_extra_routes.sid-auth's stateless edge migrated end-to-end; gateway (transcode + OIDC discovery + forward-auth + shield + health + metrics) works on one port.Follow-ups (separate issues)
bfffeature: stateful BFF (cookie ↔ token sessions) + OIDC authorization server (authorize/token), default-off, composing viawith_extra_routes.