Jul.IA is a self-contained edge and protocol gateway written in Go and configured through TOML. It combines reverse proxying, load balancing, static serving, FastCGI/uWSGI, HTTP and gRPC gateway behavior, optional L4 proxying, TLS/mTLS, policy, observability, configuration lifecycle, and an embedded operations Console in a single static, dependency-free binary.
- Binary / module / service name:
jul - Product name:
Jul.IA - Language: Go 1.26.5
- License: AGPL-3.0
Current direction and sequencing are governed by the combined repository audit and the master programme. The durable product direction remains in the vision, roadmap, engineering specs, and ADRs. The permanent OSS/open-core boundary is defined in ADR 0012.
New to HTTP, proxies, TLS, caching, or observability? The concepts appendix walks through how a request travels through modern edge infrastructure from first principles.
Jul.IA is for: solo operators and small infrastructure teams who want NGINX-grade routing and reverse proxying — plus protocol gateway capabilities (gRPC transcoding, L4 TCP/UDP proxying) — in a single binary, configured in TOML, without NGINX's operational complexity. Operators who want a built-in observability console without standing up a separate tool. Teams migrating from NGINX who want a gentler on-ramp.
Jul.IA is not the right tool if you need:
- Fleet or multi-node management — Jul.IA runs on one node; fleet control planes are a demand-gated future milestone.
- Kubernetes Ingress or Gateway API — no K8s controller ships today; use NGINX Ingress, Envoy Gateway, or Traefik.
- Full service mesh or xDS — use Envoy, Istio, or Linkerd.
- Multi-tenant edge / SaaS CDN — Jul.IA has no tenant isolation layer.
- Envoy-level extensibility or ecosystem — Jul.IA's plugin system is WASM-based and purpose-fit, not a general xDS/filter chain.
Jul.IA is completing the bounded standalone gateway first: current correctness, trusted client/backend identity, generic resilience, safe configuration automation, and selected high-value runtime dynamics. Fleet, hosted cloud, GraphQL composition and AI remain optional horizons or bounded experiments; none is required for the single-node product to remain useful. See the operating model, Core Gateway Completeness, and roadmap v2.0.
The canonical maturity matrix lives in docs/status.md. At a
glance:
| Maturity | Features |
|---|---|
| GA | Core HTTP, TLS & ACME, Authentication, mTLS, Console, Active health checks, WAF, Rate limiting, Compression, OTel tracing, Response cache, Zero-config + jul lint, NGINX importer, HTTP/3, gRPC transcoding + passthrough, Service discovery, Secrets references, WASM plugins, L4 stream proxy |
| GA — soak pending | (none — all shipped features are GA) |
All shipped features meet Jul.IA's nine-criteria GA bar (conformance matrix, benchmarks, docs, examples, security threat note, fuzzing where applicable, stable config/API contract, and Console surface), including the post-GA soak gate per ADR 0005. See
docs/status.mdfor the full matrix and per-feature soak-evidence links. A soak failure on a GA feature is a release-blocking regression, not a reason to retract the label.
Delivery state ≠ maturity. "GA" describes the stable released maturity contract. The immutable
v1.32.1-rc.1tag is an independently verified published prerelease candidate, not a stable release: it contains all 12 platform/architecture/profile archives, verified checksums and embedded SPDX SBOMs, plus SLSA provenance and SPDX SBOM attestations for every binary. It covers strict unknown- and known-value validation, HTTP/3 mTLS parity, compressionno-transform, exclusive ACME challenge selection, the frozen Prometheus contract, bounded/redacted WAF request logging, explicit access-log enablement, and dependency/CI fixes. See the candidate evidence. Currentmainhas since completed the response-cache correctness programme: the cache retains GA on the strength of the 2026-08-07 recertification — source audit, executable behavior matrix, race and protocol evidence, benchmarks and soak. Seedocs/cache.mdanddocs/status.md; the combined audit is retained as the dated record that programme closed out.
Many features require an opt-in build tag (e.g. grpc, acme,
wasmplugins, stream, http3, waf, consul, kubernetes). The default
lean binary ships the core GA surface plus core compression (gzip). Build
with -tags "…" or download the full release profile to enable everything.
| Area | Capability |
|---|---|
| Static files | Document root serving, index files, try_files, optional directory listing, hidden-file control, Cache-Control headers |
| Reverse proxy | proxy_pass to a concrete URL or a named upstream; per-location connect/read/send timeouts; custom upstream headers with variable expansion |
| WebSocket & SSE | Transparent passthrough of Connection: Upgrade (HTTP 101) connections — text and binary frames spliced bidirectionally (Apollo GraphQL subscriptions, Socket.IO) — and text/event-stream / chunked responses streamed per write, never buffered (Node/Python SSE) |
| Load balancing | round_robin, weighted_round_robin, and least_conn strategies across an upstream pool |
| Health & failover | Passive health checking (max_fails / fail_timeout) plus optional active HTTP/TCP probes ([upstreams.health_check]), with automatic retry of idempotent requests against healthy backends |
| Service discovery | Resolve an upstream's backends dynamically and refresh the pool live without a reload ([upstreams.discovery]): DNS A/AAAA and DNS SRV in every build, plus Consul and Kubernetes EndpointSlices behind the consul/kubernetes build tags — failed or empty resolves keep the last-good backends |
| App gateways | fastcgi_pass (e.g. PHP-FPM) and uwsgi_pass (Python/WSGI) with full CGI parameter mapping |
| gRPC transcoding | Expose a gRPC service as a RESTful JSON API via google.api.http annotations (grpc_transcode) — unary and streaming (server/client/bidi, NDJSON or SSE) — from a compiled descriptor set or server reflection, opt-in grpc build tag |
| gRPC passthrough | Reverse-proxy native gRPC end to end over HTTP/2 (grpc = true) — trailers preserved, streaming frames flushed immediately, load balancing and health checks applied — with cleartext h2c inbound (h2c = true) for clients without TLS, opt-in grpc build tag |
| Response cache | Two-tier (in-memory + optional disk overflow) cache with TTL, stale-while-revalidate, and admin purge; currently under correctness recertification — see docs/cache.md |
| Compression | On-the-fly gzip (every build) plus br/zstd codings (via the brotli/zstd build tags); Accept-Encoding negotiation, Cache-Control: no-transform, MIME allow-list, size threshold, and precompressed .br/.gz sidecars |
| Rate limiting | Token-bucket request limiting keyed by client IP, a request header, or a JWT claim, with burst, global or per-location policy, and 429 + Retry-After; plus a per-listener concurrent-connection cap |
| Access control | Per-location CIDR allow/deny lists plus one credential method — HTTP Basic (bcrypt htpasswd), JWT bearer tokens validated against a JWKS endpoint (asymmetric algorithms only, none rejected), or forward-auth to an external service |
| WAF | ModSecurity-compatible web application firewall (Coraza) with the OWASP Core Rule Set embedded in the binary ([waf], global or per-location): block/detect modes, paranoia levels, your own SecLang files or inline rules, request/response body inspection, and a jul_waf_events_total metric — opt-in waf build tag (docs/waf.md) |
| Secrets references | Keep credentials out of the config file: any string field accepts ${env:NAME}, ${file:/path}, or ${secret:/path} references resolved at serve time, resolved values are masked from logs, and jul lint flags literal admin/Consul/Kubernetes tokens — core, no build tag (docs/secrets.md) |
| Egress allow-list | Optional hardening ([egress]) that constrains the server's own config-driven fetches — JWKS, forward-auth, Consul/Kubernetes discovery, ACME/OCSP, and the WASM plugin fetch intersection — to an approved set of hosts/CIDRs, refused at connect time; bounds the SSRF blast radius of a misconfigured or compromised config, disabled by default — core, no build tag (docs/egress.md) |
| TLS | TLS 1.2/1.3 termination per server block, configurable minimum version, optional HTTP→HTTPS redirect |
| Automatic HTTPS | ACME certificate issuance and auto-renewal using the configured exclusive HTTP-01 or TLS-ALPN-01 challenge, with on-disk account/certificate cache — opt-in acme build tag |
| HTTP/3 | HTTP/3 over QUIC on the same address (UDP), sharing the complete server TLS/mTLS policy and certificate provider, advertised through Alt-Svc; static certificate-file changes remain restart-bound — opt-in http3 build tag |
| Routing | exact, prefix, and regex location matching; regex rewrites with last/break/redirect/permanent flags |
| Virtual hosts | Multiple server_names per listener; multiple listen addresses |
| Limits & timeouts | client_max_body_size, header size caps, read/write/idle/header timeouts (per-server, location overrides for body size) |
| Redirects | return, redirect, and deny (403) location actions; custom error pages |
| Hot reload | Zero-downtime config reload via SIGHUP, file-watch, or the admin API — invalid configs are rejected and the old config keeps serving |
| Observability | Structured logging (text/JSON), pluggable access-log sinks (file/syslog with rotation), Prometheus metrics, OpenTelemetry tracing, health/readiness probes |
| Admin GUI | Loopback-bound web console with live metrics, upstream/certificate status, config history and rollback, setup and structured editors. Legacy shared-token mode remains available; opt-in local multi-principal RBAC with predefined/custom roles, scoped revocable tokens and per-principal audit attribution is shipped (console build tag). External OIDC/SAML/SCIM identity is not shipped. |
| Developer experience | Zero-config jul run --serve/--proxy (no file needed), jul lint best-practice checks with CI-friendly exit codes, and jul fmt canonical formatting |
| Migration | jul import nginx translates an existing NGINX config to Jul.IA TOML, reporting every directive it could not map — opt-in importer build tag |
| WebAssembly plugins | Sandboxed request middleware and handlers compiled to WASM and run on the embedded wazero runtime (pure Go, no cgo): per-plugin memory and time limits, panic isolation, capability-gated key/value store, hot-reloadable — opt-in wasmplugins build tag |
| L4 stream proxy | TCP and UDP reverse proxying ([[stream]]) with load balancing and health checks across an upstream pool, TLS SNI routing by host without terminating, and HAProxy PROXY protocol v1/v2 (in and out) to preserve the client address — survives hot reload, opt-in stream build tag |
| Portability | Single static binary, no runtime dependencies; Windows, Linux, and macOS on amd64/arm64 |
Note: The
[[stream]](L4 proxy) table is active in binaries built with thestreamtag; in a binary without that tag a populated[[stream]]table is rejected at startup. The[plugins]table is active in binaries built with thewasmpluginstag; in a binary without that tag a populated[plugins]table is rejected at startup. The[waf]table (and per-locationwafoverride) is active in binaries built with thewaftag; in a binary without that tag an enabled WAF config is rejected at startup.
Jul.IA ships with an in-tree benchmark suite covering the hot path (routing, TLS, auth, proxy, cache, gRPC, and HTTP/3). For how to run benchmarks, what each measures, and tuning recommendations (connection pooling, cache sizing, compression levels, worker limits), see docs/benchmarks.md.
Download the archive for your platform from a release
(or build it yourself — see Building from source)
and extract it. Each archive contains the binary, a sample server.toml, the
SBOM, and README/SECURITY docs.
Archives are named jul_<version>_<os>_<arch>_<profile>.(tar.gz|zip) and ship in
two profiles: lean (the default build, no optional features) and full
(every opt-in feature — Brotli/Zstd, ACME, console, OTel, gRPC, HTTP/3, importer,
WASM plugins, stream proxy, Consul/Kubernetes discovery, WAF). Pick full unless
you specifically want the smaller lean binary.
| Platform | Archive (full profile) |
|---|---|
| Windows (Intel/AMD 64-bit) | jul_<version>_windows_amd64_full.zip |
| Windows (ARM64) | jul_<version>_windows_arm64_full.zip |
| Linux (Intel/AMD 64-bit) | jul_<version>_linux_amd64_full.tar.gz |
| Linux (ARM64) | jul_<version>_linux_arm64_full.tar.gz |
| macOS (Apple Silicon) | jul_<version>_darwin_arm64_full.tar.gz |
| macOS (Intel) | jul_<version>_darwin_amd64_full.tar.gz |
Swap full for lean for the minimal build. Verify your download and the
build provenance before running it — see docs/release.md for
the sha256 checksums, SBOM, and gh attestation verify steps, plus the full
list of variants and per-platform install notes.
Not sure which architecture you need?
- Windows:
echo $env:PROCESSOR_ARCHITECTURE→AMD64orARM64 - Linux/macOS:
uname -m→x86_64(amd64) oraarch64/arm64
From the extracted folder (or the repo root if running from source):
Windows (PowerShell):
.\jul.exe --config .\server.tomlLinux / macOS:
chmod +x ./jul
./jul --config ./server.tomlFrom source (Go installed):
go run ./cmd/jul --config server.tomlValidate a configuration without starting the server:
jul check -config server.tomlPrint the version:
./jul --versionjul [flags] run the server (default)
jul serve [-config f] run the server (explicit form)
jul check [-config f] [-json] [-quiet] full runtime preflight check
jul healthcheck [-config f] [-addr h:p | -url u] [-ready] [-timeout d] [-json] [-quiet]
probe a running server's health endpoint
jul lint [-config f] [-strict] [-json] [-quiet]
validate + best-practice checks
jul fmt [-config f] [-w] [-diff] rewrite the config in canonical TOML
jul run --serve <dir> | --proxy <target> [--listen addr]
run a zero-config server (no file)
jul import nginx [-o out.toml] [-strict] <nginx.conf>
translate an NGINX config (importer tag)
jul version [-json] print version and build metadata
jul completion <bash|zsh|fish|powershell> print a shell completion script
Legacy flags (default command, still supported; deprecated — prefer the subcommands above):
--config string path to the TOML configuration file (default "server.toml")
--check validate the configuration and exit (prefer "jul check")
--version print version and exit (prefer "jul version")
Performs a full runtime preflight: it validates structurally and dry-runs
every component that could fail during serve/reload (WAF rule compilation, auth
initialisation, compression encoder availability, plugin compile, etc.). This
is stronger than jul lint, which only checks schema and best-practice
warnings. Use check in CI before deploying, or locally when you want
certainty that the binary you built can actually start with this config.
jul check -config server.toml
jul check -config server.toml -json # machine-readable outputExit codes: 0 ok, 1 validation or runtime error. The legacy --check flag
on the default command (jul -check) is equivalent but jul check is the
canonical subcommand.
Probes a running server's admin health endpoint and exits with a
deterministic status, so it can drive container, systemd, and Kubernetes
liveness/readiness checks — including from a shell-less distroless image where
curl/wget are unavailable. It reads the [admin] listen address from the
config (or takes -addr/-url) and GETs /healthz (liveness) or, with
-ready, /readyz (readiness). The admin listener must be enabled.
jul healthcheck # discover [admin] listen from server.toml
jul healthcheck -config /etc/jul/server.toml -ready # readiness probe
jul healthcheck -addr 127.0.0.1:9090 -quiet # exit code only, no output
jul healthcheck -url http://127.0.0.1:9090/healthz -jsonExit codes: 0 healthy (endpoint returned 2xx), 1 unhealthy (non-2xx, or
the server was unreachable / timed out), 2 usage or config error (bad flags,
unreadable config, or the admin listener is disabled). The health verdict is
strictly 0/1, so the command is safe to use directly in a Docker
HEALTHCHECK — see deployment.md.
Parses and validates the configuration and additionally reports best-practice
warnings in a single pass: an unauthenticated off-loopback admin listener,
disabled compression, TLS without an explicit min_version, unreachable
(duplicate) locations, directory listing exposure, and servers without
locations. Each finding includes a hint. Exit codes are CI-friendly: 0 when
there are no errors, 1 on validation errors, and 2 when warnings are present
under -strict. Parse errors point at the offending line and column.
Validates and rewrites the configuration into canonical TOML. Invalid known
values are rejected before anything is printed or written. By default it prints
to stdout; -w writes the result back to the file. -diff shows a unified diff
of the changes without writing: exits 0 when nothing would change, 1 when
changes are needed (useful for CI enforcement). Comments and original formatting
are not preserved.
Starts a server from a synthesized profile without any config file:
jul run --serve ./public # serve a directory of static files
jul run --proxy 127.0.0.1:3000 # reverse-proxy everything to a backend
jul run --proxy :3000 --listen :80 # proxy to loopback :3000, listen on :80Zero-config defaults enable compression and sensible timeouts. The default
listen address is :8080.
Translates an existing NGINX configuration into Jul.IA TOML. It is gated behind
the importer build tag (see Migrating from NGINX):
jul import nginx /etc/nginx/nginx.conf # write TOML to stdout
jul import nginx -o server.toml /etc/nginx/nginx.confEvery directive it cannot translate is reported with its source line, both on
stderr and as a comment header in the output, so nothing is dropped silently.
The generated config is re-parsed and validated before it is emitted. Exit
codes: 0 ok, 1 parse/translate error or invalid output, 2 warnings under
-strict.
Prints the version and build metadata. Human-readable by default; -json emits
a stable machine object (keys: product, version, commit, build_date,
dirty, go_version, os, arch) for scripts and CI.
jul version # human-readable
jul version -json # machine-readable objectThe version string is stamped by the release pipeline (and make build); the
commit, build date, and dirty flag are read from the Go build info the toolchain
embeds automatically, so they populate for any go build from the repository and
degrade to unknown when VCS metadata is absent. To stamp a custom version:
go build -ldflags "-X main.version=1.2.3" -o jul ./cmd/julGenerates a shell completion script for bash, zsh, fish, or powershell
(pwsh). Source it for the current session, or install it into the shell's
completion directory:
source <(jul completion bash) # bash, current session
jul completion zsh > "${fpath[1]}/_jul" # zsh, installed
jul completion fish > ~/.config/fish/completions/jul.fish # fish
jul completion powershell | Out-String | Invoke-Expression # PowerShellCompletion covers the subcommand verbs and the arguments of completion and
version; file paths complete elsewhere.
For a full incident playbook — startup, reloads and restart-required changes, service discovery, plugins, and soak interpretation — see docs/troubleshooting.md.
- Config rejected on start or reload? Run
jul lint -config server.tomlto see every error and warning at once; the running server keeps its last valid configuration on a failed reload. - Not sure a change is safe?
jul lintbefore reloading, or rely on the admin GUI which validates before applying. - Want a quick local server?
jul run --serve .needs no config file.
jul import nginx reads an NGINX configuration file and produces an equivalent
Jul.IA TOML config. It is a best-effort migration aid, not a 1:1 converter:
common directives are translated, and everything it cannot map is reported with
its source line so you can port it by hand. The importer is gated behind the
importer build tag to keep the default binary lean:
go build -tags importer -o jul ./cmd/jul
jul import nginx -o server.toml /etc/nginx/nginx.confThe generated file is re-parsed and validated exactly as the server would load
it, so a successful run always yields a config that passes jul lint. Run the
import, then review the # TODO comments at the top of the output and the
summary printed to stderr.
| NGINX | Jul.IA |
|---|---|
http { ... } |
top-level config |
server { ... } |
[[servers]] |
listen 80; / listen 443 ssl; |
listen = ":80" / ":443" + [servers.tls] |
server_name a b; (_ dropped) |
server_names = ["a", "b"] |
root / index / try_files |
location root / index / try_files |
location / { ... } |
[[servers.locations]] with a prefix match |
location = /p / ^~ /p / ~ re / ~* re |
exact / prefix / regex / regex match |
proxy_pass http://name; |
location proxy_pass (a bare host gets http://) |
fastcgi_pass |
location fastcgi_pass |
return 301 https://h/; / return 404; |
location redirect + return / return |
rewrite re repl flag; |
location [[...rewrites]] |
upstream name { server ... } |
[[upstreams]] with servers |
server h weight=3; |
upstream server weight (→ weighted_round_robin) |
least_conn; |
upstream strategy = "least_conn" |
ssl_certificate / ssl_certificate_key |
[servers.tls] cert / key |
ssl_protocols TLSv1.2 TLSv1.3; |
[servers.tls] min_version |
gzip on; |
[compression] enabled = true |
includedirectives are not followed — import each included file separately, or concatenate them first.- Process-level directives (
worker_processes,events,pid,user, …) have no per-server equivalent and are ignored. stream/mailmodules,map/geo/ifblocks, named locations (@name), Lua, and any directive without a Jul.IA equivalent (add_header,proxy_set_header,client_max_body_size,autoindex, …) are listed in the report for manual porting.
See examples/migrate for a sample nginx.conf, the config
it produces, and a walkthrough.
Jul.IA is configured by a single TOML document. The top-level tables are
[global], [[servers]], [[upstreams]], [cache], and [admin].
A minimal, working example:
[global]
log_level = "info"
shutdown_timeout = "30s"
[[servers]]
listen = "0.0.0.0:8080"
server_names = ["localhost", "example.com"]
[[servers.locations]]
match = { type = "prefix", path = "/" }
root = "/srv/www/example"
index = ["index.html", "index.htm"]
try_files = ["$uri", "$uri/", "/index.html"]
[[servers.locations]]
match = { type = "prefix", path = "/api/" }
proxy_pass = "http://backend"
cache = true
[servers.locations.headers]
Host = "$host"
X-Real-IP = "$remote_addr"
X-Forwarded-For = "$proxy_add_x_forwarded_for"
[[upstreams]]
name = "backend"
strategy = "round_robin"
servers = ["127.0.0.1:3000", "127.0.0.1:3001"]
[cache]
enabled = true
memory_max_size = "64m"
default_ttl = "60s"
stale_while_revalidate = "30s"
stale_if_error = "300s"
[admin]
enabled = true
listen = "127.0.0.1:9090"
token = "change-me"
console = trueThe full configuration reference — every key, type, default, and example —
lives in docs/configuration.md so it can be updated
independently and deep-linked.
Key sections covered there:
[global]— worker threads, logging, shutdown[[servers]]— listeners, TLS, timeouts[[servers.locations]]— matching, static files, proxy, auth, rate limits[[upstreams]]— load balancing, health checks, service discovery[cache]— two-tier cache, stale-while-revalidate, stale-if-error[compression]— gzip, Brotli, Zstd[rate_limit]— token-bucket limiting[admin]— admin listener and console[observability.*]— tracing, metrics, and explicitly enableable/disableable access logs- TLS — static certificates, mTLS
- ACME — Let's Encrypt automation
- HTTP/3 — QUIC listener
[plugins]— WASM plugins[[stream]]— L4 TCP/UDP proxy
