# Photo Wall — upload from your phone to R2 (★★)

Build a private photo wall for me and my friends. On a phone, tap "Add photos", pick a few from the camera roll, and they appear on the wall a moment later, on every device that has the wall password. The photos are stored as files in Cloudflare R2. A small D1 database keeps the list of photos: who added them, when, the caption and the size. The point of the project is to see the difference between the two: files go in file storage, facts about the files go in a database.

## Done looks like

- A password screen. The wall password is a Worker secret, never in the code or the repo. The right password is remembered on this device.
- "Add photos" opens the phone's photo picker (or camera). Several photos can be picked at once. Each shows a progress bar and then appears at the top of the wall.
- Before upload, the browser shrinks each photo to at most 2000 px on the long side and makes a 400 px thumbnail. Re-encoding through a canvas also drops the location data hidden in the photo.
- The wall is a grid of thumbnails, newest first, with lazy loading and "Load more" after 30. Nothing jumps around while images load.
- Tapping a thumbnail opens the full photo with its caption. Swipe or arrow keys move between photos.
- Delete removes the photo files from R2 and the row from D1, after a confirm.
- The Worker refuses anything that is not JPEG, PNG or WebP (it checks the first bytes of the file, not just the name), and anything over 5 MB, with a clear message.
- Photos are not public: image addresses expire after an hour and stop working without a valid signature.
- Live at the address `npx wrangler deploy` prints. No console errors.

## Constraints

- One Cloudflare Worker with static assets, an R2 bucket binding and a D1 binding. Use wrangler. Free plan only.
- Front end: plain HTML, CSS and JavaScript, no frameworks, no build step.
- Secret: `WALL_PASSWORD`, set with `npx wrangler secret put WALL_PASSWORD`. A local `.dev.vars` file for testing, listed in `.gitignore`.
- Every `/api/` request carries the password in a header. The Worker compares it in constant time and answers 401 if it is wrong.
- R2 keys: `photos/<id>.jpg` and `thumbs/<id>.jpg` (or `.webp`), where `id` is random. Never use the uploaded file name.
- D1 table: `photos(id, caption, added_by, width, height, bytes, created_at)`. Captions up to 140 characters, names up to 20.
- Image addresses: the list endpoint returns `/img/<key>?exp=<time>&sig=<hmac>`, where the signature is an HMAC-SHA256 of key and expiry using a second secret, `SIGNING_KEY`. The `/img/` route checks both before reading from R2.
- No accounts, no tracking. Phone works.

## Milestones

1. **The wall, on one device.** A page with an "Add photos" button that takes files from the phone or laptop, shrinks each one in the browser to 2000 px plus a 400 px thumbnail with a canvas, and shows the thumbnails in a grid. Nothing is uploaded yet.
   Check: pick five phone photos; the grid shows them within a few seconds, and the shrunk copies are under 1 MB each (log their sizes in the console).
> That is the wall for one device, and it forgets everything on reload. The rest of the project is the server that keeps the photos and shares them.
2. **Worker and password.** Set up the Worker with static assets and a password screen. `GET /api/check` returns 200 with the right password and 401 without.
   Check: running locally, a wrong password shows "Wrong password" and the right one opens an empty wall.
3. **Upload to R2.** `PUT /api/photos` stores the body in R2 under a random key, once for the big copy and once for the thumbnail, with progress shown. The Worker checks type by first bytes and size, and refuses anything else.
   Check: a 4 MB phone photo arrives in R2 as a file under 1 MB plus a small thumbnail; a text file renamed to `.jpg` is refused; a 20 MB file is refused with "Too big".
4. **The list in D1.** Create the `photos` table with a migration. Upload writes a row. `GET /api/photos?before=<time>` returns 30 rows, newest first. Show the thumbnail grid with `loading="lazy"` and width and height set.
   Check: upload 40 photos; the wall shows 30, "Load more" shows the rest, and the page does not jump while loading.
5. **Signed image addresses.** Add `SIGNING_KEY`, sign each image address in the list, and check it in `/img/`.
   Check: copy an image address, change one letter of the signature, open it: 403. Wait past the expiry: 403.
6. **Viewer, captions, delete.** Full-screen viewer with swipe and arrow keys. Add a caption on upload. Delete removes both R2 objects and the D1 row.
   Check: delete a photo on the phone and reload the laptop: it is gone, and the R2 bucket no longer has its two files.
7. **Go live and test on a phone.** Deploy. Add the secrets to the live Worker. Upload from a real phone on mobile data.
   Check: a photo taken a minute ago on the phone shows up on the laptop's wall after a reload.

## Stretch

- Albums: an `album` column and a filter bar.
- A slideshow mode for a TV: full screen, next photo every 8 seconds, new uploads join the loop.
- Two passwords: one that can only view, one that can also add and delete.
- Drag and drop many files on a laptop.
- A storage meter that adds up `bytes` from D1 and shows how much of the 10 GB free R2 storage the wall uses.

## Leave out

- Accounts and public uploads with no password.
- Video, face recognition, filters and editing.
- Sharing to social apps, comments, likes.

Start by reading WRITING.md and the guide chapter on data if you need storage.
