Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Configuration and time

Configuration and time

Client addresses

On this page

By default a native app takes the client's address from the connection, and an app on Workers from Cloudflare's CF-Connecting-IP header. You don't write anything for that.

The address matters because rate limits count a request by its address while no one is signed in (Rate limits), and jadpo dev's request lines and the request timeline show it. Behind a load balancer or a CDN, the connection's own address is the proxy's, so every client would share one allowance. The real address is in a header the proxy adds, but anyone can send that header, so reading it blindly lets a caller choose its own address.

client_address: in app.jadpo says, once, which header holds the real address and how many proxies of your own deployment to believe:

example
use edge/types { ProxyHops }config {    proxy_hops: ProxyHops {        default: 1    }}
example
application {    client_address: {        source: ClientAddressSource.forwarded_for        hops: config.proxy_hops    }}

source: is one of the four values of ClientAddressSource:

Source The client's address is hops:
connection the connection's own address; no header is read (the native default) not allowed
forwarded_for in X-Forwarded-For required
forwarded in RFC 7239's Forwarded: for= required
cloudflare in CF-Connecting-IP (the default on Workers, the only source accepted there) not allowed

hops:

hops: is how many proxies of your own deployment sit in front of the app. It is a count, not a list of trusted address ranges, so it can't go out of date when a provider's addresses change. It is never guessed: forwarded_for and forwarded need it, and connection and cloudflare, which don't count along a header, refuse it (configuration.client_address_hops_unused).

Each trusted proxy adds the address it saw to the right of the header's list, and whatever a caller sends sits to the left of that. So with hops: N, the client is the N-th address from the right, counting every line of the header joined in order. With one proxy, X-Forwarded-For: 203.0.113.9 and, with a caller's own claim in front, X-Forwarded-For: 10.9.9.9, 203.0.113.9, both give 203.0.113.9. If the header has fewer than N addresses, or the entry chosen isn't an address, the request counts as coming from the connection's own address, never from a guess.

hops: is a whole number from 1. Write it out (hops: 1), or read it from an Int setting, or a type built on one, that declares min: 1 or more, so the app can't start with a count below 1. Anything else is configuration.client_address_hops_invalid.

How a header is read

  • Lines and entries. Every header line of the name counts, in the order received, and each is split at commas. An empty entry still takes its place in the count, so a gap never moves the client left, but it is not an address. In Forwarded, each element (split at commas outside quotes) gives one entry, from its for parameter (any case; a token or a quoted string); an element without one is an entry that is not an address. A Forwarded header that does not parse (an unterminated quote, two for in one element) is invalid as a whole.
  • An address. Anything IpAddress accepts, as IPv4 or IPv6, and in a header also with a port (203.0.113.9:4711) or in brackets ([2001:db8::17]:4711, [2001:db8::17]). It is kept in one spelling, so a client is one count whichever way a proxy writes it: IPv6 in lower case with the zeros shortened, and an IPv4 address written as IPv6 (::ffff:203.0.113.9) as IPv4. The connection's own address is spelled the same way. A name, unknown, an obfuscated identifier (_hidden) or a zone (fe80::1%eth0) is not an address.
  • cloudflare. Exactly one CF-Connecting-IP line holding one address; anything else falls back, as a header that is short or invalid does.
  • Falling back is whole. The connection's address replaces the choice; part of a header is never mixed with it.

The address chosen is the one the default rate limits count (an IPv6 address by its /64), the one jadpo dev's request lines show (after the time, muted, and as address with --json), and the one the request timeline records. A native deployable chooses an address the same way jadpo dev does.

On Workers

On Workers there is no connection address to fall back to. Cloudflare sets CF-Connecting-IP on every request from its edge, replacing any value a caller sent. So cloudflare is the only source accepted there, and it is what a Workers deployable uses when there is no client_address:. An address Cloudflare didn't give is unknown there, and requests with no known address share one allowance.

Any other source is refused when the project is built for Workers. jadpo check refuses it for each deployable with target: Target.wasm (deployable.target_unsupported, see A Workers deployable). A Workers deployable that reads one doesn't start: it logs client_address_refused and answers every request with a 503.

Markdown for agents: /docs/client-addresses.md.Built from Jadpo main at a707efb.