Souvenir iOS App

What leaves the device

Privacy Policy · last checked against the code on August 31, 2026

Souvenir reads a photo library and prints books out of it. Most of that happens on the phone. One part does not, and this file is the exact account of which part.

It is written to be accurate rather than reassuring. Where something is not ours to promise, it says so.


The short version

  • Reading the library is local. Dates, coordinates and orientations are read on the device and kept in an index on the device. No photograph is read by anything but this app.
  • Pressing a book sends photographs. One frame per place — the representative photo the book prints — goes to Souvenir's generation service and on to OpenAI, which draws the poster. Its caption text goes with it.
  • Nothing else is sent by the press. Not the library, not the index, not the other photos of that place, not GPS.
  • We store no photograph anywhere but your phone. Our service keeps a spend ledger with no image data in it. What OpenAI keeps is OpenAI's policy, quoted below, and is the one link in this chain we cannot verify from here.

What leaves, exactly

1. When you press a book — one photograph per place

Per place spread, one request to POST https://viqxskzgvvchrvhbbwia.supabase.co/functions/v1/generate-poster over HTTPS, carrying:

fieldwhat it is
imageBase64the place's representative photograph, re-encoded as a PNG and downscaled to at most 1536 px on its long edge
facts.placethe place's name as the book prints it, e.g. PORTO VENERE
facts.nothe spread number, e.g. 03
facts.keywordsthe three words the caption prints: the month, the frame count, the hour's light (e.g. may, 9 frames, dusk)
facts.yearthe four-digit year
placeKeythe spread number again, for our logs
deviceIda random UUID this app minted (see below)

A five-place book therefore sends five photographs. The book's other photos — usually hundreds — never leave.

The file we send carries no EXIF. It is a freshly rendered bitmap encoded as PNG, not a copy of the original file, so the original's metadata, including its GPS fix, is not in it. The photograph's content is of course still the photograph.

This is also what the pre-prompt says before the system's own dialog, at the same length the screen allows: the photographs stay here, naming a place asks Apple's map, pressing a book sends one frame per place plus the caption words and the rate-limiting id. If that screen and this file ever disagree, this file is the one that is wrong — the screen is the promise a person actually reads.

2. When a place is named — coordinates go to Apple

To turn a cluster of photos into "Porto Venere", the cluster's centre coordinate is reverse-geocoded with Apple's CLGeocoder. That is a network call to Apple carrying a latitude and longitude, once per place and once per trip, cached on disk so the same coordinate is never looked up twice. This is the standard iOS geocoding API and is governed by Apple's privacy policy.

3. Always — fleet telemetry

The app calls PlatformKit.configure at launch, which sends to the developer's own control plane (fqnphkqymzbxdzyqgsji.supabase.co):

  • an app_opened analytics event: product id, event name, platform, OS and OS version, app version, environment, timestamp, and a per-launch session id that is regenerated every process start and never stored;
  • a registry heartbeat: product id, app version, platform, OS, OS version, device model (e.g. iPhone16,2), locale.

No photo data, no place, no persistent identifier. Sentry crash reporting is compiled in but not enabled — this build ships an empty SENTRY_DSN, so nothing is sent to it.


What does not leave

  • The photo library, and any photograph other than the one chosen per place.
  • The asset index (asset-index.json): PhotoKit local identifiers, dates, coordinates, orientations. On-device only, always.
  • Photo coordinates, in the press path. The service is told a place name; it is never told where that place is. (Apple's geocoder is told — see above.)
  • The books themselves (books.json): titles, dates, routes, sampled inks, stamp assignments, place-centre coordinates, poster keys. On-device only.
  • Generated posters: they come back in the response and are written to Caches/. They are never uploaded anywhere. Each launch drops any poster no book on the shelf still refers to — a run you backed out of leaves nothing behind — and the system may purge the rest at any time, in which case the spread falls back to the photograph it was made from.
  • Any account, email, name, contact, or advertising identifier. Souvenir has no account system and asks for nothing.

The device id

The first time a book is pressed, the app mints a random UUID and keeps it in the keychain. It is sent with every generation request.

  • It is not a hardware identifier. identifierForVendor was deliberately not used: it is shared with every other app from this developer and cannot be reset.
  • It exists for one purpose: the daily spending ceiling (40 generations per device per UTC day, 400 across all devices). It identifies a device to that ceiling and to nothing else.
  • It lives in the keychain rather than in app storage, which means it survives deleting and reinstalling the app. That is deliberate: a ceiling you can reset by reinstalling is not a ceiling. It is also the honest downside of this design, and it is stated here rather than buried.

Where a photograph actually goes

your phone ──https──▶ generate-poster            ──https──▶ OpenAI
                      (Supabase Edge Function,              /v1/images/edits
                       project viqxskzgvvchrvhbbwia,        model gpt-image-2
                       us-west-1)
                          │
                          └── writes one ledger row: device id, spread number,
                              status, token counts, cost. No image. No place name.

The function holds the photograph in memory for the length of the request and writes it to no storage bucket, no table and no log. The poster comes back in the same response and is returned to the phone.

Retention

  • Us: nothing. public.generation_ledger contains device id, the spread number, an outcome, token counts and a cost in micro-dollars. There is no column that could hold an image, and none that holds a place name.
  • OpenAI: their published API policy states that data submitted through the API is not used to train their models by default, and may be retained for up to 30 days for abuse monitoring before deletion. We pass their policy on; we cannot enforce or verify it from here. If that is not acceptable to you, the honest answer today is not to press a book.

Turning it off

There is no in-app switch for generation yet. What exists:

  • A build without a generation key never sends anything. isConfigured is false, the press has nothing to send to, and every spread is printed by the app's own renderer. The press bench then says nothing about sending, because nothing is sent.
  • Denying photo library access stops everything, including the press: there is nothing to read and nothing to print.

A per-book "print this one locally" toggle is a real gap. It is written down here rather than left implicit.


App Store privacy labels this implies

Data typeCollectedPurposeLinked to identityUsed for tracking
Photos or VideosYesApp FunctionalityNoNo
Coarse LocationYesApp FunctionalityNoNo
Identifiers → Device IDYesApp FunctionalityNoNo
Diagnostics / Usage DataNo———

Reasoning, so the answers can be checked rather than trusted:

  • Photos. Apple counts data as collected when it is transmitted off the device and can be accessed for longer than servicing the request in real time. Our own service does not retain it, but OpenAI's abuse-monitoring window can — so this is declared as collected, not as merely transmitted.
  • Coarse Location. The place name (a city or district) is transmitted as caption text, and coordinates are sent to Apple's geocoder. A city-level name is coarser than a city block, so it is Coarse rather than Precise. Precise coordinates never reach our service.
  • Device ID. The random per-install UUID is transmitted for rate limiting. It is not linked to any identity because there is no identity in this app to link it to, and it is never used to track across apps or websites.
  • Diagnostics / Usage Data. The fleet analytics event and heartbeat go to the developer's own control plane and carry no persistent identifier — but they are still product analytics. If this app is submitted with those calls live, Usage Data → Product Interaction → App Functionality/Analytics, not linked, not tracking must be declared as well. Declared here as a requirement rather than an afterthought at submission time.

Last checked against the code on 2026-08-31: Souvenir/Generation/, Souvenir/Views/Press/PressComposer.swift, Souvenir/Library/Geocoder.swift, Souvenir/SouvenirApp.swift, supabase/functions/generate-poster/index.ts.

← Back to isaacperez.co