From d76070bcb776cbe93899280201d3519c60e49b0a Mon Sep 17 00:00:00 2001 From: Bello Qudus Folohunsho Date: Sat, 10 Oct 2026 10:43:39 +0100 Subject: [PATCH 1/5] feat(architecture): Add API proxy, deploy docs, and verification record --- client/.env.example | 3 + client/next.config.ts | 16 +++++ docs/deploy.md | 71 +++++++++++++++++++ docs/scope/scope.md | 8 +-- .../0001-stack-and-architecture/verify.md | 42 +++++++++++ render.yaml | 22 ++++++ test-preferences.json | 9 +++ 7 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 client/.env.example create mode 100644 docs/deploy.md create mode 100644 docs/specs/0001-stack-and-architecture/verify.md create mode 100644 render.yaml create mode 100644 test-preferences.json diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..b7ccdf6 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,3 @@ +# The API's base address. The web app never talks to the API directly from the +# browser: Next.js rewrites /api/* to this address, so session cookies stay first party. +API_ORIGIN="http://localhost:4000" diff --git a/client/next.config.ts b/client/next.config.ts index bad166a..eb5c97f 100644 --- a/client/next.config.ts +++ b/client/next.config.ts @@ -1,5 +1,13 @@ import type { NextConfig } from "next"; +const apiOrigin = process.env["API_ORIGIN"]; + +if (!apiOrigin) { + throw new Error( + "API_ORIGIN is not set. Copy client/.env.example to client/.env.local and fill it in.", + ); +} + const nextConfig: NextConfig = { /* config options here */ experimental: { @@ -15,6 +23,14 @@ const nextConfig: NextConfig = { }, }, }, + async rewrites() { + return [ + { + source: "/api/:path*", + destination: `${apiOrigin}/api/:path*`, + }, + ]; + }, }; export default nextConfig; diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..95a68a8 --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,71 @@ +# Deploy + +One repository, two apps. Vercel runs the web app (`client/`), Render runs the API (`backend/`), Neon holds the database. The browser only ever talks to the web app: Next.js rewrites `/api/*` to the API, so session cookies stay first party. + +## Local development + +Node 20 or newer. The Better Auth schema CLI (`npx auth generate`) wants Node 22. + +```bash +# API on port 4000 +cd backend +cp .env.example .env # fill in the Neon URLs and a secret: openssl rand -base64 32 +npm install +npm run db:migrate # applies prisma/migrations +npm run seed # creates the first accounts +npm run dev + +# Web app on port 3000, in a second terminal +cd client +cp .env.example .env.local +npm install +npm run dev +``` + +Open http://localhost:3000. Requests to `/api/*` reach the API on port 4000 through the rewrite. + +Useful commands in `backend/`: `npm run typecheck`, `npm run build`, `npm run db:studio`, `npm run db:deploy` (apply migrations without creating one). + +## Environment variables + +### Vercel (client) + +| Name | Value | Why | +|---|---|---| +| `API_ORIGIN` | `https://.onrender.com` | The API's base address. The rewrite in `next.config.ts` sends `/api/*` here. Local default is `http://localhost:4000`. | + +No `vercel.json` is needed: Vercel detects Next.js. Set the Root Directory to `client` in the project settings. + +### Render (backend) + +Described by `render.yaml` at the repository root. The build command runs `prisma generate`, `tsc`, then `prisma migrate deploy`. + +| Name | Value | Why | +|---|---|---| +| `DATABASE_URL` | Neon pooled string (host contains `-pooler`) | The app's database connections. | +| `DATABASE_URL_UNPOOLED` | Neon direct string (no `-pooler`) | Migrations only. Pooling breaks migration tooling in ways that never say "pooling". | +| `BETTER_AUTH_SECRET` | 32+ random characters | Signs session cookies. `render.yaml` generates one for you. | +| `BETTER_AUTH_URL` | The Vercel app URL, e.g. `https://your-app.vercel.app` | The address the browser talks to, so origin checks and links match. | +| `PORT` | Set by Render | The app binds to whatever is here. | +| `NODE_ENV` | `production` | Turns on secure cookies. | +| `LOG_LEVEL` | `info` (optional) | pino log level. Logs are JSON on stdout, which Render captures. | + +Keep it on the paid Starter tier: the free tier sleeps, which would silently stop scheduled reminders when those arrive. + +### Not used yet + +These are named by the stack decision and arrive with their own features, so leave them unset for now: `EMAIL_FROM` and `APP_URL` (Brevo, with tasks and reminders), Upstash Redis variables (background jobs), Cloudinary credentials (file attachments), `SENTRY_AUTH_TOKEN` and the Sentry DSN (error tracking). + +## First deploy + +1. Push the repository to GitHub (the `origin` remote already points there). +2. **Render**: create a service from Blueprint, point it at this repository. `render.yaml` supplies the API service; fill in the `sync: false` variables by hand. +3. **Vercel**: import the repository, set Root Directory to `client`, add `API_ORIGIN`. +4. Set `BETTER_AUTH_URL` on Render to the Vercel URL, then redeploy the API so origin checks match. +5. Seed the first accounts once against the production database: copy production `DATABASE_URL` into `backend/.env` locally and run `npm run seed`. + +## Costs worth knowing + +- Vercel's free Hobby plan forbids commercial use, and an internal company tool may count as one. Budget for Vercel Pro (about 20 dollars a month) or confirm the terms. +- Render Starter (about 7 dollars a month) is effectively required for the reason above. +- Neon, Upstash and Sentry all have free tiers that cover the start. diff --git a/docs/scope/scope.md b/docs/scope/scope.md index 3fb091b..fa565ce 100644 --- a/docs/scope/scope.md +++ b/docs/scope/scope.md @@ -44,10 +44,10 @@ Express and TypeScript started but not running yet: the dev script points at a m Execute the stack decision from spec 0001: one git repo at the root, both apps booting locally on the decided services, the database migrated, auth mounted, and the proxy working. **Done when:** both apps run locally against Neon with migrations applied, Better Auth sign in works through the proxy, and the first deploy targets Vercel and Render with the environment variables documented. - [x] Design it (spec): `/architect stack & architecture` -- [ ] Scaffold from the decision: `/develop stack & architecture` -- [ ] Verify it: `/check verify stack & architecture` -- [ ] Test it: `/test stack & architecture` -Spec [0001](../specs/0001-stack-and-architecture/index.md) +- [x] Scaffold from the decision: `/develop stack & architecture` +- [x] Verify it: `/check verify stack & architecture` +- [x] Test it: `/test stack & architecture` +Spec [0001](../specs/0001-stack-and-architecture/index.md) · code in `backend/`, `client/`, `render.yaml`, `docs/deploy.md` ### 1. Coding standards & tooling Capture conventions from the real scaffold, then install lint, format, and pre commit checks. There is no root `AGENTS.md` yet, so this is the first stop for real context. diff --git a/docs/specs/0001-stack-and-architecture/verify.md b/docs/specs/0001-stack-and-architecture/verify.md new file mode 100644 index 0000000..9e6e30b --- /dev/null +++ b/docs/specs/0001-stack-and-architecture/verify.md @@ -0,0 +1,42 @@ +# Verify: Stack & architecture · spec 0001 · updated 2026-10-09 + +_Steps derived from spec 0001 and the feature's Done when. `/check verify` runs these; `/test` locks the durable ones._ + +Acceptance criteria, taken from the scope's Done when: + +- AC-1 both apps run locally against Neon with migrations applied +- AC-2 Better Auth sign in works through the proxy +- AC-3 the first deploy targets Vercel and Render with the environment variables documented + +## UI / manual + +- [x] Open http://localhost:3000, then http://localhost:3000/api/v1/health in the browser → the health JSON comes back from the web app's own origin → AC-1, AC-2 + +## Commands + +- [x] `cd backend && npm run dev` → log line `"msg":"api listening"` with port 4000 → AC-1 +- [x] `cd client && npm run dev` → `Ready` on http://localhost:3000 → AC-1 +- [x] `curl http://localhost:4000/api/v1/health` → `{"status":"ok","database":"up"}` → API reaches Neon on its own → AC-1 +- [x] `curl http://localhost:3000/api/v1/health` → `{"status":"ok","database":"up"}` → the rewrite proxies to the API → AC-1 +- [x] `psql "$DATABASE_URL_UNPOOLED" -c '\dt'` → `user`, `session`, `account`, `verification`, `_prisma_migrations` → migration applied and schema live → AC-1 +- [x] `cd backend && npm run seed` → `created` or `exists` for both accounts → AC-2 +- [x] `curl -X POST http://localhost:3000/api/auth/sign-in/email -H 'Content-Type: application/json' -H 'Origin: http://localhost:3000' -d '{"email":"pa@example.com","password":""}' -c cookies.txt` → HTTP 200, `set-cookie: better-auth.session_token=...; HttpOnly; SameSite=Lax`, user JSON → AC-2 +- [x] `curl -b cookies.txt http://localhost:3000/api/auth/get-session` → session JSON for `pa@example.com` → cookie is usable on the web app's own origin → AC-2 +- [x] `curl http://localhost:3000/api/v1/nope` → HTTP 404 → unknown API routes do not fall through to the web app → AC-2 +- [x] `cd backend && npm run typecheck` → passes → AC-1 +- [x] `cd backend && npm run build && PORT=4100 node dist/index.js` → `api listening`, health returns ok → production API build runs → AC-3 +- [x] `cd client && npm run build` → build finishes → production web build runs → AC-3 +- [ ] `test -f render.yaml && grep -q 'prisma migrate deploy' render.yaml` → Render build applies migrations → AC-3 +- [x] `test -f docs/deploy.md && test -f backend/.env.example && test -f client/.env.example` → environment variables documented → AC-3 +- [x] `git -C . check-ignore backend/.env client/.env.local` → both ignored → secrets stay out of the repo → AC-3 + +## Acceptance-criteria coverage + +- AC-1 covered by the boot, health, proxy and migration steps above +- AC-2 covered by the seed, sign in, get session and 404 steps +- AC-3 covered by the build, `render.yaml`, deploy doc and ignore checks + +## Known edges worth varying + +- Change `SEED_PASSWORD` or use `ceo@example.com` and repeat the sign in step, so a wrong or missing credential path is exercised, not just the happy one. +- Stop the API and repeat `curl http://localhost:3000/api/v1/health` → expect a 500 rather than a silent hang, which shows the proxy fails loudly when the API is down. diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..a3abe95 --- /dev/null +++ b/render.yaml @@ -0,0 +1,22 @@ +services: + - type: web + name: ceo-pa-api + runtime: node + rootDir: backend + numReplicas: 1 + healthCheckPath: /api/v1/health + buildCommand: npm ci && npm run build && npm run db:deploy + startCommand: npm start + envVars: + - key: NODE_ENV + value: production + - key: DATABASE_URL + sync: false + - key: DATABASE_URL_UNPOOLED + sync: false + - key: BETTER_AUTH_SECRET + generateValue: true + - key: BETTER_AUTH_URL + sync: false + - key: LOG_LEVEL + value: info diff --git a/test-preferences.json b/test-preferences.json new file mode 100644 index 0000000..93926ca --- /dev/null +++ b/test-preferences.json @@ -0,0 +1,9 @@ +{ + "tool": "vitest", + "gate": null, + "additionalTools": ["supertest"], + "e2eTool": "none", + "testDir": "beside the source", + "filePattern": "*.test.ts", + "packageManager": "npm" +} From 8c2e7dc2c31eb20711091bf34c40f26f46d8e8f4 Mon Sep 17 00:00:00 2001 From: Bello Qudus Folohunsho Date: Sat, 10 Oct 2026 10:43:49 +0100 Subject: [PATCH 2/5] chore(tooling): Record coding standards and install agent skills --- .../skills/api-and-interface-design/SKILL.md | 367 ++ .agents/skills/api-design-principles/SKILL.md | 110 + .../assets/api-design-checklist.md | 155 + .../assets/rest-api-template.py | 182 + .../references/details.md | 413 ++ .../references/graphql-schema-design.md | 583 +++ .../references/rest-best-practices.md | 408 ++ .../SKILL.md | 432 ++ .agents/skills/bullmq-specialist/SKILL.md | 397 ++ .agents/skills/cloudinary-react/SKILL.md | 486 +++ .../references/signed-uploads.md | 130 + .../references/troubleshooting.md | 131 + .../references/typescript-patterns.md | 133 + .../references/video-player.md | 165 + .../cloudinary-transformations/SKILL.md | 563 +++ .../references/advanced-features.md | 727 ++++ .../references/ai-transformations.md | 133 + .../references/debugging.md | 528 +++ .../references/examples.md | 712 +++ .../references/named-transformations.md | 145 + .../references/responsive-images.md | 717 ++++ .../references/transformation-costs.md | 26 + .../references/video-transformations.md | 98 + .agents/skills/deploy-to-vercel/SKILL.md | 296 ++ .../resources/deploy-codex.sh | 301 ++ .../deploy-to-vercel/resources/deploy.sh | 301 ++ .agents/skills/e2e-testing-patterns/SKILL.md | 127 + .../references/details.md | 413 ++ .../SKILL.md | 212 + .agents/skills/neon/SKILL.md | 502 +++ .agents/skills/neon/references/auth.md | 12 + .../skills/neon/references/claimable-neon.md | 117 + .../neon/references/function-triggers.md | 53 + .agents/skills/neon/references/logs-loki.md | 61 + .agents/skills/neon/references/parse-env.md | 32 + .agents/skills/neon/references/sdk.md | 7 + .../nextjs-app-router-patterns/SKILL.md | 114 + .../references/details.md | 428 ++ .../skills/nodejs-backend-patterns/SKILL.md | 45 + .../references/advanced-patterns.md | 430 ++ .../references/details.md | 599 +++ .../SKILL.md | 238 + .../organization-best-practices/SKILL.md | 479 +++ .../skills/playwright-best-practices/SKILL.md | 303 ++ .../advanced/authentication-flows.md | 360 ++ .../advanced/authentication.md | 871 ++++ .../advanced/clock-mocking.md | 364 ++ .../advanced/mobile-testing.md | 409 ++ .../advanced/multi-context.md | 288 ++ .../advanced/multi-user.md | 393 ++ .../advanced/network-advanced.md | 452 ++ .../advanced/third-party.md | 464 ++ .../architecture/pom-vs-fixtures.md | 363 ++ .../architecture/test-architecture.md | 369 ++ .../architecture/when-to-mock.md | 383 ++ .../browser-apis/browser-apis.md | 391 ++ .../browser-apis/iframes.md | 403 ++ .../browser-apis/service-workers.md | 504 +++ .../browser-apis/websockets.md | 403 ++ .../core/annotations.md | 424 ++ .../core/assertions-waiting.md | 361 ++ .../core/configuration.md | 452 ++ .../core/fixtures-hooks.md | 417 ++ .../core/global-setup.md | 434 ++ .../core/locators.md | 242 ++ .../core/page-object-model.md | 315 ++ .../core/projects-dependencies.md | 453 ++ .../core/test-data.md | 492 +++ .../core/test-suite-structure.md | 361 ++ .../core/test-tags.md | 298 ++ .../debugging/console-errors.md | 420 ++ .../debugging/debugging.md | 504 +++ .../debugging/error-testing.md | 360 ++ .../debugging/flaky-tests.md | 496 +++ .../frameworks/angular.md | 530 +++ .../frameworks/nextjs.md | 469 ++ .../frameworks/react.md | 531 +++ .../frameworks/vue.md | 574 +++ .../infrastructure-ci-cd/ci-cd.md | 468 ++ .../infrastructure-ci-cd/docker.md | 283 ++ .../infrastructure-ci-cd/github-actions.md | 546 +++ .../infrastructure-ci-cd/gitlab.md | 397 ++ .../infrastructure-ci-cd/other-providers.md | 521 +++ .../infrastructure-ci-cd/parallel-sharding.md | 371 ++ .../infrastructure-ci-cd/performance.md | 453 ++ .../infrastructure-ci-cd/reporting.md | 424 ++ .../infrastructure-ci-cd/test-coverage.md | 497 +++ .../testing-patterns/accessibility.md | 359 ++ .../testing-patterns/api-testing.md | 719 ++++ .../testing-patterns/browser-extensions.md | 506 +++ .../testing-patterns/canvas-webgl.md | 493 +++ .../testing-patterns/component-testing.md | 500 +++ .../testing-patterns/drag-drop.md | 576 +++ .../testing-patterns/electron.md | 509 +++ .../testing-patterns/file-operations.md | 377 ++ .../testing-patterns/file-upload-download.md | 562 +++ .../testing-patterns/forms-validation.md | 561 +++ .../testing-patterns/graphql-testing.md | 331 ++ .../testing-patterns/i18n.md | 508 +++ .../testing-patterns/performance-testing.md | 476 ++ .../testing-patterns/security-testing.md | 430 ++ .../testing-patterns/visual-regression.md | 634 +++ .agents/skills/playwright-cli/SKILL.md | 489 +++ .../references/element-attributes.md | 23 + .../references/playwright-tests.md | 39 + .../references/pr-attachments.md | 60 + .../references/request-mocking.md | 87 + .../playwright-cli/references/running-code.md | 245 ++ .../references/session-management.md | 227 + .../references/storage-state.md | 275 ++ .../references/test-generation.md | 433 ++ .../playwright-cli/references/tracing.md | 139 + .../references/video-recording.md | 216 + .../skills/postgresql-optimization/SKILL.md | 404 ++ .../SKILL.md | 270 ++ .agents/skills/prisma-upgrade-v7/SKILL.md | 259 ++ .../references/accelerate-users.md | 151 + .../references/driver-adapters.md | 267 ++ .../references/env-variables.md | 161 + .../references/esm-support.md | 128 + .../references/prisma-config.md | 203 + .../references/removed-features.md | 230 + .../references/schema-changes.md | 164 + .agents/skills/sentry-create-alert/SKILL.md | 210 + .agents/skills/sentry-fix-issues/SKILL.md | 126 + .agents/skills/sentry-nextjs-sdk/SKILL.md | 485 +++ .../references/ai-monitoring.md | 408 ++ .../sentry-nextjs-sdk/references/crons.md | 371 ++ .../references/error-monitoring.md | 959 +++++ .../sentry-nextjs-sdk/references/logging.md | 375 ++ .../sentry-nextjs-sdk/references/profiling.md | 385 ++ .../references/session-replay.md | 436 ++ .../sentry-nextjs-sdk/references/tracing.md | 616 +++ .agents/skills/sentry-react-sdk/SKILL.md | 444 ++ .../references/error-monitoring.md | 1543 +++++++ .../sentry-react-sdk/references/logging.md | 350 ++ .../sentry-react-sdk/references/profiling.md | 296 ++ .../references/react-features.md | 1218 ++++++ .../references/session-replay.md | 1125 +++++ .../sentry-react-sdk/references/tracing.md | 1367 ++++++ .../skills/tailwind-design-system/SKILL.md | 186 + .../references/advanced-patterns.md | 319 ++ .../references/details.md | 385 ++ .agents/skills/tailwind-v4-shadcn/SKILL.md | 574 +++ .../references/advanced-usage.md | 171 + .../references/common-gotchas.md | 469 ++ .../references/dark-mode.md | 239 ++ .../references/migration-guide.md | 313 ++ .../references/plugins-reference.md | 135 + .../templates/components.json | 20 + .../tailwind-v4-shadcn/templates/index.css | 140 + .../templates/theme-provider.tsx | 92 + .../templates/tsconfig.app.json | 31 + .../tailwind-v4-shadcn/templates/utils.ts | 6 + .../templates/vite.config.ts | 17 + .../SKILL.md | 331 ++ .agents/skills/upstash-qstash-js/SKILL.md | 90 + .../upstash-qstash-js/advanced/callbacks.md | 147 + .../advanced/deduplication.md | 86 + .../skills/upstash-qstash-js/advanced/dlq.md | 164 + .../advanced/multi-region/summary.md | 388 ++ .../multi-region/verify-multi-region-setup.ts | 388 ++ .../fundamentals/local-development.md | 106 + .../fundamentals/publishing-messages.md | 144 + .../fundamentals/queues-and-flow-control.md | 148 + .../fundamentals/schedules.md | 177 + .../fundamentals/url-groups.md | 127 + .../verification/platform-specific/nextjs.md | 171 + .../verification/receiver.md | 213 + .agents/skills/upstash-ratelimit-js/SKILL.md | 35 + .../skills/upstash-ratelimit-js/algorithms.md | 84 + .../skills/upstash-ratelimit-js/features.md | 117 + .../methods-getting-started.md | 77 + .../upstash-ratelimit-js/pricing-cost.md | 146 + .../traffic-protection.md | 77 + .agents/skills/upstash-redis-js/SKILL.md | 204 + .../advanced-features/auto-pipeline.md | 49 + .../pipeline-and-transactions.md | 70 + .../advanced-features/scripting.md | 124 + .../data-structures/arrays.md | 75 + .../data-structures/hashes.md | 71 + .../upstash-redis-js/data-structures/json.md | 133 + .../upstash-redis-js/data-structures/lists.md | 72 + .../upstash-redis-js/data-structures/sets.md | 90 + .../data-structures/sorted-sets.md | 126 + .../data-structures/streams.md | 196 + .../data-structures/strings.md | 73 + .../data-structures/vector-indexes.md | 55 + .../migrations/from-ioredis.md | 163 + .../migrations/from-redis-node.md | 205 + .../upstash-redis-js/patterns/caching.md | 58 + .../patterns/distributed-locks.md | 136 + .../upstash-redis-js/patterns/leaderboard.md | 135 + .../patterns/rate-limiting.md | 187 + .../patterns/session-management.md | 217 + .../performance/batching-operations.md | 76 + .../performance/data-serialization.md | 134 + .../performance/error-handling.md | 118 + .../performance/pipeline-optimization.md | 105 + .../performance/redis-replicas.md | 14 + .../performance/ttl-expiration.md | 32 + .../upstash-redis-js/search/adapters.md | 99 + .../search/commands/aggregating.md | 234 + .../search/commands/aliases.md | 82 + .../search/commands/index-management.md | 165 + .../search/commands/querying.md | 315 ++ .../upstash-redis-js/search/overview.md | 156 + .agents/skills/vercel-optimize/AGENTS.md | 48 + .../skills/vercel-optimize/CONTRIBUTING.md | 41 + .agents/skills/vercel-optimize/README.md | 91 + .agents/skills/vercel-optimize/SKILL.md | 322 ++ .../skills/vercel-optimize/lib/auth-route.mjs | 23 + .../vercel-optimize/lib/budget-summary.mjs | 182 + .../skills/vercel-optimize/lib/citations.mjs | 139 + .../vercel-optimize/lib/cost-coverage.mjs | 143 + .../skills/vercel-optimize/lib/dedup-recs.mjs | 325 ++ .../skills/vercel-optimize/lib/deep-dive.mjs | 350 ++ .../vercel-optimize/lib/display-labels.mjs | 185 + .../vercel-optimize/lib/extract-claims.mjs | 550 +++ .../vercel-optimize/lib/framework-support.mjs | 67 + .../lib/gates/build-minutes-fanout.mjs | 69 + .../vercel-optimize/lib/gates/cold-start.mjs | 66 + .../vercel-optimize/lib/gates/contract.mjs | 79 + .../vercel-optimize/lib/gates/cwv-poor.mjs | 87 + .../lib/gates/external-api-slow.mjs | 55 + .../vercel-optimize/lib/gates/hard-gates.mjs | 73 + .../vercel-optimize/lib/gates/index.mjs | 45 + .../lib/gates/isr-overrevalidation.mjs | 62 + .../lib/gates/middleware-heavy.mjs | 51 + .../observability-events-attribution.mjs | 56 + .../lib/gates/platform-bot-protection.mjs | 115 + .../lib/gates/platform-fluid-compute.mjs | 83 + .../lib/gates/region-misconfig.mjs | 64 + .../lib/gates/route-errors.mjs | 80 + .../lib/gates/scanner-driven.mjs | 122 + .../lib/gates/select-candidates.mjs | 134 + .../vercel-optimize/lib/gates/slow-route.mjs | 88 + .../vercel-optimize/lib/gates/types.d.ts | 38 + .../lib/gates/uncached-route.mjs | 93 + .../lib/gates/usage-spike-triage.mjs | 121 + .../lib/grade-recommendation.mjs | 155 + .../vercel-optimize/lib/impact-label.mjs | 126 + .../vercel-optimize/lib/impact-magnitude.mjs | 60 + .../lib/investigation-brief.mjs | 610 +++ .../lib/observation-safety.mjs | 174 + .../vercel-optimize/lib/project-facts.mjs | 99 + .../skills/vercel-optimize/lib/queries.mjs | 315 ++ .../lib/reconcile-candidates.mjs | 372 ++ .../vercel-optimize/lib/render-report.mjs | 955 +++++ .../skills/vercel-optimize/lib/repo-root.mjs | 86 + .../vercel-optimize/lib/route-normalize.mjs | 220 + .../sanitizers/bot-protection-certainty.mjs | 38 + .../cache-tag-invalidation-certainty.mjs | 30 + .../lib/sanitizers/count-correct.mjs | 52 + .../function-duration-invocations.mjs | 38 + .../vercel-optimize/lib/sanitizers/index.mjs | 79 + .../lib/sanitizers/middleware-conflict.mjs | 36 + .../lib/sanitizers/missing-citation.mjs | 16 + .../lib/sanitizers/pre-release.mjs | 74 + .../lib/sanitizers/rate-limit.mjs | 67 + .../sanitizers/rendering-mode-mislabel.mjs | 38 + .../lib/sanitizers/undeclared-dep.mjs | 78 + .../lib/sanitizers/vercel-directive-strip.mjs | 37 + .../lib/sanitizers/window-units.mjs | 32 + .../cache-components-suspense-dedupe.mjs | 109 + .../lib/scanners/edge-heavy-import.mjs | 94 + .../lib/scanners/force-dynamic.mjs | 42 + .../lib/scanners/headers-in-page.mjs | 44 + .../vercel-optimize/lib/scanners/index.mjs | 35 + .../lib/scanners/large-static-asset.mjs | 92 + .../lib/scanners/max-age-without-s-maxage.mjs | 42 + .../lib/scanners/middleware-broad-matcher.mjs | 55 + .../lib/scanners/missing-cache-headers.mjs | 90 + .../lib/scanners/prisma-include-tree.mjs | 42 + .../lib/scanners/region-pin-in-config.mjs | 88 + .../lib/scanners/source-maps-production.mjs | 36 + .../scanners/sveltekit-prerender-missing.mjs | 43 + .../lib/scanners/turbo-force-bypass.mjs | 129 + .../lib/scanners/unoptimized-image.mjs | 113 + .../lib/scanners/use-cache-date-stamp.mjs | 106 + .../vercel-optimize/lib/support-topics.mjs | 355 ++ .../skills/vercel-optimize/lib/throttle.mjs | 273 ++ .agents/skills/vercel-optimize/lib/util.mjs | 17 + .agents/skills/vercel-optimize/lib/vercel.mjs | 864 ++++ .../vercel-optimize/lib/verify-claim.mjs | 1296 ++++++ .../lib/workspace-resolver.mjs | 521 +++ .agents/skills/vercel-optimize/metadata.json | 14 + .../vercel-optimize/references/candidates.md | 176 + .../references/data-collection.md | 218 + .../references/docs-library.json | 683 +++ .../vercel-optimize/references/doctrine.md | 105 + .../references/observability-plus.md | 108 + .../references/playbooks/README.md | 53 + .../references/playbooks/ai-application.md | 32 + .../references/playbooks/api-service.md | 30 + .../references/playbooks/content-site.md | 30 + .../references/playbooks/ecommerce.md | 30 + .../references/playbooks/marketing.md | 30 + .../references/playbooks/saas.md | 31 + .../references/playbooks/sveltekit.md | 75 + .../references/recommendations.md | 203 + .../references/scanner-patterns.md | 251 ++ .../vercel-optimize/references/scoring.md | 205 + .../references/support-topics/README.md | 46 + .../astro-edge-middleware-scope.md | 22 + .../astro-output-mode-and-isr.md | 22 + .../auth-preserving-parallelization.md | 22 + .../bot-protection-product-guardrails.md | 22 + .../build-minutes-monorepo-fanout.md | 23 + ...ache-components-static-shell-boundaries.md | 22 + ...ache-components-suspense-dedupe-pitfall.md | 23 + .../support-topics/cdn-cache-auth-safety.md | 22 + .../cold-start-initialization-bundle.md | 22 + .../core-web-vitals-client-bottlenecks.md | 22 + .../database-egress-pooling-region.md | 22 + .../support-topics/dynamic-rendering-traps.md | 22 + .../external-api-critical-path-platform.md | 22 + .../external-api-critical-path.md | 22 + .../fast-data-transfer-payloads.md | 22 + .../support-topics/fluid-compute-caveats.md | 22 + .../function-duration-io-and-after.md | 22 + .../function-invocation-reduction.md | 22 + .../function-region-misconfiguration-ttfb.md | 23 + .../image-optimization-cost-control.md | 22 + .../isr-revalidation-static-generation.md | 22 + .../middleware-proxy-edge-cost.md | 22 + .../next-fetch-revalidate-floor.md | 22 + .../next-font-cls-self-hosting.md | 23 + .../next-heavy-ui-lazy-load-boundaries.md | 23 + .../next-image-lcp-preload-sizes.md | 23 + .../next-route-handler-get-cache-defaults.md | 22 + .../next-script-third-party-strategy.md | 23 + .../nextjs-version-cache-semantics.md | 22 + .../not-found-catchall-request-waste.md | 23 + .../nuxt-route-rules-cache-isr.md | 22 + .../observability-events-cost-attribution.md | 22 + .../post-response-work-waituntil.md | 22 + .../route-error-durable-offload.md | 22 + .../route-error-runtime-limits.md | 22 + .../runtime-cache-reusable-data.md | 22 + .../sveltekit-isr-prerender-safety.md | 22 + .../sveltekit-split-cold-start-tradeoff.md | 22 + .../support-topics/usage-spike-triage.md | 22 + ...se-cache-date-stamp-isr-write-amplifier.md | 23 + .../use-cache-remote-shared-origin-data.md | 22 + .../workflow-resumable-stream-routes.md | 23 + .../references/verification.md | 102 + .../vercel-optimize/references/voice.md | 76 + .../scripts/budget-summary.mjs | 56 + .../vercel-optimize/scripts/build-docs.mjs | 74 + .../scripts/check-citations.mjs | 81 + .../scripts/check-docs-fresh.mjs | 93 + .../scripts/collect-signals.mjs | 576 +++ .../scripts/collect-sub-agent-outputs.mjs | 296 ++ .../vercel-optimize/scripts/deep-dive.mjs | 319 ++ .../scripts/gate-investigations.mjs | 166 + .../vercel-optimize/scripts/merge-signals.mjs | 192 + .../scripts/prepare-investigation-brief.mjs | 231 + .../scripts/reconcile-candidates.mjs | 62 + .../vercel-optimize/scripts/render-report.mjs | 437 ++ .../vercel-optimize/scripts/scan-codebase.mjs | 313 ++ .../scripts/verify-and-regen.mjs | 346 ++ .../scripts/verify-finding.mjs | 19 + .../vercel-react-best-practices/AGENTS.md | 3810 +++++++++++++++++ .../vercel-react-best-practices/README.md | 123 + .../vercel-react-best-practices/SKILL.md | 149 + .../vercel-react-best-practices/metadata.json | 15 + .../rules/_sections.md | 46 + .../rules/_template.md | 28 + .../rules/advanced-effect-event-deps.md | 56 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-init-once.md | 42 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 38 + .../async-cheap-condition-before-await.md | 37 + .../rules/async-defer-await.md | 82 + .../rules/async-dependencies.md | 51 + .../rules/async-parallel.md | 28 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-analyzable-paths.md | 63 + .../rules/bundle-barrel-imports.md | 60 + .../rules/bundle-conditional.md | 31 + .../rules/bundle-defer-third-party.md | 49 + .../rules/bundle-dynamic-imports.md | 35 + .../rules/bundle-preload.md | 50 + .../rules/client-event-listeners.md | 74 + .../rules/client-localstorage-schema.md | 71 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 107 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-flatmap-filter.md | 60 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 49 + .../rules/js-min-max-loop.md | 82 + .../rules/js-request-idle-callback.md | 105 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 47 + .../rules/rendering-conditional-render.md | 40 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 46 + .../rules/rendering-hydration-no-flicker.md | 82 + .../rendering-hydration-suppress-warning.md | 30 + .../rules/rendering-resource-hints.md | 85 + .../rules/rendering-script-defer-async.md | 68 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state-no-effect.md | 40 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 74 + .../rules/rerender-lazy-state-init.md | 58 + .../rules/rerender-memo-with-default-value.md | 38 + .../rules/rerender-memo.md | 44 + .../rules/rerender-move-effect-to-event.md | 45 + .../rules/rerender-no-inline-components.md | 82 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-split-combined-hooks.md | 64 + .../rules/rerender-transitions.md | 40 + .../rules/rerender-use-deferred-value.md | 59 + .../rerender-use-ref-transient-values.md | 73 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-hoist-static-io.md | 149 + .../rules/server-no-shared-module-state.md | 50 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-parallel-nested-fetching.md | 34 + .../rules/server-serialization.md | 38 + .../vercel-react-view-transitions/AGENTS.md | 1380 ++++++ .../vercel-react-view-transitions/README.md | 44 + .../vercel-react-view-transitions/SKILL.md | 332 ++ .../metadata.json | 12 + .../references/css-recipes.md | 312 ++ .../references/implementation.md | 170 + .../references/nextjs.md | 233 + .../references/patterns.md | 274 ++ .../references/troubleshooting.md | 47 + .agents/skills/vitest/GENERATION.md | 5 + .agents/skills/vitest/SKILL.md | 55 + .../references/advanced-environments.md | 272 ++ .../vitest/references/advanced-projects.md | 341 ++ .../references/advanced-type-testing.md | 239 ++ .../skills/vitest/references/advanced-vi.md | 290 ++ .agents/skills/vitest/references/core-cli.md | 189 + .../skills/vitest/references/core-config.md | 216 + .../skills/vitest/references/core-describe.md | 195 + .../skills/vitest/references/core-expect.md | 311 ++ .../skills/vitest/references/core-hooks.md | 244 ++ .../skills/vitest/references/core-test-api.md | 267 ++ .../references/features-benchmarking.md | 100 + .../vitest/references/features-concurrency.md | 223 + .../vitest/references/features-context.md | 163 + .../vitest/references/features-coverage.md | 226 + .../vitest/references/features-filtering.md | 212 + .../vitest/references/features-mocking.md | 334 ++ .../vitest/references/features-reporters.md | 110 + .../vitest/references/features-snapshots.md | 253 ++ .../vitest/references/features-test-tags.md | 123 + .agents/skills/zod/AGENTS.md | 97 + .agents/skills/zod/README.md | 79 + .agents/skills/zod/SKILL.md | 127 + .../skills/zod/assets/templates/_template.md | 30 + .agents/skills/zod/references/_sections.md | 46 + .../zod/references/compose-intersection.md | 144 + .../zod/references/compose-lazy-recursive.md | 143 + .agents/skills/zod/references/compose-pipe.md | 113 + .../zod/references/compose-preprocess.md | 142 + .../zod/references/compose-shared-schemas.md | 137 + .../error-avoid-throwing-in-refine.md | 127 + .../zod/references/error-custom-messages.md | 118 + .agents/skills/zod/references/error-i18n.md | 145 + .../zod/references/error-path-for-nested.md | 130 + .../zod/references/error-use-flatten.md | 131 + .../references/object-discriminated-unions.md | 138 + .../object-extend-for-composition.md | 147 + .../references/object-optional-vs-nullable.md | 117 + .../references/object-partial-for-updates.md | 123 + .../skills/zod/references/object-pick-omit.md | 146 + .../zod/references/object-strict-vs-strip.md | 109 + .../parse-async-for-async-refinements.md | 118 + .../parse-avoid-double-validation.md | 129 + .../zod/references/parse-handle-all-issues.md | 125 + .../zod/references/parse-never-trust-json.md | 113 + .../zod/references/parse-use-safeparse.md | 102 + .../zod/references/parse-validate-early.md | 120 + .agents/skills/zod/references/perf-arrays.md | 153 + .../references/perf-avoid-dynamic-creation.md | 139 + .../zod/references/perf-cache-schemas.md | 138 + .../zod/references/perf-lazy-loading.md | 135 + .../skills/zod/references/perf-zod-mini.md | 117 + .../skills/zod/references/refine-add-path.md | 141 + .agents/skills/zod/references/refine-catch.md | 143 + .../skills/zod/references/refine-defaults.md | 131 + .../zod/references/refine-transform-coerce.md | 105 + .../zod/references/refine-vs-superrefine.md | 152 + .../references/schema-avoid-optional-abuse.md | 93 + .../schema-coercion-for-form-data.md | 88 + .../references/schema-string-validations.md | 90 + .../skills/zod/references/schema-use-enums.md | 107 + .../schema-use-primitives-correctly.md | 61 + .../references/schema-use-unknown-not-any.md | 88 + .../zod/references/type-branded-types.md | 102 + .../zod/references/type-enable-strict-mode.md | 130 + .../type-export-schemas-and-types.md | 116 + .../zod/references/type-input-vs-output.md | 116 + .../skills/zod/references/type-use-z-infer.md | 110 + .claude/skills/api-and-interface-design | 1 + .claude/skills/api-design-principles | 1 + .../better-auth-security-best-practices | 1 + .claude/skills/bullmq-specialist | 1 + .claude/skills/cloudinary-react | 1 + .claude/skills/cloudinary-transformations | 1 + .claude/skills/deploy-to-vercel | 1 + .claude/skills/e2e-testing-patterns | 1 + .../skills/email-and-password-best-practices | 1 + .claude/skills/neon | 1 + .claude/skills/nextjs-app-router-patterns | 1 + .claude/skills/nodejs-backend-patterns | 1 + .../skills/observability-and-instrumentation | 1 + .claude/skills/organization-best-practices | 1 + .claude/skills/playwright-best-practices | 1 + .claude/skills/playwright-cli | 1 + .claude/skills/postgresql-optimization | 1 + .../prisma-driver-adapter-implementation | 1 + .claude/skills/prisma-upgrade-v7 | 1 + .claude/skills/sentry-create-alert | 1 + .claude/skills/sentry-fix-issues | 1 + .claude/skills/sentry-nextjs-sdk | 1 + .claude/skills/sentry-react-sdk | 1 + .claude/skills/tailwind-design-system | 1 + .claude/skills/tailwind-v4-shadcn | 1 + .../two-factor-authentication-best-practices | 1 + .claude/skills/upstash-qstash-js | 1 + .claude/skills/upstash-ratelimit-js | 1 + .claude/skills/upstash-redis-js | 1 + .claude/skills/vercel-optimize | 1 + .claude/skills/vercel-react-best-practices | 1 + .claude/skills/vercel-react-view-transitions | 1 + .claude/skills/vitest | 1 + .claude/skills/zod | 1 + AGENTS.md | 117 + CLAUDE.md | 6 + .../0001-stack-and-architecture/index.md | 2 +- skills-lock.json | 204 + 555 files changed, 104622 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/api-and-interface-design/SKILL.md create mode 100644 .agents/skills/api-design-principles/SKILL.md create mode 100644 .agents/skills/api-design-principles/assets/api-design-checklist.md create mode 100644 .agents/skills/api-design-principles/assets/rest-api-template.py create mode 100644 .agents/skills/api-design-principles/references/details.md create mode 100644 .agents/skills/api-design-principles/references/graphql-schema-design.md create mode 100644 .agents/skills/api-design-principles/references/rest-best-practices.md create mode 100644 .agents/skills/better-auth-security-best-practices/SKILL.md create mode 100644 .agents/skills/bullmq-specialist/SKILL.md create mode 100644 .agents/skills/cloudinary-react/SKILL.md create mode 100644 .agents/skills/cloudinary-react/references/signed-uploads.md create mode 100644 .agents/skills/cloudinary-react/references/troubleshooting.md create mode 100644 .agents/skills/cloudinary-react/references/typescript-patterns.md create mode 100644 .agents/skills/cloudinary-react/references/video-player.md create mode 100644 .agents/skills/cloudinary-transformations/SKILL.md create mode 100644 .agents/skills/cloudinary-transformations/references/advanced-features.md create mode 100644 .agents/skills/cloudinary-transformations/references/ai-transformations.md create mode 100644 .agents/skills/cloudinary-transformations/references/debugging.md create mode 100644 .agents/skills/cloudinary-transformations/references/examples.md create mode 100644 .agents/skills/cloudinary-transformations/references/named-transformations.md create mode 100644 .agents/skills/cloudinary-transformations/references/responsive-images.md create mode 100644 .agents/skills/cloudinary-transformations/references/transformation-costs.md create mode 100644 .agents/skills/cloudinary-transformations/references/video-transformations.md create mode 100644 .agents/skills/deploy-to-vercel/SKILL.md create mode 100644 .agents/skills/deploy-to-vercel/resources/deploy-codex.sh create mode 100644 .agents/skills/deploy-to-vercel/resources/deploy.sh create mode 100644 .agents/skills/e2e-testing-patterns/SKILL.md create mode 100644 .agents/skills/e2e-testing-patterns/references/details.md create mode 100644 .agents/skills/email-and-password-best-practices/SKILL.md create mode 100644 .agents/skills/neon/SKILL.md create mode 100644 .agents/skills/neon/references/auth.md create mode 100644 .agents/skills/neon/references/claimable-neon.md create mode 100644 .agents/skills/neon/references/function-triggers.md create mode 100644 .agents/skills/neon/references/logs-loki.md create mode 100644 .agents/skills/neon/references/parse-env.md create mode 100644 .agents/skills/neon/references/sdk.md create mode 100644 .agents/skills/nextjs-app-router-patterns/SKILL.md create mode 100644 .agents/skills/nextjs-app-router-patterns/references/details.md create mode 100644 .agents/skills/nodejs-backend-patterns/SKILL.md create mode 100644 .agents/skills/nodejs-backend-patterns/references/advanced-patterns.md create mode 100644 .agents/skills/nodejs-backend-patterns/references/details.md create mode 100644 .agents/skills/observability-and-instrumentation/SKILL.md create mode 100644 .agents/skills/organization-best-practices/SKILL.md create mode 100644 .agents/skills/playwright-best-practices/SKILL.md create mode 100644 .agents/skills/playwright-best-practices/advanced/authentication-flows.md create mode 100644 .agents/skills/playwright-best-practices/advanced/authentication.md create mode 100644 .agents/skills/playwright-best-practices/advanced/clock-mocking.md create mode 100644 .agents/skills/playwright-best-practices/advanced/mobile-testing.md create mode 100644 .agents/skills/playwright-best-practices/advanced/multi-context.md create mode 100644 .agents/skills/playwright-best-practices/advanced/multi-user.md create mode 100644 .agents/skills/playwright-best-practices/advanced/network-advanced.md create mode 100644 .agents/skills/playwright-best-practices/advanced/third-party.md create mode 100644 .agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md create mode 100644 .agents/skills/playwright-best-practices/architecture/test-architecture.md create mode 100644 .agents/skills/playwright-best-practices/architecture/when-to-mock.md create mode 100644 .agents/skills/playwright-best-practices/browser-apis/browser-apis.md create mode 100644 .agents/skills/playwright-best-practices/browser-apis/iframes.md create mode 100644 .agents/skills/playwright-best-practices/browser-apis/service-workers.md create mode 100644 .agents/skills/playwright-best-practices/browser-apis/websockets.md create mode 100644 .agents/skills/playwright-best-practices/core/annotations.md create mode 100644 .agents/skills/playwright-best-practices/core/assertions-waiting.md create mode 100644 .agents/skills/playwright-best-practices/core/configuration.md create mode 100644 .agents/skills/playwright-best-practices/core/fixtures-hooks.md create mode 100644 .agents/skills/playwright-best-practices/core/global-setup.md create mode 100644 .agents/skills/playwright-best-practices/core/locators.md create mode 100644 .agents/skills/playwright-best-practices/core/page-object-model.md create mode 100644 .agents/skills/playwright-best-practices/core/projects-dependencies.md create mode 100644 .agents/skills/playwright-best-practices/core/test-data.md create mode 100644 .agents/skills/playwright-best-practices/core/test-suite-structure.md create mode 100644 .agents/skills/playwright-best-practices/core/test-tags.md create mode 100644 .agents/skills/playwright-best-practices/debugging/console-errors.md create mode 100644 .agents/skills/playwright-best-practices/debugging/debugging.md create mode 100644 .agents/skills/playwright-best-practices/debugging/error-testing.md create mode 100644 .agents/skills/playwright-best-practices/debugging/flaky-tests.md create mode 100644 .agents/skills/playwright-best-practices/frameworks/angular.md create mode 100644 .agents/skills/playwright-best-practices/frameworks/nextjs.md create mode 100644 .agents/skills/playwright-best-practices/frameworks/react.md create mode 100644 .agents/skills/playwright-best-practices/frameworks/vue.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md create mode 100644 .agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/accessibility.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/api-testing.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/component-testing.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/drag-drop.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/electron.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/file-operations.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/forms-validation.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/i18n.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/performance-testing.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/security-testing.md create mode 100644 .agents/skills/playwright-best-practices/testing-patterns/visual-regression.md create mode 100644 .agents/skills/playwright-cli/SKILL.md create mode 100644 .agents/skills/playwright-cli/references/element-attributes.md create mode 100644 .agents/skills/playwright-cli/references/playwright-tests.md create mode 100644 .agents/skills/playwright-cli/references/pr-attachments.md create mode 100644 .agents/skills/playwright-cli/references/request-mocking.md create mode 100644 .agents/skills/playwright-cli/references/running-code.md create mode 100644 .agents/skills/playwright-cli/references/session-management.md create mode 100644 .agents/skills/playwright-cli/references/storage-state.md create mode 100644 .agents/skills/playwright-cli/references/test-generation.md create mode 100644 .agents/skills/playwright-cli/references/tracing.md create mode 100644 .agents/skills/playwright-cli/references/video-recording.md create mode 100644 .agents/skills/postgresql-optimization/SKILL.md create mode 100644 .agents/skills/prisma-driver-adapter-implementation/SKILL.md create mode 100644 .agents/skills/prisma-upgrade-v7/SKILL.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/accelerate-users.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/driver-adapters.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/env-variables.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/esm-support.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/prisma-config.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/removed-features.md create mode 100644 .agents/skills/prisma-upgrade-v7/references/schema-changes.md create mode 100644 .agents/skills/sentry-create-alert/SKILL.md create mode 100644 .agents/skills/sentry-fix-issues/SKILL.md create mode 100644 .agents/skills/sentry-nextjs-sdk/SKILL.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/ai-monitoring.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/crons.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/error-monitoring.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/logging.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/profiling.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/session-replay.md create mode 100644 .agents/skills/sentry-nextjs-sdk/references/tracing.md create mode 100644 .agents/skills/sentry-react-sdk/SKILL.md create mode 100644 .agents/skills/sentry-react-sdk/references/error-monitoring.md create mode 100644 .agents/skills/sentry-react-sdk/references/logging.md create mode 100644 .agents/skills/sentry-react-sdk/references/profiling.md create mode 100644 .agents/skills/sentry-react-sdk/references/react-features.md create mode 100644 .agents/skills/sentry-react-sdk/references/session-replay.md create mode 100644 .agents/skills/sentry-react-sdk/references/tracing.md create mode 100644 .agents/skills/tailwind-design-system/SKILL.md create mode 100644 .agents/skills/tailwind-design-system/references/advanced-patterns.md create mode 100644 .agents/skills/tailwind-design-system/references/details.md create mode 100644 .agents/skills/tailwind-v4-shadcn/SKILL.md create mode 100644 .agents/skills/tailwind-v4-shadcn/references/advanced-usage.md create mode 100644 .agents/skills/tailwind-v4-shadcn/references/common-gotchas.md create mode 100644 .agents/skills/tailwind-v4-shadcn/references/dark-mode.md create mode 100644 .agents/skills/tailwind-v4-shadcn/references/migration-guide.md create mode 100644 .agents/skills/tailwind-v4-shadcn/references/plugins-reference.md create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/components.json create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/index.css create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/theme-provider.tsx create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/tsconfig.app.json create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/utils.ts create mode 100644 .agents/skills/tailwind-v4-shadcn/templates/vite.config.ts create mode 100644 .agents/skills/two-factor-authentication-best-practices/SKILL.md create mode 100644 .agents/skills/upstash-qstash-js/SKILL.md create mode 100644 .agents/skills/upstash-qstash-js/advanced/callbacks.md create mode 100644 .agents/skills/upstash-qstash-js/advanced/deduplication.md create mode 100644 .agents/skills/upstash-qstash-js/advanced/dlq.md create mode 100644 .agents/skills/upstash-qstash-js/advanced/multi-region/summary.md create mode 100644 .agents/skills/upstash-qstash-js/advanced/multi-region/verify-multi-region-setup.ts create mode 100644 .agents/skills/upstash-qstash-js/fundamentals/local-development.md create mode 100644 .agents/skills/upstash-qstash-js/fundamentals/publishing-messages.md create mode 100644 .agents/skills/upstash-qstash-js/fundamentals/queues-and-flow-control.md create mode 100644 .agents/skills/upstash-qstash-js/fundamentals/schedules.md create mode 100644 .agents/skills/upstash-qstash-js/fundamentals/url-groups.md create mode 100644 .agents/skills/upstash-qstash-js/verification/platform-specific/nextjs.md create mode 100644 .agents/skills/upstash-qstash-js/verification/receiver.md create mode 100644 .agents/skills/upstash-ratelimit-js/SKILL.md create mode 100644 .agents/skills/upstash-ratelimit-js/algorithms.md create mode 100644 .agents/skills/upstash-ratelimit-js/features.md create mode 100644 .agents/skills/upstash-ratelimit-js/methods-getting-started.md create mode 100644 .agents/skills/upstash-ratelimit-js/pricing-cost.md create mode 100644 .agents/skills/upstash-ratelimit-js/traffic-protection.md create mode 100644 .agents/skills/upstash-redis-js/SKILL.md create mode 100644 .agents/skills/upstash-redis-js/advanced-features/auto-pipeline.md create mode 100644 .agents/skills/upstash-redis-js/advanced-features/pipeline-and-transactions.md create mode 100644 .agents/skills/upstash-redis-js/advanced-features/scripting.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/arrays.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/hashes.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/json.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/lists.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/sets.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/sorted-sets.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/streams.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/strings.md create mode 100644 .agents/skills/upstash-redis-js/data-structures/vector-indexes.md create mode 100644 .agents/skills/upstash-redis-js/migrations/from-ioredis.md create mode 100644 .agents/skills/upstash-redis-js/migrations/from-redis-node.md create mode 100644 .agents/skills/upstash-redis-js/patterns/caching.md create mode 100644 .agents/skills/upstash-redis-js/patterns/distributed-locks.md create mode 100644 .agents/skills/upstash-redis-js/patterns/leaderboard.md create mode 100644 .agents/skills/upstash-redis-js/patterns/rate-limiting.md create mode 100644 .agents/skills/upstash-redis-js/patterns/session-management.md create mode 100644 .agents/skills/upstash-redis-js/performance/batching-operations.md create mode 100644 .agents/skills/upstash-redis-js/performance/data-serialization.md create mode 100644 .agents/skills/upstash-redis-js/performance/error-handling.md create mode 100644 .agents/skills/upstash-redis-js/performance/pipeline-optimization.md create mode 100644 .agents/skills/upstash-redis-js/performance/redis-replicas.md create mode 100644 .agents/skills/upstash-redis-js/performance/ttl-expiration.md create mode 100644 .agents/skills/upstash-redis-js/search/adapters.md create mode 100644 .agents/skills/upstash-redis-js/search/commands/aggregating.md create mode 100644 .agents/skills/upstash-redis-js/search/commands/aliases.md create mode 100644 .agents/skills/upstash-redis-js/search/commands/index-management.md create mode 100644 .agents/skills/upstash-redis-js/search/commands/querying.md create mode 100644 .agents/skills/upstash-redis-js/search/overview.md create mode 100644 .agents/skills/vercel-optimize/AGENTS.md create mode 100644 .agents/skills/vercel-optimize/CONTRIBUTING.md create mode 100644 .agents/skills/vercel-optimize/README.md create mode 100644 .agents/skills/vercel-optimize/SKILL.md create mode 100644 .agents/skills/vercel-optimize/lib/auth-route.mjs create mode 100644 .agents/skills/vercel-optimize/lib/budget-summary.mjs create mode 100644 .agents/skills/vercel-optimize/lib/citations.mjs create mode 100644 .agents/skills/vercel-optimize/lib/cost-coverage.mjs create mode 100644 .agents/skills/vercel-optimize/lib/dedup-recs.mjs create mode 100644 .agents/skills/vercel-optimize/lib/deep-dive.mjs create mode 100644 .agents/skills/vercel-optimize/lib/display-labels.mjs create mode 100644 .agents/skills/vercel-optimize/lib/extract-claims.mjs create mode 100644 .agents/skills/vercel-optimize/lib/framework-support.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/build-minutes-fanout.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/cold-start.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/contract.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/cwv-poor.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/external-api-slow.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/hard-gates.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/index.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/isr-overrevalidation.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/middleware-heavy.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/observability-events-attribution.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/platform-bot-protection.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/platform-fluid-compute.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/region-misconfig.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/route-errors.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/scanner-driven.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/select-candidates.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/slow-route.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/types.d.ts create mode 100644 .agents/skills/vercel-optimize/lib/gates/uncached-route.mjs create mode 100644 .agents/skills/vercel-optimize/lib/gates/usage-spike-triage.mjs create mode 100644 .agents/skills/vercel-optimize/lib/grade-recommendation.mjs create mode 100644 .agents/skills/vercel-optimize/lib/impact-label.mjs create mode 100644 .agents/skills/vercel-optimize/lib/impact-magnitude.mjs create mode 100644 .agents/skills/vercel-optimize/lib/investigation-brief.mjs create mode 100644 .agents/skills/vercel-optimize/lib/observation-safety.mjs create mode 100644 .agents/skills/vercel-optimize/lib/project-facts.mjs create mode 100644 .agents/skills/vercel-optimize/lib/queries.mjs create mode 100644 .agents/skills/vercel-optimize/lib/reconcile-candidates.mjs create mode 100644 .agents/skills/vercel-optimize/lib/render-report.mjs create mode 100644 .agents/skills/vercel-optimize/lib/repo-root.mjs create mode 100644 .agents/skills/vercel-optimize/lib/route-normalize.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/bot-protection-certainty.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/cache-tag-invalidation-certainty.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/count-correct.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/function-duration-invocations.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/index.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/middleware-conflict.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/missing-citation.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/pre-release.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/rate-limit.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/rendering-mode-mislabel.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/undeclared-dep.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/vercel-directive-strip.mjs create mode 100644 .agents/skills/vercel-optimize/lib/sanitizers/window-units.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/cache-components-suspense-dedupe.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/edge-heavy-import.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/force-dynamic.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/headers-in-page.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/index.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/large-static-asset.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/max-age-without-s-maxage.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/middleware-broad-matcher.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/missing-cache-headers.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/prisma-include-tree.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/region-pin-in-config.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/source-maps-production.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/sveltekit-prerender-missing.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/turbo-force-bypass.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/unoptimized-image.mjs create mode 100644 .agents/skills/vercel-optimize/lib/scanners/use-cache-date-stamp.mjs create mode 100644 .agents/skills/vercel-optimize/lib/support-topics.mjs create mode 100644 .agents/skills/vercel-optimize/lib/throttle.mjs create mode 100644 .agents/skills/vercel-optimize/lib/util.mjs create mode 100644 .agents/skills/vercel-optimize/lib/vercel.mjs create mode 100644 .agents/skills/vercel-optimize/lib/verify-claim.mjs create mode 100644 .agents/skills/vercel-optimize/lib/workspace-resolver.mjs create mode 100644 .agents/skills/vercel-optimize/metadata.json create mode 100644 .agents/skills/vercel-optimize/references/candidates.md create mode 100644 .agents/skills/vercel-optimize/references/data-collection.md create mode 100644 .agents/skills/vercel-optimize/references/docs-library.json create mode 100644 .agents/skills/vercel-optimize/references/doctrine.md create mode 100644 .agents/skills/vercel-optimize/references/observability-plus.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/README.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/ai-application.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/api-service.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/content-site.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/ecommerce.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/marketing.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/saas.md create mode 100644 .agents/skills/vercel-optimize/references/playbooks/sveltekit.md create mode 100644 .agents/skills/vercel-optimize/references/recommendations.md create mode 100644 .agents/skills/vercel-optimize/references/scanner-patterns.md create mode 100644 .agents/skills/vercel-optimize/references/scoring.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/README.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/astro-edge-middleware-scope.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/astro-output-mode-and-isr.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/auth-preserving-parallelization.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/bot-protection-product-guardrails.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/build-minutes-monorepo-fanout.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/cache-components-static-shell-boundaries.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/cache-components-suspense-dedupe-pitfall.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/cdn-cache-auth-safety.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/cold-start-initialization-bundle.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/core-web-vitals-client-bottlenecks.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/database-egress-pooling-region.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/dynamic-rendering-traps.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/external-api-critical-path-platform.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/external-api-critical-path.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/fast-data-transfer-payloads.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/fluid-compute-caveats.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/function-duration-io-and-after.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/function-invocation-reduction.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/function-region-misconfiguration-ttfb.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/image-optimization-cost-control.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/isr-revalidation-static-generation.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/middleware-proxy-edge-cost.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-fetch-revalidate-floor.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-font-cls-self-hosting.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-heavy-ui-lazy-load-boundaries.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-image-lcp-preload-sizes.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-route-handler-get-cache-defaults.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/next-script-third-party-strategy.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/nextjs-version-cache-semantics.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/not-found-catchall-request-waste.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/nuxt-route-rules-cache-isr.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/observability-events-cost-attribution.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/post-response-work-waituntil.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/route-error-durable-offload.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/route-error-runtime-limits.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/runtime-cache-reusable-data.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/sveltekit-isr-prerender-safety.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/sveltekit-split-cold-start-tradeoff.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/usage-spike-triage.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/use-cache-date-stamp-isr-write-amplifier.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/use-cache-remote-shared-origin-data.md create mode 100644 .agents/skills/vercel-optimize/references/support-topics/workflow-resumable-stream-routes.md create mode 100644 .agents/skills/vercel-optimize/references/verification.md create mode 100644 .agents/skills/vercel-optimize/references/voice.md create mode 100644 .agents/skills/vercel-optimize/scripts/budget-summary.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/build-docs.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/check-citations.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/check-docs-fresh.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/collect-signals.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/collect-sub-agent-outputs.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/deep-dive.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/gate-investigations.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/merge-signals.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/prepare-investigation-brief.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/reconcile-candidates.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/render-report.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/scan-codebase.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/verify-and-regen.mjs create mode 100644 .agents/skills/vercel-optimize/scripts/verify-finding.mjs create mode 100644 .agents/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agents/skills/vercel-react-best-practices/README.md create mode 100644 .agents/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/metadata.json create mode 100644 .agents/skills/vercel-react-best-practices/rules/_sections.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/_template.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-init-once.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-request-idle-callback.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .agents/skills/vercel-react-view-transitions/AGENTS.md create mode 100644 .agents/skills/vercel-react-view-transitions/README.md create mode 100644 .agents/skills/vercel-react-view-transitions/SKILL.md create mode 100644 .agents/skills/vercel-react-view-transitions/metadata.json create mode 100644 .agents/skills/vercel-react-view-transitions/references/css-recipes.md create mode 100644 .agents/skills/vercel-react-view-transitions/references/implementation.md create mode 100644 .agents/skills/vercel-react-view-transitions/references/nextjs.md create mode 100644 .agents/skills/vercel-react-view-transitions/references/patterns.md create mode 100644 .agents/skills/vercel-react-view-transitions/references/troubleshooting.md create mode 100644 .agents/skills/vitest/GENERATION.md create mode 100644 .agents/skills/vitest/SKILL.md create mode 100644 .agents/skills/vitest/references/advanced-environments.md create mode 100644 .agents/skills/vitest/references/advanced-projects.md create mode 100644 .agents/skills/vitest/references/advanced-type-testing.md create mode 100644 .agents/skills/vitest/references/advanced-vi.md create mode 100644 .agents/skills/vitest/references/core-cli.md create mode 100644 .agents/skills/vitest/references/core-config.md create mode 100644 .agents/skills/vitest/references/core-describe.md create mode 100644 .agents/skills/vitest/references/core-expect.md create mode 100644 .agents/skills/vitest/references/core-hooks.md create mode 100644 .agents/skills/vitest/references/core-test-api.md create mode 100644 .agents/skills/vitest/references/features-benchmarking.md create mode 100644 .agents/skills/vitest/references/features-concurrency.md create mode 100644 .agents/skills/vitest/references/features-context.md create mode 100644 .agents/skills/vitest/references/features-coverage.md create mode 100644 .agents/skills/vitest/references/features-filtering.md create mode 100644 .agents/skills/vitest/references/features-mocking.md create mode 100644 .agents/skills/vitest/references/features-reporters.md create mode 100644 .agents/skills/vitest/references/features-snapshots.md create mode 100644 .agents/skills/vitest/references/features-test-tags.md create mode 100644 .agents/skills/zod/AGENTS.md create mode 100644 .agents/skills/zod/README.md create mode 100644 .agents/skills/zod/SKILL.md create mode 100644 .agents/skills/zod/assets/templates/_template.md create mode 100644 .agents/skills/zod/references/_sections.md create mode 100644 .agents/skills/zod/references/compose-intersection.md create mode 100644 .agents/skills/zod/references/compose-lazy-recursive.md create mode 100644 .agents/skills/zod/references/compose-pipe.md create mode 100644 .agents/skills/zod/references/compose-preprocess.md create mode 100644 .agents/skills/zod/references/compose-shared-schemas.md create mode 100644 .agents/skills/zod/references/error-avoid-throwing-in-refine.md create mode 100644 .agents/skills/zod/references/error-custom-messages.md create mode 100644 .agents/skills/zod/references/error-i18n.md create mode 100644 .agents/skills/zod/references/error-path-for-nested.md create mode 100644 .agents/skills/zod/references/error-use-flatten.md create mode 100644 .agents/skills/zod/references/object-discriminated-unions.md create mode 100644 .agents/skills/zod/references/object-extend-for-composition.md create mode 100644 .agents/skills/zod/references/object-optional-vs-nullable.md create mode 100644 .agents/skills/zod/references/object-partial-for-updates.md create mode 100644 .agents/skills/zod/references/object-pick-omit.md create mode 100644 .agents/skills/zod/references/object-strict-vs-strip.md create mode 100644 .agents/skills/zod/references/parse-async-for-async-refinements.md create mode 100644 .agents/skills/zod/references/parse-avoid-double-validation.md create mode 100644 .agents/skills/zod/references/parse-handle-all-issues.md create mode 100644 .agents/skills/zod/references/parse-never-trust-json.md create mode 100644 .agents/skills/zod/references/parse-use-safeparse.md create mode 100644 .agents/skills/zod/references/parse-validate-early.md create mode 100644 .agents/skills/zod/references/perf-arrays.md create mode 100644 .agents/skills/zod/references/perf-avoid-dynamic-creation.md create mode 100644 .agents/skills/zod/references/perf-cache-schemas.md create mode 100644 .agents/skills/zod/references/perf-lazy-loading.md create mode 100644 .agents/skills/zod/references/perf-zod-mini.md create mode 100644 .agents/skills/zod/references/refine-add-path.md create mode 100644 .agents/skills/zod/references/refine-catch.md create mode 100644 .agents/skills/zod/references/refine-defaults.md create mode 100644 .agents/skills/zod/references/refine-transform-coerce.md create mode 100644 .agents/skills/zod/references/refine-vs-superrefine.md create mode 100644 .agents/skills/zod/references/schema-avoid-optional-abuse.md create mode 100644 .agents/skills/zod/references/schema-coercion-for-form-data.md create mode 100644 .agents/skills/zod/references/schema-string-validations.md create mode 100644 .agents/skills/zod/references/schema-use-enums.md create mode 100644 .agents/skills/zod/references/schema-use-primitives-correctly.md create mode 100644 .agents/skills/zod/references/schema-use-unknown-not-any.md create mode 100644 .agents/skills/zod/references/type-branded-types.md create mode 100644 .agents/skills/zod/references/type-enable-strict-mode.md create mode 100644 .agents/skills/zod/references/type-export-schemas-and-types.md create mode 100644 .agents/skills/zod/references/type-input-vs-output.md create mode 100644 .agents/skills/zod/references/type-use-z-infer.md create mode 120000 .claude/skills/api-and-interface-design create mode 120000 .claude/skills/api-design-principles create mode 120000 .claude/skills/better-auth-security-best-practices create mode 120000 .claude/skills/bullmq-specialist create mode 120000 .claude/skills/cloudinary-react create mode 120000 .claude/skills/cloudinary-transformations create mode 120000 .claude/skills/deploy-to-vercel create mode 120000 .claude/skills/e2e-testing-patterns create mode 120000 .claude/skills/email-and-password-best-practices create mode 120000 .claude/skills/neon create mode 120000 .claude/skills/nextjs-app-router-patterns create mode 120000 .claude/skills/nodejs-backend-patterns create mode 120000 .claude/skills/observability-and-instrumentation create mode 120000 .claude/skills/organization-best-practices create mode 120000 .claude/skills/playwright-best-practices create mode 120000 .claude/skills/playwright-cli create mode 120000 .claude/skills/postgresql-optimization create mode 120000 .claude/skills/prisma-driver-adapter-implementation create mode 120000 .claude/skills/prisma-upgrade-v7 create mode 120000 .claude/skills/sentry-create-alert create mode 120000 .claude/skills/sentry-fix-issues create mode 120000 .claude/skills/sentry-nextjs-sdk create mode 120000 .claude/skills/sentry-react-sdk create mode 120000 .claude/skills/tailwind-design-system create mode 120000 .claude/skills/tailwind-v4-shadcn create mode 120000 .claude/skills/two-factor-authentication-best-practices create mode 120000 .claude/skills/upstash-qstash-js create mode 120000 .claude/skills/upstash-ratelimit-js create mode 120000 .claude/skills/upstash-redis-js create mode 120000 .claude/skills/vercel-optimize create mode 120000 .claude/skills/vercel-react-best-practices create mode 120000 .claude/skills/vercel-react-view-transitions create mode 120000 .claude/skills/vitest create mode 120000 .claude/skills/zod create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.agents/skills/api-and-interface-design/SKILL.md b/.agents/skills/api-and-interface-design/SKILL.md new file mode 100644 index 0000000..3e2e37f --- /dev/null +++ b/.agents/skills/api-and-interface-design/SKILL.md @@ -0,0 +1,367 @@ +--- +name: api-and-interface-design +description: Guides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend. +--- + +# API and Interface Design + +## Overview + +Design stable, well-documented interfaces that are hard to misuse. Good interfaces make the right thing easy and the wrong thing hard. This applies to REST APIs, GraphQL schemas, module boundaries, component props, and any surface where one piece of code talks to another. + +## When to Use + +- Designing new API endpoints +- Defining module boundaries or contracts between teams +- Creating component prop interfaces +- Establishing database schema that informs API shape +- Changing existing public interfaces + +## Core Principles + +### Hyrum's Law + +> With a sufficient number of users of an API, all observable behaviors of your system will be depended on by somebody, regardless of what you promise in the contract. + +This means: every public behavior — including undocumented quirks, error message text, timing, and ordering — becomes a de facto contract once users depend on it. Design implications: + +- **Be intentional about what you expose.** Every observable behavior is a potential commitment. +- **Don't leak implementation details.** If users can observe it, they will depend on it. +- **Plan for deprecation at design time.** See `deprecation-and-migration` for how to safely remove things users depend on. +- **Tests are not enough.** Even with perfect contract tests, Hyrum's Law means "safe" changes can break real users who depend on undocumented behavior. + +### The One-Version Rule + +Avoid forcing consumers to choose between multiple versions of the same dependency or API. Diamond dependency problems arise when different consumers need different versions of the same thing. Design for a world where only one version exists at a time — extend rather than fork. + +### 1. Contract First + +Define the interface before implementing it. The contract is the spec — implementation follows. + +```typescript +// Define the contract first +interface TaskAPI { + // Creates a task and returns the created task with server-generated fields + createTask(input: CreateTaskInput): Promise; + + // Returns paginated tasks matching filters + listTasks(params: ListTasksParams): Promise>; + + // Returns a single task or throws NotFoundError + getTask(id: string): Promise; + + // Partial update — only provided fields change + updateTask(id: string, input: UpdateTaskInput): Promise; + + // Idempotent delete — succeeds even if already deleted + deleteTask(id: string): Promise; +} +``` + +### 2. Consistent Error Semantics + +Pick one error strategy and use it everywhere: + +```typescript +// REST: HTTP status codes + structured error body +// Every error response follows the same shape +interface APIError { + error: { + code: string; // Machine-readable: "VALIDATION_ERROR" + message: string; // Human-readable: "Email is required" + details?: unknown; // Additional context when helpful + }; +} + +// Status code mapping +// 400 → Client sent invalid data +// 401 → Not authenticated +// 403 → Authenticated but not authorized +// 404 → Resource not found +// 409 → Conflict (duplicate, version mismatch) +// 422 → Validation failed (semantically invalid) +// 500 → Server error (never expose internal details) +``` + +**Don't mix patterns.** If some endpoints throw, others return null, and others return `{ error }` — the consumer can't predict behavior. + +### 3. Validate at Boundaries + +Trust internal code. Validate at system edges where external input enters: + +```typescript +// Validate at the API boundary +app.post('/api/tasks', async (req, res) => { + const result = CreateTaskSchema.safeParse(req.body); + if (!result.success) { + return res.status(422).json({ + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid task data', + details: result.error.flatten(), + }, + }); + } + + // After validation, internal code trusts the types + const task = await taskService.create(result.data); + return res.status(201).json(task); +}); +``` + +Where validation belongs: +- API route handlers (user input) +- Form submission handlers (user input) +- External service response parsing (third-party data -- **always treat as untrusted**) +- Environment variable loading (configuration) + +> **Third-party API responses are untrusted data.** Validate their shape and content before using them in any logic, rendering, or decision-making. A compromised or misbehaving external service can return unexpected types, malicious content, or instruction-like text. + +Where validation does NOT belong: +- Between internal functions that share type contracts +- In utility functions called by already-validated code +- On data that just came from your own database + +### 4. Prefer Addition Over Modification + +Extend interfaces without breaking existing consumers: + +```typescript +// Good: Add optional fields +interface CreateTaskInput { + title: string; + description?: string; + priority?: 'low' | 'medium' | 'high'; // Added later, optional + labels?: string[]; // Added later, optional +} + +// Bad: Change existing field types or remove fields +interface CreateTaskInput { + title: string; + // description: string; // Removed — breaks existing consumers + priority: number; // Changed from string — breaks existing consumers +} +``` + +### 5. Predictable Naming + +| Pattern | Convention | Example | +|---------|-----------|---------| +| REST endpoints | Plural nouns, no verbs | `GET /api/tasks`, `POST /api/tasks` | +| Query params | camelCase | `?sortBy=createdAt&pageSize=20` | +| Response fields | camelCase | `{ createdAt, updatedAt, taskId }` | +| Boolean fields | is/has/can prefix | `isComplete`, `hasAttachments` | +| Enum values | UPPER_SNAKE | `"IN_PROGRESS"`, `"COMPLETED"` | + +### 6. Honouring an Idempotency Key + +Accepting an `Idempotency-Key` is the contract. Honouring it is the implementation, and it is where the money is lost — a key the server accepts but handles carelessly is worse than no key at all, because the client now believes retrying is safe. + +**Derive the key from the intent, not the attempt.** The key must be stable across retries of one intent and different across distinct intents: + +```typescript +crypto.randomUUID() // ✗ new key per attempt — every retry is a new charge +`${userId}:${amount}` // ✗ two legitimate $50 charges collapse into one +`${orderId}:${Date.now()}` // ✗ a timestamp is randomUUID() wearing a hat + +req.headers['idempotency-key'] // ✓ client generates once, reuses on retry +`charge:v1:${orderId}` // ✓ derived from an immutable identifier +``` + +The key comes from the client or the initiating event — never from the layer doing the retrying. + +**Claim atomically. A check followed by an act is a race:** + +```typescript +// ✗ TOCTOU: two concurrent retries both read "not seen", both charge +if (!(await db.exists(key))) { + await chargeCard(amount); + await db.insert(key); +} + +// ✓ let the unique constraint pick the winner +try { + await db.insert({ key, state: 'in_progress', requestHash }); +} catch (e) { + if (isUniqueViolation(e)) return replayOrReject(key); + throw e; +} +const result = await chargeCard(amount); +await db.update({ key, state: 'succeeded', response: result }); +``` + +The unique constraint *is* the mechanism. A store that cannot enforce uniqueness in one operation cannot back this. + +**Guard the payload.** Same key with a different body is a client bug, and must fail loudly rather than serving the first response to a second request: + +```typescript +if (existing.requestHash !== hash(req.body)) { + return res.status(422).json({ error: 'idempotency key reused with a different payload' }); +} +``` + +**Decide what an in-flight duplicate gets.** The first request is still running when the second arrives — the common case under retry storms: + +| Strategy | Response | Use when | +|---|---|---| +| Reject | `409 Conflict` | Client can retry later; simplest and safest | +| Wait | Block for the result, bounded | Caller needs it synchronously | +| Return pending | `202` + status URL | Long-running effects | + +Never let the second caller through because the first "seems stuck". A stalled attempt whose fate is unknown is exactly when duplicating costs most. + +**Every call has three outcomes, not two: success, failure, and _unknown_.** A timeout tells you nothing about whether the effect applied. Record the intent *before* calling out, so a crash between the call and the response leaves evidence something must resolve later — rather than a silently retried charge. + +**Set retention from the longest retry chain**, not from disk cost. Keys must outlive every path that can re-deliver the same intent, including a dead-letter queue replayed a week later and any provider dispute window. A 24-hour key TTL behind a 7-day DLQ is a duplicate waiting to happen. + +## REST API Patterns + +### Resource Design + +``` +GET /api/tasks → List tasks (with query params for filtering) +POST /api/tasks → Create a task +GET /api/tasks/:id → Get a single task +PATCH /api/tasks/:id → Update a task (partial) +DELETE /api/tasks/:id → Delete a task + +GET /api/tasks/:id/comments → List comments for a task (sub-resource) +POST /api/tasks/:id/comments → Add a comment to a task +``` + +### Pagination + +Paginate list endpoints: + +```typescript +// Request +GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc + +// Response +{ + "data": [...], + "pagination": { + "page": 1, + "pageSize": 20, + "totalItems": 142, + "totalPages": 8 + } +} +``` + +### Filtering + +Use query parameters for filters: + +``` +GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01 +``` + +### Partial Updates (PATCH) + +Accept partial objects — only update what's provided: + +```typescript +// Only title changes, everything else preserved +PATCH /api/tasks/123 +{ "title": "Updated title" } +``` + +## TypeScript Interface Patterns + +### Use Discriminated Unions for Variants + +```typescript +// Good: Each variant is explicit +type TaskStatus = + | { type: 'pending' } + | { type: 'in_progress'; assignee: string; startedAt: Date } + | { type: 'completed'; completedAt: Date; completedBy: string } + | { type: 'cancelled'; reason: string; cancelledAt: Date }; + +// Consumer gets type narrowing +function getStatusLabel(status: TaskStatus): string { + switch (status.type) { + case 'pending': return 'Pending'; + case 'in_progress': return `In progress (${status.assignee})`; + case 'completed': return `Done on ${status.completedAt}`; + case 'cancelled': return `Cancelled: ${status.reason}`; + } +} +``` + +### Input/Output Separation + +```typescript +// Input: what the caller provides +interface CreateTaskInput { + title: string; + description?: string; +} + +// Output: what the system returns (includes server-generated fields) +interface Task { + id: string; + title: string; + description: string | null; + createdAt: Date; + updatedAt: Date; + createdBy: string; +} +``` + +### Use Branded Types for IDs + +```typescript +type TaskId = string & { readonly __brand: 'TaskId' }; +type UserId = string & { readonly __brand: 'UserId' }; + +// Prevents accidentally passing a UserId where a TaskId is expected +function getTask(id: TaskId): Promise { ... } +``` + +## Common Rationalizations + +| Rationalization | Reality | +|---|---| +| "We'll document the API later" | The types ARE the documentation. Define them first. | +| "We don't need pagination for now" | You will the moment someone has 100+ items. Add it from the start. | +| "PATCH is complicated, let's just use PUT" | PUT requires the full object every time. PATCH is what clients actually want. | +| "We'll version the API when we need to" | Breaking changes without versioning break consumers. Design for extension from the start. | +| "Nobody uses that undocumented behavior" | Hyrum's Law: if it's observable, somebody depends on it. Treat every public behavior as a commitment. | +| "We can just maintain two versions" | Multiple versions multiply maintenance cost and create diamond dependency problems. Prefer the One-Version Rule. | +| "Internal APIs don't need contracts" | Internal consumers are still consumers. Contracts prevent coupling and enable parallel work. | +| "Accepting the Idempotency-Key header is enough" | The header is the contract; storing the key against the result is the implementation. A key you accept but don't honour tells the client retrying is safe when it isn't. | +| "Our queue guarantees exactly-once delivery" | No queue does across a consumer crash — the broker's ack and your side effect are not in one transaction. Design for at-least-once with idempotent processing. | +| "Duplicate requests are rare" | They're *correlated*. Retries spike exactly when a dependency is degraded — the moment duplicates are most likely and most expensive. | + +## Red Flags + +- Endpoints that return different shapes depending on conditions +- Inconsistent error formats across endpoints +- Validation scattered throughout internal code instead of at boundaries +- Breaking changes to existing fields (type changes, removals) +- List endpoints without pagination +- Verbs in REST URLs (`/api/createTask`, `/api/getUsers`) +- Third-party API responses used without validation or sanitization +- A `SELECT` for an idempotency key followed by an `INSERT` — that's a race, not a guard +- An idempotency key derived from a UUID, timestamp, or anything else regenerated per attempt +- The same key accepted with a different request body, silently returning the first response +- A key retention window shorter than the longest path that can re-deliver the request + +## Verification + +After designing an API: + +- [ ] Every endpoint has typed input and output schemas +- [ ] Error responses follow a single consistent format +- [ ] Validation happens at system boundaries only +- [ ] List endpoints support pagination +- [ ] New fields are additive and optional (backward compatible) +- [ ] Naming follows consistent conventions across all endpoints +- [ ] API documentation or types are committed alongside the implementation +- [ ] State-changing endpoints either honour an idempotency key or are documented as unsafe to retry +- [ ] The key is claimed in one atomic operation, guarded by a unique constraint +- [ ] A reused key with a different payload fails loudly rather than replaying the wrong response +- [ ] The in-flight-duplicate response is a deliberate choice (409, wait, or 202) rather than whatever falls out +- [ ] Key retention outlives the longest retry path, including dead-letter replay diff --git a/.agents/skills/api-design-principles/SKILL.md b/.agents/skills/api-design-principles/SKILL.md new file mode 100644 index 0000000..e914fb4 --- /dev/null +++ b/.agents/skills/api-design-principles/SKILL.md @@ -0,0 +1,110 @@ +--- +name: api-design-principles +description: Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards. +--- + +# API Design Principles + +Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time. + +## When to Use This Skill + +- Designing new REST or GraphQL APIs +- Refactoring existing APIs for better usability +- Establishing API design standards for your team +- Reviewing API specifications before implementation +- Migrating between API paradigms (REST to GraphQL, etc.) +- Creating developer-friendly API documentation +- Optimizing APIs for specific use cases (mobile, third-party integrations) + +## Core Concepts + +### 1. RESTful Design Principles + +**Resource-Oriented Architecture** + +- Resources are nouns (users, orders, products), not verbs +- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE) +- URLs represent resource hierarchies +- Consistent naming conventions + +**HTTP Methods Semantics:** + +- `GET`: Retrieve resources (idempotent, safe) +- `POST`: Create new resources +- `PUT`: Replace entire resource (idempotent) +- `PATCH`: Partial resource updates +- `DELETE`: Remove resources (idempotent) + +### 2. GraphQL Design Principles + +**Schema-First Development** + +- Types define your domain model +- Queries for reading data +- Mutations for modifying data +- Subscriptions for real-time updates + +**Query Structure:** + +- Clients request exactly what they need +- Single endpoint, multiple operations +- Strongly typed schema +- Introspection built-in + +### 3. API Versioning Strategies + +**URL Versioning:** + +``` +/api/v1/users +/api/v2/users +``` + +**Header Versioning:** + +``` +Accept: application/vnd.api+json; version=1 +``` + +**Query Parameter Versioning:** + +``` +/api/users?version=1 +``` + +## Detailed patterns and worked examples + +Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient. + +## Best Practices + +### REST APIs + +1. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`) +2. **Stateless**: Each request contains all necessary information +3. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors +4. **Version Your API**: Plan for breaking changes from day one +5. **Pagination**: Always paginate large collections +6. **Rate Limiting**: Protect your API with rate limits +7. **Documentation**: Use OpenAPI/Swagger for interactive docs + +### GraphQL APIs + +1. **Schema First**: Design schema before writing resolvers +2. **Avoid N+1**: Use DataLoaders for efficient data fetching +3. **Input Validation**: Validate at schema and resolver levels +4. **Error Handling**: Return structured errors in mutation payloads +5. **Pagination**: Use cursor-based pagination (Relay spec) +6. **Deprecation**: Use `@deprecated` directive for gradual migration +7. **Monitoring**: Track query complexity and execution time + +## Common Pitfalls + +- **Over-fetching/Under-fetching (REST)**: Fixed in GraphQL but requires DataLoaders +- **Breaking Changes**: Version APIs or use deprecation strategies +- **Inconsistent Error Formats**: Standardize error responses +- **Missing Rate Limits**: APIs without limits are vulnerable to abuse +- **Poor Documentation**: Undocumented APIs frustrate developers +- **Ignoring HTTP Semantics**: POST for idempotent operations breaks expectations +- **Tight Coupling**: API structure shouldn't mirror database schema diff --git a/.agents/skills/api-design-principles/assets/api-design-checklist.md b/.agents/skills/api-design-principles/assets/api-design-checklist.md new file mode 100644 index 0000000..b78148b --- /dev/null +++ b/.agents/skills/api-design-principles/assets/api-design-checklist.md @@ -0,0 +1,155 @@ +# API Design Checklist + +## Pre-Implementation Review + +### Resource Design + +- [ ] Resources are nouns, not verbs +- [ ] Plural names for collections +- [ ] Consistent naming across all endpoints +- [ ] Clear resource hierarchy (avoid deep nesting >2 levels) +- [ ] All CRUD operations properly mapped to HTTP methods + +### HTTP Methods + +- [ ] GET for retrieval (safe, idempotent) +- [ ] POST for creation +- [ ] PUT for full replacement (idempotent) +- [ ] PATCH for partial updates +- [ ] DELETE for removal (idempotent) + +### Status Codes + +- [ ] 200 OK for successful GET/PATCH/PUT +- [ ] 201 Created for POST +- [ ] 204 No Content for DELETE +- [ ] 400 Bad Request for malformed requests +- [ ] 401 Unauthorized for missing auth +- [ ] 403 Forbidden for insufficient permissions +- [ ] 404 Not Found for missing resources +- [ ] 422 Unprocessable Entity for validation errors +- [ ] 429 Too Many Requests for rate limiting +- [ ] 500 Internal Server Error for server issues + +### Pagination + +- [ ] All collection endpoints paginated +- [ ] Default page size defined (e.g., 20) +- [ ] Maximum page size enforced (e.g., 100) +- [ ] Pagination metadata included (total, pages, etc.) +- [ ] Cursor-based or offset-based pattern chosen + +### Filtering & Sorting + +- [ ] Query parameters for filtering +- [ ] Sort parameter supported +- [ ] Search parameter for full-text search +- [ ] Field selection supported (sparse fieldsets) + +### Versioning + +- [ ] Versioning strategy defined (URL/header/query) +- [ ] Version included in all endpoints +- [ ] Deprecation policy documented + +### Error Handling + +- [ ] Consistent error response format +- [ ] Detailed error messages +- [ ] Field-level validation errors +- [ ] Error codes for client handling +- [ ] Timestamps in error responses + +### Authentication & Authorization + +- [ ] Authentication method defined (Bearer token, API key) +- [ ] Authorization checks on all endpoints +- [ ] 401 vs 403 used correctly +- [ ] Token expiration handled + +### Rate Limiting + +- [ ] Rate limits defined per endpoint/user +- [ ] Rate limit headers included +- [ ] 429 status code for exceeded limits +- [ ] Retry-After header provided + +### Documentation + +- [ ] OpenAPI/Swagger spec generated +- [ ] All endpoints documented +- [ ] Request/response examples provided +- [ ] Error responses documented +- [ ] Authentication flow documented + +### Testing + +- [ ] Unit tests for business logic +- [ ] Integration tests for endpoints +- [ ] Error scenarios tested +- [ ] Edge cases covered +- [ ] Performance tests for heavy endpoints + +### Security + +- [ ] Input validation on all fields +- [ ] SQL injection prevention +- [ ] XSS prevention +- [ ] CORS configured correctly +- [ ] HTTPS enforced +- [ ] Sensitive data not in URLs +- [ ] No secrets in responses + +### Performance + +- [ ] Database queries optimized +- [ ] N+1 queries prevented +- [ ] Caching strategy defined +- [ ] Cache headers set appropriately +- [ ] Large responses paginated + +### Monitoring + +- [ ] Logging implemented +- [ ] Error tracking configured +- [ ] Performance metrics collected +- [ ] Health check endpoint available +- [ ] Alerts configured for errors + +## GraphQL-Specific Checks + +### Schema Design + +- [ ] Schema-first approach used +- [ ] Types properly defined +- [ ] Non-null vs nullable decided +- [ ] Interfaces/unions used appropriately +- [ ] Custom scalars defined + +### Queries + +- [ ] Query depth limiting +- [ ] Query complexity analysis +- [ ] DataLoaders prevent N+1 +- [ ] Pagination pattern chosen (Relay/offset) + +### Mutations + +- [ ] Input types defined +- [ ] Payload types with errors +- [ ] Optimistic response support +- [ ] Idempotency considered + +### Performance + +- [ ] DataLoader for all relationships +- [ ] Query batching enabled +- [ ] Persisted queries considered +- [ ] Response caching implemented + +### Documentation + +- [ ] All fields documented +- [ ] Deprecations marked +- [ ] Examples provided +- [ ] Schema introspection enabled diff --git a/.agents/skills/api-design-principles/assets/rest-api-template.py b/.agents/skills/api-design-principles/assets/rest-api-template.py new file mode 100644 index 0000000..2a78401 --- /dev/null +++ b/.agents/skills/api-design-principles/assets/rest-api-template.py @@ -0,0 +1,182 @@ +""" +Production-ready REST API template using FastAPI. +Includes pagination, filtering, error handling, and best practices. +""" + +from fastapi import FastAPI, HTTPException, Query, Path, Depends, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field, EmailStr, ConfigDict +from typing import Optional, List, Any +from datetime import datetime +from enum import Enum + +app = FastAPI( + title="API Template", + version="1.0.0", + docs_url="/api/docs" +) + +# Security Middleware +# Trusted Host: Prevents HTTP Host Header attacks +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["*"] # TODO: Configure this in production, e.g. ["api.example.com"] +) + +# CORS: Configures Cross-Origin Resource Sharing +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # TODO: Update this with specific origins in production + allow_credentials=False, # TODO: Set to True if you need cookies/auth headers, but restrict origins + allow_methods=["*"], + allow_headers=["*"], +) + +# Models +class UserStatus(str, Enum): + ACTIVE = "active" + INACTIVE = "inactive" + SUSPENDED = "suspended" + +class UserBase(BaseModel): + email: EmailStr + name: str = Field(..., min_length=1, max_length=100) + status: UserStatus = UserStatus.ACTIVE + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + name: Optional[str] = Field(None, min_length=1, max_length=100) + status: Optional[UserStatus] = None + +class User(UserBase): + id: str + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + +# Pagination +class PaginationParams(BaseModel): + page: int = Field(1, ge=1) + page_size: int = Field(20, ge=1, le=100) + +class PaginatedResponse(BaseModel): + items: List[Any] + total: int + page: int + page_size: int + pages: int + +# Error handling +class ErrorDetail(BaseModel): + field: Optional[str] = None + message: str + code: str + +class ErrorResponse(BaseModel): + error: str + message: str + details: Optional[List[ErrorDetail]] = None + +@app.exception_handler(HTTPException) +async def http_exception_handler(request, exc): + return JSONResponse( + status_code=exc.status_code, + content=ErrorResponse( + error=exc.__class__.__name__, + message=exc.detail if isinstance(exc.detail, str) else exc.detail.get("message", "Error"), + details=exc.detail.get("details") if isinstance(exc.detail, dict) else None + ).model_dump() + ) + +# Endpoints +@app.get("/api/users", response_model=PaginatedResponse, tags=["Users"]) +async def list_users( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + status: Optional[UserStatus] = Query(None), + search: Optional[str] = Query(None) +): + """List users with pagination and filtering.""" + # Mock implementation + total = 100 + items = [ + User( + id=str(i), + email=f"user{i}@example.com", + name=f"User {i}", + status=UserStatus.ACTIVE, + created_at=datetime.now(), + updated_at=datetime.now() + ).model_dump() + for i in range((page-1)*page_size, min(page*page_size, total)) + ] + + return PaginatedResponse( + items=items, + total=total, + page=page, + page_size=page_size, + pages=(total + page_size - 1) // page_size + ) + +@app.post("/api/users", response_model=User, status_code=status.HTTP_201_CREATED, tags=["Users"]) +async def create_user(user: UserCreate): + """Create a new user.""" + # Mock implementation + return User( + id="123", + email=user.email, + name=user.name, + status=user.status, + created_at=datetime.now(), + updated_at=datetime.now() + ) + +@app.get("/api/users/{user_id}", response_model=User, tags=["Users"]) +async def get_user(user_id: str = Path(..., description="User ID")): + """Get user by ID.""" + # Mock: Check if exists + if user_id == "999": + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={"message": "User not found", "details": {"id": user_id}} + ) + + return User( + id=user_id, + email="user@example.com", + name="User Name", + status=UserStatus.ACTIVE, + created_at=datetime.now(), + updated_at=datetime.now() + ) + +@app.patch("/api/users/{user_id}", response_model=User, tags=["Users"]) +async def update_user(user_id: str, update: UserUpdate): + """Partially update user.""" + # Validate user exists + existing = await get_user(user_id) + + # Apply updates + update_data = update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(existing, field, value) + + existing.updated_at = datetime.now() + return existing + +@app.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Users"]) +async def delete_user(user_id: str): + """Delete user.""" + await get_user(user_id) # Verify exists + return None + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/.agents/skills/api-design-principles/references/details.md b/.agents/skills/api-design-principles/references/details.md new file mode 100644 index 0000000..ba9a034 --- /dev/null +++ b/.agents/skills/api-design-principles/references/details.md @@ -0,0 +1,413 @@ +# api-design-principles — detailed patterns and worked examples + +## REST API Design Patterns + +### Pattern 1: Resource Collection Design + +```python +# Good: Resource-oriented endpoints +GET /api/users # List users (with pagination) +POST /api/users # Create user +GET /api/users/{id} # Get specific user +PUT /api/users/{id} # Replace user +PATCH /api/users/{id} # Update user fields +DELETE /api/users/{id} # Delete user + +# Nested resources +GET /api/users/{id}/orders # Get user's orders +POST /api/users/{id}/orders # Create order for user + +# Bad: Action-oriented endpoints (avoid) +POST /api/createUser +POST /api/getUserById +POST /api/deleteUser +``` + +### Pattern 2: Pagination and Filtering + +```python +from typing import List, Optional +from pydantic import BaseModel, Field + +class PaginationParams(BaseModel): + page: int = Field(1, ge=1, description="Page number") + page_size: int = Field(20, ge=1, le=100, description="Items per page") + +class FilterParams(BaseModel): + status: Optional[str] = None + created_after: Optional[str] = None + search: Optional[str] = None + +class PaginatedResponse(BaseModel): + items: List[dict] + total: int + page: int + page_size: int + pages: int + + @property + def has_next(self) -> bool: + return self.page < self.pages + + @property + def has_prev(self) -> bool: + return self.page > 1 + +# FastAPI endpoint example +from fastapi import FastAPI, Query, Depends + +app = FastAPI() + +@app.get("/api/users", response_model=PaginatedResponse) +async def list_users( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + status: Optional[str] = Query(None), + search: Optional[str] = Query(None) +): + # Apply filters + query = build_query(status=status, search=search) + + # Count total + total = await count_users(query) + + # Fetch page + offset = (page - 1) * page_size + users = await fetch_users(query, limit=page_size, offset=offset) + + return PaginatedResponse( + items=users, + total=total, + page=page, + page_size=page_size, + pages=(total + page_size - 1) // page_size + ) +``` + +### Pattern 3: Error Handling and Status Codes + +```python +from fastapi import HTTPException, status +from pydantic import BaseModel + +class ErrorResponse(BaseModel): + error: str + message: str + details: Optional[dict] = None + timestamp: str + path: str + +class ValidationErrorDetail(BaseModel): + field: str + message: str + value: Any + +# Consistent error responses +STATUS_CODES = { + "success": 200, + "created": 201, + "no_content": 204, + "bad_request": 400, + "unauthorized": 401, + "forbidden": 403, + "not_found": 404, + "conflict": 409, + "unprocessable": 422, + "internal_error": 500 +} + +def raise_not_found(resource: str, id: str): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": "NotFound", + "message": f"{resource} not found", + "details": {"id": id} + } + ) + +def raise_validation_error(errors: List[ValidationErrorDetail]): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail={ + "error": "ValidationError", + "message": "Request validation failed", + "details": {"errors": [e.dict() for e in errors]} + } + ) + +# Example usage +@app.get("/api/users/{user_id}") +async def get_user(user_id: str): + user = await fetch_user(user_id) + if not user: + raise_not_found("User", user_id) + return user +``` + +### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State) + +```python +class UserResponse(BaseModel): + id: str + name: str + email: str + _links: dict + + @classmethod + def from_user(cls, user: User, base_url: str): + return cls( + id=user.id, + name=user.name, + email=user.email, + _links={ + "self": {"href": f"{base_url}/api/users/{user.id}"}, + "orders": {"href": f"{base_url}/api/users/{user.id}/orders"}, + "update": { + "href": f"{base_url}/api/users/{user.id}", + "method": "PATCH" + }, + "delete": { + "href": f"{base_url}/api/users/{user.id}", + "method": "DELETE" + } + } + ) +``` + +## GraphQL Design Patterns + +### Pattern 1: Schema Design + +```graphql +# schema.graphql + +# Clear type definitions +type User { + id: ID! + email: String! + name: String! + createdAt: DateTime! + + # Relationships + orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection! + + profile: UserProfile +} + +type Order { + id: ID! + status: OrderStatus! + total: Money! + items: [OrderItem!]! + createdAt: DateTime! + + # Back-reference + user: User! +} + +# Pagination pattern (Relay-style) +type OrderConnection { + edges: [OrderEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type OrderEdge { + node: Order! + cursor: String! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +# Enums for type safety +enum OrderStatus { + PENDING + CONFIRMED + SHIPPED + DELIVERED + CANCELLED +} + +# Custom scalars +scalar DateTime +scalar Money + +# Query root +type Query { + user(id: ID!): User + users(first: Int = 20, after: String, search: String): UserConnection! + + order(id: ID!): Order +} + +# Mutation root +type Mutation { + createUser(input: CreateUserInput!): CreateUserPayload! + updateUser(input: UpdateUserInput!): UpdateUserPayload! + deleteUser(id: ID!): DeleteUserPayload! + + createOrder(input: CreateOrderInput!): CreateOrderPayload! +} + +# Input types for mutations +input CreateUserInput { + email: String! + name: String! + password: String! +} + +# Payload types for mutations +type CreateUserPayload { + user: User + errors: [Error!] +} + +type Error { + field: String + message: String! +} +``` + +### Pattern 2: Resolver Design + +```python +from typing import Optional, List +from ariadne import QueryType, MutationType, ObjectType +from dataclasses import dataclass + +query = QueryType() +mutation = MutationType() +user_type = ObjectType("User") + +@query.field("user") +async def resolve_user(obj, info, id: str) -> Optional[dict]: + """Resolve single user by ID.""" + return await fetch_user_by_id(id) + +@query.field("users") +async def resolve_users( + obj, + info, + first: int = 20, + after: Optional[str] = None, + search: Optional[str] = None +) -> dict: + """Resolve paginated user list.""" + # Decode cursor + offset = decode_cursor(after) if after else 0 + + # Fetch users + users = await fetch_users( + limit=first + 1, # Fetch one extra to check hasNextPage + offset=offset, + search=search + ) + + # Pagination + has_next = len(users) > first + if has_next: + users = users[:first] + + edges = [ + { + "node": user, + "cursor": encode_cursor(offset + i) + } + for i, user in enumerate(users) + ] + + return { + "edges": edges, + "pageInfo": { + "hasNextPage": has_next, + "hasPreviousPage": offset > 0, + "startCursor": edges[0]["cursor"] if edges else None, + "endCursor": edges[-1]["cursor"] if edges else None + }, + "totalCount": await count_users(search=search) + } + +@user_type.field("orders") +async def resolve_user_orders(user: dict, info, first: int = 20) -> dict: + """Resolve user's orders (N+1 prevention with DataLoader).""" + # Use DataLoader to batch requests + loader = info.context["loaders"]["orders_by_user"] + orders = await loader.load(user["id"]) + + return paginate_orders(orders, first) + +@mutation.field("createUser") +async def resolve_create_user(obj, info, input: dict) -> dict: + """Create new user.""" + try: + # Validate input + validate_user_input(input) + + # Create user + user = await create_user( + email=input["email"], + name=input["name"], + password=hash_password(input["password"]) + ) + + return { + "user": user, + "errors": [] + } + except ValidationError as e: + return { + "user": None, + "errors": [{"field": e.field, "message": e.message}] + } +``` + +### Pattern 3: DataLoader (N+1 Problem Prevention) + +```python +from aiodataloader import DataLoader +from typing import List, Optional + +class UserLoader(DataLoader): + """Batch load users by ID.""" + + async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]: + """Load multiple users in single query.""" + users = await fetch_users_by_ids(user_ids) + + # Map results back to input order + user_map = {user["id"]: user for user in users} + return [user_map.get(user_id) for user_id in user_ids] + +class OrdersByUserLoader(DataLoader): + """Batch load orders by user ID.""" + + async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]: + """Load orders for multiple users in single query.""" + orders = await fetch_orders_by_user_ids(user_ids) + + # Group orders by user_id + orders_by_user = {} + for order in orders: + user_id = order["user_id"] + if user_id not in orders_by_user: + orders_by_user[user_id] = [] + orders_by_user[user_id].append(order) + + # Return in input order + return [orders_by_user.get(user_id, []) for user_id in user_ids] + +# Context setup +def create_context(): + return { + "loaders": { + "user": UserLoader(), + "orders_by_user": OrdersByUserLoader() + } + } +``` diff --git a/.agents/skills/api-design-principles/references/graphql-schema-design.md b/.agents/skills/api-design-principles/references/graphql-schema-design.md new file mode 100644 index 0000000..beca5f4 --- /dev/null +++ b/.agents/skills/api-design-principles/references/graphql-schema-design.md @@ -0,0 +1,583 @@ +# GraphQL Schema Design Patterns + +## Schema Organization + +### Modular Schema Structure + +```graphql +# user.graphql +type User { + id: ID! + email: String! + name: String! + posts: [Post!]! +} + +extend type Query { + user(id: ID!): User + users(first: Int, after: String): UserConnection! +} + +extend type Mutation { + createUser(input: CreateUserInput!): CreateUserPayload! +} + +# post.graphql +type Post { + id: ID! + title: String! + content: String! + author: User! +} + +extend type Query { + post(id: ID!): Post +} +``` + +## Type Design Patterns + +### 1. Non-Null Types + +```graphql +type User { + id: ID! # Always required + email: String! # Required + phone: String # Optional (nullable) + posts: [Post!]! # Non-null array of non-null posts + tags: [String!] # Nullable array of non-null strings +} +``` + +### 2. Interfaces for Polymorphism + +```graphql +interface Node { + id: ID! + createdAt: DateTime! +} + +type User implements Node { + id: ID! + createdAt: DateTime! + email: String! +} + +type Post implements Node { + id: ID! + createdAt: DateTime! + title: String! +} + +type Query { + node(id: ID!): Node +} +``` + +### 3. Unions for Heterogeneous Results + +```graphql +union SearchResult = User | Post | Comment + +type Query { + search(query: String!): [SearchResult!]! +} + +# Query example +{ + search(query: "graphql") { + ... on User { + name + email + } + ... on Post { + title + content + } + ... on Comment { + text + author { + name + } + } + } +} +``` + +### 4. Input Types + +```graphql +input CreateUserInput { + email: String! + name: String! + password: String! + profileInput: ProfileInput +} + +input ProfileInput { + bio: String + avatar: String + website: String +} + +input UpdateUserInput { + id: ID! + email: String + name: String + profileInput: ProfileInput +} +``` + +## Pagination Patterns + +### Relay Cursor Pagination (Recommended) + +```graphql +type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type UserEdge { + node: User! + cursor: String! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +type Query { + users(first: Int, after: String, last: Int, before: String): UserConnection! +} + +# Usage +{ + users(first: 10, after: "cursor123") { + edges { + cursor + node { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Offset Pagination (Simpler) + +```graphql +type UserList { + items: [User!]! + total: Int! + page: Int! + pageSize: Int! +} + +type Query { + users(page: Int = 1, pageSize: Int = 20): UserList! +} +``` + +## Mutation Design Patterns + +### 1. Input/Payload Pattern + +```graphql +input CreatePostInput { + title: String! + content: String! + tags: [String!] +} + +type CreatePostPayload { + post: Post + errors: [Error!] + success: Boolean! +} + +type Error { + field: String + message: String! + code: String! +} + +type Mutation { + createPost(input: CreatePostInput!): CreatePostPayload! +} +``` + +### 2. Optimistic Response Support + +```graphql +type UpdateUserPayload { + user: User + clientMutationId: String + errors: [Error!] +} + +input UpdateUserInput { + id: ID! + name: String + clientMutationId: String +} + +type Mutation { + updateUser(input: UpdateUserInput!): UpdateUserPayload! +} +``` + +### 3. Batch Mutations + +```graphql +input BatchCreateUserInput { + users: [CreateUserInput!]! +} + +type BatchCreateUserPayload { + results: [CreateUserResult!]! + successCount: Int! + errorCount: Int! +} + +type CreateUserResult { + user: User + errors: [Error!] + index: Int! +} + +type Mutation { + batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload! +} +``` + +## Field Design + +### Arguments and Filtering + +```graphql +type Query { + posts( + # Pagination + first: Int = 20 + after: String + + # Filtering + status: PostStatus + authorId: ID + tag: String + + # Sorting + orderBy: PostOrderBy = CREATED_AT + orderDirection: OrderDirection = DESC + + # Searching + search: String + ): PostConnection! +} + +enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +enum PostOrderBy { + CREATED_AT + UPDATED_AT + TITLE +} + +enum OrderDirection { + ASC + DESC +} +``` + +### Computed Fields + +```graphql +type User { + firstName: String! + lastName: String! + fullName: String! # Computed in resolver + posts: [Post!]! + postCount: Int! # Computed, doesn't load all posts +} + +type Post { + likeCount: Int! + commentCount: Int! + isLikedByViewer: Boolean! # Context-dependent +} +``` + +## Subscriptions + +```graphql +type Subscription { + postAdded: Post! + + postUpdated(postId: ID!): Post! + + userStatusChanged(userId: ID!): UserStatus! +} + +type UserStatus { + userId: ID! + online: Boolean! + lastSeen: DateTime! +} + +# Client usage +subscription { + postAdded { + id + title + author { + name + } + } +} +``` + +## Custom Scalars + +```graphql +scalar DateTime +scalar Email +scalar URL +scalar JSON +scalar Money + +type User { + email: Email! + website: URL + createdAt: DateTime! + metadata: JSON +} + +type Product { + price: Money! +} +``` + +## Directives + +### Built-in Directives + +```graphql +type User { + name: String! + email: String! @deprecated(reason: "Use emails field instead") + emails: [String!]! + + # Conditional inclusion + privateData: PrivateData @include(if: $isOwner) +} + +# Query +query GetUser($isOwner: Boolean!) { + user(id: "123") { + name + privateData @include(if: $isOwner) { + ssn + } + } +} +``` + +### Custom Directives + +```graphql +directive @auth(requires: Role = USER) on FIELD_DEFINITION + +enum Role { + USER + ADMIN + MODERATOR +} + +type Mutation { + deleteUser(id: ID!): Boolean! @auth(requires: ADMIN) + updateProfile(input: ProfileInput!): User! @auth +} +``` + +## Error Handling + +### Union Error Pattern + +```graphql +type User { + id: ID! + email: String! +} + +type ValidationError { + field: String! + message: String! +} + +type NotFoundError { + message: String! + resourceType: String! + resourceId: ID! +} + +type AuthorizationError { + message: String! +} + +union UserResult = User | ValidationError | NotFoundError | AuthorizationError + +type Query { + user(id: ID!): UserResult! +} + +# Usage +{ + user(id: "123") { + ... on User { + id + email + } + ... on NotFoundError { + message + resourceType + } + ... on AuthorizationError { + message + } + } +} +``` + +### Errors in Payload + +```graphql +type CreateUserPayload { + user: User + errors: [Error!] + success: Boolean! +} + +type Error { + field: String + message: String! + code: ErrorCode! +} + +enum ErrorCode { + VALIDATION_ERROR + UNAUTHORIZED + NOT_FOUND + INTERNAL_ERROR +} +``` + +## N+1 Query Problem Solutions + +### DataLoader Pattern + +```python +from aiodataloader import DataLoader + +class PostLoader(DataLoader): + async def batch_load_fn(self, post_ids): + posts = await db.posts.find({"id": {"$in": post_ids}}) + post_map = {post["id"]: post for post in posts} + return [post_map.get(pid) for pid in post_ids] + +# Resolver +@user_type.field("posts") +async def resolve_posts(user, info): + loader = info.context["loaders"]["post"] + return await loader.load_many(user["post_ids"]) +``` + +### Query Depth Limiting + +```python +from graphql import GraphQLError + +def depth_limit_validator(max_depth: int): + def validate(context, node, ancestors): + depth = len(ancestors) + if depth > max_depth: + raise GraphQLError( + f"Query depth {depth} exceeds maximum {max_depth}" + ) + return validate +``` + +### Query Complexity Analysis + +```python +def complexity_limit_validator(max_complexity: int): + def calculate_complexity(node): + # Each field = 1, lists multiply + complexity = 1 + if is_list_field(node): + complexity *= get_list_size_arg(node) + return complexity + + return validate_complexity +``` + +## Schema Versioning + +### Field Deprecation + +```graphql +type User { + name: String! @deprecated(reason: "Use firstName and lastName") + firstName: String! + lastName: String! +} +``` + +### Schema Evolution + +```graphql +# v1 - Initial +type User { + name: String! +} + +# v2 - Add optional field (backward compatible) +type User { + name: String! + email: String +} + +# v3 - Deprecate and add new field +type User { + name: String! @deprecated(reason: "Use firstName/lastName") + firstName: String! + lastName: String! + email: String +} +``` + +## Best Practices Summary + +1. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed +2. **Input Types**: Always use input types for mutations +3. **Payload Pattern**: Return errors in mutation payloads +4. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases +5. **Naming**: Use camelCase for fields, PascalCase for types +6. **Deprecation**: Use `@deprecated` instead of removing fields +7. **DataLoaders**: Always use for relationships to prevent N+1 +8. **Complexity Limits**: Protect against expensive queries +9. **Custom Scalars**: Use for domain-specific types (Email, DateTime) +10. **Documentation**: Document all fields with descriptions diff --git a/.agents/skills/api-design-principles/references/rest-best-practices.md b/.agents/skills/api-design-principles/references/rest-best-practices.md new file mode 100644 index 0000000..676be29 --- /dev/null +++ b/.agents/skills/api-design-principles/references/rest-best-practices.md @@ -0,0 +1,408 @@ +# REST API Best Practices + +## URL Structure + +### Resource Naming + +``` +# Good - Plural nouns +GET /api/users +GET /api/orders +GET /api/products + +# Bad - Verbs or mixed conventions +GET /api/getUser +GET /api/user (inconsistent singular) +POST /api/createOrder +``` + +### Nested Resources + +``` +# Shallow nesting (preferred) +GET /api/users/{id}/orders +GET /api/orders/{id} + +# Deep nesting (avoid) +GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews +# Better: +GET /api/order-items/{id}/reviews +``` + +## HTTP Methods and Status Codes + +### GET - Retrieve Resources + +``` +GET /api/users → 200 OK (with list) +GET /api/users/{id} → 200 OK or 404 Not Found +GET /api/users?page=2 → 200 OK (paginated) +``` + +### POST - Create Resources + +``` +POST /api/users + Body: {"name": "John", "email": "john@example.com"} + → 201 Created + Location: /api/users/123 + Body: {"id": "123", "name": "John", ...} + +POST /api/users (validation error) + → 422 Unprocessable Entity + Body: {"errors": [...]} +``` + +### PUT - Replace Resources + +``` +PUT /api/users/{id} + Body: {complete user object} + → 200 OK (updated) + → 404 Not Found (doesn't exist) + +# Must include ALL fields +``` + +### PATCH - Partial Update + +``` +PATCH /api/users/{id} + Body: {"name": "Jane"} (only changed fields) + → 200 OK + → 404 Not Found +``` + +### DELETE - Remove Resources + +``` +DELETE /api/users/{id} + → 204 No Content (deleted) + → 404 Not Found + → 409 Conflict (can't delete due to references) +``` + +## Filtering, Sorting, and Searching + +### Query Parameters + +``` +# Filtering +GET /api/users?status=active +GET /api/users?role=admin&status=active + +# Sorting +GET /api/users?sort=created_at +GET /api/users?sort=-created_at (descending) +GET /api/users?sort=name,created_at + +# Searching +GET /api/users?search=john +GET /api/users?q=john + +# Field selection (sparse fieldsets) +GET /api/users?fields=id,name,email +``` + +## Pagination Patterns + +### Offset-Based Pagination + +```python +GET /api/users?page=2&page_size=20 + +Response: +{ + "items": [...], + "page": 2, + "page_size": 20, + "total": 150, + "pages": 8 +} +``` + +### Cursor-Based Pagination (for large datasets) + +```python +GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ + +Response: +{ + "items": [...], + "next_cursor": "eyJpZCI6MTQzfQ", + "has_more": true +} +``` + +### Link Header Pagination (RESTful) + +``` +GET /api/users?page=2 + +Response Headers: +Link: ; rel="next", + ; rel="prev", + ; rel="first", + ; rel="last" +``` + +## Versioning Strategies + +### URL Versioning (Recommended) + +``` +/api/v1/users +/api/v2/users + +Pros: Clear, easy to route +Cons: Multiple URLs for same resource +``` + +### Header Versioning + +``` +GET /api/users +Accept: application/vnd.api+json; version=2 + +Pros: Clean URLs +Cons: Less visible, harder to test +``` + +### Query Parameter + +``` +GET /api/users?version=2 + +Pros: Easy to test +Cons: Optional parameter can be forgotten +``` + +## Rate Limiting + +### Headers + +``` +X-RateLimit-Limit: 1000 +X-RateLimit-Remaining: 742 +X-RateLimit-Reset: 1640000000 + +Response when limited: +429 Too Many Requests +Retry-After: 3600 +``` + +### Implementation Pattern + +```python +from fastapi import HTTPException, Request +from datetime import datetime, timedelta + +class RateLimiter: + def __init__(self, calls: int, period: int): + self.calls = calls + self.period = period + self.cache = {} + + def check(self, key: str) -> bool: + now = datetime.now() + if key not in self.cache: + self.cache[key] = [] + + # Remove old requests + self.cache[key] = [ + ts for ts in self.cache[key] + if now - ts < timedelta(seconds=self.period) + ] + + if len(self.cache[key]) >= self.calls: + return False + + self.cache[key].append(now) + return True + +limiter = RateLimiter(calls=100, period=60) + +@app.get("/api/users") +async def get_users(request: Request): + if not limiter.check(request.client.host): + raise HTTPException( + status_code=429, + headers={"Retry-After": "60"} + ) + return {"users": [...]} +``` + +## Authentication and Authorization + +### Bearer Token + +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... + +401 Unauthorized - Missing/invalid token +403 Forbidden - Valid token, insufficient permissions +``` + +### API Keys + +``` +X-API-Key: your-api-key-here +``` + +## Error Response Format + +### Consistent Structure + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Request validation failed", + "details": [ + { + "field": "email", + "message": "Invalid email format", + "value": "not-an-email" + } + ], + "timestamp": "2025-10-16T12:00:00Z", + "path": "/api/users" + } +} +``` + +### Status Code Guidelines + +- `200 OK`: Successful GET, PATCH, PUT +- `201 Created`: Successful POST +- `204 No Content`: Successful DELETE +- `400 Bad Request`: Malformed request +- `401 Unauthorized`: Authentication required +- `403 Forbidden`: Authenticated but not authorized +- `404 Not Found`: Resource doesn't exist +- `409 Conflict`: State conflict (duplicate email, etc.) +- `422 Unprocessable Entity`: Validation errors +- `429 Too Many Requests`: Rate limited +- `500 Internal Server Error`: Server error +- `503 Service Unavailable`: Temporary downtime + +## Caching + +### Cache Headers + +``` +# Client caching +Cache-Control: public, max-age=3600 + +# No caching +Cache-Control: no-cache, no-store, must-revalidate + +# Conditional requests +ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" +If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" +→ 304 Not Modified +``` + +## Bulk Operations + +### Batch Endpoints + +```python +POST /api/users/batch +{ + "items": [ + {"name": "User1", "email": "user1@example.com"}, + {"name": "User2", "email": "user2@example.com"} + ] +} + +Response: +{ + "results": [ + {"id": "1", "status": "created"}, + {"id": null, "status": "failed", "error": "Email already exists"} + ] +} +``` + +## Idempotency + +### Idempotency Keys + +``` +POST /api/orders +Idempotency-Key: unique-key-123 + +If duplicate request: +→ 200 OK (return cached response) +``` + +## CORS Configuration + +```python +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=["https://example.com"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +``` + +## Documentation with OpenAPI + +```python +from fastapi import FastAPI + +app = FastAPI( + title="My API", + description="API for managing users", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc" +) + +@app.get( + "/api/users/{user_id}", + summary="Get user by ID", + response_description="User details", + tags=["Users"] +) +async def get_user( + user_id: str = Path(..., description="The user ID") +): + """ + Retrieve user by ID. + + Returns full user profile including: + - Basic information + - Contact details + - Account status + """ + pass +``` + +## Health and Monitoring Endpoints + +```python +@app.get("/health") +async def health_check(): + return { + "status": "healthy", + "version": "1.0.0", + "timestamp": datetime.now().isoformat() + } + +@app.get("/health/detailed") +async def detailed_health(): + return { + "status": "healthy", + "checks": { + "database": await check_database(), + "redis": await check_redis(), + "external_api": await check_external_api() + } + } +``` diff --git a/.agents/skills/better-auth-security-best-practices/SKILL.md b/.agents/skills/better-auth-security-best-practices/SKILL.md new file mode 100644 index 0000000..5abc5a8 --- /dev/null +++ b/.agents/skills/better-auth-security-best-practices/SKILL.md @@ -0,0 +1,432 @@ +--- +name: better-auth-security-best-practices +description: Configure rate limiting, manage auth secrets, set up CSRF protection, define trusted origins, secure sessions and cookies, encrypt OAuth tokens, track IP addresses, and implement audit logging for Better Auth. Use when users need to secure their auth setup, prevent brute force attacks, or harden a Better Auth deployment. +--- + +## Secret Management + +### Configuring the Secret + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env +}); +``` + +Better Auth looks for secrets in this order: +1. `options.secret` in your config +2. `BETTER_AUTH_SECRET` environment variable +3. `AUTH_SECRET` environment variable + +### Secret Requirements + +- Rejects default/placeholder secrets in production +- Warns if shorter than 32 characters or entropy below 120 bits +- Generate: `openssl rand -base64 32` +- Never commit secrets to version control + +## Rate Limiting + +Enabled in production by default. Applies to all endpoints. Plugins can override per-endpoint. + +### Default Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + rateLimit: { + enabled: true, // Default: true in production + window: 10, // Time window in seconds (default: 10) + max: 100, // Max requests per window (default: 100) + }, +}); +``` + +### Storage Options + +Options: `"memory"` (resets on restart, avoid on serverless), `"database"` (persistent), `"secondary-storage"` (Redis, default when available). + +```ts +rateLimit: { + storage: "database", +} +``` + +### Custom Storage + +Implement your own rate limit storage: + +```ts +rateLimit: { + customStorage: { + get: async (key) => { + // Return { count: number, expiresAt: number } or null + }, + set: async (key, data) => { + // Store the rate limit data + }, + }, +} +``` + +### Per-Endpoint Rules + +Sensitive endpoints default to 3 requests per 10 seconds (`/sign-in`, `/sign-up`, `/change-password`, `/change-email`). Override: + +```ts +rateLimit: { + customRules: { + "/api/auth/sign-in/email": { + window: 60, // 1 minute window + max: 5, // 5 attempts + }, + "/api/auth/some-safe-endpoint": false, // Disable rate limiting + }, +} +``` + +## CSRF Protection + +Multi-layer protection: origin header validation, Fetch Metadata checks, and first-login protection. + +### Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + disableCSRFCheck: false, // Default: false (keep enabled) + }, +}); +``` + +Only disable for testing or with an alternative CSRF mechanism. + +## Trusted Origins + +### Configuring Trusted Origins + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + baseURL: "https://api.example.com", + trustedOrigins: [ + "https://app.example.com", + "https://admin.example.com", + ], +}); +``` + +The `baseURL` origin is automatically trusted. Also configurable via env: `BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com` + +### Wildcard Patterns + +```ts +trustedOrigins: [ + "*.example.com", // Matches any subdomain + "https://*.example.com", // Protocol-specific wildcard + "exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo) +] +``` + +### Dynamic Trusted Origins + +Compute trusted origins based on the request: + +```ts +trustedOrigins: async (request) => { + // Validate against database, header, etc. + const tenant = getTenantFromRequest(request); + return [`https://${tenant}.myapp.com`]; +} +``` + +Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403. + +## Session Security + +### Session Expiration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days (default) + updateAge: 60 * 60 * 24, // Refresh session every 24 hours (default) + }, +}); +``` + +### Session Caching Strategies + +Cache session data in cookies to reduce database queries: + +```ts +session: { + cookieCache: { + enabled: true, + maxAge: 60 * 5, // 5 minutes + strategy: "compact", // Options: "compact", "jwt", "jwe" + }, +} +``` + +Strategies: `"compact"` (Base64url + HMAC, smallest), `"jwt"` (HS256, standard), `"jwe"` (encrypted, use when session has sensitive data). + +## Cookie Security + +Defaults: `secure: true` (HTTPS/production), `sameSite: "lax"`, `httpOnly: true`, `path: "/"`, prefix `__Secure-`. + +### Custom Cookie Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + useSecureCookies: true, // Force secure cookies + cookiePrefix: "myapp", // Custom prefix (default: "better-auth") + defaultCookieAttributes: { + sameSite: "strict", // Stricter CSRF protection + path: "/auth", // Limit cookie scope + }, + }, +}); +``` + +### Cross-Subdomain Cookies + +```ts +advanced: { + crossSubDomainCookies: { + enabled: true, + domain: ".example.com", // Note the leading dot + additionalCookies: ["session_token", "session_data"], + }, +} +``` + +Only enable if you need authentication sharing and trust all subdomains. + +## OAuth / Social Provider Security + +PKCE is automatic for all OAuth flows. State tokens are 32-char random strings expiring after 10 minutes. + +### State Parameter Storage + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + account: { + storeStateStrategy: "cookie", // Options: "cookie" (default), "database" + }, +}); +``` + +### Encrypting OAuth Tokens + +```ts +account: { + encryptOAuthTokens: true, // Uses AES-256-GCM +} +``` + +Enable if storing OAuth tokens for API access on behalf of users. Use `skipStateCookieCheck: true` only for mobile apps that cannot maintain cookies. + +## IP-Based Security + +### IP Address Configuration + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + ipAddress: { + ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check + disableIpTracking: false, // Keep enabled for rate limiting + }, + }, +}); +``` + +Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `trustedProxyHeaders: true` only if behind a trusted reverse proxy. + +## Database Hooks for Security Auditing + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + databaseHooks: { + session: { + create: { + after: async ({ data, ctx }) => { + await auditLog("session.created", { + userId: data.userId, + ip: ctx?.request?.headers.get("x-forwarded-for"), + userAgent: ctx?.request?.headers.get("user-agent"), + }); + }, + }, + delete: { + before: async ({ data }) => { + await auditLog("session.revoked", { sessionId: data.id }); + }, + }, + }, + user: { + update: { + after: async ({ data, oldData }) => { + if (oldData?.email !== data.email) { + await auditLog("user.email_changed", { + userId: data.id, + oldEmail: oldData?.email, + newEmail: data.email, + }); + } + }, + }, + }, + account: { + create: { + after: async ({ data }) => { + await auditLog("account.linked", { + userId: data.userId, + provider: data.providerId, + }); + }, + }, + }, + }, +}); +``` + +Return `false` from a `before` hook to prevent an operation. + +## Background Tasks + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + advanced: { + backgroundTasks: { + handler: (promise) => { + // Platform-specific handler + // Vercel: waitUntil(promise) + // Cloudflare: ctx.waitUntil(promise) + waitUntil(promise); + }, + }, + }, +}); +``` + +Ensures operations like sending emails don't affect response timing. + +## Account Enumeration Prevention + +Built-in: consistent response messages, dummy operations on invalid requests, background email sending. Return generic error messages ("Invalid credentials") rather than specific ones ("User not found"). + +## Complete Security Configuration Example + +```ts +import { betterAuth } from "better-auth"; + +export const auth = betterAuth({ + secret: process.env.BETTER_AUTH_SECRET, + baseURL: "https://api.example.com", + trustedOrigins: [ + "https://app.example.com", + "https://*.preview.example.com", + ], + + // Rate limiting + rateLimit: { + enabled: true, + storage: "secondary-storage", + customRules: { + "/api/auth/sign-in/email": { window: 60, max: 5 }, + "/api/auth/sign-up/email": { window: 60, max: 3 }, + }, + }, + + // Session security + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 24 hours + freshAge: 60 * 60, // 1 hour for sensitive actions + cookieCache: { + enabled: true, + maxAge: 300, + strategy: "jwe", // Encrypted session data + }, + }, + + // OAuth security + account: { + encryptOAuthTokens: true, + storeStateStrategy: "cookie", + }, + + + // Advanced settings + advanced: { + useSecureCookies: true, + cookiePrefix: "myapp", + defaultCookieAttributes: { + sameSite: "lax", + }, + ipAddress: { + ipAddressHeaders: ["x-forwarded-for"], + ipv6Subnet: 64, + }, + backgroundTasks: { + handler: (promise) => waitUntil(promise), + }, + }, + + // Security auditing + databaseHooks: { + session: { + create: { + after: async ({ data, ctx }) => { + console.log(`New session for user ${data.userId}`); + }, + }, + }, + user: { + update: { + after: async ({ data, oldData }) => { + if (oldData?.email !== data.email) { + console.log(`Email changed for user ${data.id}`); + } + }, + }, + }, + }, +}); +``` + +## Security Checklist + +Before deploying to production: + +- [ ] **Secret**: Use a strong, unique secret (32+ characters, high entropy) +- [ ] **HTTPS**: Ensure `baseURL` uses HTTPS +- [ ] **Trusted Origins**: Configure all valid origins (frontend, mobile apps) +- [ ] **Rate Limiting**: Keep enabled with appropriate limits +- [ ] **CSRF Protection**: Keep enabled (`disableCSRFCheck: false`) +- [ ] **Secure Cookies**: Enabled automatically with HTTPS +- [ ] **OAuth Tokens**: Consider `encryptOAuthTokens: true` if storing tokens +- [ ] **Background Tasks**: Configure for serverless platforms +- [ ] **Audit Logging**: Implement via `databaseHooks` or `hooks` +- [ ] **IP Tracking**: Configure headers if behind a proxy diff --git a/.agents/skills/bullmq-specialist/SKILL.md b/.agents/skills/bullmq-specialist/SKILL.md new file mode 100644 index 0000000..a694a16 --- /dev/null +++ b/.agents/skills/bullmq-specialist/SKILL.md @@ -0,0 +1,397 @@ +--- +name: bullmq-specialist +description: BullMQ expert for Redis-backed job queues, background processing, + and reliable async execution in Node.js/TypeScript applications. +risk: none +source: vibeship-spawner-skills (Apache 2.0) +date_added: 2026-02-27 +--- + +# BullMQ Specialist + +BullMQ expert for Redis-backed job queues, background processing, and +reliable async execution in Node.js/TypeScript applications. + +## Principles + +- Jobs are fire-and-forget from the producer side - let the queue handle delivery +- Always set explicit job options - defaults rarely match your use case +- Idempotency is your responsibility - jobs may run more than once +- Backoff strategies prevent thundering herds - exponential beats linear +- Dead letter queues are not optional - failed jobs need a home +- Concurrency limits protect downstream services - start conservative +- Job data should be small - pass IDs, not payloads +- Graceful shutdown prevents orphaned jobs - handle SIGTERM properly + +## Capabilities + +- bullmq-queues +- job-scheduling +- delayed-jobs +- repeatable-jobs +- job-priorities +- rate-limiting-jobs +- job-events +- worker-patterns +- flow-producers +- job-dependencies + +## Scope + +- redis-infrastructure -> redis-specialist +- serverless-queues -> upstash-qstash +- workflow-orchestration -> temporal-craftsman +- event-sourcing -> event-architect +- email-delivery -> email-systems + +## Tooling + +### Core + +- bullmq +- ioredis + +### Hosting + +- upstash +- redis-cloud +- elasticache +- railway + +### Monitoring + +- bull-board +- arena +- bullmq-pro + +### Patterns + +- delayed-jobs +- repeatable-jobs +- job-flows +- rate-limiting +- sandboxed-processors + +## Patterns + +### Basic Queue Setup + +Production-ready BullMQ queue with proper configuration + +**When to use**: Starting any new queue implementation + +import { Queue, Worker, QueueEvents } from 'bullmq'; +import IORedis from 'ioredis'; + +// Shared connection for all queues +const connection = new IORedis(process.env.REDIS_URL, { + maxRetriesPerRequest: null, // Required for BullMQ + enableReadyCheck: false, +}); + +// Create queue with sensible defaults +const emailQueue = new Queue('emails', { + connection, + defaultJobOptions: { + attempts: 3, + backoff: { + type: 'exponential', + delay: 1000, + }, + removeOnComplete: { count: 1000 }, + removeOnFail: { count: 5000 }, + }, +}); + +// Worker with concurrency limit +const worker = new Worker('emails', async (job) => { + await sendEmail(job.data); +}, { + connection, + concurrency: 5, + limiter: { + max: 100, + duration: 60000, // 100 jobs per minute + }, +}); + +// Handle events +worker.on('failed', (job, err) => { + console.error(`Job ${job?.id} failed:`, err); +}); + +### Delayed and Scheduled Jobs + +Jobs that run at specific times or after delays + +**When to use**: Scheduling future tasks, reminders, or timed actions + +// Delayed job - runs once after delay +await queue.add('reminder', { userId: 123 }, { + delay: 24 * 60 * 60 * 1000, // 24 hours +}); + +// Repeatable job - runs on schedule +await queue.add('daily-digest', { type: 'summary' }, { + repeat: { + pattern: '0 9 * * *', // Every day at 9am + tz: 'America/New_York', + }, +}); + +// Remove repeatable job +await queue.removeRepeatable('daily-digest', { + pattern: '0 9 * * *', + tz: 'America/New_York', +}); + +### Job Flows and Dependencies + +Complex multi-step job processing with parent-child relationships + +**When to use**: Jobs depend on other jobs completing first + +import { FlowProducer } from 'bullmq'; + +const flowProducer = new FlowProducer({ connection }); + +// Parent waits for all children to complete +await flowProducer.add({ + name: 'process-order', + queueName: 'orders', + data: { orderId: 123 }, + children: [ + { + name: 'validate-inventory', + queueName: 'inventory', + data: { orderId: 123 }, + }, + { + name: 'charge-payment', + queueName: 'payments', + data: { orderId: 123 }, + }, + { + name: 'notify-warehouse', + queueName: 'notifications', + data: { orderId: 123 }, + }, + ], +}); + +### Graceful Shutdown + +Properly close workers without losing jobs + +**When to use**: Deploying or restarting workers + +const shutdown = async () => { + console.log('Shutting down gracefully...'); + + // Stop accepting new jobs + await worker.pause(); + + // Wait for current jobs to finish (with timeout) + await worker.close(); + + // Close queue connection + await queue.close(); + + process.exit(0); +}; + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); + +### Bull Board Dashboard + +Visual monitoring for BullMQ queues + +**When to use**: Need visibility into queue status and job states + +import { createBullBoard } from '@bull-board/api'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; +import { ExpressAdapter } from '@bull-board/express'; + +const serverAdapter = new ExpressAdapter(); +serverAdapter.setBasePath('/admin/queues'); + +createBullBoard({ + queues: [ + new BullMQAdapter(emailQueue), + new BullMQAdapter(orderQueue), + ], + serverAdapter, +}); + +app.use('/admin/queues', serverAdapter.getRouter()); + +## Validation Checks + +### Redis connection missing maxRetriesPerRequest + +Severity: ERROR + +BullMQ requires maxRetriesPerRequest null for proper reconnection handling + +Message: BullMQ queue/worker created without maxRetriesPerRequest: null on Redis connection. This will cause workers to stop on Redis connection issues. + +### No stalled job event handler + +Severity: WARNING + +Workers should handle stalled events to detect crashed workers + +Message: Worker created without 'stalled' event handler. Stalled jobs indicate worker crashes and should be monitored. + +### No failed job event handler + +Severity: WARNING + +Workers should handle failed events for monitoring and alerting + +Message: Worker created without 'failed' event handler. Failed jobs should be logged and monitored. + +### No graceful shutdown handling + +Severity: WARNING + +Workers should gracefully shut down on SIGTERM/SIGINT + +Message: Worker file without graceful shutdown handling. Jobs may be orphaned on deployment. + +### Awaiting queue.add in request handler + +Severity: INFO + +Queue additions should be fire-and-forget in request handlers + +Message: Queue.add awaited in request handler. Consider fire-and-forget for faster response. + +### Potentially large data in job payload + +Severity: WARNING + +Job data should be small - pass IDs not full objects + +Message: Job appears to have large inline data. Pass IDs instead of full objects to keep Redis memory low. + +### Job without timeout configuration + +Severity: INFO + +Jobs should have timeouts to prevent infinite execution + +Message: Job added without explicit timeout. Consider adding timeout to prevent stuck jobs. + +### Retry without backoff strategy + +Severity: WARNING + +Retries should use exponential backoff to avoid thundering herd + +Message: Job has retry attempts but no backoff strategy. Use exponential backoff to prevent thundering herd. + +### Repeatable job without explicit timezone + +Severity: WARNING + +Repeatable jobs should specify timezone to avoid DST issues + +Message: Repeatable job without explicit timezone. Will use server local time which can drift with DST. + +### Potentially high worker concurrency + +Severity: INFO + +High concurrency can overwhelm downstream services + +Message: Worker concurrency is high. Ensure downstream services can handle this load (DB connections, API rate limits). + +## Collaboration + +### Delegation Triggers + +- redis infrastructure|redis cluster|memory tuning -> redis-specialist (Queue needs Redis infrastructure) +- serverless queue|edge queue|no redis -> upstash-qstash (Need queues without managing Redis) +- complex workflow|saga|compensation|long-running -> temporal-craftsman (Need workflow orchestration beyond simple jobs) +- event sourcing|CQRS|event streaming -> event-architect (Need event-driven architecture) +- deploy|kubernetes|scaling|infrastructure -> devops (Queue needs infrastructure) +- monitor|metrics|alerting|dashboard -> performance-hunter (Queue needs monitoring) + +### Email Queue Stack + +Skills: bullmq-specialist, email-systems, redis-specialist + +Workflow: + +``` +1. Email request received (API) +2. Job queued with rate limiting (bullmq-specialist) +3. Worker processes with backoff (bullmq-specialist) +4. Email sent via provider (email-systems) +5. Status tracked in Redis (redis-specialist) +``` + +### Background Processing Stack + +Skills: bullmq-specialist, backend, devops + +Workflow: + +``` +1. API receives request (backend) +2. Long task queued for background (bullmq-specialist) +3. Worker processes async (bullmq-specialist) +4. Result stored/notified (backend) +5. Workers scaled per load (devops) +``` + +### AI Processing Pipeline + +Skills: bullmq-specialist, ai-workflow-automation, performance-hunter + +Workflow: + +``` +1. AI task submitted (ai-workflow-automation) +2. Job flow created with dependencies (bullmq-specialist) +3. Workers process stages (bullmq-specialist) +4. Performance monitored (performance-hunter) +5. Results aggregated (ai-workflow-automation) +``` + +### Scheduled Tasks Stack + +Skills: bullmq-specialist, backend, redis-specialist + +Workflow: + +``` +1. Repeatable jobs defined (bullmq-specialist) +2. Cron patterns with timezone (bullmq-specialist) +3. Jobs execute on schedule (bullmq-specialist) +4. State managed in Redis (redis-specialist) +5. Results handled (backend) +``` + +## Related Skills + +Works well with: `redis-specialist`, `backend`, `nextjs-app-router`, `email-systems`, `ai-workflow-automation`, `performance-hunter` + +## When to Use +- User mentions or implies: bullmq +- User mentions or implies: bull queue +- User mentions or implies: redis queue +- User mentions or implies: background job +- User mentions or implies: job queue +- User mentions or implies: delayed job +- User mentions or implies: repeatable job +- User mentions or implies: worker process +- User mentions or implies: job scheduling +- User mentions or implies: async processing + +## Limitations +- Use this skill only when the task clearly matches the scope described above. +- Do not treat the output as a substitute for environment-specific validation, testing, or expert review. +- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing. diff --git a/.agents/skills/cloudinary-react/SKILL.md b/.agents/skills/cloudinary-react/SKILL.md new file mode 100644 index 0000000..993df03 --- /dev/null +++ b/.agents/skills/cloudinary-react/SKILL.md @@ -0,0 +1,486 @@ +--- +name: cloudinary-react +description: Provides opinionated React SDK patterns for configuration, common integration scenarios, and troubleshooting for frequent errors and TypeScript pitfalls. Use when writing code or answering questions related to the Cloudinary React SDK. +license: MIT +metadata: + author: cloudinary + version: '1.0.2' +--- + +# Cloudinary React Skill + +## When to Use + +- When a user is building or debugging Cloudinary in a **React** app (Vite, Create React App, Parcel, etc.). +- When implementing or fixing: Upload Widget, AdvancedImage/AdvancedVideo, transformations, overlays, image galleries, video player, or signed/unsigned uploads. +- When the user sees errors like "createUploadWidget is not a function", wrong imports from `@cloudinary/url-gen`, upload preset issues, or video player DOM errors. + + +## Quick Start + +**Most common operations:** +1. **Setup**: Create config file with `cld` instance (see Project setup section) +2. **Display image**: `const img = cld.image('id').resize(fill().width(800)); ` +3. **Upload Widget**: Script in index.html + poll for `createUploadWidget` in useEffect +4. **Image overlay**: Use `source(text(...))` or `source(image(...))` - see Import reference table for exact paths +5. **Signed uploads**: See [references/signed-uploads.md](references/signed-uploads.md) +6. **Troubleshooting**: See [references/troubleshooting.md](references/troubleshooting.md) + +**For TypeScript**: See [references/typescript-patterns.md](references/typescript-patterns.md) +**For Video Player**: See [references/video-player.md](references/video-player.md) + + + +## Instructions + +When helping with Cloudinary in React, follow the patterns and rules below. Use the **exact** import paths and code patterns specified; do not guess subpaths or invent APIs. + +--- + +# Cloudinary React SDK Patterns & Common Errors + +**Scope**: These rules apply to **React (web)** with the browser Upload Widget. The **default** is **Vite** (create-cloudinary-react uses Vite). They also work with **other bundlers** (Create React App, Next.js, Parcel, etc.): only **how you read env vars** changes; see **"Other bundlers (non-Vite)"** below. Rules-only users: see **"Project setup (rules-only / without CLI)"** for the reusable Cloudinary instance, env, Upload Widget (unsigned/signed), and video player. For **React Native** uploads (including signed upload), see: https://cloudinary.com/documentation/react_native_image_and_video_upload?install_source=skillspack&referrer=react-skill#signed_upload — same "never expose secret, generate signature on backend" principle, but React Native uses the `upload()` method and backend SDKs differently. + +## Official Documentation + +**For complete API references and all configuration options**, consult the Cloudinary documentation via **llms.txt**: https://cloudinary.com/documentation/llms.txt?install_source=skillspack&referrer=react-skill + +This skill provides React patterns and common errors. For detailed API options (e.g., all Upload Widget config properties, Video Player API methods), use the documentation markdown files available through llms.txt. + +### Key References +- **Transformation Rules**: https://cloudinary.com/documentation/cloudinary_transformation_rules.md?install_source=skillspack&referrer=react-skill +- **Transformation Reference**: https://cloudinary.com/documentation/transformation_reference.md?install_source=skillspack&referrer=react-skill +- **React Image Transformations & Plugins**: https://cloudinary.com/documentation/react_image_transformations.md?install_source=skillspack&referrer=react-skill#plugins +- **React Video Transformations**: https://cloudinary.com/documentation/react_video_transformations.md?install_source=skillspack&referrer=react-skill +- **Upload Widget Reference** (complete config options): https://cloudinary.com/documentation/upload_widget_reference.md?install_source=skillspack&referrer=react-skill +- **Upload Widget (signed uploads)**: https://cloudinary.com/documentation/upload_widget.md?install_source=skillspack&referrer=react-skill#signed_uploads +- **Cloudinary Video Player** (standalone player): https://cloudinary.com/documentation/cloudinary_video_player.md?install_source=skillspack&referrer=react-skill +- **Video Player API Reference** (complete API methods): https://cloudinary.com/documentation/video_player_api_reference.md?install_source=skillspack&referrer=react-skill +- **Video Player React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial.md?install_source=skillspack&referrer=react-skill +- **Upload assets in Next.js (backend signature)**: https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial.md?install_source=skillspack&referrer=react-skill +- **Cloudinary Node.js SDK (server-side signing)** — use **v2**: `import { v2 as cloudinary } from 'cloudinary'`; do not use v1 (e.g. 1.47.0). https://cloudinary.com/documentation/node_integration.md?install_source=skillspack&referrer=react-skill +- **React Native image and video upload (signed)**: https://cloudinary.com/documentation/react_native_image_and_video_upload.md?install_source=skillspack&referrer=react-skill#signed_upload +- Always consult the official transformation rules when creating transformations +- Use only officially supported parameters from the transformation reference + +**Golden rule for @cloudinary/url-gen:** When generating overlay or transformation code, use **only** the "Import reference" table and "Canonical overlay block" in these rules. Copy those import paths and usage patterns exactly; do not guess or derive paths (e.g. `text` and `image` are from `qualifiers/source`, not `actions/overlay`; `Position` is from `qualifiers/position`, not `positioning/Position`). + +--- + +# 📋 PATTERNS (How to Do It Right) + +## Project setup (rules-only / without CLI) + +If the user is **not** using the create-cloudinary-react CLI and only has these rules, generate the following so they get correct config, env, and widget setup. + +**1. Environment (.env)** +Create a `.env` file in the project root with **Vite prefix** (required for client access): +- `VITE_CLOUDINARY_CLOUD_NAME=my_cloud` (required — use your actual cloud name, **never** the literal string `your_cloud_name` which causes 401) +- `VITE_CLOUDINARY_UPLOAD_PRESET=my_preset` (optional; required for unsigned upload widget — use your actual preset name) +- **Restart the dev server** after adding or changing `.env`. Use `import.meta.env.VITE_*` in code, not `process.env`. +- **If env var still empty in browser after restart**: Vite may cache the old value. Clear `node_modules/.vite/`, restart dev server, and do a hard refresh (Cmd+Shift+R / Ctrl+Shift+F5). If still empty, see "Vite env not reaching client" in Common Errors. + +**2. Reusable Cloudinary instance (config)** +Create a config file (e.g. `src/cloudinary/config.ts`) so the rest of the app can use a single `cld` instance: +```ts +import { Cloudinary } from '@cloudinary/url-gen'; + +const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME; +if (!cloudName) { + throw new Error('VITE_CLOUDINARY_CLOUD_NAME is not set. Add it to .env with the VITE_ prefix.'); +} + +export const cld = new Cloudinary({ cloud: { cloudName } }); +export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || ''; +``` +- Use **this** pattern for the reusable instance. Everywhere else: `import { cld } from './cloudinary/config'` (or the path the user chose) and call `cld.image(publicId)` / `cld.video(publicId)`. + +**3. Upload Widget (unsigned, from scratch)** + +**Strict pattern (always follow this exactly):** +1. **Script in `index.html`** (required): Add `` to `index.html`. Do **not** rely only on dynamic script injection from React — it's fragile. +2. **Poll in useEffect** (required): In `useEffect`, poll with `setInterval` (e.g. every 100ms) until `typeof window.cloudinary?.createUploadWidget === 'function'`. Only then create the widget. A single check (even in `onload`) is **not** reliable because `window.cloudinary` can exist before `createUploadWidget` is attached. +3. **Add a timeout**: Set a timeout (e.g. 10 seconds) to stop polling and show an error if the script never loads. Clear both interval and timeout in cleanup. +4. **Create widget once**: When `createUploadWidget` is available, create the widget and store it in a **ref**. Clear the interval and timeout. Pass options: `{ cloudName, uploadPreset, sources: ['local', 'camera', 'url'], multiple: false }`. +5. **Open on click**: Attach a click listener to a button that calls `widgetRef.current?.open()`. Remove the listener in useEffect cleanup. + +❌ **Do NOT**: Check only `window.cloudinary` (not enough); do a single check in `onload` (unreliable); skip the script in `index.html`; poll forever without a timeout. +- **Signed uploads**: Do not use only `uploadPreset`; use the pattern under "Secure (Signed) Uploads" (uploadSignature as function, fetch api_key, server includes upload_preset in signature). + +**4. Video player** +- Use imperative video element only (create with document.createElement, append to container ref, pass to videoPlayer). See "Cloudinary Video Player (The Player)" for the full pattern. + +**5. Summary for rules-only users** +- **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite). +- **Reusable instance**: One config file that creates and exports `cld` (and optionally `uploadPreset`) from `@cloudinary/url-gen`; use it everywhere. +- **Upload widget**: Script in index.html (required); in useEffect, **poll** until `createUploadWidget` is a function, then create widget once and store in ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend. +- **Video player**: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails. + +**If the user is not using Vite:** Use their bundler's client env prefix and access in the config file and everywhere you read env. Examples: Create React App → `REACT_APP_CLOUDINARY_CLOUD_NAME`, `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`; Next.js (client) → `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`. The rest (cld instance, widget options, video player) is the same. + +## Environment Variables +- **Default: Vite** — Vite requires `VITE_` prefix; use `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`). Restart dev server after changing `.env`. +- ✅ CORRECT (Vite): `VITE_CLOUDINARY_CLOUD_NAME=mycloud` in `.env`; `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` +- ❌ **WRONG**: `VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name` — **Never** use the literal placeholder `your_cloud_name`; it causes 401 errors. Use your actual cloud name from the Cloudinary dashboard. +- ⚠️ **Vite env not reaching client**: If `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` is still `undefined` in the browser after restarting: + 1. Clear Vite cache: `rm -rf node_modules/.vite/` (or delete `node_modules/.vite` folder) + 2. Restart dev server: `npm run dev` + 3. Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+F5 (Windows/Linux) + 4. If still empty, use a **static config** workaround (create a `cloudinaryConfig.ts` with a hardcoded cloud name for dev, then switch back to env later) and see Common Errors → "VITE_ prefix required or env var is undefined" + +## Other bundlers (non-Vite) +- **Only the env access changes.** All other patterns (reusable `cld`, Upload Widget, Video Player, overlays, signed uploads) are bundler-agnostic. +- **Create React App**: Prefix `REACT_APP_`; access `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`, `process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET`. Restart dev server after `.env` changes. +- **Next.js (client)**: Prefix `NEXT_PUBLIC_` for client; access `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, etc. Server-side can use `process.env.CLOUDINARY_*` without `NEXT_PUBLIC_`. +- **Parcel / other**: Check the bundler's docs for "exposing environment variables to the client" (often a prefix or allowlist). Use that prefix and the documented access (e.g. `process.env.*`). +- **Config file**: In `src/cloudinary/config.ts` (or equivalent), read cloud name and upload preset using the **user's bundler** env API (e.g. for CRA: `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`). Same `new Cloudinary({ cloud: { cloudName } })` and exports; only the env read line changes. + +## Upload Presets +- **Unsigned** = client-only uploads (no backend). **Signed** = backend required, more secure. See **"Signed vs unsigned uploads"** below for when to use which. +- ✅ Create unsigned upload preset (for simple client uploads): https://console.cloudinary.com/app/settings/upload/presets +- ✅ Set preset in `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name` +- ✅ Use in code: `import { uploadPreset } from './cloudinary/config'` +- ⚠️ If upload preset is missing, the Upload Widget will show an error message +- ⚠️ Upload presets must be set to "Unsigned" mode for client-side usage (no API key/secret needed) +- **When unsigned upload fails**: First check that the user configured their upload preset: + 1. Is `VITE_CLOUDINARY_UPLOAD_PRESET` set in `.env`? (must match preset name exactly) + 2. Does the preset exist in the dashboard under Settings → Upload → Upload presets? + 3. Is the preset set to **Unsigned** (not Signed)? + 4. Was the dev server restarted after adding/updating `.env`? + +## Installing Cloudinary packages +- ✅ **Install the latest**: When adding Cloudinary packages, use `npm install ` **with no version** so npm installs the latest compatible version (e.g. `npm install cloudinary-video-player`). In package.json use a **caret range** (e.g. `"cloudinary-video-player": "^1.0.0"`) so future installs get the latest compatible. Do not pin to an exact version unless you have verified it exists on npm. +- ✅ **Package names only**: Use **only** these names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player` (standalone player), `cloudinary` (Node server-side only). Do not invent names (e.g. no `@cloudinary/video-player`). +- ❌ **WRONG**: `npm install cloudinary-video-player@1.2.3` or `"cloudinary-video-player": "1.2.3"` (exact pin) — versions may not exist and break installs. +- ✅ **Correct**: `npm install cloudinary-video-player` (no version) or in package.json: `"cloudinary-video-player": "^1.0.0"` (caret = latest compatible). + +## Import Patterns +- ✅ Import Cloudinary instance: `import { cld } from './cloudinary/config'` +- ✅ Import components: `import { AdvancedImage, AdvancedVideo } from '@cloudinary/react'` +- ✅ Import plugins: `import { responsive, lazyload, placeholder } from '@cloudinary/react'` +- ✅ **For transformations and overlays**, use **only** the exact paths in "Import reference: @cloudinary/url-gen" and the "Canonical overlay block" below. Do **not** guess subpaths (e.g. `text` and `image` are from `qualifiers/source`, not `actions/overlay`). + +## Import reference: @cloudinary/url-gen (use these exact paths only) + +**Rule:** Do not invent or guess import paths for `@cloudinary/url-gen`. Use **only** the paths in the table and canonical block below. Copy the import statements exactly; do not derive paths (e.g. `@cloudinary/url-gen/overlay` exports only `source` — `text` and `image` are from **`qualifiers/source`**; `Position` is from **`qualifiers/position`**, not `positioning/Position`). Wrong paths cause "module not found" or "does not exist". + +| Purpose | Exact import | +|--------|----------------| +| Cloudinary instance (config) | `import { Cloudinary } from '@cloudinary/url-gen';` | +| Resize (fill) | `import { fill } from '@cloudinary/url-gen/actions/resize';` | +| Resize (scale, for overlays) | `import { scale } from '@cloudinary/url-gen/actions/resize';` | +| Delivery format/quality | `import { format, quality } from '@cloudinary/url-gen/actions/delivery';` | +| Format qualifier (auto) | `import { auto } from '@cloudinary/url-gen/qualifiers/format';` | +| Quality qualifier (auto) | `import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';` | +| Effects (e.g. blur) | `import { blur } from '@cloudinary/url-gen/actions/effect';` | +| Overlay source | `import { source } from '@cloudinary/url-gen/actions/overlay';` | +| Overlay text / image (source types) | `import { text, image } from '@cloudinary/url-gen/qualifiers/source';` | +| Overlay image transformation | `import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';` | +| Position (overlay) | `import { Position } from '@cloudinary/url-gen/qualifiers/position';` | +| Gravity/compass | `import { compass } from '@cloudinary/url-gen/qualifiers/gravity';` | +| Text style (overlay) | `import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';` | +| Types | `import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';` | + +**Canonical overlay block (copy these imports and patterns exactly):** +```ts +// Overlay imports — text/image from qualifiers/source, NOT actions/overlay +import { source } from '@cloudinary/url-gen/actions/overlay'; +import { text, image } from '@cloudinary/url-gen/qualifiers/source'; +import { Position } from '@cloudinary/url-gen/qualifiers/position'; +import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle'; +import { compass } from '@cloudinary/url-gen/qualifiers/gravity'; +import { Transformation } from '@cloudinary/url-gen/transformation/Transformation'; +import { scale } from '@cloudinary/url-gen/actions/resize'; + +// Text overlay (compass with underscores: 'south_east', 'center') +cld.image('id').overlay( + source(text('Hello', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')) + .position(new Position().gravity(compass('center'))) +); + +// Image overlay (logo/image with resize) +cld.image('id').overlay( + source(image('logo').transformation(new Transformation().resize(scale().width(100)))) + .position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)) +); +``` + +- **Components** (AdvancedImage, AdvancedVideo, plugins) come from **`@cloudinary/react`**, not from `@cloudinary/url-gen`. +- **Transformation actions and qualifiers** (resize, delivery, effect, overlay, etc.) come from **`@cloudinary/url-gen/actions/*`** and **`@cloudinary/url-gen/qualifiers/*`** with the exact subpaths above. +- If an import fails, verify the package version (`@cloudinary/url-gen` in package.json) and the [Cloudinary URL-Gen SDK docs](https://cloudinary.com/documentation/sdks/js/url-gen/index.html?install_source=skillspack&referrer=react-skill) or [Transformation Builder reference](https://cloudinary.com/documentation/sdks/js/transformation_builder_reference?install_source=skillspack&referrer=react-skill). + +## Creating Image & Video Instances +- ✅ Create image instance: `const img = cld.image(publicId)` +- ✅ Create video instance: `const video = cld.video(publicId)` (same pattern as images) +- ✅ Public ID format: Use forward slashes for folders (e.g., `'folder/subfolder/image'`) +- ✅ Public IDs are case-sensitive and should not include file extensions +- ✅ **Sample assets**: Cloudinary may provide sample assets under `samples/`. **Assume they might not exist** (users can delete them); always handle load errors and provide fallbacks (see Image gallery). When they exist, use them for examples and demos instead of requiring uploads first. +- ✅ **Sample public IDs that may be available** (use for galleries, demos; handle onError if missing): + - Images: `samples/cloudinary-icon`, `samples/two-ladies`, `samples/food/spices`, `samples/landscapes/beach-boat`, `samples/bike`, `samples/landscapes/girl-urban-view`, `samples/animals/reindeer`, `samples/food/pot-mussels` + - Video: `samples/elephants` +- ✅ **Default / most reliable**: Start with `samples/cloudinary-icon` for a single image; use the list above for galleries or variety. Prefer uploaded assets when the user has them. +- ✅ Examples: + ```tsx + const displayImage = cld.image('samples/cloudinary-icon'); + const displayVideo = cld.video('samples/elephants'); + // Gallery: e.g. ['samples/bike', 'samples/landscapes/beach-boat', 'samples/food/spices', ...] + ``` + +## Transformation Patterns + +### Image Transformations +- ✅ Chain transformations on image instance: + ```tsx + const img = cld.image('id') + .resize(fill().width(800).height(600)) + .effect(blur(800)) + .delivery(format(auto())) + .delivery(quality(autoQuality())); + ``` +- ✅ Pass to component: `` + +### Video Transformations +- ✅ Chain transformations on video instance (same pattern as images): + ```tsx + const video = cld.video('id') + .resize(fill().width(800).height(600)) + .delivery(format(auto())); + ``` +- ✅ Pass to component: `` +- ✅ Video transformations work the same way as image transformations + +### Transformation Best Practices +- ✅ Format and quality must use separate `.delivery()` calls +- ✅ Always end with auto format/quality: `.delivery(format(auto())).delivery(quality(autoQuality()))` unless user specifies a particular format or quality +- ✅ Use `gravity(auto())` unless user specifies a focal point +- ✅ Same transformation syntax works for both images and videos + +## Plugin Patterns +- ✅ **When the user asks for lazy loading or responsive images**: Use the **Cloudinary plugins** from `@cloudinary/react` — `responsive()`, `lazyload()`, `placeholder()` — with `AdvancedImage`. Do not use only native `loading="lazy"` or CSS-only responsive; the Cloudinary plugins handle breakpoints, lazy loading, and placeholders for Cloudinary URLs. +- ✅ Import plugins from `@cloudinary/react` +- ✅ Pass plugins as array: `plugins={[responsive(), lazyload(), placeholder()]}` +- ✅ Recommended plugin order: + 1. `responsive()` - First (handles breakpoints) + 2. `placeholder()` - Second (shows placeholder while loading) + 3. `lazyload()` - Third (delays loading until in viewport) + 4. `accessibility()` - Last (if needed) +- ✅ Always add `width` and `height` attributes to prevent layout shift +- ✅ Example: + ```tsx + + ``` + +## Responsive Images Pattern +- ✅ **Responsive images**: Use the Cloudinary `responsive()` plugin with `fill()` resize (not only CSS). **Lazy loading**: Use the Cloudinary `lazyload()` plugin with `AdvancedImage` (not only `loading="lazy"`). +- ✅ Use `responsive()` plugin with `fill()` resize +- ✅ Combine with `placeholder()` and `lazyload()` plugins +- ✅ Example: + ```tsx + const img = cld.image('id').resize(fill().width(800)); + + ``` + +## Image gallery with lazy loading and responsive +- ✅ **When the user asks for an image gallery with lazy loading and responsive**: Use Cloudinary **plugins** with `AdvancedImage`: `responsive()`, `lazyload()`, `placeholder()` (see Plugin Patterns). Use `fill()` resize with the responsive plugin. Add `width` and `height` to prevent layout shift. +- ✅ **Sample assets in galleries**: Use the sample public IDs from "Creating Image & Video Instances" (e.g. `samples/bike`, `samples/landscapes/beach-boat`, `samples/food/spices`, `samples/two-ladies`, `samples/landscapes/girl-urban-view`, `samples/animals/reindeer`, `samples/food/pot-mussels`, `samples/cloudinary-icon`). **Assume any sample might not exist** — users can delete them. Start with one reliable sample (e.g. `samples/cloudinary-icon`) or a short list; add **onError** handling and remove/hide failed images. Prefer **uploaded** assets when available (e.g. from UploadWidget) over samples. +- ✅ **Handle load errors**: Use `onError` on `AdvancedImage` to hide or remove failed images (e.g. set state to filter out the publicId, or hide the parent). Provide user feedback (e.g. "Some images could not be loaded. Try uploading your own!") and upload functionality so users can add their own images. +- ✅ **Fallback**: Default gallery list can be a subset of the sample list (e.g. `['samples/cloudinary-icon', 'samples/bike', 'samples/landscapes/beach-boat']`); when user uploads, append `result.public_id`. If an image fails to load, remove it from the list or hide it so the UI doesn't show broken images. + +## Image Overlays (text or logos) +- ✅ **When the user asks for image overlays with text or logos**: Use `@cloudinary/url-gen` overlay APIs. Copy imports and patterns from the **"Import reference"** table and **"Canonical overlay block"** in these rules. Do not import `text` or `image` from `actions/overlay` — they are from **`qualifiers/source`**; only `source` is from `actions/overlay`. +- ✅ **Import** `source` from `actions/overlay`; **`text` and `image` from `qualifiers/source`**. Also: `Position` from `qualifiers/position`, `TextStyle` from `qualifiers/textStyle`, `compass` from `qualifiers/gravity`, `Transformation` from `transformation/Transformation`, `scale` from `actions/resize`. +- ✅ **compass()** takes **string** values, with **underscores**: `compass('center')`, `compass('south_east')`, `compass('north_west')`. ❌ WRONG: `compass(southEast)` or `'southEast'` (no camelCase). +- ✅ **Overlay image**: Use `new Transformation()` **inside** `.transformation()`: `image('logo').transformation(new Transformation().resize(scale().width(100)))`. ❌ WRONG: `image('logo').transformation().resize(...)` (`.transformation()` does not return a chainable object). +- ✅ **Text overlay**: `fontWeight` goes on **TextStyle**: `new TextStyle('Arial', 60).fontWeight('bold')`. `textColor` goes on the **text source** (chained after `text(...)`): `text('Hello', new TextStyle('Arial', 60)).textColor('white')`. +- ✅ **Position** is chained **after** `source(...)`, not inside: `source(image('logo').transformation(...)).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))`. +- ✅ **Image overlay pattern**: `baseImage.overlay(source(image('id').transformation(new Transformation().resize(scale().width(100)))).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)))`. (Import `scale` from `@cloudinary/url-gen/actions/resize` if needed.) +- ✅ **Text overlay pattern**: `baseImage.overlay(source(text('Your Text', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')).position(new Position().gravity(compass('center'))))`. +- ✅ Docs: React Image Transformations and transformation reference for overlay syntax. + +## Upload Widget Pattern +- ✅ Use component: `import { UploadWidget } from './cloudinary/UploadWidget'` + +**Strict initialization pattern (always follow this exactly):** +1. ✅ **Script in `index.html`** (required): + ```html + + ``` +2. ✅ **Poll in useEffect until `createUploadWidget` is available** (required): Use `setInterval` (e.g. every 100ms) to check `typeof window.cloudinary?.createUploadWidget === 'function'`. Only create the widget when this returns `true`. Clear the interval once ready. +3. ✅ **Add a timeout** (e.g. 10 seconds) to stop polling and show an error state if the script never loads. Clear both interval and timeout in cleanup and when ready. +4. ✅ **Create widget once**, store in a ref. Cleanup: clear interval, clear timeout, remove click listener. + +❌ **Do NOT**: Check only `window.cloudinary` (the function may not be attached yet); do a single check in `onload` (unreliable timing); skip `index.html` and rely only on dynamic injection; poll forever without a timeout. + +- ✅ Create unsigned upload preset in dashboard at `settings/upload/presets` +- ✅ Add to `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name` +- ✅ Handle callbacks: + ```tsx + { + console.log('Public ID:', result.public_id); + }} + onUploadError={(error) => { + console.error('Upload failed:', error); + }} + /> + ``` +- ✅ Upload result contains: `public_id`, `secure_url`, `width`, `height`, etc. + +**For complete Upload Widget configuration options** (all properties for `createUploadWidget`), see the official reference: https://cloudinary.com/documentation/upload_widget_reference.md?install_source=skillspack&referrer=react-skill + +## Signed vs unsigned uploads (when to use which) + +**Unsigned uploads** (simpler, no backend required): +- Use when: Quick prototypes, low-risk apps, or when anyone with the preset name may upload. +- Preset: Create an **Unsigned** upload preset in Cloudinary dashboard (Settings → Upload → Upload presets). Put preset name in `.env` as `VITE_CLOUDINARY_UPLOAD_PRESET`. +- Client: Widget needs only `cloudName` and `uploadPreset`. No API key or secret; no backend. +- Trade-off: Anyone who knows the preset name can upload. Use only when that is acceptable. + +**Signed uploads** (more secure, backend required): +- Use when: Production apps, authenticated users, or when you need to control who can upload. +- Preset: Create a **Signed** upload preset in the dashboard. The backend generates a signature using your API secret; the client never sees the secret. +- Client: Widget gets `api_key` (from your backend), `uploadPreset`, and an `uploadSignature` **function** that calls your backend for each upload. API secret stays on server only. +- Trade-off: Requires a backend (Node/Express, Next.js API route, etc.) to sign requests. More secure; signature validates each upload. + +**Rule of thumb**: **Default to unsigned uploads** unless the user explicitly asks for "secure" or "signed" uploads. Do not default to signed — it requires a running backend and will fail out of the box. Use **signed** only when the user explicitly requests secure/signed uploads or needs to restrict who can upload. + +## Secure (Signed) Uploads + +**When to use:** Production apps, authenticated users, or when you need to control who can upload (more secure than unsigned). + +**Golden rules:** +1. Never expose or commit the API secret (server-only) +2. Use `server/.env` in `.gitignore` for API key/secret +3. API key can be sent to client; API secret must stay server-only + +**Quick implementation:** +- Use `uploadSignature` as function (not `signatureEndpoint`) +- Fetch `api_key` from server before creating widget +- Include `uploadPreset` in widget config +- Server includes `upload_preset` in signed params +- Use Cloudinary Node.js SDK v2 on server + +For complete implementation patterns, client/server code examples, and troubleshooting, see [references/signed-uploads.md](references/signed-uploads.md) + +## Video Patterns + +- ✅ **Display a video** → Use **AdvancedVideo** (`@cloudinary/react`). It just displays a video (with optional transformations). Not a full player. +- ✅ **A video player** → Use **Cloudinary Video Player** (`cloudinary-video-player`). That is the actual player (styled UI, controls, playlists, etc.). + +### ⚠️ IMPORTANT: Two Different Approaches + +**1. AdvancedVideo** (`@cloudinary/react`) — For **displaying** a video +- React component similar to `AdvancedImage`; just displays a video with Cloudinary transformations +- Not a full "player" — it's video display (native HTML5 video with optional controls) +- Use when: user wants to show/display a video. Works with `cld.video()` like images with `cld.image()` + +**2. Cloudinary Video Player** (`cloudinary-video-player`) — The **player** +- Full-featured video player (styled UI, controls, playlists). Use when the user asks for a "video player." +- **Use imperative video element only** (create with document.createElement, append to container ref); do not pass a React-managed `