Build Zenfolio

KINDAreplaces $9/mosaves $108/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 local photo catalogue and delivery tool whose hard problems are never writing to an original, building previews for tens of thousands of files without falling over on the large ones, and selling a download in a way that survives a refund.

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

CatalogueIndex files in place by path and content hash, never copy or move themPreviewsA derivative cache keyed by hash and size, built by a queue that records failuresEditsAdjustment parameters stored per asset and per version, applied at render timeDeliveryGalleries served behind an unguessable token with an expiry and a download policyPaymentsA hosted checkout, delivery only after settlement, licence text stored on the order

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 catalogue, the preview queue and the gallery server.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Every preview and every rendered export goes through it, and its default limit and failure behaviour are what phase two is built around.

    Get it Install it for your runtime and read its constructor options before writing the queue.

    Verify A deliberately oversize image is refused with the limit named, and the queue records it as a failure

  3. have readyfree, plus disk for derivatives

    Why Every hard part here is a scale problem, and a folder of twenty test images will tell you nothing about the preview queue or the integrity scan.

    Get it Point the catalogue at a copy of a real shoot, including a panorama and at least one file you know is corrupt.

    Verify A full import and preview build completes and reports its failures

  4. a percentage plus a fixed fee per sale; the fee is not returned on a refund

    Why Selling downloads means settlement events, refunds and fees, and all three shape the delivery design.

    Get it Create the account, complete verification, and take the keys plus the webhook signing secret.

    Verify A test payment settles and your endpoint receives a signed event

  5. decidefree to decide; possibly not free to comply with

    Why You are the merchant of record, and deciding this after the first international sale means deciding it retrospectively.

    Get it Check the registration thresholds for the places your buyers are, write your refund policy, and put both in the checkout.

    Verify The checkout shows tax where you charge it and the refund policy text is stored with each order

Data model

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

- assets(id, path, sha256, bytes, width, height, taken_at, orientation, camera, missing_since) · a path and a hash, and nothing that could be used to write to the file
- previews(asset_id, width, format, path, built_at, failed_reason) · a failure is a recorded row, not a stopped import
- edits(asset_id, version, params_json, created_at) · crop, exposure, contrast, white balance and saturation as numbers
- galleries(id, name, client_name, token_hash, expires_at, download_policy, watermark, bytes_served)
- gallery_assets(gallery_id, asset_id, position) and selections(id, gallery_id, asset_id, chosen_by, chosen_at, note)
- orders(id, gallery_id, buyer_email, amount_minor, tax_minor, fee_minor, state, settled_at, delivered_at, licence_text)
- downloads(order_id, asset_id, token_hash, expires_at, used_at, bytes) · one use, one record, so a forwarded link is visible
- Nothing in this schema writes to an original. The only file paths stored for originals are read paths, and the test suite hashes them before and after every run.

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
CATALOGUE_PATHrequired./data/catalogue.dbAssets, edits, galleries, selections and orders. Not the photographs.
LIBRARY_ROOTSrequired/mnt/photos/2026,/mnt/photos/2025Folders indexed in place. Opened read-only, never written.
PREVIEW_CACHErequired./data/previewsDerivatives keyed by hash and width. Deletable at the cost of a long rebuild.
MAX_INPUT_PIXELSrequired268402689The decoder's input limit, set deliberately. Larger files become recorded failures.
GALLERY_LINK_DAYSrequired90Default expiry set when a gallery link is created.
MONTHLY_BYTES_CEILINGrequired100000000000Public gallery serving stops here. The local catalogue keeps working.
PROCESSOR_SECRET_KEYsecretrequiredsk_live_...redactedServer side only, for checkout sessions and refunds.
PROCESSOR_WEBHOOK_SECRETsecretrequiredwhsec_...redactedVerifies settlement and refund events before anything is delivered.

The build, in order

  1. A catalogue that cannot touch your photographs

    Importing indexes files in place, records hashes, and provably leaves every original byte identical.

    1. The hash is both the identity and the proof.

      Files src/catalogue/import.tstests/originals-unchanged.test.ts

    2. Otherwise reorganising a folder loses a year of edits.

      Files src/catalogue/repair.ts

    3. An unplugged drive is not a deletion.

      Files src/catalogue/scan.ts

    done when · tick each as it passes
    watch out
    • A catalogue that writes metadata back into the file, or rewrites orientation to save a rotation, changes the original, and the first person to notice is somebody restoring from a backup that no longer matches. Keep every change in your own database, open originals read-only, and hash before and after in the test suite so the guarantee is checked rather than promised.
  2. Previews for twelve thousand files

    Derivatives build through a queue that survives large files, corrupt files and interruption.

    1. The default will refuse a panorama and the maximum will exhaust memory.

      Files src/previews/decode.tsdocs/limits.md

    2. Twelve thousand files will contain a few bad ones.

      Files src/previews/queue.tstests/queue-failure.test.ts

    3. Preview builds take hours and machines restart.

      Files src/previews/queue.ts

    done when · tick each as it passes
    watch out
    • Image libraries refuse very large input by default, and one common library's documented default limit is 268402689 pixels, which a stitched panorama passes without being unusual at all. Accepting the default means the import stops or silently skips your best work; raising it without a limit means one malicious or broken file exhausts memory. Set the number deliberately, record it, and turn a refusal into a recorded failure rather than a crash.
  3. Galleries and proofing that remember who chose

    A client gallery is a selection served behind a token, and every choice is attributed and kept.

    1. The link will be forwarded; plan for that from the start.

      Files src/galleries/tokens.tstests/token.test.ts

    2. The argument about what was chosen is the one you must win.

      Files src/galleries/selections.ts

    3. Delivery is the cost that surprises people.

      Files src/galleries/usage.ts

    done when · tick each as it passes
    watch out
    • Treating the gallery link as private means a forwarded link gives the whole wedding to anybody who receives it, with no expiry and no record. Store the token hashed, set an expiry at creation, count what each token has served, and let a link be revoked and reissued without rebuilding the gallery.
  4. Selling a download honestly

    A digital sale delivers only after settlement, records its licence, and accounts for what a refund costs.

    1. The return page is a redirect, not a payment.

      Files src/orders/fulfil.tstests/settlement.test.ts

    2. A download link is a bearer credential.

      Files src/orders/downloads.ts

    3. So your accounting shows what a refund really cost.

      Files src/orders/fees.ts

    done when · tick each as it passes
    watch out
    • Delivering the file the moment the checkout page returns means a payment that later fails or is refunded has already handed over the full resolution image, and the processor's own documentation is clear that its processing fees from the original transaction are not returned, so you are out the file and the fee. Wait for settlement, issue a single-use token, and revoke unused tokens on refund.
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.