Build Pic-Time
KINDAreplaces $8/mosaves $96/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - accountstorage 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/galleries | Galleries, client references and selections. |
STORAGE_BUCKET | required | s3://gallery-originals | Originals. Derivatives may live separately since they are regenerable. |
CDN_BASE_URL | required | https://img.example.com | Where signed derivative addresses are served from. |
SIGNED_URL_TTL_MINUTES | required | 60 | Long enough to browse a gallery, short enough that a copied address expires. |
GALLERY_DEFAULT_EXPIRY_DAYS | required | 365 | Galleries accumulate and storage costs money indefinitely. |
WATERMARK_PAID_DOWNLOADS | required | never | Present as a constant. A client who paid gets the file. |
The build, in order
Uploading a shoot without losing it
Eight hundred images upload and process without the photographer babysitting it.
Through the application server is how a big shoot fails.
Files
src/upload.tsSynchronous processing ties up the request and loses on restart.
Files
src/worker.tsOne corrupt file must not fail eight hundred.
Files
src/processing.ts
done when · tick each as it passeswatch 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.
A gallery that is fast on a phone
A client scrolls hundreds of images without waiting.
Full resolution scaled in the browser is why galleries feel broken.
Files
src/serve.tsA photographer's colours are the product.
Files
src/derivatives.tsA desktop connection hides everything that matters here.
Files
tests/performance.md
done when · tick each as it passeswatch 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.
Who can see it, and for how long
Gallery access is controlled without making it awkward for the client.
Protecting only the page leaves the photographs public.
Files
src/access.tsGalleries accumulate and storage costs money indefinitely.
Files
src/galleries.tsIt is how families see the photographs. Design for it.
Files
src/access.ts
done when · tick each as it passeswatch 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.
Favourites, per person
Two family members can each pick their favourites without overwriting each other.
Not to the gallery. The second person overwrites otherwise.
Files
src/selections.tsClients will not sign up to pick photographs.
Files
src/visitors.tsWhose favourites these are is the useful part.
Files
src/review.ts
done when · tick each as it passeswatch 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.
Downloads, watermarks and getting paidproduct builder
Clients get their files under the policy the photographer set.
Building in memory fails at exactly the gallery sizes that matter.
Files
src/bulk.tsPreviews yes, paid downloads never.
Files
src/watermark.tsSo a photographer can answer what a client already has.
Files
src/downloads.ts
done when · tick each as it passeswatch out- Building a zip of eight hundred full resolution images in memory before sending it will exhaust the server, and doing it on every request will do so repeatedly. Stream the archive as it is built, and cache the result for a gallery whose contents have not changed.
That is the whole plan for Pic-Time. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Print fulfilment and lab integration.
- Image editing.
- Studio management with contracts and scheduling.
- Screenshot prevention.
- gallery delivery, store automation, print labs, marketing, and support
- proprietary raw-processing quality
- cloud sync and sharing
- large AI models
- camera and print ecosystem
- Client proofing rounds with a stated deadline, which is the workflow photographers actually run
- Storage lifecycle moving old galleries to cheaper tiers, since bandwidth and storage are the ongoing cost
- A photographer-facing view of which galleries are approaching expiry
Need the files? The project pack on the verdict page hands your agent the whole brief · more photo editing.