diff --git a/docs/architecture.md b/docs/architecture.md
index ebe7622..67452c4 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,8 +1,10 @@
# Architecture
-Two independent pieces talk over plain HTTP on the local network: the
-ESP32-C6 firmware, and a small FastAPI server that sits between it and
-Immich.
+Two independent pieces talk over HTTP or HTTPS (the server itself always
+speaks plain HTTP; HTTPS means a reverse proxy in front of it, see
+[`firmware/README.md`](../firmware/README.md#http-vs-https)) on the local
+network: the ESP32-C6 firmware, and a small FastAPI server that sits
+between it and Immich.
```mermaid
sequenceDiagram
@@ -16,11 +18,14 @@ sequenceDiagram
Note over Frame: User scans WiFi QR, then config QR -> fills in
home WiFi + "Tools Server" host:port
Frame->>Frame: Save config to NVS, reboot
- Note over Frame: Every wake (deep sleep timer, next-photo button,
or any other reboot)
+ Note over Frame: Every wake (deep sleep timer, next/back-photo button,
or any other reboot)
Frame->>Frame: Connect to home WiFi
alt next-photo button pressed
Frame->>Server: POST /frame/advance
Server->>Server: Force-advance to next queued photo, reset interval clock
+ else back-photo button pressed
+ Frame->>Server: POST /frame/back
+ Server->>Server: Return to previously-current photo (bounded history),
reset interval clock
else normal wake
Frame->>Server: GET /frame/image
Server->>Server: Advance only if refresh_interval_s has elapsed
since the current photo was set -- otherwise a no-op
@@ -52,12 +57,15 @@ sequenceDiagram
2. **Stored config exists**: connect to the saved WiFi network (a few
retries before falling back to provisioning if it fails), then run the
fetch cycle in `frame_client.c`:
- - Check the next-photo button (`next_button_check()`) -- if it was
- what woke the device (checked via the latched
- `esp_sleep_get_gpio_wakeup_status()`, not a live pin read, since a
- quick tap can release before boot gets around to polling it) or is
- currently held, the fetch below hits `POST /frame/advance` instead
- of `GET /frame/image`, forcing the server to skip ahead immediately.
+ - Check the next-photo and back-photo buttons (`next_button_check()`,
+ `back_button_check()`) -- if either was what woke the device
+ (checked via the latched `esp_sleep_get_gpio_wakeup_status()`, not
+ a live pin read, since a quick tap can release before boot gets
+ around to polling it) or is currently held, the fetch below hits
+ `POST /frame/advance` or `POST /frame/back` instead of
+ `GET /frame/image`, forcing the server to move in that direction
+ immediately (next takes priority if somehow both read pressed at
+ once).
- Fetch the frame and write it into the panel's SPI buffer
(`epd_write_frame()`), computing a CRC32 as it streams -- never
buffering the full ~192KB frame in RAM. The panel driver refuses to
diff --git a/firmware/README.md b/firmware/README.md
index b6bfb1e..40771af 100644
--- a/firmware/README.md
+++ b/firmware/README.md
@@ -96,17 +96,30 @@ The Tools Server field accepts either:
manage-menu overlay data, the QR codes' own links) uses whichever
scheme you enter.
-The firmware trusts the standard public CA bundle ESP-IDF ships
-(`esp_crt_bundle_attach`, the same root store a browser trusts) -- so
-any reverse proxy with a normal publicly-trusted certificate just
-works: Let's Encrypt, a Cloudflare-proxied hostname (Cloudflare's own
-edge certificate, issued by Google Trust Services or similar -- **not**
-Cloudflare's Origin CA cert, which only ever sits on the Cloudflare-to-
-origin leg and is never presented to a public client, ESP32 or browser
-alike), or any other public CA. If your proxy uses a private/self-signed
-cert instead (no public CA in the chain at all), the public bundle won't
-trust it -- that's not supported today, would need switching back to
-embedding that specific cert.
+The firmware does **not** use ESP-IDF's general public CA bundle --
+it pins one specific certificate, embedded at build time from
+[`main/certs/tools_server_ca.pem`](main/certs/tools_server_ca.pem) and
+trusted directly via `cert_pem` on every request. (The public bundle
+was tried first and rejected: it does an exact byte-level match against
+its compiled-in table, and a real-world root that's been re-issued
+under a new serial/signature but the same name and key -- as Google did
+for GTS Root R4 -- doesn't match it, confirmed on hardware.)
+
+This means a normal publicly-trusted certificate (Let's Encrypt, a
+Cloudflare-proxied hostname, etc.) does **not** automatically work --
+only whichever certificate is actually embedded in
+`certs/tools_server_ca.pem` is trusted. To point the device at a
+different reverse proxy, extract that proxy's actual certificate and
+replace the file's contents:
+
+```
+openssl s_client -connect :443 -showcerts