# Attachments, and what they would actually cost

*3 September 2026. Written in response to: "consider whether we can/should support the camera
and photo sending, videos? … what about other attachments of reasonable size … do the math just
to have an idea for the future."*

## The headline, before the numbers

**The intuition that we cannot afford attachments is correct on Netlify and wrong in general.**
Netlify meters *bandwidth*; every byte a phone collects is billed. Object stores built for this
job meter *operations* and give egress away. On Cloudflare R2 the bytes are close to free and the
cost is the number of requests.

That inverts the conclusion. On R2, **video is cheaper than photos** — not because video is small,
but because people send one video where they send three photos, and requests are what we pay for.

And it exposes the real cost driver, which is neither:

> **Polling costs more than every attachment combined.** At 100,000 users, payload runs about
> $80/month and polling about $1,300/month. Push notifications are not a nicety; they are the
> difference between those two numbers.

## The numbers

Verified 3 Sep 2026 from the vendors' own pricing pages.

**Netlify** (what we run today): bandwidth 20 credits/GB; web requests 2 credits per 10,000;
functions compute 10 credits per GB-hour. A Pro credit is about $0.0067, so **bandwidth is roughly
$0.13/GB**. Blobs storage is not separately published — it draws on the same credit pool.

**Cloudflare R2**: storage $0.015/GB-month, Class A (writes, deletes, lists) $4.50/million,
Class B (reads) $0.36/million, **egress free**. Free monthly: 10 GB, 1M Class A, 10M Class B.

### What a message costs to carry

The server is a relay, not an archive: a message is deleted the moment it is confirmed, so
steady-state storage is only what is *in flight*, not everything ever sent. Bandwidth, by
contrast, is paid twice on every delivered byte — once in, once out.

Sizes used below: voice note (Opus 24 kbps, 30 s) 90 KB; photo at WhatsApp-grade compression
150 KB; photo at good quality 500 KB; 30-second video at WhatsApp-grade 3 MB.

**Monthly cost of payload, by scale** — Netlify bandwidth vs R2 operations:

| Traffic | 1k users | 10k users | 100k users | 100k on R2 |
|---|---|---|---|---|
| Text only (50 msg/day) | $0.40 | $4 | $40 | ~$8 |
| Voice notes (2/day, 90 KB) | $1.44 | $14 | $144 | ~$50 |
| Photos (3/day, 150 KB) | $3.60 | $36 | $360 | **~$77** |
| Photos (3/day, 500 KB) | $12 | $120 | $1,200 | **~$77** |
| Video (1/day, 3 MB) | $24 | $240 | $2,400 | **~$24** |

The R2 column barely moves with size and moves a lot with frequency. That is the whole point:
three photos a day costs three times one video a day, even though the video is twenty times
bigger. **We are not paying for storage. We are paying for requests.**

### And then there is polling

Every background check is a request whether or not a message is waiting. At 100,000 users
checking every 15 minutes, that is 96 checks a day, about **288 million requests a month**:

- On Netlify web requests: roughly **$384/month**, plus function compute on top.
- On R2, if each check is a list operation (Class A): roughly **$1,300/month**.

Against $77/month for the photos those checks are looking for. Polling is 5–17× the cost of the
payload, and it scales with *users*, not with usage — a user who sends nothing at all still costs
us every fifteen minutes, forever.

**This is the strongest argument yet for finishing push.** A wake-up push costs nothing on either
platform (APNs and FCM are free), and it replaces the poll that costs everything. It also happens
to be the better product: messages arrive when they arrive rather than up to fifteen minutes
later. The privacy story does not change — the push carries no content, only "wake up," which is
what `DECISIONS.md` already says it is.

## So: should we ship attachments?

**Photos and voice notes: yes, and the cost is not the reason to hesitate.** At any scale we can
plausibly reach in the next two years, compressed photos on R2 cost less per month than a single
hour of contract work. The reasons to hesitate are engineering, not money.

**Video: technically affordable, but it is the biggest job on the list.** It needs transcoding,
codec choices, thumbnails, playback, and scrubbing — and every one of those is a place to leak
metadata or ship a crash. The cost math says yes; the calendar says much later.

**Other attachments (PDFs, documents): nearly free to carry and cheap to build**, because there is
no rendering problem — hand the file to the OS and let it open. If anything ships after photos,
this is the one with the best ratio of usefulness to work.

### What the engineering actually requires

Not in the protocol — the sealed envelope does not care what is inside it. The work is at the edges:

1. **Raise `MAX_ENVELOPE_BYTES`.** It is 32,768 today (`server/netlify/functions/_shared.mts:20`),
   which is a text-message limit. Attachments need chunking above some threshold rather than one
   enormous envelope, so that a failed transfer resumes instead of restarting.
2. **Strip EXIF before encrypting.** A photo carries GPS coordinates, the camera's serial number,
   and a timestamp. Shipping photo support without stripping these would undo, in one feature,
   every location claim we make. This is not optional and it is not a later refinement.
3. **Thumbnails**, so a conversation does not download megabytes to draw a list.
4. **Progress and resume.** A 3 MB transfer over a bad connection fails often enough that "try
   again from the start" is not acceptable.
5. **The direct path gets *better* here, not worse.** A photo over Wi-Fi Direct is free, fast, and
   never touches a server — the pitch is stronger for photos than it is for text, because the
   saving is visible. Sending a holiday album across a table with no internet at all is a demo
   nobody else can give.

### The migration this implies

If attachments ship, **the mailbox should move to R2 and Netlify should keep the API**. It is not
a hard migration — `deposit`/`collect`/`confirm` are the only functions that touch storage — and
it is worth roughly a factor of ten immediately and much more later. Doing it *before* attachments
is easier than doing it after, because there is less in flight to move.

One caveat worth stating rather than discovering: Cloudflare would then see our traffic patterns.
It never sees plaintext, and it never learns identities, because the tokens are already rotating
and the envelopes are already sealed. But it is a second party in the path, and the privacy policy
would have to say so plainly. That is a real cost, not a bookkeeping one.

## What this does not change

Nothing here argues for storing more, keeping it longer, or knowing more about who sent it. The
30-day ceiling, the rotating tokens, the delete-on-confirm, and the absence of logs are all
unaffected by how big the payload is — which is exactly why attachments are a cost question rather
than a privacy question. The privacy question is EXIF, and the answer is: strip it.
