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"}