diff --git a/backend/src/services/compression.rs b/backend/src/services/compression.rs
index d42e990..504f278 100644
--- a/backend/src/services/compression.rs
+++ b/backend/src/services/compression.rs
@@ -93,8 +93,21 @@ impl CompressionWorker {
// Run blocking image operations in a spawn_blocking task
tokio::task::spawn_blocking(move || -> Result<()> {
- let img = image::open(&original)
- .context("failed to open image")?;
+ // Reject decompression bombs *before* fully decoding: the upload body
+ // cap bounds the file size on disk, but a small file can still decode to
+ // enormous dimensions (e.g. a ~1 MB image expanding to 50k×50k px →
+ // gigabytes), OOM-ing the box during decode/resize. 12000×12000 covers
+ // any real phone photo; max_alloc hard-caps the decode allocation.
+ let mut reader = image::ImageReader::open(&original)
+ .context("failed to open image")?
+ .with_guessed_format()
+ .context("failed to read image header")?;
+ let mut limits = image::Limits::default();
+ limits.max_image_width = Some(12_000);
+ limits.max_image_height = Some(12_000);
+ limits.max_alloc = Some(256 * 1024 * 1024);
+ reader.limits(limits);
+ let img = reader.decode().context("failed to decode image")?;
// Resize to max 800px wide, preserving aspect ratio
let preview = img.resize(800, 800, image::imageops::FilterType::Lanczos3);
diff --git a/docs/DECISION-media-auth.md b/docs/DECISION-media-auth.md
new file mode 100644
index 0000000..6db4208
--- /dev/null
+++ b/docs/DECISION-media-auth.md
@@ -0,0 +1,79 @@
+# Decision: media serving — unauthenticated UUID vs. signed gateway
+
+**Status:** OPEN — needs a call. Written 2026-06-30 from the 2026-06-27 audit
+(`fix/audit-2026-06-27-critical-medium`), which implemented the signed-gateway option that
+`main` did not adopt.
+
+**Scope:** how `main` serves uploaded photos/videos (originals + previews/thumbnails) to the
+`
`/`