Notes / 1 of 3

Your image never leaves the device —
and that is an architecture decision.

Bandwidth arithmetic, privacy you can actually verify, and the four cases where you should not move processing to the browser.

loner10 Nov 20267 minarchitecture

Every photo tool starts the same way: upload the file, process it on a server, send it back. It is the obvious design, every tutorial does it that way, and for a lot of products it is the wrong one.

I built a passport photo tool where the image never leaves the browser. No upload endpoint exists. That is not a privacy marketing line — it removes an entire category of work, cost and risk from the system.

01The bandwidth argument

A phone photo is 3–6 MB. If a user resizes it, you pay to receive it, pay to store it while you work, pay to send the result back, then pay to delete it. For a 400 KB output you moved 6 MB across the network twice over.

Do the arithmetic on any traffic estimate and it stops making sense. Ten thousand sessions a month at 6 MB in and 400 KB out is roughly 64 GB of transfer to produce maybe 4 GB of finished files. The server does almost nothing but shuffle bytes.

Client-side, that cost is zero. The bytes are already on the device; the CPU is already idle.

02Privacy is a feature you can prove

"We do not store your photos" is a promise. Anyone can write it in a policy and nobody can check it.

"There is no upload endpoint" is a fact. A user can open dev tools, watch the network tab, and see nothing leave. That difference matters more than it sounds: for passport photos you are handling a document image that people are already nervous about.

Trust that can be verified beats trust that has to be taken on faith — especially when you are a stranger on the internet asking for money.

03What actually breaks

It is not free. The failure modes move from your server to their device:

04The core of it

One function does the actual work. Everything else is UI.

function draw(ctx, W, H, img, zoom, ox, oy) {
  // cover: fill W×H, never letterbox
  var s  = Math.max(W / img.naturalWidth, H / img.naturalHeight) * zoom;
  var dw = img.naturalWidth * s, dh = img.naturalHeight * s;

  // pan range, normalised so preview and export share one state
  var maxX = Math.max(0, (dw - W) / 2);
  var maxY = Math.max(0, (dh - H) / 2);

  ctx.drawImage(
    img,
    (W - dw) / 2 + ox * maxX,
    (H - dh) / 2 + oy * maxY,
    dw, dh
  );
}

The important detail is that ox and oy are normalised to [-1, 1] rather than stored as pixels. That way the same state renders correctly on a 300 px preview canvas and a 413 × 531 export canvas. Store pixels and you get two sources of truth that drift apart the moment the window resizes.

05When you should not do this

Client-side is not a rule, it is a trade. Do it on the server when:

For a crop-and-compress tool, none of those apply. So it runs in the browser, and the server only exists to serve the HTML.

Worth keeping

  1. Move work to the client when the operation is simple, deterministic and sensitive.
  2. Normalise interaction state, not pixels, if preview and export need to agree.
  3. "No upload endpoint" is a stronger claim than "we delete your files" — because it is checkable.
IndexAll notesNextTen passport specs, one table