Legal
Privacy Policy
Plain-English summary of what ShotSelect does and does not do with your data. Written by photographers, for photographers — but it's still the document we'd rely on in a dispute.
1. Who we are
ShotSelect ("we", "us") is a desktop application for macOS that helps photographers cull and rate large shoots locally. The app is built and operated by the developer behind shotselect.app. Contact: [email protected].
2. Photos and image content
On the free tier, your photos stay on your device. ShotSelect reads images from folders you open and writes XMP sidecar files alongside them. We do not upload your photos, thumbnails, EXIF data, derivatives, embeddings, or face data to any server under the free tier.
-
AI features run on-device. Semantic search uses a local OpenCLIP model
bundled with the app. Blur detection, closed-eye detection, object detection, and face
grouping (when enabled) run via
onnxruntime-nodeon your Mac. - No model training on your images. We do not feed your photos to any model, ours or anyone else's.
- Feedback attachments are opt-in. If you use Send Feedback and attach a screenshot, PDF, text file, CSV, JSON, or log, that file is uploaded with your message because you explicitly selected it. We cap attachments to three small files and never attach photos, screenshots, logs, paths, or diagnostics automatically.
-
XMP sidecars only. Ratings, picks, color labels, and keywords are
written to
.xmpfiles next to your originals. We never modify the original RAW or JPEG files.
3. License keys
For paid tiers (when offered), the license server stores only what's needed to authorize the seat:
- The email address you purchased with
- The license key, expiry date, and seat status
- Timestamps of activation events
License validation is signed locally; the app does not phone home on every launch.
4. Anonymous diagnostics (Tier A)
By default, ShotSelect sends a small, anonymous session ping at startup. This helps us know whether updates are installing, which macOS versions to support, and how many people are using the app — without identifying anyone.
Each ping contains exactly five fields:
-
device_id— a random UUIDv4 generated on first launch and stored on your Mac. It is not a hardware fingerprint and is not derived from any identifying value. Turning diagnostics off deletes this file. app_version— e.g.1.3.0os_version— e.g.darwin 14.5arch—arm64orx64launch_count— how many times the app has been opened on this device
What we do not send in Tier A:
-
No IP address — our endpoint reads only Cloudflare's two-letter country code header
(
CF-IPCountry, e.g.US,JP) for coverage stats;CF-Connecting-IPis never read and the IP itself is never written to our database. - No file paths, folder names, photo metadata, or filenames
- No email, name, license key, or any other identifying data
You can preview the exact JSON that would be sent in
Settings → Privacy → "See exactly what gets sent". You can turn
diagnostics off at any time and we'll stop sending and delete the local
device_id.
5. Usage data (Tier B) — opt-in, off by default
If you turn this on, ShotSelect sends counts of which features you use (e.g. how many
photos you culled, how many times you opened the search panel, when a session is removed
from history or that removal is undone). Each event carries the same five Tier A fields
plus a fixed event name from a short whitelist and bucketed values (e.g.
<100, 100-1k) — never raw counts that could uniquely identify
a session, and never folder paths or filenames.
The whitelist and the buckets are enforced server-side; arbitrary event names or raw values are dropped at intake. This is off by default. Turn it on in Settings → Privacy if you'd like to help us prioritize features.
The bucketed dimensions we collect (no raw numbers ever leave your Mac):
-
Photo counts — bucketed into
<100,100-1k,1k-10k,10k+. -
Workflow duration — time spent in a phase, bucketed into
<1min,1-5min,5-30min,30min+. -
Performance latency — how fast folder open, photo navigation, and first
decision feel, bucketed into
<100ms,100-500ms,500ms-2s,2s+. This lets us compare ShotSelect's speed against competitors' published claims at the bucket level — your individual timings never ship. -
Cull throughput — decisions per minute when you're culling, bucketed
into
<10,10-30,30-60,60+. Validates the "keyboard-first" speed claim against real sessions. -
Keyboard shortcuts — named action (e.g.
like,reject,star) when you press a culling key. Never a raw keycode; never the photo it acted on.
Examples of events on the whitelist (the full list lives at
worker-telemetry/telemetry.js in the source repo and is enforced at the
server):
-
folder.opened,cull.decision— how many photos a session contained; each keep / reject keystroke carries a boundeddecisionprop (literallykeeporreject) so we can compute the keep-rate. No filenames, no folder paths, no per-photo data. Folder opens may also carry boundedentry_path(menu,drag_drop,recent,device,resume,cli) andmedia_type(photo,video,mixed). -
cull.session_complete,cull.session_abandoned— whether a culling session finished (every photo reviewed) or was left incomplete, plus bucketed counts of decisions made and time spent. No folder paths, no filenames, no timestamps beyond the request timestamp. -
perf.first_decision_latency,perf.folder_scan_ms,perf.photo_nav,perf.thumbnail_first_paint,perf.thumbnail_cache_hit_rate,perf.timeline_build_ms,perf.burst_group_ms,perf.face_group_ms_per_1k— bucketed latency and cache-health counters for the activation funnel, folder scan, navigation, thumbnails, timeline building, burst grouping, and face grouping. Lets us see whether the workflow is getting faster or regressing — no folder names, no filenames, no raw face data, no exact timings. -
feature.view_mode,feature.filter_applied,feature.undo,feature.timeline_event_selected— which view mode you switched to (loupe / grid / timeline / compare), which filter you toggled (blurry / closed-eyes / duplicates / rating / star / tag / face / emotion / media kind / burst), whether you used undo, and whether you scoped culling to timeline events. Tells us which features are used, which to retire. No photo identifiers, no tag content. -
export.started,feature.export,export.duration,export.format_chosen,export.bucket_chosen,export.xmp_written,export.reveal_clicked,feature.clip_search— whether you started and completed export, how long it took in a bucket, which manifest format and photo bucket you chose, whether XMP sidecars were written, whether you revealed the destination, and whether you ran a search. Export failures carry only a boundedreasonsuch aspermission,disk_full,write_failed,user_cancelled,validation, orunknown. -
error.media_load_failed— a bounded media-load error type (decode,missing,permission,network,unsupported,timeout). No filenames, paths, codecs, or image data are sent. -
device.backup_started,device.backup_completed,device.backup_failed— backup operation health for removable-device workflows. We send only device class (removable,local,network,unknown), destination-count bucket, transfer-speed bucket, time bucket, count bucket, and failure reason. We do not send volume names, serial numbers, paths, filenames, exact byte counts, or exact transfer speed. -
feature.burst_collapse,feature.burst_pick,feature.burst_grouped,feature.burst_gap_changed,feature.burst_detection— whether you toggled burst grouping, how big the resulting groups were, whether you tuned the gap, and whether the master burst-detection switch is on or off. Counts only; no filenames, timestamps, or camera identifiers are included. -
feature.face_group_renamed,feature.face_group_merged— whether you corrected face grouping labels or merged duplicate person clusters. We send only bounded counts; names, faces, crops, embeddings, filenames, and paths are never sent. -
update.banner_shown,crash.handled— quality and reliability counters.
6. Crash reports (Tier C) — opt-in
If a crash occurs and you choose to send a report, ShotSelect submits a scrubbed stack
trace, the app version, OS version, and architecture. Before sending we redact
/Users/<name>/, /Volumes/<name>/, and email
addresses. The server runs the same scrubbing again as defense-in-depth.
You choose how this works: Always send, Ask each time (default), or Never send.
7. Auto-updates
The app checks our update feed periodically. The check looks like a regular HTTPS request for a small static file hosted on Cloudflare R2 — there is no body identifying you, your license, or your install. Cloudflare's standard request logs (IP, timestamp, requested file) are subject to Cloudflare's privacy policy and we do not retain or correlate them.
8. Where data goes
Diagnostics, opt-in usage events, and opt-in crash reports are sent to a Cloudflare Worker we operate, and stored in a Cloudflare D1 database in our account. We do not share this data with any third party. We do not use any third-party analytics, advertising, or tracking SDK in the app or on this website.
In-app feedback messages, optional reply emails, and optional user-selected attachments are sent to a Cloudflare Pages Function and stored in our Cloudflare D1 feedback inbox. They are used only to debug issues, understand requests, and reply when you provide an email.
9. Retention
- Tier A session pings: aggregated weekly; raw rows pruned after 90 days.
- Tier B usage events: raw rows pruned after 180 days.
- Tier C crash reports: kept until resolved, then pruned after 365 days.
- Feedback messages and attachments: kept until reviewed or resolved, then pruned after 365 days.
- License records: kept for the lifetime of the seat plus 7 years (tax / accounting requirement).
10. Your rights
Because Tier A is anonymous (we have no map from device_id to you), there is
nothing to export or delete on a per-person basis — turning the toggle off is the
deletion. For license records, email
[email protected] with the address you
purchased with and we'll respond within 30 days.
If you're in the EU/UK or California, you have rights under GDPR / UK GDPR / CCPA respectively. We honor those rights regardless of where you are.
11. Children
ShotSelect is built for working photographers and is not directed at children under 13 (or 16 in the EU). We do not knowingly collect personal data from children.
12. Future paid tier
We may introduce paid features in the future (for example, hosted client review links). Any feature that involves uploading content off-device will be clearly marked, opt-in per use, and documented here before it ships. The free tier and its workflow will not be moved to a paid tier retroactively. The on-device guarantee in section 2 applies to the free tier permanently.
13. Changes
We'll update this page when something material changes, increment the version number, and surface an in-app notice on next launch. The change log below records every revision.
14. Contact
Questions, complaints, takedown requests, or privacy concerns: [email protected].
Change log
See also: Terms of Use