Skip to content

Latest commit

 

History

History
100 lines (71 loc) · 7.47 KB

AGENTS.md

File metadata and controls

100 lines (71 loc) · 7.47 KB

Your training data may be stale

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.


unshutter — Agent & AI Tooling Guide

No credentials? Provision, don't ask

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, writes CLOUDINARY_URL to .env, prints a claim URL (expires 24h unclaimed). Also set NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME from 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 to DATABASE_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.

Project overview

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.

Stack

  • 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 cloudinary Node SDK (v2) for server-side calls; next-cloudinary (CldImage via src/components/PreviewImage.tsx) for storefront rendering
  • pnpm — package manager

Key conventions (non-negotiable)

  • 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.ts is 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 from buildPreviewUrl / buildDownloadUrl / buildOgUrl in lib/cloudinary/
  • Prices come from Payload docs server-side — never trust client-supplied amounts
  • Entitlement = Stripe session verification — /download verifies payment_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) and lib/env.client.ts (public)

Image delivery & optimization

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.

Where things live

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)

Test + build commands

pnpm test      # Vitest — unit tests colocated in lib/
pnpm lint      # ESLint
pnpm build     # TypeScript check + production build
pnpm dev       # Dev server

Local Stripe webhooks: stripe listen --forward-to localhost:3000/api/stripe/webhook

Env vars

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

Model delegation policy

  • 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.

Agentic tooling