State as of 2026-07-08: all 9 kickoff build-order steps complete and committed; SPEC e2e test plan passed 10/10 against real Neon + Cloudinary + Stripe test mode. See docs/VERIFIED.md for the current-docs facts the implementation relies on.
Post-v1 changes (2026-07-08, same day): design system applied; storefront images migrated from next/image to next-cloudinary's CldImage via src/components/PreviewImage.tsx; sharp removed (dimensions now from Cloudinary's upload response); powered-by footer, favicon, cart badge, purchase-clears-cart. See docs/ARCHITECTURE.md + the next-cloudinary section in docs/VERIFIED.md.
Video support added 2026-07-09: the Photos collection ("Media" in the admin) now also accepts video/*. Videos upload as type: authenticated video Originals (NOT private — the private + strict-mode model did not prove a reliable fit for video delivery when tested 2026-07-09 in this environment; authenticated requires a signature on the original and every derived asset regardless of strict-mode config) with eager-generated watermarked derived assets delivered via signed URLs (5s e_preview clip + six so_<N>p stills — see src/lib/cloudinary/README.md and videoTransformations.ts); tags are manual (built-in auto_video_details is dialogue-only, rejected for silent footage); /api/cloudinary/notify flips previewReady when async eager generation completes. The video_preview/video_still named transformations were created in the Cloudinary environment via the Admin API the same day.
Photoset support added 2026-07-09 (later same day): new photosets (title/price/manual tags, join view) + photosetImages (upload, belongs-to photoset, isPreview, no price, no AI) collections; folder ingestion via pnpm payload run scripts/ingest-photoset.ts <folder> "<title>" <priceCents> [tags] (first 4 members flagged preview); members are private images under unshutter/sets/<setId>/<filename-slug> (filename in public_id = readable zip entries; note this makes public_ids guessable — see the accepted-trade-off note below); purchase delivers ONE signed ~10-min zip via generate_archive (buildPhotosetZipUrl.ts); cart/checkout/Orders gained a photoset dimension (CartItem.kind, metadata.photosetIds, Orders photosets rel); deleting a set cascade-deletes members + their Cloudinary assets. Migration history was consolidated the same day into a single 20260709_145911_initial covering the full schema (photos incl. video fields, photosets, orders) — a demo deploy against a fresh database needs exactly one migration, no incremental chain.
-
Video webhook in dev — Cloudinary can't POST
eager_notification_urlto localhost, sopreviewReadynever flips automatically in dev: tick the checkbox in the admin once the derived assets exist (or tunnel via ngrok/cloudflared and pointNEXT_PUBLIC_SERVER_URLat it). -
Short videos —
e_preview:duration_5needs a source longer than ~5s; uploading a shorter clip likely fails eager generation (upload succeeds,previewReadystays false). Surface in admin docs if it bites. -
Photoset previews are all-visible-if-guessed (accepted, not a bug) — Strict Transformations allowlists
t_previewaccount-wide, so a watermarked low-res rendition of every set member (not just theisPreviewones) is publicly derivable by guessing its public_id. Verified 2026-07-09. Decision: fine to leave — only the full-res original is truly gated. To harden later, upload members astype: authenticatedand sign only curated previews. Details insrc/lib/cloudinary/README.md(security note). -
Design system— applied 2026-07-08:src/styles/sheets imported in(store)/layout.tsx, page-scoping classes added (unshutter-gallery/unshutter-cart/unshutter-download— the sheet's element-only selectors collided across pages sharing thesection>ul>liskeleton),height: autoadded soaspect-ratiobeats next/image's height attribute, photo-page grid placement pinned (image col 1, text col 2). All five views verified in-browser incl. mobile. -
README.md— rewritten 2026-07-08, modeled on kickoff-cards' README: showcase framing (Cloudinary alongside agent-first platforms), demo beats, quick start incl. Cloudinary console requirements, docs index. Placeholder clone URL (cloudinary-devs/unshutter) — fix when the repo gets a real remote. No LICENSE file exists yet despite the MIT claim (package.json also says MIT) — add one. -
Stripe CLI — not installed on this machine.
STRIPE_WEBHOOK_SECRETin.envis a locally generated value used to sign simulated webhook deliveries during e2e. When the CLI is installed:stripe listen --forward-to localhost:3000/api/stripe/webhookand put itswhsec_…in.env. No code change needed. -
Production migrations — initial migration generated (
src/migrations/) and aciscript (payload migrate && pnpm build) exists for clean-database deploys. The demo deployment deliberately does NOT use it: it shares the dev Neon database (schema already pushed, demo data kept), andpayload migrateagainst a push-managed db warns "data loss will occur" — verified 2026-07-08. Demo Vercel build command is plainpnpm build; schema changes reach prod by running dev locally against the shared db. Usepnpm run cionly when deploying against a fresh database. -
Admin thumbnail 500s — two benign 500s per photo save in dev logs: Payload's
/api/photos/file/<name>preview endpoint can't serve bytes becausedisableLocalStorage: true(by design; Originals live only in Cloudinary). Cosmetic; could be silenced withadmin.thumbnailURLpointing atbuildPreviewUrl. -
Port 3000 collision — an unrelated Docker container (Rails welcome app) occupies 3000 on this machine; Next dev auto-binds 3001, but Stripe redirect URLs come from
NEXT_PUBLIC_SERVER_URL=http://localhost:3000. Free the port or change the env var when testing checkout locally.
- Email receipts/download links (Resend) — webhook already has email + session id;
sendDownloadEmail()slots in there. - License tiers (entitlement becomes photo+tier, tier caps
w_in signed URL). - Galleries/collections taxonomy (Payload relationship field).
- OG generative fill (
b_gen_fill) for awkward aspect ratios —ognamed transformation is currently plainw_1200,h_630,c_auto,g_auto.
- 3 seeded Photos (ids 1–3, Cloudinary
unshutter/originals/1..3) and 1 Order (id 4). - Admin user
admin@example.com— password was reported in the 2026-07-08 build session (change it or reset via DB if lost).