Next.js, Payload, Stripe, and Cloudinary SDK APIs may differ from your training data. Read the relevant guides in node_modules/*/dist/docs/ or fetch current docs before writing code. Heed deprecation notices. Verify Payload ↔ Next.js version compatibility before scaffolding.
Missing service credentials are not a blocker in this stack — provision claimable environments yourself and surface the claim URLs to the user:
- Cloudinary:
npx @cloudinary/cloud— provisions a Claimable Cloud, writesCLOUDINARY_URLto.env, prints a claim URL (expires 24h unclaimed). Also setNEXT_PUBLIC_CLOUDINARY_CLOUD_NAMEfrom the cloud name. Unclaimed clouds lock media delivery to this machine's IP — fine for local dev; tell the user to claim before sharing a deployed URL. Docs: Agents: start here, Claimable Cloud provisioning. - Neon:
npx neon-new --yes— instant claimable Postgres, credentials to.env(copy the connection string toDATABASE_URI), claim URL (72h). - Stripe:
stripe sandbox create— anonymous sandbox with working test keys.
Always report claim URLs and expiry to the user immediately — they are the handover.
Stop at a working local site. Deploying (Vercel or anywhere else) is the human's step, not yours — don't run vercel deploy, create hosting accounts, or push env vars to a hosting provider unless explicitly asked. Two reasons: hosting is tied to the human's identity, and an unclaimed Cloudinary cloud only delivers media to the provisioning machine's IP, so a deploy would look broken to everyone else until they claim it. Full walkthrough: docs/AGENT-PROVISIONING.md.
Open-source reference app: Cloudinary + Stripe + Payload CMS + Next.js + Vercel.
Photography storefront: watermarked compressed previews are public; payment unlocks a time-limited signed download of the private full-size original. See docs/SPEC.md for architecture, CONTEXT.md for canonical vocabulary — use its terms exactly.
- Next.js App Router, TypeScript
- Payload 3 — embedded CMS, admin at
/admin; owns the catalog (Photos) and Orders - Neon Postgres — via
@payloadcms/db-postgres - Stripe — Checkout Sessions (test mode), webhook for Order recording
- Cloudinary — base
cloudinaryNode SDK (v2) for server-side calls;next-cloudinary(CldImageviasrc/components/PreviewImage.tsx) for storefront rendering - pnpm — package manager
- All Cloudinary + Stripe + Payload data calls live in
lib/— never in components or pages directly - Originals are
type: private— never upload a sellable image as a public asset - Deterministic public_ids —
lib/cloudinary/publicIds.tsis the single source for all path conventions - One delivery-URL source of truth per context — JSX previews render only through
components/PreviewImage.tsx; URL strings come only frombuildPreviewUrl/buildDownloadUrl/buildOgUrlinlib/cloudinary/ - Prices come from Payload docs server-side — never trust client-supplied amounts
- Entitlement = Stripe session verification —
/downloadverifiespayment_status === 'paid'against the Stripe API; the webhook records Orders for reporting but grants nothing - Webhook is idempotent on Checkout Session id
- Env validation via Zod —
lib/env.ts(server, throws on missing) andlib/env.client.ts(public)
Cloudinary owns all optimization. Preview rendering in JSX goes through src/components/PreviewImage.tsx — a CldImage wrapper pinned to deliveryType: private + strictTransformations + namedTransformations: ['preview/f_auto'], which emits exactly the strict-mode-allowlisted t_preview/f_auto chain (f_auto can't live inside a named transformation; the slash-in-name value is deliberate). Server-side URL strings (Stripe thumbnails, OG, signed downloads) still come from the lib/cloudinary builders. Previews deliberately use a low quality tier — degradation is the product. Signed download URLs pin nothing: the buyer gets the original bytes.
No sharp: Payload never stores image bytes (disableLocalStorage), and Photo width/height are written from Cloudinary's upload response in syncCloudinaryOriginal rather than extracted locally.
lib/cloudinary/ # config, publicIds, uploadOriginal, buildPreviewUrl, buildDownloadUrl, buildOgUrl
lib/stripe/ # client config, createCheckoutSession, verifySession, webhook event handling
lib/payload/ # typed accessors for Photos and Orders (wrap payload local API)
collections/ # Payload collection configs: Photos, Orders (+ hooks calling lib/)
app/api/checkout/ # POST — build Checkout Session from cart
app/api/stripe/webhook/ # POST — verify signature, record Order
app/(store)/ # gallery, photo page, cart, /download
app/(payload)/ # Payload admin routes (scaffolded)
pnpm test # Vitest — unit tests colocated in lib/
pnpm lint # ESLint
pnpm build # TypeScript check + production build
pnpm dev # Dev serverLocal Stripe webhooks: stripe listen --forward-to localhost:3000/api/stripe/webhook
CLOUDINARY_URL # or CLOUDINARY_CLOUD_NAME / API_KEY / API_SECRET — npx @cloudinary/cloud if absent
STRIPE_SECRET_KEY # sk_test_... — stripe sandbox create if absent
STRIPE_WEBHOOK_SECRET # whsec_... (from stripe listen locally)
DATABASE_URI # Neon Postgres connection string — npx neon-new --yes if absent
PAYLOAD_SECRET # Payload cookie/JWT secret
NEXT_PUBLIC_SERVER_URL # absolute URL for success/cancel redirects
- Fable (claude-fable-5) plans and orchestrates: architecture decisions, task breakdown, reviewing subagent output, resolving anything ambiguous or spec-contradicting.
- Sonnet (
model: "sonnet") does implementation: delegate well-scoped coding tasks (scaffolding, writing modules, tests, wiring) to Sonnet subagents via the Agent tool. - Escalation: Sonnet subagent prompts must include this instruction — if the task turns out ambiguous, blocked, or harder than scoped (failing repeatedly, spec conflict, unfamiliar API behavior), stop and return a clear description of the problem instead of guessing; the orchestrating Fable session will resolve it and re-delegate or handle it directly.
- Cloudinary agent entry point — https://cloudinary.com/documentation/agents_start_here — credentials → tools → machine-readable docs, in order
- Cloudinary MCP / agent skills — https://cloudinary.com/documentation/cloudinary_llm_mcp — inspect assets, test transformation chains, verify private-asset behavior
- Cloudinary machine-readable docs — https://cloudinary.com/documentation/llms.txt (top-level index) and https://cloudinary.com/documentation/llms-image-and-video-apis.txt (image/video APIs)
- Stripe CLI — trigger test events (
stripe trigger checkout.session.completed), tail webhooks,stripe sandbox createfor anonymous test environments - Vercel CLI/MCP — deployments, env vars, logs — only when the user explicitly asks you to deploy; never as part of an unattended run