Build Pic-Time

KINDAreplaces $8/mosaves $96/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A client gallery platform where the engineering is delivering large images fast enough that a gallery of eight hundred photographs is browsable, and the discipline is that a photographer's delivery to a client is a professional commitment with a deadline attached.

estimated effort multi-daythe files for this build are in the project pack

StorageOriginals in object storage, derivatives generated once and cachedDeliveryA content network in front, with signed access per galleryProcessingA worker queue generating sizes on upload, resumableAccessPer-gallery links with optional password and expiryRuntimeTypeScript with PostgreSQL

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the processing workers.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Galleries, selections and orders with per-visitor keys.

    Get it Docker or your package manager. open ↗

    Verify psql connects and creates a table

  3. storage is cheap; bandwidth is the real cost

    Why Delivering hundreds of images per gallery is a bandwidth problem before it is anything else, and serving directly from storage is slow and expensive.

    Get it A storage provider with a network in front, supporting signed access and direct uploads.

    Verify A signed image address works and stops working after expiry

  4. have readyfree if you have one

    Why Upload resumption, processing time and gallery scrolling are all uninteresting below a few hundred images and become the whole problem above it.

    Get it A real set of full resolution files, including a mixture of orientations and at least one corrupt file.

    Verify Six hundred full resolution images are available including a deliberately corrupt one

  5. have readyfree

    Why Gallery performance is judged on a phone by a client who will not wait, and a desktop connection hides every problem that matters.

    Get it Any phone, tested away from a fast local network.

    Verify A gallery is browsed end to end on mobile data

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- galleries(id, client_ref, title, shoot_date, access_kind, password_hash, expires_at, download_policy, watermark_policy)
- photos(id, gallery_id, original_key, filename, captured_at, width, height, orientation, hidden)
- derivatives(photo_id, size, key, bytes, generated_at) · generated once, cached, regenerable
- selections(gallery_id, visitor_id, photo_id, kind, at) · kind is favourite, hidden or comment_anchor
- comments(id, photo_id, visitor_id, body, at)
- downloads(id, gallery_id, photo_id, visitor_id, size, at) · so a photographer knows what was taken
- orders(id, gallery_id, items_json, total_minor, status, placed_at) · print orders where offered
- A visitor identity is per gallery rather than global, because clients share links with family and each person's favourites should be their own.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/galleriesGalleries, client references and selections.
STORAGE_BUCKETrequireds3://gallery-originalsOriginals. Derivatives may live separately since they are regenerable.
CDN_BASE_URLrequiredhttps://img.example.comWhere signed derivative addresses are served from.
SIGNED_URL_TTL_MINUTESrequired60Long enough to browse a gallery, short enough that a copied address expires.
GALLERY_DEFAULT_EXPIRY_DAYSrequired365Galleries accumulate and storage costs money indefinitely.
WATERMARK_PAID_DOWNLOADSrequiredneverPresent as a constant. A client who paid gets the file.

The build, in order

  1. Uploading a shoot without losing it

    Eight hundred images upload and process without the photographer babysitting it.

    1. Through the application server is how a big shoot fails.

      Files src/upload.ts

    2. Synchronous processing ties up the request and loses on restart.

      Files src/worker.ts

    3. One corrupt file must not fail eight hundred.

      Files src/processing.ts

    done when · tick each as it passes
    watch out
    • Uploading through the application server and processing synchronously means a large shoot takes hours, ties up the request path and fails entirely if the connection drops, losing work the photographer thought was done. Upload directly to storage, process in workers, and make both resumable.
  2. A gallery that is fast on a phone

    A client scrolls hundreds of images without waiting.

    1. Full resolution scaled in the browser is why galleries feel broken.

      Files src/serve.ts

    2. A photographer's colours are the product.

      Files src/derivatives.ts

    3. A desktop connection hides everything that matters here.

      Files tests/performance.md

    done when · tick each as it passes
    watch out
    • Serving full resolution images scaled down in the browser makes a gallery unusable on a phone and expensive to deliver, and the photographer only finds out when a client says the gallery does not work. Serve the right size per viewport and measure on an actual device rather than a fast desktop connection.
  3. Who can see it, and for how long

    Gallery access is controlled without making it awkward for the client.

    1. Protecting only the page leaves the photographs public.

      Files src/access.ts

    2. Galleries accumulate and storage costs money indefinitely.

      Files src/galleries.ts

    3. It is how families see the photographs. Design for it.

      Files src/access.ts

    done when · tick each as it passes
    watch out
    • Protecting the gallery page while serving images from permanently public addresses means the photographs themselves are accessible to anyone with an address, which they will be, since they appear in browser history and get forwarded. Sign the image addresses too, with the same lifetime as the gallery access.
  4. Favourites, per person

    Two family members can each pick their favourites without overwriting each other.

    1. Not to the gallery. The second person overwrites otherwise.

      Files src/selections.ts

    2. Clients will not sign up to pick photographs.

      Files src/visitors.ts

    3. Whose favourites these are is the useful part.

      Files src/review.ts

    done when · tick each as it passes
    watch out
    • Storing selections against the gallery rather than the visitor means the second person to open the link overwrites the first person's favourites, and neither of them realises until the photographer receives a list that is missing half of it. Key selections to a per-gallery visitor identity.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more photo editing.