Demo

This is the real BareProxy code, compiled to WebAssembly and running in your browser. Nothing you type is sent anywhere.

The demo also has a page of its own, with the whole screen to work in.

Below is what the same commands print on a server, run against the config on the config reference page. Each one reads the same compiled config the server routes with, so what it prints is what the server does. The names, addresses, times and IDs are example values.

Explain a Page Before Anyone Asks for It

explain walks the rules from the top, says why each one did or didn’t match, and shows the file that would be sent, with its type and the precompressed copies next to it:

$ bareproxy explain GET https://example.com/about/
Running config /etc/bareproxy/bareproxy.conf, version 12
Site example.com (line 2): exact host match
Certificate: from the file /etc/bareproxy/example.com.crt (line 3)
  line 5    route /healthz -> respond 200 "ok"          no: path is not /healthz
  line 6    route /api/* -> api strip                   no: path is not /api or below
  line 7    route /* -> files /var/www/example/public   match
Path: /about/
File: /var/www/example/public/about/index.html
Exists: yes
Content-Type: text/html; charset=utf-8
Also on disk: index.html.br, index.html.gz, sent to clients that accept them
Action: serve 200

A page that isn’t there gets the site’s 404 page, and explain says which file that is:

$ bareproxy explain GET https://example.com/old-page/
Running config /etc/bareproxy/bareproxy.conf, version 12
Site example.com (line 2): exact host match
Certificate: from the file /etc/bareproxy/example.com.crt (line 3)
  line 5    route /healthz -> respond 200 "ok"          no: path is not /healthz
  line 6    route /api/* -> api strip                   no: path is not /api or below
  line 7    route /* -> files /var/www/example/public   match
Path: /old-page/
Checked: /var/www/example/public/old-page/index.html
Exists: no (no such file)
Error page: /404.html (line 4), from /var/www/example/public/404.html
Action: 404

A symlink that leads out of the site folder is refused, and explain says so:

$ bareproxy explain GET https://example.com/docs/etc-link/passwd
Running config /etc/bareproxy/bareproxy.conf, version 12
Site example.com (line 2): exact host match
Certificate: from the file /etc/bareproxy/example.com.crt (line 3)
  line 5    route /healthz -> respond 200 "ok"          no: path is not /healthz
  line 6    route /api/* -> api strip                   no: path is not /api or below
  line 7    route /* -> files /var/www/example/public   match
Path: /docs/etc-link/passwd
Checked: /var/www/example/public/docs/etc-link/passwd
Exists: no (it leads outside the site folder)
Error page: /404.html (line 4), from /var/www/example/public/404.html
Action: 404

Explain a Request to the API

For a pool, explain shows the path the backend will see and which backend would get the request right now:

$ bareproxy explain GET https://example.com/api/orders
Running config /etc/bareproxy/bareproxy.conf, version 12
Site example.com (line 2): exact host match
Certificate: from the file /etc/bareproxy/example.com.crt (line 3)
  line 5    route /healthz -> respond 200 "ok"   no: path is not /healthz
  line 6    route /api/* -> api strip            match
Path: /api/orders
Sent upstream as GET /orders with Host: example.com
Pool api (line 13): 2 of 3 up, fewest in flight wins
  10.0.0.11:8080   up, 0 in flight                                          next pick
  10.0.0.12:8080   up, 2 in flight
  10.0.0.13:8080   down since 14:02:10, 3 failed checks (connect refused)

Ask What Happened to a Request

Every response carries a BareProxy-Id header, and every error page shows it. Give any unique part of it to why:

$ bareproxy why 7f3a9c
Request 7f3a9c0d12e4b5a6, 1 Oct 2026 14:03:22.418 UTC, config version 12
GET https://example.com/api/orders from 203.0.113.7, TLS 1.3, HTTP/2.0

Site example.com (line 2)
Rule line 6: route /api/* -> api strip
  sent upstream as /orders
Pool api (line 13): 2 of 3 up
  10.0.0.13:8080   skipped: down since 14:02:10, 3 failed checks (connect refused)
  10.0.0.11:8080   tried first, fewest in flight: connect refused after 0.4 ms
  10.0.0.12:8080   tried next: 200 OK, first byte after 37 ms
Response 200, 5.0 KB, 38 ms in total (outcome ok)

The same works for a file:

$ bareproxy why 9b1e44
Request 9b1e44c07d2a5f31, 2 Oct 2026 09:14:05.112 UTC, config version 12
GET https://example.com/images/photo.jpg from 203.0.113.7, TLS 1.3, HTTP/2.0

Site example.com (line 2)
Rule line 7: route /* -> files /var/www/example/public
File: /var/www/example/public/images/photo.jpg
Representation: photo.jpg
Content-Type: image/jpeg
Response 200, 482.0 KB, 1.3 ms in total (outcome file)

See What a Change Will Do

Here a new API version gets its own pool, the site moves to a new release folder and a backend is added to the old pool. plan compares the file with the running config and lists the requests that change hands, and nothing else:

$ bareproxy plan
Compared with running version 12:
Plan d56c1653fe8f: 2 routing changes, 2 other changes
Routing
  example.com (port 443), any method, /api/v2 and below
      pool api, strip /api  ->  pool api-v2, strip /api/v2
  example.com (port 443), any method, every path except /healthz and /api and below
      files /var/www/example/public  ->  files /var/www/example/releases/2026-10-05
Other changes
  pool api: backend 10.0.0.14:8080 added (line 18)
  pool api-v2 added (line 21): backend 10.0.1.11:8080, backend 10.0.1.12:8080, health /healthz

Each plan has an ID. A rule that only moves to another line with the same action isn’t a change, so it isn’t listed. A rule that can never match gets a warning. Here a beta route sits below the /api/v2/* rule, which already takes every request the beta route would get:

$ bareproxy plan
Compared with running version 13:
No changes.
Warnings
  line 7: route /api/v2/beta/* -> api-v2 never matches: line 6 takes every request it would get

Make It Live, and Go Back

apply checks the whole file again, shows the plan, asks, and swaps the config in at once. With --plan it refuses if the file or the running config changed since that plan was made, so two people can’t apply over each other. Requests in flight finish on the old config, and a backend that was removed drains:

$ bareproxy apply --plan d56c1653fe8f
Compared with running version 12:
(the plan above, shown again)
Apply? [y/N] y
Version 13 is running (it was 12).

The last 100 versions are kept in the state folder. history lists them, and rollback makes an earlier one live, by default the version before the running one. It goes through the same check and swap, writes the config file so that the file holds what is running, and says so with a line such as /etc/bareproxy/bareproxy.conf now holds version 14. It leaves a line in the history:

$ bareproxy history
Version  Time (UTC)           How       User        Plan
     10  2026-10-01 07:12:44  startup   root
     11  2026-10-01 07:58:03  apply     deploy      a41f09c7e2b3
     12  2026-10-01 13:20:51  apply     deploy      7be03d19c4a8
     13  2026-10-05 09:14:22  apply     deploy      d56c1653fe8f
     14  2026-10-05 09:31:40  rollback  deploy      e07a4b19d3c5 (version 12 again) (running)

The Record Behind It

Each request leaves one JSON line in the trace log:

{"id":"7f3a9c0d12e4b5a6","time":"2026-10-01T14:03:22.418Z","config":12,
 "client":"203.0.113.7","tls":"1.3","sni":"example.com","proto":"HTTP/2.0",
 "method":"GET","scheme":"https","host":"example.com","path":"/api/orders",
 "site":"example.com","site_line":2,"line":6,"rule":"route /api/* -> api strip",
 "upstream_path":"/orders","pool":"api","pool_line":13,"pool_up":2,"pool_size":3,
 "skipped":[{"backend":"10.0.0.13:8080","state":"down",
   "why":"down since 14:02:10, 3 failed checks (connect refused)"}],
 "attempts":[{"backend":"10.0.0.11:8080","error":"connect refused","ms":0.4},
   {"backend":"10.0.0.12:8080","status":200,"first_byte_ms":36.9}],
 "status":200,"bytes_in":0,"bytes_out":5120,"ms":38.2,"outcome":"ok"}