← Back to Library

Routing & Ingress (/api + /mcp)

concept ·  operators
routingingresscaddynginx-ingressrouterdeployer

Routing & Ingress (/api + /mcp)

In one sentence. The deployer turns a single declared set of routes into concrete reverse-proxy configuration for two distinct hops — a browser-facing edge (Caddy on the compose target, nginx-ingress on the k8s target) and a server-side internal aggregator (the wip-router component, Caddy on both targets) — so the two deployment targets and the two hops can't disagree about what's exposed.

Why it exists. WIP ships two deployment targets (compose, k8s) and needs two different request paths: one for browsers hitting the edge (TLS, auth delegation to the gateway) and one for apps that want to call WIP's stores server-side without going back out through the public edge. wip-router exists to replace an older, compose-only hardcoded internal address with an explicit component visible to both renderers. On the edge side, both Caddy (compose) and nginx-ingress (k8s) are built to consume the same route list so the two renderers can't drift on what a given path does.

The model

Route fields, as read by the renderers in scope. The route/rule objects themselves are constructed elsewhere (wip_deploy.config_gen, outside this doc's scope — see the gap note below); this table lists only the attributes the renderers in scope actually consume.

FieldMeaningRead by
pathThe route's mount point (e.g. an /api/<service> prefix, or /mcp)router_caddy.py, compose_caddy.py, k8s.py
backend_host / backend_portInternal host:port the router forwards torouter_caddy.py
backend_component / backend_portComponent name (rendered as wip-<component>) + port for the edge reverse_proxycompose_caddy.py
backend_service / backend_portk8s Service name + port for an Ingress backendk8s.py
streamingIf true: Caddy sets flush_interval -1; nginx-ingress sets proxy-buffering offall three renderers
auth_protectedIf true: the route is wrapped in forward_auth (edge Caddy) or gets auth-url/auth-signin/auth-response-headers annotations (nginx-ingress). Not read by the internal router — it never auth-gates.compose_caddy.py, k8s.py
redirect_bare_pathIf true (default posture): a bare path 301-redirects to <path>/. If false: no redirect is emitted, and the renderer instead emits an explicit sibling handle for the bare path alongside the <path>/* wildcard.router_caddy.py, compose_caddy.py
strip_prefixIf true: the route's own path prefix is stripped before the backend sees the request (handle_path in Caddy; a regex path + rewrite-target annotation in nginx-ingress)compose_caddy.py, k8s.py
preserve_prefix_subpathsA list of subpaths under a strip_prefix route that keep their prefix unstripped and are served from their own, earlier-matching blockcompose_caddy.py, k8s.py

Config objects per renderer.

Config objectCarries (as read in scope)Rendered by
RouterConfiglisten_port, routesrender_router_caddyfile (router_caddy.py) — internal router, both targets
CaddyConfighostname, https_port, tls_mode, root_redirect, gateway_service, gateway_port, routesrender_caddyfile (compose_caddy.py) — edge, compose target
IngressConfighostname, tls_secret_name, ingress_class, proxy_body_size, gateway_auth_url, root_redirect_target, rules_render_ingress (k8s.py) — edge, k8s target

⚠ GAP: RouterConfig, CaddyConfig, IngressConfig and the route/rule objects they carry are constructed in deployer/src/wip_deploy/config_gen/{router,caddy,nginx_ingress}.py. None of those files are in this doc's source_scope, so the tables above list only the fields the in-scope renderers actually touch — not necessarily the complete shape those objects carry.

How it works

Edge, compose target (compose_caddy.render_caddyfile): one Caddy site block on <hostname>:<https_port> (or localhost:<https_port> when hostname == "localhost").

  1. TLS is configured per tls_mode: internal emits a self-signed issuer with a 720h lifetime; external emits tls off (TLS is terminated upstream); letsencrypt emits nothing beyond the global auto_https directive.
  2. If no component declares its own / route, a bare-host redirect sends / to cfg.root_redirect.
  3. Routes are sorted by descending path length and rendered as handle/handle_path blocks: an optional bare-path redirect, an optional forward_auth wrapper, strip_prefix unwrapping (with preserve_prefix_subpaths carved out into their own blocks first), and a reverse_proxy to wip-<backend_component>:<backend_port> (with flush_interval -1 when streaming).
  4. A terminal handle /api/* { respond 404 } block is written before the catch-all.
  5. The catch-all (path == "/" route, if declared) is rendered last, wrapping the same auth logic.

Internal router, both targets (router_caddy.render_router_caddyfile): a second, separate Caddy instance. auto_https off (it only ever serves internal plain HTTP — without the directive Caddy probes upstream ACME on startup even with no HTTPS site declared). For each route: a handle <path>/* block reverse-proxying to <backend_host>:<backend_port> (flush_interval -1 when streaming); if redirect_bare_path is false, a sibling handle <path> block is emitted first for the same backend. No forward_auth anywhere — callers present X-API-Key themselves. Same terminal /api/* 404 guard as the edge Caddyfile.

Edge, k8s target (k8s._render_ingress): the same logical route set is classified into buckets and rendered as separate nginx-ingress Ingress resources rather than ordered handle blocks:

Internal router, k8s target: no separate rendering path — the same RouterConfig/Caddyfile produced by router_caddy.render_router_caddyfile is written into a wip-router-config ConfigMap (key Caddyfile) by k8s._render_configmaps, gated on whether the router component is active, and mounted at /etc/caddy/Caddyfile via subPath by k8s._render_component. The router component's own descriptor supplies the image (caddy:2), the listen port (8080, named http), and the run command (caddy run --config /etc/caddy/Caddyfile --adapter caddyfile) — identical on both targets; only the mount mechanism differs (bind-mount on compose, ConfigMap on k8s).

Router activation. The router component declares requires_core: true: in an apps-only install (no core services deployed) there is nothing internal to route to, so the router doesn't render. Apps that need a remote WIP base URL in that mode point at it through a spec-level external base URL instead of through the router component.

Rules & invariants

Gotchas

See also

⚠ GAP: the canonical reading list's fixed cross-reference map states what points to Routing & Ingress (every API doc, and the Deployment doc) but does not enumerate Routing & Ingress's own outbound "See also" targets. The entries below are inferred from that same map's directionality, not discovered by reading sibling docs.


Generated from real source code, not hand-written — see the WIP Technical Library.