# Proposal: `postcard` — turn a phone photo into a sendable postcard **Status:** proposed, not built. Self-contained — written so a fresh agent with no prior conversation context can pick this up and implement it without anything explained first. No repo exists yet; this file is the seed of one. ## Motivation Upload a photo from your phone (or any device), it gets composited onto a vintage postcard template — border, stamp corner, a handwritten-style caption — and you get a shareable link or a downloadable image. The caption is auto-drafted from whatever context is available for that day (git activity, what was playing, a note title) so the postcard reads like it was actually written *from* that day, not just a filter applied to a photo. Structurally this is `latent`'s upload → transform → deliver shape, with the transform swapped from film-emulation filters to postcard compositing, and one addition: a short generated caption line instead of (or alongside) canvas filters. Reuse `latent`'s validated pieces wholesale rather than re-deriving them — same phone-upload flow, same blob+SQLite+TTL server, same no-accounts/link-is-the-credential model. ## Architecture Static/client-heavy frontend (canvas compositing, same "no build step" family as `latent`/`typo`/`flit`/`wisp`) plus a small single-process server, copied from `latent/server` almost unchanged: `db.js` (SQLite metadata), `ids.js` (opaque random IDs), `cleanup.js` (interval TTL sweep), `ratelimit.js` (basic IP throttle on upload), `routes.js`, `index.js`. No accounts, no auth — the link is the credential, same as `latent`/`wisp`. ### Editing (client-side, ``) 1. **Upload** — `` for direct camera access, normal file picker as fallback (same open question `latent` already flagged, same answer: ship both). 2. **Template** — a handful of postcard border/stamp-corner presets (plain cream border, torn-edge, faded color-photo-era border) — pick a vibe, not a full editor. Reuse `latent`'s "presets, not sliders" call for the same reason: faster to ship, more toy than tool. 3. **Caption** — an editable text field, pre-filled by a generated draft (see below), rendered onto the card in a handwriting-style webfont. Editable because auto-drafted text should be a starting point, not the final word — the person sending it should be able to make it actually theirs. 4. **Postmark/stamp corner** — today's date and a small generated "location" stamp if EXIF GPS is present (round-tripped through the same reverse-geocoding approach `rewind`/`galr` already use, if one exists — otherwise just the date, no invented location). 5. Compositing is canvas-based, non-destructive within the session (recompute from the original photo + a param object, matching `latent`'s pattern) so switching templates doesn't require re-uploading. ### Caption generation The interesting new piece `latent` didn't need. Draft a one-line caption from whatever of these is available and non-empty for the photo's date (EXIF date if present, else upload date): - Git activity that day (via the same Gitea-API aggregate-count approach `rewind` already built — "spent the day untangling a deploy script" / "quiet one, no commits") - What was playing (Spotify plays log, same source `/wrapped`/`/now` already read — "[artist] on repeat") - A note/blog title from that date, if one exists This is explicitly a **remix of existing fragments**, not a generative free-text model call — same spirit as the haiku/found-poetry idea below: assemble from real fragments of that day, phrase them into one short sentence with a small set of template shapes ("Spent the day {git_fragment}. {spotify_fragment} the whole time." / "{spotify_fragment}. {git_fragment}, mostly."), and always leave it editable. If none of the three sources have data for that date, fall back to a small set of generic postcard-voice lines ("Wish you were here. Mostly wasn't.") rather than leaving the field empty. ### Finishing paths Same two-path split as `latent`: 1. **Instant** — composite, then download directly, entirely client-side, no server touched. Optional "generate share link" (explicit action, not automatic). 2. **Send** — uploads the composited image to server storage, returns a link. No delayed-reveal mechanic here (that's `latent`'s gimmick, not this one) — the postcard metaphor's own delay is real mail taking a few days to arrive, which the link doesn't need to fake. ### Server Copied from `latent/server`'s shape almost directly: **Table `postcards`:** | column | type | notes | |---|---|---| | `id` | text PK | random opaque ID, used in the URL | | `blob_path` | text | composited image on disk | | `mime` | text | | | `created_at` | integer | | | `expires_at` | integer | TTL sweep target, same as `latent` | **Endpoints:** - `POST /api/upload` — body: composited image bytes. Returns `{ id, url }`. - `GET /p/:id` — serves the postcard image directly (no develop-later gate — this app's whole point is immediacy, unlike `latent`). - TTL sweep, same interval-based pattern as `latent`/`wisp`. No auth. Rate-limit `/api/upload` the same way `latent` does. ## Non-goals - No accounts, no "your postcards" history — every card stands alone via its own link, same throwaway philosophy as `latent`/`wisp`. - No delayed-reveal mechanic — that's `latent`'s gimmick; this one's point is speed, not anticipation. - No free-text LLM-generated captions — template-and-fragment assembly from real personal data only, kept editable, never presented as if the app "wrote" it. - No collaborative/multi-recipient cards — one photo, one card, one link. ## Open questions - **Where the day-context lookups live** — `rewind` and `/wrapped`/`/now` already have working Gitea-API and Spotify-log integrations; decide whether `postcard`'s server calls those APIs directly (network dependency on goonk's API being reachable) or whether this only makes sense as a goonk-hosted feature rather than a fully separate sibling project. Worth resolving before writing `routes.js`. - **Location stamp source** — confirm whether `galr`/`rewind` already do reverse geocoding anywhere reusable, or whether this ships without a location stamp in a first pass. - **Handwriting webfont choice and licensing** — pick one, check its license allows self-hosting. - **Image size limits** — same question `latent` flagged, same likely answer (downscale on upload, cap stored size). - **Retention window** — shorter than `latent`'s, probably — a postcard link is meant to be opened once by the recipient, not browsed later.