Build Pixieset
KINDAreplaces $8/mosaves $96/yrback to the verdict
A catalogue of your local photographs that publishes a password-protected gallery to storage you own: generated previews at sensible sizes, client favouriting for proofing, download permissions per gallery, expiry dates, and originals that never leave your disk unless you decide they do.
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 catalogue, derivative generation and publishing.
Get it python.org or your package manager, then a virtual environment.
Verify
python3 --version prints 3.12 or higher - installfree
Why Generating thousands of derivatives with a memory-hungry library will exhaust a laptop. libvips is substantially faster and lighter for exactly this workload.
Get it brew install vips, or your package manager, then pip install pyvips. open ↗
Verify
python -c "import pyvips; print(pyvips.version(0))" runs - API keystorage is cheap; egress varies enormously by provider
Why Galleries are opened from phones on poor connections, and photographs are large. Egress rate is the cost that matters.
Get it Create a private bucket and credentials scoped to it, and put a delivery network in front. Compare egress pricing before choosing.
Verify
A test object uploads, is refused when requested directly, and is served through the network with a signature - have readyfree
Why Indexing time, derivative generation time and gallery weight are all meaningless at a hundred images and decisive at ten thousand.
Get it Use your own library. If it is smaller, duplicate it to test scale before trusting the performance numbers.
Verify
The library is available and its size is recorded - installfree
Why Phase 2's claim that location metadata is stripped must be verified with a tool rather than assumed, because publishing a client's location is a real harm.
Get it brew install exiftool, or your package manager.
Verify
It reports metadata for a source image and finds none in a derivative - decidevaries with library size; this is usually a photographer's largest storage cost
Why Originals are the irreplaceable asset and the catalogue does not contain them.
Get it An external drive plus an offsite copy, or object storage. Decide before Phase 6, and prefer two locations to one.
Verify
A restore of one original has been performed
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- photos(id, path, taken_at, width, height, orientation, camera, content_hash) · the catalogue, pointing at originals in place - galleries(id, name, client_name, slug, password_hash, expires_at, download_policy, published_at) - gallery_photos(gallery_id, photo_id, position, favourited_at, comment) - derivatives(photo_id, size, key, bytes, generated_at) · thumbnail, preview and optional full size - downloads(gallery_id, photo_id, at, ip_hash) · so you can answer what a client actually took - Originals are never uploaded unless the download policy explicitly allows them, and that is a per-gallery decision.
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 |
|---|---|---|---|
LIBRARY_PATH | required | ~/Pictures/Library | Where your originals live. Read only; nothing is written here. |
CATALOGUE_DB | required | ./data/catalogue.db | The index over your library. Rebuildable from the library, so not the important backup. |
BUCKET_NAME | required | client-galleries | Private bucket holding derivatives. Originals go here only when a gallery policy allows downloads. |
BUCKET_SECRET_KEYsecret | required | replace-with-secret | Scoped to the gallery bucket only. |
CDN_BASE_URL | required | https://galleries.example.com | What clients fetch from. The bucket hostname never appears in a gallery. |
SIGNED_URL_TTL_SECONDS | required | 3600 | How long an image URL lasts after the password is entered. |
PREVIEW_LONG_EDGE | required | 2048 | Preview size. Large enough to look sharp, small enough to open on mobile data. |
The build, in order
Cataloguing what you already have
Your photographs are indexed in place, with correct orientation and dates.
Read only. The catalogue points at files in place; it never moves or rewrites them.
Files
src/catalogue.pyterminalpip install pyvips
From metadata, with an explicit fallback that is recorded rather than silent.
Files
src/metadata.pyContent hash, so the same image in two folders is one photograph with two paths.
Files
src/catalogue.py
done when · tick each as it passeswatch out- A photograph's stored orientation is metadata, not pixels, and ignoring it means every portrait shot appears sideways in your previews while looking correct in every viewer you check it in. Apply the orientation when generating derivatives and record that you did.
Derivatives that look right and are not enormous
Previews are sharp on a retina screen and small enough to load on mobile data.
Convert, resize, convert back, then sharpen. Sharpening before the resize does the wrong thing.
Files
src/derivatives.pyRemove location and personal fields, keep copyright if you set it. Verify with a metadata tool rather than assuming.
terminalexiftool -G -a preview.jpg | grep -i gps
Total bytes for a typical gallery. This is what a client on a phone will download.
Files
src/derivatives.py
done when · tick each as it passeswatch out- Resizing in gamma-encoded space darkens images and loses fine detail, which is visible on anything with texture. Resize in linear light and convert back.
- Metadata travels with a photograph unless you strip it, and that includes location. Publishing a client gallery with location metadata intact tells anyone who downloads an image where it was taken, which for a wedding or a family shoot is information they did not agree to share.
Galleries and a gate that is real
A client can open their gallery, and nobody else can open it or its images.
A static page with responsive images and lazy loading. It should open quickly on a phone.
Files
src/gallery.pytemplates/gallery.htmlThe password check issues short-lived signed URLs. No image URL exists before authentication.
Files
src/access.pyRequest an object directly, reuse an expired signature, and open an expired gallery. All three must fail.
done when · tick each as it passeswatch out- A password on the page with images at predictable public URLs is not access control, because anyone with an image URL skips the page entirely and image URLs get shared. Issue signed URLs after authentication and keep the objects private, or the password is decoration.
Proofing
A client can mark favourites and leave a note, and you get a clean list.
Against the gallery, keyed by a session established at password entry.
Files
src/proofing.pyFilenames and comments in a form you can hand to your editing workflow.
Files
src/export.pyEither separate sessions or a shared selection, but say which in the interface rather than leaving it ambiguous.
Files
templates/gallery.html
done when · tick each as it passeswatch out- Storing selections in browser storage means a client who starts on their phone and finishes on a laptop loses half their choices and does not realise. Store selections server side against a gallery session, and let the client resume with the same password.
Downloads on your terms
Clients get exactly what you agreed to give them, and you know what they took.
The policy decides which signatures are issued at all. An original URL should not exist for a previews-only gallery.
Files
src/downloads.pyBuild and send simultaneously so memory stays flat. Test with a large gallery and watch memory.
Files
src/zip.pyPer image and per gallery, with a hashed address rather than a stored one.
Files
src/downloads.py
done when · tick each as it passeswatch out- Generating a zip of two hundred originals in memory will exhaust it and take the process down. Stream the archive as it is built, and cap the request so an unattended retry loop cannot generate the same archive repeatedly.
Backups, storage costs and handoverproduct builder
Your originals survive, the storage bill is predictable, and a client can be handed everything at the end.
The catalogue is not a backup. Originals go to a second location, and a restore is performed once and dated.
Files
scripts/backup.shREADME.mdBytes stored and egress served, priced. Old galleries accumulate cost quietly.
Files
src/costs.pyArchival frees storage without losing the record. Handover produces everything a client is owed in one action.
Files
src/archive.py
done when · tick each as it passeswatch out- The catalogue is an index and contains no photographs, so backing it up protects nothing. The originals need their own backup, and a photographer's originals are usually the largest and most irreplaceable data they own.
That is the whole plan for Pixieset. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Raw processing quality, which is the real craft here.
- Print store and fulfilment.
- Client mobile applications.
- Library sync across your own machines.
- gallery hosting, proofing, downloads, store, mobile apps, and photographer workflow
- proprietary raw-processing quality
- cloud sync and sharing
- large AI models
- camera and print ecosystem
- Watermarking previews for galleries where downloads are disabled
- A client-facing selection limit, so a package of forty images is enforced rather than negotiated
- Automatic archival of galleries past their expiry, with a warning first
Need the files? The project pack on the verdict page hands your agent the whole brief · more photo editing.