diff --git a/.agents/skills/apollo-client/SKILL.md b/.agents/skills/apollo-client/SKILL.md
new file mode 100644
index 000000000..cdc564f52
--- /dev/null
+++ b/.agents/skills/apollo-client/SKILL.md
@@ -0,0 +1,168 @@
+---
+name: apollo-client
+description: >
+ Guide for building React applications with Apollo Client 4.x. Use this skill when:
+ (1) setting up Apollo Client in a React project,
+ (2) writing GraphQL queries or mutations with hooks,
+ (3) configuring caching or cache policies,
+ (4) managing local state with reactive variables,
+ (5) troubleshooting Apollo Client errors or performance issues.
+license: MIT
+compatibility: React 18+, React 19 (Suspense/RSC). Works with Next.js, Vite, CRA, and other React frameworks.
+metadata:
+ author: apollographql
+ version: "1.0.0"
+allowed-tools: Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
+---
+
+# Apollo Client 4.x Guide
+
+Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Version 4.x brings improved caching, better TypeScript support, and React 19 compatibility.
+
+## Integration Guides
+
+Choose the integration guide that matches your application setup:
+
+- **[Client-Side Apps](references/integration-client.md)** - For client-side React applications without SSR (Vite, Create React App, etc.)
+- **[Next.js App Router](references/integration-nextjs.md)** - For Next.js applications using the App Router with React Server Components
+- **[React Router Framework Mode](references/integration-react-router.md)** - For React Router 7 applications with streaming SSR
+- **[TanStack Start](references/integration-tanstack-start.md)** - For TanStack Start applications with modern routing
+
+Each guide includes installation steps, configuration, and framework-specific patterns optimized for that environment.
+
+## Quick Reference
+
+### Basic Query
+
+```tsx
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+ }
+`;
+
+function UserProfile({ userId }: { userId: string }) {
+ const { loading, error, data, dataState } = useQuery(GET_USER, {
+ variables: { id: userId },
+ });
+
+ if (loading) return
Loading...
;
+ if (error) return
Error: {error.message}
;
+
+ // TypeScript: dataState === "ready" provides better type narrowing than just checking data
+ return
;
+}
+
+function App() {
+ return (
+ Loading user...}>
+
+
+ );
+}
+```
+
+## Reference Files
+
+Detailed documentation for specific topics:
+
+- [TypeScript Code Generation](references/typescript-codegen.md) - GraphQL Code Generator setup for type-safe operations
+- [Queries](references/queries.md) - useQuery, useLazyQuery, polling, refetching
+- [Suspense Hooks](references/suspense-hooks.md) - useSuspenseQuery, useBackgroundQuery, useReadQuery, useLoadableQuery
+- [Mutations](references/mutations.md) - useMutation, optimistic UI, cache updates
+- [Fragments](references/fragments.md) - Fragment colocation, useFragment, useSuspenseFragment, data masking
+- [Caching](references/caching.md) - InMemoryCache, typePolicies, cache manipulation
+- [State Management](references/state-management.md) - Reactive variables, local state
+- [Error Handling](references/error-handling.md) - Error policies, error links, retries
+- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions
+
+## Key Rules
+
+### Query Best Practices
+
+- **Each page should generally only have one query, composed from colocated fragments.** Use `useFragment` or `useSuspenseFragment` in all non-page-components. Use `@defer` to allow slow fields below the fold to stream in later and avoid blocking the page load.
+- **Fragments are for colocation, not reuse.** Each fragment should describe exactly the data needs of a specific component, not be shared across components for common fields. See [Fragments reference](references/fragments.md) for details on fragment colocation and data masking.
+- Always handle `loading` and `error` states in UI when using non-suspenseful hooks (`useQuery`, `useLazyQuery`). When using Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`), React handles this through `` boundaries and error boundaries.
+- Use `fetchPolicy` to control cache behavior per query
+- Use the TypeScript type server to look up documentation for functions and options (Apollo Client has extensive docblocks)
+
+### Mutation Best Practices
+
+- **If the schema permits, mutation return values should return everything necessary to update the cache.** Neither manual updates nor refetching should be necessary.
+- If the mutation response is insufficient, carefully weigh manual cache manipulation vs refetching. Manual updates risk missing server logic. Consider optimistic updates with a granular refetch if needed.
+- Handle errors gracefully in the UI
+- Use `refetchQueries` sparingly (prefer letting the cache update automatically)
+
+### Caching Best Practices
+
+- Configure `keyFields` for types without `id` field
+- Disable normalization by setting `keyFields: false` for types that don't include an identifier and are meant to group related fields under the parent
+- Use `typePolicies` for pagination and computed fields
+- Understand cache normalization to debug issues
+- **Enable [data masking](references/fragments.md#data-masking) for all new applications** - it prevents components from accessing fragment data they don't own, enforcing proper data boundaries and preventing over-rendering
+
+### Performance
+
+- Avoid over-fetching with proper field selection
+- Configure appropriate `fetchPolicy` per use case
+- Use `@defer` for incremental delivery of deferred query parts, and `@stream` for streaming list fields (`@stream` available in Apollo Client 4.1+)
+- **Prefer Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications** for better loading state handling and code simplicity
+
+## Ground Rules
+
+- ALWAYS use Apollo Client 4.x patterns (not v3 or earlier)
+- ALWAYS wrap your app with `ApolloProvider`
+- ALWAYS handle loading and error states when using non-suspenseful hooks
+- PREFER Suspense hooks (`useSuspenseQuery`, `useBackgroundQuery`) in modern applications for better DX
+- NEVER store Apollo Client in React state (use module-level or context)
+- PREFER `cache-first` for read-heavy data, `network-only` for real-time data
+- USE TypeScript for better type safety with GraphQL
+- IMPLEMENT proper cache updates instead of refetching entire queries
+- ADVISE the human steering you to look into Apollo DevTools when collaboratively debugging Apollo Client issues
diff --git a/.agents/skills/apollo-client/references/caching.md b/.agents/skills/apollo-client/references/caching.md
new file mode 100644
index 000000000..7009edd18
--- /dev/null
+++ b/.agents/skills/apollo-client/references/caching.md
@@ -0,0 +1,557 @@
+# Caching Reference
+
+## Table of Contents
+
+- [InMemoryCache Setup](#inmemorycache-setup)
+- [Cache Normalization](#cache-normalization)
+- [Type Policies](#type-policies)
+- [Field Policies](#field-policies)
+- [Pagination](#pagination)
+- [Cache Manipulation](#cache-manipulation)
+- [Garbage Collection](#garbage-collection)
+
+## InMemoryCache Setup
+
+### Basic Configuration
+
+```typescript
+import { InMemoryCache } from "@apollo/client";
+
+const cache = new InMemoryCache({
+ // Custom type policies
+ typePolicies: {
+ Query: {
+ fields: {
+ // Query-level field policies
+ },
+ },
+ User: {
+ keyFields: ["id"],
+ fields: {
+ // User-level field policies
+ },
+ },
+ },
+
+ // Custom type name handling (rare)
+ possibleTypes: {
+ Character: ["Human", "Droid"],
+ Node: ["User", "Post", "Comment"],
+ },
+});
+```
+
+### Constructor Options
+
+```typescript
+new InMemoryCache({
+ // Define how types are identified in cache
+ typePolicies: {
+ /* ... */
+ },
+
+ // Interface/union type mappings between supertypes and their subtypes
+ possibleTypes: {
+ /* ... */
+ },
+
+ // Custom function to generate cache IDs (rare)
+ dataIdFromObject: (object) => {
+ if (object.__typename === "Book") {
+ return `Book:${object.isbn}`;
+ }
+ return defaultDataIdFromObject(object);
+ },
+});
+```
+
+## Cache Normalization
+
+Apollo Client normalizes data by splitting query results into individual objects and storing them by unique identifier.
+
+### How Normalization Works
+
+```graphql
+# Query
+query GetPost {
+ post(id: "1") {
+ id
+ title
+ author {
+ id
+ name
+ }
+ }
+}
+```
+
+```typescript
+// Normalized cache structure
+{
+ 'Post:1': {
+ __typename: 'Post',
+ id: '1',
+ title: 'Hello World',
+ author: { __ref: 'User:42' }
+ },
+ 'User:42': {
+ __typename: 'User',
+ id: '42',
+ name: 'John'
+ },
+ ROOT_QUERY: {
+ 'post({"id":"1"})': { __ref: 'Post:1' }
+ }
+}
+```
+
+### Benefits of Normalization
+
+1. **Automatic updates**: When `User:42` is updated anywhere, all components showing that user update
+2. **Deduplication**: Same objects aren't stored multiple times
+3. **Efficient updates**: Only changed objects trigger re-renders
+
+## Type Policies
+
+### keyFields
+
+Customize how objects are identified in the cache.
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ // Use ISBN instead of id for books
+ Book: {
+ keyFields: ["isbn"],
+ },
+
+ // Composite key
+ UserSession: {
+ keyFields: ["userId", "deviceId"],
+ },
+
+ // Nested key
+ Review: {
+ keyFields: ["book", ["isbn"], "reviewer", ["id"]],
+ },
+
+ // No key fields (singleton, only one object in cache per type)
+ AppSettings: {
+ keyFields: [],
+ },
+
+ // Disable normalization (objects of this type will be stored with their
+ // parent entity. The same object might end up multiple times in the cache
+ // and run out of sync. Use with caution, only if this object really relates
+ // to a property of their parent entity and cannot exist on its own.)
+ Address: {
+ keyFields: false,
+ },
+ },
+});
+```
+
+### merge Functions
+
+Control how incoming data merges with existing data.
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ User: {
+ fields: {
+ // Deep merge profile object
+ profile: {
+ merge: true, // Shorthand for deep merge
+ },
+
+ // Custom merge logic
+ notifications: {
+ merge(existing = [], incoming, { mergeObjects }) {
+ // Prepend new notifications
+ return [...incoming, ...existing];
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+## Field Policies
+
+### read Function
+
+Transform cached data when reading.
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ User: {
+ fields: {
+ // Computed field
+ fullName: {
+ read(_, { readField }) {
+ const firstName = readField("firstName");
+ const lastName = readField("lastName");
+ return `${firstName} ${lastName}`;
+ },
+ },
+
+ // Transform existing field
+ birthDate: {
+ read(existing) {
+ return existing ? new Date(existing) : null;
+ },
+ },
+
+ // Default value
+ role: {
+ read(existing = "USER") {
+ return existing;
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### merge Function
+
+Control how incoming data is stored.
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ User: {
+ fields: {
+ // Accumulate items instead of replacing
+ friends: {
+ merge(existing = [], incoming) {
+ return [...existing, ...incoming];
+ },
+ },
+
+ // Merge objects deeply
+ settings: {
+ merge(existing, incoming, { mergeObjects }) {
+ return mergeObjects(existing, incoming);
+ },
+ },
+ },
+ },
+
+ Query: {
+ fields: {
+ // Merge paginated results
+ posts: {
+ keyArgs: ["category"], // Only category affects cache key
+ merge(existing = { items: [] }, incoming) {
+ return {
+ ...incoming,
+ items: [...existing.items, ...incoming.items],
+ };
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### keyArgs
+
+Control which arguments affect cache storage.
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ // Different cache entry per userId only
+ // (limit, offset don't create new entries)
+ userPosts: {
+ keyArgs: ["userId"],
+ },
+
+ // No arguments affect cache key
+ // (useful for pagination)
+ feed: {
+ keyArgs: false,
+ },
+
+ // Nested argument
+ search: {
+ keyArgs: ["filter", ["category", "status"]],
+ },
+ },
+ },
+ },
+});
+```
+
+## Pagination
+
+### Offset-Based Pagination
+
+```typescript
+import { offsetLimitPagination } from "@apollo/client/utilities";
+
+const cache = new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ posts: offsetLimitPagination(),
+
+ // With key arguments
+ userPosts: offsetLimitPagination(["userId"]),
+ },
+ },
+ },
+});
+```
+
+### Cursor-Based Pagination (Relay Style)
+
+```typescript
+import { relayStylePagination } from "@apollo/client/utilities";
+
+const cache = new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ posts: relayStylePagination(),
+
+ // With key arguments
+ userPosts: relayStylePagination(["userId"]),
+ },
+ },
+ },
+});
+```
+
+### Custom Pagination
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ feed: {
+ keyArgs: false,
+
+ merge(existing, incoming, { args }) {
+ const merged = existing ? existing.slice(0) : [];
+ const offset = args?.offset ?? 0;
+
+ for (let i = 0; i < incoming.length; i++) {
+ merged[offset + i] = incoming[i];
+ }
+
+ return merged;
+ },
+
+ read(existing, { args }) {
+ const offset = args?.offset ?? 0;
+ const limit = args?.limit ?? existing?.length ?? 0;
+ return existing?.slice(offset, offset + limit);
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### fetchMore for Pagination
+
+```tsx
+function PostList() {
+ const { data, fetchMore, loading } = useQuery(GET_POSTS, {
+ variables: { offset: 0, limit: 10 },
+ });
+
+ const loadMore = () => {
+ fetchMore({
+ variables: {
+ offset: data.posts.length,
+ },
+ // With proper type policies, no updateQuery needed
+ });
+ };
+
+ return (
+
+ {data?.posts.map((post) => (
+
+ ))}
+
+
+ );
+}
+```
+
+## Cache Manipulation
+
+### cache.readQuery
+
+```typescript
+// Read data from cache
+const data = cache.readQuery({
+ query: GET_TODOS,
+});
+
+// With variables
+const userData = cache.readQuery({
+ query: GET_USER,
+ variables: { id: "1" },
+});
+```
+
+### cache.writeQuery
+
+```typescript
+// Write data to cache
+cache.writeQuery({
+ query: GET_TODOS,
+ data: {
+ todos: [{ __typename: "Todo", id: "1", text: "Buy milk", completed: false }],
+ },
+});
+
+// With variables
+cache.writeQuery({
+ query: GET_USER,
+ variables: { id: "1" },
+ data: {
+ user: { __typename: "User", id: "1", name: "John" },
+ },
+});
+```
+
+### cache.readFragment / cache.writeFragment
+
+```typescript
+// Read a specific object - use cache.identify for safety
+const user = cache.readFragment({
+ id: cache.identify({ __typename: "User", id: "1" }),
+ fragment: gql`
+ fragment UserFragment on User {
+ id
+ name
+ email
+ }
+ `,
+});
+
+// Apollo Client 4.1+: Use 'from' parameter (recommended)
+const user = cache.readFragment({
+ from: { __typename: "User", id: "1" },
+ fragment: gql`
+ fragment UserFragment on User {
+ id
+ name
+ email
+ }
+ `,
+});
+
+// Update a specific object
+cache.writeFragment({
+ id: cache.identify({ __typename: "User", id: "1" }),
+ fragment: gql`
+ fragment UpdateUser on User {
+ name
+ }
+ `,
+ data: {
+ name: "Jane",
+ },
+});
+
+// Apollo Client 4.1+: Use 'from' parameter (recommended)
+cache.writeFragment({
+ from: { __typename: "User", id: "1" },
+ fragment: gql`
+ fragment UpdateUser on User {
+ name
+ }
+ `,
+ data: {
+ name: "Jane",
+ },
+});
+```
+
+### cache.modify
+
+```typescript
+// Modify fields directly
+cache.modify({
+ id: cache.identify(user),
+ fields: {
+ // Set new value
+ name: () => "New Name",
+
+ // Transform existing value
+ postCount: (existing) => existing + 1,
+
+ // Delete field
+ temporaryField: (_, { DELETE }) => DELETE,
+
+ // Add to array
+ friends: (existing, { toReference }) => [...existing, toReference({ __typename: "User", id: "2" })],
+ },
+});
+```
+
+### cache.evict
+
+```typescript
+// Remove object from cache
+cache.evict({ id: "User:1" });
+
+// Remove specific field
+cache.evict({ id: "User:1", fieldName: "friends" });
+
+// Remove with broadcast (trigger re-renders)
+cache.evict({ id: "User:1", broadcast: true });
+```
+
+## Garbage Collection
+
+### Manual Garbage Collection
+
+```typescript
+// After evicting objects, clean up dangling references
+cache.evict({ id: "User:1" });
+cache.gc();
+```
+
+### Retaining Objects
+
+```typescript
+// Prevent objects from being garbage collected
+const release = cache.retain("User:1");
+
+// Later, allow GC
+release();
+cache.gc();
+```
+
+### Inspecting Cache
+
+```typescript
+// Get all cached data
+const cacheContents = cache.extract();
+
+// Restore cache state
+cache.restore(previousCacheContents);
+
+// Get identified object cache key
+const userId = cache.identify({ __typename: "User", id: "1" });
+// Returns: 'User:1'
+```
diff --git a/.agents/skills/apollo-client/references/error-handling.md b/.agents/skills/apollo-client/references/error-handling.md
new file mode 100644
index 000000000..8f95ebe01
--- /dev/null
+++ b/.agents/skills/apollo-client/references/error-handling.md
@@ -0,0 +1,337 @@
+# Error Handling Reference (Apollo Client 4.x)
+
+Note that Apollo Client 4.x handles errors differently than Apollo Client 3.x.
+This reference documents the updated error handling mechanisms, error types, and best practices for managing errors in your Apollo Client applications.
+For older Apollo Client 3.x error handling documentation, see [Apollo Client 3.x Error Handling](https://www.apollographql.com/docs/react/v3/data/error-handling).
+
+## Table of Contents
+
+- [Understanding Errors](#understanding-errors)
+- [Error Types](#error-types)
+- [Identifying Error Types](#identifying-error-types)
+- [GraphQL Error Policies](#graphql-error-policies)
+- [Error Links](#error-links)
+- [Retry Logic](#retry-logic)
+- [Error Boundaries](#error-boundaries)
+
+## Understanding Errors
+
+Errors in Apollo Client fall into two main categories: **GraphQL errors** and **network errors**. Each category has specific error classes that provide detailed information about what went wrong.
+
+### GraphQL Errors
+
+GraphQL errors are related to server-side execution of a GraphQL operation:
+
+- **Syntax errors** (e.g., malformed query)
+- **Validation errors** (e.g., query includes a non-existent schema field)
+- **Resolver errors** (e.g., error while populating a query field)
+
+If a syntax or validation error occurs, the server doesn't execute the operation. If resolver errors occur, the server can still return partial data.
+
+Example server response with GraphQL error:
+
+```json
+{
+ "errors": [
+ {
+ "message": "Cannot query field \"nonexistentField\" on type \"Query\".",
+ "locations": [{ "line": 2, "column": 3 }],
+ "extensions": {
+ "code": "GRAPHQL_VALIDATION_FAILED"
+ }
+ }
+ ],
+ "data": null
+}
+```
+
+In Apollo Client 4.x, GraphQL errors are represented by the [`CombinedGraphQLErrors`](https://apollographql.com/docs/react/api/errors/CombinedGraphQLErrors) error type.
+
+### Network Errors
+
+Network errors occur when attempting to communicate with your GraphQL server:
+
+- `4xx` or `5xx` HTTP response status codes
+- Network unavailability
+- JSON parsing failures
+- Custom errors from Apollo Link request handlers
+
+Network errors might be represented by special error types, but if an api such as the `fetch` API throws a native error (e.g., `TypeError`), Apollo Client will pass it through as-is.
+Thrown values that don't fulfill the standard `ErrorLike` interface are wrapped in the [`UnconventionalError`](https://apollographql.com/docs/react/api/errors/UnconventionalError) class, which fulfills the `ErrorLike` interface. As such, you can expect any error returned by Apollo Client to fulfill the `ErrorLike` interface.
+
+```ts
+export interface ErrorLike {
+ message: string;
+ name: string;
+ stack?: string;
+}
+```
+
+## Error Types
+
+Apollo Client 4.x provides specific error classes for different error scenarios:
+
+### CombinedGraphQLErrors
+
+Represents GraphQL errors returned by the server. Most common error type in applications.
+
+```tsx
+import { CombinedGraphQLErrors } from "@apollo/client/errors";
+
+function UserProfile({ userId }: { userId: string }) {
+ const { data, error } = useQuery(GET_USER, {
+ variables: { id: userId },
+ });
+
+ // no need to check for nullishness of error, CombinedGraphQLErrors.is handles that
+ if (CombinedGraphQLErrors.is(error)) {
+ // Handle GraphQL errors
+ return (
+
+ {error.graphQLErrors.map((err, i) => (
+
GraphQL Error: {err.message}
+ ))}
+
+ );
+ }
+
+ return data ? : null;
+}
+```
+
+### CombinedProtocolErrors
+
+Represents fatal transport-level errors during multipart HTTP subscription execution.
+
+### ServerError
+
+Occurs when the server responds with a non-200 HTTP status code.
+
+```tsx
+import { ServerError } from "@apollo/client/errors";
+
+if (ServerError.is(error)) {
+ console.error("Server error:", error.statusCode, error.result);
+}
+```
+
+### ServerParseError
+
+Occurs when the server response cannot be parsed as valid JSON.
+
+```tsx
+import { ServerParseError } from "@apollo/client/errors";
+
+if (ServerParseError.is(error)) {
+ console.error("Invalid JSON response:", error.bodyText);
+}
+```
+
+### LocalStateError
+
+Represents errors in local state configuration or execution.
+
+### UnconventionalError
+
+Wraps non-standard errors (e.g., thrown symbols or objects) to ensure consistent error handling.
+
+## Identifying Error Types
+
+Every Apollo Client error class provides a static `is` method that reliably determines whether an error is of that specific type. This is more robust than `instanceof` because it avoids false positives/negatives.
+
+```ts
+import {
+ CombinedGraphQLErrors,
+ CombinedProtocolErrors,
+ LocalStateError,
+ ServerError,
+ ServerParseError,
+ UnconventionalError,
+ ErrorLike,
+} from "@apollo/client/errors";
+
+// Anything returned in the `error` field of Apollo Client hooks or methods is of type `ErrorLike` or `undefined`.
+function handleError(error?: ErrorLike) {
+ if (CombinedGraphQLErrors.is(error)) {
+ // Handle GraphQL errors
+ console.error("GraphQL errors:", error.graphQLErrors);
+ } else if (CombinedProtocolErrors.is(error)) {
+ // Handle multipart subscription protocol errors
+ } else if (LocalStateError.is(error)) {
+ // Handle errors thrown by the LocalState class
+ } else if (ServerError.is(error)) {
+ // Handle server HTTP errors
+ console.error("Server error:", error.statusCode);
+ } else if (ServerParseError.is(error)) {
+ // Handle JSON parse errors
+ } else if (UnconventionalError.is(error)) {
+ // Handle errors thrown by irregular types
+ } else if (error) {
+ // Handle other errors
+ }
+}
+```
+
+## GraphQL Error Policies
+
+If a GraphQL operation produces errors, the server's response might still include partial data:
+
+```json
+{
+ "data": {
+ "getInt": 12,
+ "getString": null
+ },
+ "errors": [
+ {
+ "message": "Failed to get string!"
+ }
+ ]
+}
+```
+
+By default, Apollo Client throws away partial data and populates the `error` field. You can use partial results by defining an **error policy**:
+
+| Policy | Description |
+| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `none` | (Default) If the response includes errors, they are returned in `error` and response `data` is set to `undefined` even if the server returns `data`. |
+| `ignore` | Errors are ignored (`error` is not populated), and any returned `data` is cached and rendered as if no errors occurred. `data` may be `undefined` if a network error occurs. |
+| `all` | Both `data` and `error` are populated and any returned `data` is cached, enabling you to render both partial results and error information. |
+
+### Setting an Error Policy
+
+```tsx
+const MY_QUERY = gql`
+ query WillFail {
+ badField # This field's resolver produces an error
+ goodField # This field is populated successfully
+ }
+`;
+
+function ShowingSomeErrors() {
+ const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: "all" });
+
+ if (loading) return loading...;
+
+ return (
+
+
Good: {data?.goodField}
+ {error &&
Bad: {error.message}
}
+
+ );
+}
+```
+
+### Avoid setting a Global Error Policy
+
+While it is possible to set a global error policy using `defaultOptions`, in practice this is discouraged as it can lead to unexpected behavior and type safety issues. The return types of the TypeScript hooks may change depending on the `errorPolicy` passed into the hook, and this can conceptually not take global `defaultOptions` error policies into account. As such, it is best to set the `errorPolicy` per operation as needed.
+
+## Error Links
+
+The `ErrorLink` can be used to e.g. log error globally or perform specific side effects based on errors happening.
+
+An `ErrorLink` can't be used to swallow errors fully, but it can be used to retry an operation after handling an error, in which case the error wouldn't propagate. Otherwise, the most common use for `ErrorLink` is logging.
+
+```ts
+import { ErrorLink } from "@apollo/client/link/error";
+
+const errorLink = new ErrorLink(({ error, operation, forward }) => {
+ if (someCondition(error)) {
+ // Retry the request, returning the new observable
+ return forward(operation);
+ }
+
+ // Log the error for any unhandled GraphQL errors or network errors.
+ console.log(`[Error]: ${error.message}`);
+
+ // If nothing is returned from the error handler callback, the error will be
+ // emitted from the link chain as normal.
+});
+```
+
+### Retry Link
+
+Alternatively, you can use the `RetryLink` from `@apollo/client/link/retry` to implement retry logic for failed operations.
+
+```typescript
+import { RetryLink } from "@apollo/client/link/retry";
+
+const retryLink = new RetryLink({
+ delay: {
+ initial: 300,
+ max: Infinity,
+ jitter: true,
+ },
+ attempts: {
+ max: 5,
+ retryIf: (error, operation) => {
+ // Retry on network errors
+ return !!error && operation.operationName !== "SensitiveOperation";
+ },
+ },
+});
+
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ link: from([retryLink, errorLink, httpLink]),
+});
+```
+
+### Custom Retry Logic
+
+```typescript
+const retryLink = new RetryLink({
+ attempts: (count, operation, error) => {
+ // Don't retry mutations
+ if (operation.query.definitions.some((def) => def.kind === "OperationDefinition" && def.operation === "mutation")) {
+ return false;
+ }
+
+ // Retry up to 3 times on network errors
+ return count < 3 && !!error;
+ },
+ delay: (count) => {
+ // Exponential backoff
+ return Math.min(1000 * Math.pow(2, count), 30000);
+ },
+});
+```
+
+## Error Boundaries
+
+When using suspenseful hooks, you should use React Error Boundaries for graceful error handling.
+
+### Non-suspense per-Component Error Handling
+
+```tsx
+function SafeUserList() {
+ const { data, error, loading, refetch } = useQuery(GET_USERS, {
+ errorPolicy: "all",
+ notifyOnNetworkStatusChange: true,
+ });
+
+ // Handle network errors
+ if (error?.networkError) {
+ return (
+
+ Connection Error
+ Failed to load users. Please check your internet connection.
+
+
+ );
+ }
+
+ // Handle GraphQL errors but still show available data
+ return (
+
+ {error?.graphQLErrors && (
+ Some data may be incomplete: {error.graphQLErrors[0].message}
+ )}
+
+ {loading && }
+
+ {data?.users && }
+
+ );
+}
+```
diff --git a/.agents/skills/apollo-client/references/fragments.md b/.agents/skills/apollo-client/references/fragments.md
new file mode 100644
index 000000000..9df398061
--- /dev/null
+++ b/.agents/skills/apollo-client/references/fragments.md
@@ -0,0 +1,773 @@
+# Fragments Reference
+
+GraphQL fragments define a set of fields for a specific type. In Apollo Client, fragments are especially powerful when colocated with components to define each component's data requirements independently, creating a clear separation of concerns and enabling better component composition.
+
+## Table of Contents
+
+- [What Are Fragments](#what-are-fragments)
+- [Basic Fragment Syntax](#basic-fragment-syntax)
+- [Fragment Colocation](#fragment-colocation)
+- [Fragment Reading Hooks](#fragment-reading-hooks)
+- [Data Masking](#data-masking)
+- [Fragment Registry](#fragment-registry)
+- [TypeScript Integration](#typescript-integration)
+- [Best Practices](#best-practices)
+
+## What Are Fragments
+
+A GraphQL fragment defines a set of fields for a specific GraphQL type. Fragments are defined on a specific GraphQL type and can be included in operations using the spread operator (`...`).
+
+In Apollo Client, fragments serve a specific purpose:
+
+**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment. This allows components to independently evolve their data requirements without creating artificial dependencies between unrelated parts of your application.
+
+Fragments enable:
+
+1. **Component colocation**: Define the exact data requirements for a component alongside the component code
+2. **Independent evolution**: Change a component's data needs without affecting other components
+3. **Code organization**: Compose fragments together to build complete queries that mirror your component hierarchy
+
+## Basic Fragment Syntax
+
+### Defining a Fragment
+
+```typescript
+import { gql } from "@apollo/client";
+
+const USER_FRAGMENT = gql`
+ fragment UserFields on User {
+ id
+ name
+ email
+ avatarUrl
+ }
+`;
+```
+
+Every fragment includes:
+
+- A unique name (`UserFields`)
+- The type it operates on (`User`)
+- The fields to select
+
+### Using Fragments in Queries
+
+Include fragments in queries using the spread operator:
+
+```typescript
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ ...UserFields
+ }
+ }
+
+ ${USER_FRAGMENT}
+`;
+```
+
+When using GraphQL Code Generator with the recommended configuration (typescript, typescript-operations, and typed-document-node plugins), fragments defined in your source files are automatically picked up and generated into typed document nodes. The generated fragment documents already include the fragment definition, so you don't need to interpolate them manually into queries.
+
+## Fragment Colocation
+
+Fragment colocation is the practice of defining fragments in the same file as the component that uses them. This creates a clear contract between components and their data requirements.
+
+### Why Colocate Fragments
+
+- **Locality**: Data requirements live next to the code that uses them
+- **Maintainability**: Changes to component UI and data needs happen together
+- **Type safety**: TypeScript can infer exact types from colocated fragments
+- **Independence**: Components can evolve their data requirements without affecting other components
+
+### Colocation Pattern
+
+The recommended pattern for colocating fragments with components:
+
+```tsx
+import { gql, FragmentType } from "@apollo/client";
+import { useSuspenseFragment } from "@apollo/client/react";
+
+// Fragment definition
+// This will be picked up by Codegen to create `UserCard_UserFragmentDoc` in `./fragments.generated.ts`.
+// As that generated fragment document is correctly typed, we use that in the code going forward.
+// This fragment will never be consumed in runtime code, so it is wrapped in `if (false)` so the bundler can omit it when bundling.
+if (false) {
+ gql`
+ fragment UserCard_user on User {
+ id
+ name
+ email
+ avatarUrl
+ }
+ `;
+}
+
+// This has been created from above fragment definition by CodeGen and is a correctly typed `TypedDocumentNode`
+import { UserCard_UserFragmentDoc } from "./fragments.generated.ts";
+
+// Component receives the (partially masked) parent object
+export function UserCard({ user }: { user: FragmentType }) {
+ // Creates a subscription to the fragment in the cache
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ fragmentName: "UserCard_user",
+ from: user,
+ });
+
+ return (
+
+
+
{data.name}
+
{data.email}
+
+ );
+}
+```
+
+### Naming Convention
+
+A suggested naming pattern for fragments follows this convention:
+
+```
+{ComponentName}_{propName}
+```
+
+Where `propName` is the name of the prop the component receives containing the fragment data.
+
+Examples:
+
+- `UserCard_user` - Fragment for the `user` prop in the UserCard component
+- `PostList_posts` - Fragment for the `posts` prop in the PostList component
+- `CommentItem_comment` - Fragment for the `comment` prop in the CommentItem component
+
+This convention makes it clear which component owns which fragment. However, you can choose a different naming convention based on your project's needs.
+
+**Note**: A component might accept fragment data through multiple props, in which case it would have multiple associated fragments. For example, a `CommentCard` component might accept both a `comment` prop and an `author` prop, resulting in `CommentCard_comment` and `CommentCard_author` fragments.
+
+### Composing Fragments
+
+Parent components compose child fragments to build complete queries:
+
+```tsx
+// Child component
+import { gql } from "@apollo/client";
+
+if (false) {
+ gql`
+ fragment UserAvatar_user on User {
+ id
+ avatarUrl
+ name
+ }
+ `;
+}
+
+// Parent component composes child fragments
+if (false) {
+ gql`
+ fragment UserProfile_user on User {
+ id
+ name
+ email
+ bio
+ ...UserAvatar_user
+ }
+ `;
+}
+
+// Page-level query composes all fragments
+if (false) {
+ gql`
+ query UserProfilePage($id: ID!) {
+ user(id: $id) {
+ ...UserProfile_user
+ }
+ }
+ `;
+}
+```
+
+This creates a hierarchy that mirrors your component tree.
+
+## Fragment Reading Hooks
+
+Apollo Client provides hooks to read fragment data within components. These hooks work with data masking to ensure components only access the data they explicitly requested.
+
+### useSuspenseFragment
+
+For components using Suspense and concurrent features:
+
+```tsx
+import { useSuspenseFragment } from "@apollo/client/react";
+import { FragmentType } from "@apollo/client";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function UserCard({ user }: { user: FragmentType }) {
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ fragmentName: "UserCard_user",
+ from: user,
+ });
+
+ return
{data.name}
;
+}
+```
+
+### useFragment
+
+For components not using Suspense:
+
+```tsx
+import { useFragment } from "@apollo/client/react";
+import { FragmentType } from "@apollo/client";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function UserCard({ user }: { user: FragmentType }) {
+ const { data, complete } = useFragment({
+ fragment: UserCard_UserFragmentDoc,
+ fragmentName: "UserCard_user",
+ from: user,
+ });
+
+ if (!complete) {
+ return
Loading...
;
+ }
+
+ return
{data.name}
;
+}
+```
+
+The `complete` field indicates whether all fragment data is available in the cache.
+
+### Hook Options
+
+Both hooks accept these options:
+
+```typescript
+{
+ // The fragment document (required)
+ fragment: TypedDocumentNode,
+
+ // The fragment name (optional in most cases)
+ // Only required if the fragment document contains multiple definitions
+ fragmentName?: string,
+
+ // The source data containing the fragment (required)
+ // Can be a single object or an array of objects
+ from: FragmentType | Array>,
+
+ // Variables for the fragment (optional)
+ variables?: Variables,
+}
+```
+
+When `from` is an array, the hook returns an array of results, allowing you to read fragments from multiple objects efficiently. **Note**: Array support for the `from` parameter was added in Apollo Client 4.1.0.
+
+## Data Masking
+
+Data masking is a feature that prevents components from accessing data they didn't explicitly request through their fragments. This enforces proper data boundaries and prevents over-rendering.
+
+### Enabling Data Masking
+
+Enable data masking when creating your Apollo Client:
+
+```typescript
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ dataMasking: true, // Enable data masking
+});
+```
+
+### How Data Masking Works
+
+With data masking enabled:
+
+1. Fragments return opaque `FragmentType` objects
+2. Components must use `useFragment` or `useSuspenseFragment` to unmask data
+3. Components can only access fields defined in their own fragments
+4. TypeScript enforces these boundaries at compile time
+
+Without data masking:
+
+```tsx
+// ❌ Without data masking - component can access any data from parent
+function UserCard({ user }: { user: User }) {
+ // Can access any User field, even if not in fragment
+ return
{user.privateData}
;
+}
+```
+
+With data masking:
+
+```tsx
+// ✅ With data masking - component can only access its fragment data
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function UserCard({ user }: { user: FragmentType }) {
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ from: user,
+ });
+
+ // TypeScript error: 'privateData' doesn't exist on fragment type
+ // return
{data.privateData}
;
+
+ // Only fields from the fragment are accessible
+ return
{data.name}
;
+}
+```
+
+### Benefits of Data Masking
+
+- **Prevents over-rendering**: Components only re-render when their specific data changes
+- **Enforces boundaries**: Components can't accidentally depend on data they don't own
+- **Better refactoring**: Safe to modify parent queries without breaking child components
+- **Type safety**: TypeScript catches attempts to access unavailable fields
+
+## Fragment Registry
+
+The fragment registry is an **alternative approach** to GraphQL Code Generator's automatic fragment inlining by name. It allows you to register fragments globally, making them available throughout your application by name reference.
+
+**Important**: GraphQL Code Generator automatically inlines fragments by name wherever they're used in your queries. Either approach is sufficient on its own—**you don't need to combine them**.
+
+### Creating a Fragment Registry
+
+```typescript
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+import { createFragmentRegistry } from "@apollo/client/cache";
+
+export const fragmentRegistry = createFragmentRegistry();
+
+const client = new ApolloClient({
+ cache: new InMemoryCache({
+ fragments: fragmentRegistry,
+ }),
+});
+```
+
+### Registering Fragments
+
+Register fragments after defining them:
+
+```typescript
+import { gql } from "@apollo/client";
+import { fragmentRegistry } from "./apollo/client";
+
+const USER_FRAGMENT = gql`
+ fragment UserFields on User {
+ id
+ name
+ email
+ }
+`;
+
+fragmentRegistry.register(USER_FRAGMENT);
+```
+
+With colocated fragments:
+
+```tsx
+import { fragmentRegistry } from "@/apollo/client";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+// Register the fragment globally
+fragmentRegistry.register(UserCard_UserFragmentDoc);
+```
+
+### Using Registered Fragments
+
+Once registered, fragments can be referenced by name in queries without explicit imports:
+
+```tsx
+// Fragment is available by name because it's registered
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ ...UserCard_user
+ }
+ }
+`;
+```
+
+### Approaches for Fragment Composition
+
+There are three approaches to make child fragments available in parent queries:
+
+1. **GraphQL Code Generator inlining** (Recommended): CodeGen automatically inlines fragments by name. No manual work needed—just reference fragments by name in your queries.
+
+2. **Fragment Registry**: Manually register fragments to make them available by name. Useful for runtime scenarios where CodeGen isn't available.
+
+3. **Manual interpolation**: Explicitly import and interpolate child fragments into parent fragments:
+
+ ```typescript
+ import { CHILD_FRAGMENT } from "./ChildComponent";
+
+ const PARENT_FRAGMENT = gql`
+ fragment Parent_data on Data {
+ field
+ ...Child_data
+ }
+ ${CHILD_FRAGMENT}
+ `;
+ ```
+
+### Pros and Cons
+
+**GraphQL Code Generator inlining**:
+
+- ✅ Less work: Automatic, no manual registration needed
+- ❌ Larger bundle: Fragments are inlined into every query that uses them
+
+**Fragment Registry**:
+
+- ✅ Smaller bundle: Fragments are registered once, referenced by name
+- ❌ More work: Requires manual registration of each fragment
+- ❌ May cause issues with lazy-loaded modules if the module is not loaded before the query is executed
+- ✅ Best for deeply nested component trees where bundle size matters
+
+**Manual interpolation**:
+
+- ❌ Most work: Manual imports and interpolation required
+- ✅ Explicit: Clear fragment dependencies in code
+
+### Recommendation
+
+For most applications using GraphQL Code Generator (as shown in this guide), **use the automatic inlining**—it requires no additional setup and works seamlessly. Consider the fragment registry only if bundle size becomes a concern in applications with deeply nested component trees.
+
+## TypeScript Integration
+
+Apollo Client provides strong TypeScript support for fragments through GraphQL Code Generator.
+
+### Generated Types
+
+GraphQL Code Generator produces typed fragment documents:
+
+```typescript
+// Generated file: fragments.generated.ts
+export type UserCard_UserFragment = {
+ __typename: "User";
+ id: string;
+ name: string;
+ email: string;
+ avatarUrl: string;
+} & { " $fragmentName"?: "UserCard_UserFragment" };
+
+export const UserCard_UserFragmentDoc: TypedDocumentNode;
+```
+
+### Type-Safe Fragment Usage
+
+Use `FragmentType` to accept masked fragment data:
+
+```tsx
+import { FragmentType } from "@apollo/client";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function UserCard({ user }: { user: FragmentType }) {
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ from: user,
+ });
+
+ // 'data' is fully typed as UserCard_UserFragment
+ return
{data.name}
;
+}
+```
+
+### Fragment Type Inference
+
+TypeScript infers types from fragment documents automatically:
+
+```tsx
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+// Types are inferred from the fragment
+const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ from: user,
+});
+
+// data.name is string
+// data.email is string
+// data.nonExistentField is a TypeScript error
+```
+
+### Parent-Child Type Safety
+
+When passing fragment data from parent to child:
+
+```tsx
+// Parent query
+const { data } = useSuspenseQuery(GET_USER);
+
+// TypeScript ensures the query includes UserCard_user fragment
+// before allowing it to be passed to UserCard
+;
+```
+
+## Best Practices
+
+### Prefer Colocation Over Reuse
+
+**Fragments are for colocation, not reuse.** Each component should declare its data needs in a dedicated fragment, even if multiple components currently need the same fields.
+
+Sharing fragments between components just because they happen to need the same fields today creates artificial dependencies. When one component's requirements change, the shared fragment must be updated, causing all components using it to over-fetch data they don't need.
+
+```tsx
+// ✅ Good: Each component has its own fragment
+if (false) {
+ gql`
+ fragment UserCard_user on User {
+ id
+ name
+ email
+ avatarUrl
+ }
+ `;
+
+ gql`
+ fragment UserListItem_user on User {
+ id
+ name
+ email
+ }
+ `;
+}
+
+// If UserCard later needs 'bio', only UserCard_user changes
+// UserListItem doesn't over-fetch 'bio'
+```
+
+```tsx
+// ❌ Avoid: Sharing a generic fragment across components
+const COMMON_USER_FIELDS = gql`
+ fragment CommonUserFields on User {
+ id
+ name
+ email
+ }
+`;
+
+// UserCard and UserListItem both use CommonUserFields
+// When UserCard needs 'bio', adding it to CommonUserFields
+// causes UserListItem to over-fetch unnecessarily
+```
+
+This independence allows each component to evolve its data requirements without affecting unrelated parts of your application.
+
+### One Query Per Page
+
+Compose all page data requirements into a single query at the page level:
+
+```tsx
+// ✅ Good: Single page-level query
+if (false) {
+ gql`
+ query UserProfilePage($id: ID!) {
+ user(id: $id) {
+ ...UserHeader_user
+ ...UserPosts_user
+ ...UserFriends_user
+ }
+ }
+ `;
+}
+```
+
+```tsx
+// ❌ Avoid: Multiple queries in different components
+function UserProfile() {
+ const { data: userData } = useQuery(GET_USER);
+ const { data: postsData } = useQuery(GET_USER_POSTS);
+ const { data: friendsData } = useQuery(GET_USER_FRIENDS);
+ // ...
+}
+```
+
+### Use Fragment-Reading Hooks in Components
+
+Non-page components should use `useFragment` or `useSuspenseFragment`:
+
+```tsx
+// ✅ Good: Component reads fragment data
+import { FragmentType } from "@apollo/client";
+import { useSuspenseFragment } from "@apollo/client/react";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function UserCard({ user }: { user: FragmentType }) {
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ from: user,
+ });
+ return
;
+}
+```
+
+### Request Only Required Fields
+
+Keep fragments minimal and only request fields the component actually uses:
+
+```tsx
+// ✅ Good: Only necessary fields
+if (false) {
+ gql`
+ fragment UserListItem_user on User {
+ id
+ name
+ }
+ `;
+}
+```
+
+```tsx
+// ❌ Avoid: Requesting unused fields
+if (false) {
+ gql`
+ fragment UserListItem_user on User {
+ id
+ name
+ email
+ bio
+ friends {
+ id
+ name
+ }
+ posts {
+ id
+ title
+ }
+ }
+ `;
+}
+```
+
+### Use @defer for Below-the-Fold Content
+
+The `@defer` directive allows you to defer loading of non-critical fields, enabling faster initial page loads by prioritizing essential data. The deferred fields are delivered via incremental delivery and arrive after the non-deferred data, allowing the UI to progressively render as data becomes available.
+
+Defer slow fields that aren't immediately visible:
+
+```tsx
+if (false) {
+ gql`
+ query ProductPage($id: ID!) {
+ product(id: $id) {
+ id
+ name
+ price
+ ...ProductReviews_product @defer
+ }
+ }
+ `;
+}
+```
+
+This allows the page to render quickly while reviews load in the background.
+
+### Handle Client-Only Fields
+
+Use the `@client` directive for fields resolved locally:
+
+```tsx
+if (false) {
+ gql`
+ fragment TodoItem_todo on Todo {
+ id
+ text
+ completed
+ isSelected @client
+ }
+ `;
+}
+```
+
+### Enable Data Masking for New Applications
+
+Always enable data masking in new applications:
+
+```typescript
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ dataMasking: true,
+});
+```
+
+This enforces proper boundaries from the start and prevents accidental coupling between components.
+
+## Apollo Client Data Masking vs GraphQL-Codegen Fragment Masking
+
+Apollo Client's data masking and GraphQL Code Generator's fragment masking are different features that serve different purposes:
+
+### GraphQL-Codegen Fragment Masking
+
+GraphQL Code Generator's fragment masking (when using the client preset) is a **type-level** feature:
+
+- Masks data only at the TypeScript type level
+- The actual runtime data remains fully accessible on the object
+- Using their `useFragment` hook simply "unmasks" the data on a type level
+- Does not prevent accidental access to data at runtime
+- Parent components receive all data and pass it down
+- This means the parent component has to be subscribed to all data
+
+### Apollo Client Data Masking
+
+Apollo Client's data masking is a **runtime** feature with significant performance benefits:
+
+- Removes data at the runtime level, not just the type level
+- The `useFragment` and `useSuspenseFragment` hooks create cache subscriptions
+- Parent objects are sparse and only contain unmasked data
+- Prevents accidental access to data that should be masked
+
+### Key Benefits of Apollo Client Data Masking
+
+**1. No Accidental Data Access**
+
+With runtime data masking, masked fields are not present in the parent object at all. You cannot accidentally access them, even if you bypass TypeScript type checking.
+
+**2. Fewer Re-renders**
+
+Apollo Client's approach creates more efficient subscriptions:
+
+- **Without data masking**: Parent component subscribes to all fields (including masked ones). When a masked child field changes, the parent re-renders to pass that runtime data down the tree.
+- **With data masking**: Parent component only subscribes to its own unmasked fields. Subscriptions on masked fields happen lower in the React component tree when the child component calls `useSuspenseFragment`. When a masked field changes, only the child component that subscribed to it re-renders.
+
+### Example
+
+```tsx
+import { FragmentType } from "@apollo/client";
+import { useSuspenseQuery, useSuspenseFragment } from "@apollo/client/react";
+import { UserCard_UserFragmentDoc } from "./fragments.generated";
+
+function ParentComponent() {
+ const { data } = useSuspenseQuery(GET_USER);
+
+ // With Apollo Client data masking:
+ // - data.user only contains unmasked fields
+ // - Parent doesn't re-render when child-specific fields change
+
+ return ;
+}
+
+function UserCard({ user }: { user: FragmentType }) {
+ // Creates a cache subscription specifically for UserCard_user fields
+ const { data } = useSuspenseFragment({
+ fragment: UserCard_UserFragmentDoc,
+ from: user,
+ });
+
+ // Only this component re-renders when these fields change
+ return
{data.name}
;
+}
+```
+
+This granular subscription approach improves performance in large applications with deeply nested component trees.
diff --git a/.agents/skills/apollo-client/references/integration-client.md b/.agents/skills/apollo-client/references/integration-client.md
new file mode 100644
index 000000000..56ffef3b9
--- /dev/null
+++ b/.agents/skills/apollo-client/references/integration-client.md
@@ -0,0 +1,337 @@
+# Apollo Client Integration for Client-Side Apps
+
+This guide covers setting up Apollo Client in client-side React applications without server-side rendering (SSR). This includes applications using Vite, Parcel, Create React App, or other bundlers that don't implement SSR.
+
+For applications with SSR, use one of the framework-specific integration guides instead:
+
+- [Next.js App Router](integration-nextjs.md)
+- [React Router Framework Mode](integration-react-router.md)
+- [TanStack Start](integration-tanstack-start.md)
+
+## Installation
+
+```bash
+npm install @apollo/client graphql rxjs
+```
+
+## TypeScript Code Generation (optional but recommended)
+
+For type-safe GraphQL operations with TypeScript, see the [TypeScript Code Generation guide](typescript-codegen.md).
+
+## Setup Steps
+
+### Step 1: Create Client
+
+```typescript
+import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
+
+// Recommended: Use HttpOnly cookies for authentication
+const httpLink = new HttpLink({
+ uri: "https://your-graphql-endpoint.com/graphql",
+ credentials: "include", // Sends cookies with requests (secure when using HttpOnly cookies)
+});
+
+const client = new ApolloClient({
+ link: httpLink,
+ cache: new InMemoryCache(),
+});
+```
+
+If you need manual token management (less secure, only when HttpOnly cookies aren't available):
+
+```typescript
+import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
+import { SetContextLink } from "@apollo/client/link/context";
+
+const httpLink = new HttpLink({
+ uri: "https://your-graphql-endpoint.com/graphql",
+});
+
+const authLink = new SetContextLink(({ headers }) => {
+ const token = localStorage.getItem("token");
+ return {
+ headers: {
+ ...headers,
+ authorization: token ? `Bearer ${token}` : "",
+ },
+ };
+});
+
+const client = new ApolloClient({
+ link: authLink.concat(httpLink),
+ cache: new InMemoryCache(),
+});
+```
+
+### Step 2: Setup Provider
+
+```tsx
+import { ApolloProvider } from "@apollo/client";
+import App from "./App";
+
+function Root() {
+ return (
+
+
+
+ );
+}
+```
+
+### Step 3: Execute Query
+
+```tsx
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+
+const GET_USERS = gql`
+ query GetUsers {
+ users {
+ id
+ name
+ email
+ }
+ }
+`;
+
+function UserList() {
+ const { loading, error, data, dataState } = useQuery(GET_USERS);
+
+ if (loading) return
Loading...
;
+ if (error) return
Error: {error.message}
;
+
+ // TypeScript: dataState === "ready" provides better type narrowing than just checking data
+ return (
+
;
+
+ // TypeScript: dataState === "ready" provides better type narrowing than just checking data
+ return
{data.user.name}
;
+}
+```
+
+> **Note for TypeScript users**: Use [`dataState`](https://www.apollographql.com/docs/react/data/typescript#type-narrowing-data-with-datastate) for more robust type safety and better type narrowing in Apollo Client 4.x.
+
+### TypeScript Integration
+
+For complete examples with loading, error handling, and `dataState` for type narrowing, see [Basic Query Usage](#basic-query-usage) above.
+
+#### Usage with Generated Types
+
+For type-safe operations with code generation, see the [TypeScript Code Generation guide](typescript-codegen.md).
+
+Quick example:
+
+```typescript
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+import { GetUserDocument } from "./queries.generated";
+
+// Define your query with the if (false) pattern for code generation
+if (false) {
+ gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+ `;
+}
+
+function UserProfile({ userId }: { userId: string }) {
+ // Types are automatically inferred from GetUserDocument
+ const { data } = useQuery(GetUserDocument, {
+ variables: { id: userId },
+ });
+
+ return
{data.user.name}
;
+}
+```
+
+#### Usage with Manual Type Annotations
+
+If not using code generation, define types alongside your queries using `TypedDocumentNode`:
+
+```typescript
+import { gql, TypedDocumentNode } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+
+interface GetUserData {
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ };
+}
+
+interface GetUserVariables {
+ id: string;
+}
+
+// Types should always be defined via TypedDocumentNode alongside your queries/mutations, not at the useQuery/useMutation call site
+const GET_USER: TypedDocumentNode = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+function UserProfile({ userId }: { userId: string }) {
+ const { data } = useQuery(GET_USER, {
+ variables: { id: userId },
+ });
+
+ // data.user is automatically typed from GET_USER
+ return
{data.user.name}
;
+}
+```
+
+## Basic Mutation Usage
+
+```tsx
+import { gql, TypedDocumentNode } from "@apollo/client";
+import { useMutation } from "@apollo/client/react";
+
+interface CreateUserMutation {
+ createUser: {
+ id: string;
+ name: string;
+ email: string;
+ };
+}
+
+interface CreateUserMutationVariables {
+ input: {
+ name: string;
+ email: string;
+ };
+}
+
+const CREATE_USER: TypedDocumentNode = gql`
+ mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+function CreateUserForm() {
+ const [createUser, { loading, error }] = useMutation(CREATE_USER);
+
+ const handleSubmit = async (formData: FormData) => {
+ const { data } = await createUser({
+ variables: {
+ input: {
+ name: formData.get("name") as string,
+ email: formData.get("email") as string,
+ },
+ },
+ });
+ if (data) {
+ console.log("Created user:", data.createUser);
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+## Client Configuration Options
+
+```typescript
+const client = new ApolloClient({
+ // Required: The cache implementation
+ cache: new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ // Field-level cache configuration
+ },
+ },
+ },
+ }),
+
+ // Network layer
+ link: new HttpLink({ uri: "/graphql" }),
+
+ // Avoid defaultOptions if possible as they break TypeScript expectations.
+ // Configure options per-query/mutation instead for better type safety.
+ // defaultOptions: {
+ // watchQuery: { fetchPolicy: 'cache-and-network' },
+ // },
+
+ // DevTools are enabled by default in development
+ // Only configure when enabling in production
+ devtools: {
+ enabled: true, // Only needed for production
+ },
+
+ // Custom name for this client instance
+ clientAwareness: {
+ name: "web-client",
+ version: "1.0.0",
+ },
+});
+```
+
+## Important Considerations
+
+1. **Choose Your Hook Strategy:** Decide if your application should be based on Suspense. If it is, use suspenseful hooks like `useSuspenseQuery` (see [Suspense Hooks guide](suspense-hooks.md)), otherwise use non-suspenseful hooks like `useQuery` (see [Queries guide](queries.md)).
+
+2. **Client-Side Only:** This setup is for client-side apps without SSR. The Apollo Client instance is created once and reused throughout the application lifecycle.
+
+3. **Authentication:** Prefer HttpOnly cookies with `credentials: "include"` in `HttpLink` options to avoid exposing tokens to JavaScript. If manual token management is necessary, use `SetContextLink` to dynamically add authentication headers from `localStorage` or other client-side storage.
+
+4. **Environment Variables:** Store your GraphQL endpoint URL in environment variables for different environments (development, staging, production).
+
+5. **Error Handling:** Always handle `loading` and `error` states when using `useQuery` or `useLazyQuery`. For Suspense-based hooks (`useSuspenseQuery`), React handles this through `` boundaries and error boundaries.
diff --git a/.agents/skills/apollo-client/references/integration-nextjs.md b/.agents/skills/apollo-client/references/integration-nextjs.md
new file mode 100644
index 000000000..9dfb0a8b2
--- /dev/null
+++ b/.agents/skills/apollo-client/references/integration-nextjs.md
@@ -0,0 +1,317 @@
+# Apollo Client Integration with Next.js App Router
+
+This guide covers integrating Apollo Client in a Next.js application using the App Router architecture with support for both React Server Components (RSC) and Client Components.
+
+## What is supported?
+
+### React Server Components
+
+Apollo Client provides a shared client instance across all server components for a single request, preventing duplicate GraphQL requests and optimizing server-side rendering.
+
+### React Client Components
+
+When using the `app` directory, client components are rendered both on the server (SSR) and in the browser. Apollo Client enables you to execute GraphQL queries on the server and use the results to hydrate your browser-side cache, delivering fully-rendered pages to users.
+
+## Installation
+
+Install Apollo Client and the Next.js integration package:
+
+```bash
+npm install @apollo/client@latest @apollo/client-integration-nextjs graphql rxjs
+```
+
+> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md).
+
+## Setup for React Server Components (RSC)
+
+### Step 1: Create Apollo Client Configuration
+
+Create an `ApolloClient.ts` file in your app directory:
+
+```typescript
+import { HttpLink } from "@apollo/client";
+import { registerApolloClient, ApolloClient, InMemoryCache } from "@apollo/client-integration-nextjs";
+
+export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
+ return new ApolloClient({
+ cache: new InMemoryCache(),
+ link: new HttpLink({
+ // Use an absolute URL for SSR (relative URLs cannot be used in SSR)
+ uri: "https://your-api.com/graphql",
+ fetchOptions: {
+ // Optional: Next.js-specific fetch options for caching and revalidation
+ // See: https://nextjs.org/docs/app/api-reference/functions/fetch
+ },
+ }),
+ });
+});
+```
+
+### Step 2: Use in Server Components
+
+You can now use the `getClient` function or the `query` shortcut in your server components:
+
+```typescript
+import { query } from "./ApolloClient";
+
+async function UserProfile({ userId }: { userId: string }) {
+ const { data } = await query({
+ query: GET_USER,
+ variables: { id: userId },
+ });
+
+ return
{data.user.name}
;
+}
+```
+
+### Override Next.js Fetch Options
+
+You can override Next.js-specific `fetch` options per query using `context.fetchOptions`:
+
+```typescript
+const { data } = await getClient().query({
+ query: GET_USER,
+ context: {
+ fetchOptions: {
+ next: { revalidate: 60 }, // Revalidate every 60 seconds
+ },
+ },
+});
+```
+
+## Setup for Client Components (SSR and Browser)
+
+### Step 1: Create Apollo Wrapper Component
+
+Create `app/ApolloWrapper.tsx`:
+
+```typescript
+"use client";
+
+import { HttpLink } from "@apollo/client";
+import {
+ ApolloNextAppProvider,
+ ApolloClient,
+ InMemoryCache,
+} from "@apollo/client-integration-nextjs";
+
+function makeClient() {
+ const httpLink = new HttpLink({
+ // Use an absolute URL for SSR
+ uri: "https://your-api.com/graphql",
+ fetchOptions: {
+ // Optional: Next.js-specific fetch options
+ // Note: This doesn't work with `export const dynamic = "force-static"`
+ },
+ });
+
+ return new ApolloClient({
+ cache: new InMemoryCache(),
+ link: httpLink,
+ });
+}
+
+export function ApolloWrapper({ children }: React.PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+### Step 2: Wrap Root Layout
+
+Wrap your `RootLayout` in the `ApolloWrapper` component in `app/layout.tsx`:
+
+```typescript
+import { ApolloWrapper } from "./ApolloWrapper";
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+> **Note:** This works even if your layout is a React Server Component. It ensures all Client Components share the same Apollo Client instance through `ApolloNextAppProvider`.
+
+### Step 3: Use Apollo Client Hooks in Client Components
+
+For optimal streaming SSR, use suspense-enabled hooks like `useSuspenseQuery` and `useFragment`:
+
+```typescript
+"use client";
+
+import { useSuspenseQuery } from "@apollo/client/react";
+
+export function UserProfile({ userId }: { userId: string }) {
+ const { data } = useSuspenseQuery(GET_USER, {
+ variables: { id: userId },
+ });
+
+ return
{data.user.name}
;
+}
+```
+
+## Preloading Data from RSC to Client Components
+
+You can preload data in React Server Components to populate the cache of your Client Components.
+
+### Step 1: Use PreloadQuery in Server Components
+
+```tsx
+import { PreloadQuery } from "./ApolloClient";
+import { Suspense } from "react";
+
+export default async function Page() {
+ return (
+
+ Loading...>}>
+
+
+
+ );
+}
+```
+
+### Step 2: Consume with useSuspenseQuery in Client Components
+
+```tsx
+"use client";
+
+import { useSuspenseQuery } from "@apollo/client/react";
+
+export function ClientChild() {
+ const { data } = useSuspenseQuery(GET_USER, {
+ variables: { id: "1" },
+ });
+
+ return
{data.user.name}
;
+}
+```
+
+> **Important:** Data fetched this way should be considered client data and never referenced in Server Components. `PreloadQuery` prevents mixing server data and client data by creating a separate `ApolloClient` instance.
+
+### Using with useReadQuery
+
+For advanced use cases, you can use `PreloadQuery` with `useReadQuery` to avoid request waterfalls:
+
+```tsx
+
+ {(queryRef) => (
+ Loading...>}>
+
+
+ )}
+
+```
+
+In your Client Component:
+
+```tsx
+"use client";
+
+import { useQueryRefHandlers, useReadQuery, QueryRef } from "@apollo/client/react";
+
+export function ClientChild({ queryRef }: { queryRef: QueryRef }) {
+ const { refetch } = useQueryRefHandlers(queryRef);
+ const { data } = useReadQuery(queryRef);
+
+ return
{data.user.name}
;
+}
+```
+
+## Handling Multipart Responses (@defer) in SSR
+
+When using the `@defer` directive, `useSuspenseQuery` will only suspend until the initial response is received. To handle deferred data properly, you have three strategies:
+
+### Strategy 1: Use PreloadQuery with useReadQuery
+
+`PreloadQuery` allows deferred data to be fully transported and streamed chunk-by-chunk.
+
+### Strategy 2: Remove @defer Fragments
+
+Use `RemoveMultipartDirectivesLink` to strip `@defer` directives from queries during SSR:
+
+```typescript
+import { RemoveMultipartDirectivesLink } from "@apollo/client-integration-nextjs";
+
+new RemoveMultipartDirectivesLink({
+ stripDefer: true, // Default: true
+});
+```
+
+You can exclude specific fragments from stripping by labeling them:
+
+```graphql
+query myQuery {
+ fastField
+ ... @defer(label: "SsrDontStrip1") {
+ slowField1
+ }
+}
+```
+
+### Strategy 3: Wait for Deferred Data
+
+Use `AccumulateMultipartResponsesLink` to debounce the initial response:
+
+```typescript
+import { AccumulateMultipartResponsesLink } from "@apollo/client-integration-nextjs";
+
+new AccumulateMultipartResponsesLink({
+ cutoffDelay: 100, // Wait up to 100ms for incremental data
+});
+```
+
+### Combined Approach: SSRMultipartLink
+
+Combine both strategies with `SSRMultipartLink`:
+
+```typescript
+import { SSRMultipartLink } from "@apollo/client-integration-nextjs";
+
+new SSRMultipartLink({
+ stripDefer: true,
+ cutoffDelay: 100,
+});
+```
+
+## Testing
+
+Reset singleton instances between tests using the `resetApolloClientSingletons` helper:
+
+```typescript
+import { resetApolloClientSingletons } from "@apollo/client-integration-nextjs";
+
+afterEach(resetApolloClientSingletons);
+```
+
+## Debugging
+
+Enable verbose logging in your `app/ApolloWrapper.tsx`:
+
+```typescript
+import { setLogVerbosity } from "@apollo/client";
+
+setLogVerbosity("debug");
+```
+
+## Important Considerations
+
+1. **Separate RSC and SSR Queries:** Avoid overlapping queries between RSC and SSR. RSC queries don't update in the browser, while SSR queries can update dynamically as the cache changes.
+
+2. **Use Absolute URLs:** Always use absolute URLs in `HttpLink` for SSR, as relative URLs cannot be used in server-side rendering.
+
+3. **Streaming SSR:** For optimal performance, use `useSuspenseQuery` and `useFragment` to take advantage of React 18's streaming SSR capabilities.
+
+4. **Suspense Boundaries:** Place `Suspense` boundaries at meaningful places in your UI for the best user experience.
diff --git a/.agents/skills/apollo-client/references/integration-react-router.md b/.agents/skills/apollo-client/references/integration-react-router.md
new file mode 100644
index 000000000..627d58fd6
--- /dev/null
+++ b/.agents/skills/apollo-client/references/integration-react-router.md
@@ -0,0 +1,253 @@
+# Apollo Client Integration with React Router Framework Mode
+
+This guide covers integrating Apollo Client in a React Router 7 application with support for modern streaming SSR.
+
+## Installation
+
+Install Apollo Client and the React Router integration package:
+
+```bash
+npm install @apollo/client-integration-react-router @apollo/client graphql rxjs
+```
+
+> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md).
+
+## Setup
+
+### Step 1: Create Apollo Configuration
+
+Create an `app/apollo.ts` file that exports a `makeClient` function and an `apolloLoader`:
+
+```typescript
+import { HttpLink, InMemoryCache } from "@apollo/client";
+import { createApolloLoaderHandler, ApolloClient } from "@apollo/client-integration-react-router";
+
+// `request` will be available on the server during SSR or in loaders, but not in the browser
+export const makeClient = (request?: Request) => {
+ return new ApolloClient({
+ cache: new InMemoryCache(),
+ link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
+ });
+};
+
+export const apolloLoader = createApolloLoaderHandler(makeClient);
+```
+
+> **Important:** `ApolloClient` must be imported from `@apollo/client-integration-react-router`, not from `@apollo/client`.
+
+### Step 2: Reveal Entry Files
+
+Run the following command to create the entry files if they don't exist:
+
+```bash
+npx react-router reveal
+```
+
+This will create `app/entry.client.tsx` and `app/entry.server.tsx`.
+
+### Step 3: Configure Client Entry
+
+Adjust `app/entry.client.tsx` to wrap your app in `ApolloProvider`:
+
+```typescript
+import { makeClient } from "./apollo";
+import { ApolloProvider } from "@apollo/client";
+import { StrictMode, startTransition } from "react";
+import { hydrateRoot } from "react-dom/client";
+import { HydratedRouter } from "react-router/dom";
+
+startTransition(() => {
+ const client = makeClient();
+ hydrateRoot(
+ document,
+
+
+
+
+
+ );
+});
+```
+
+### Step 4: Configure Server Entry
+
+Adjust `app/entry.server.tsx` to wrap your app in `ApolloProvider` during SSR:
+
+```typescript
+import { makeClient } from "./apollo";
+import { ApolloProvider } from "@apollo/client";
+// ... other imports
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext
+) {
+ return new Promise((resolve, reject) => {
+ // ... existing code
+
+ const client = makeClient(request);
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [readyOption]() {
+ shellRendered = true;
+ // ... rest of the handler
+ },
+ // ... other options
+ }
+ );
+ });
+}
+```
+
+### Step 5: Add Hydration Helper
+
+Add `` to `app/root.tsx`:
+
+```typescript
+import { ApolloHydrationHelper } from "@apollo/client-integration-react-router";
+import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+```
+
+## Usage
+
+### Using apolloLoader with useReadQuery
+
+You can now use the `apolloLoader` function to create Apollo-enabled loaders for your routes:
+
+```typescript
+import { gql } from "@apollo/client";
+import { useReadQuery } from "@apollo/client/react";
+import { useLoaderData } from "react-router";
+import type { Route } from "./+types/my-route";
+import type { TypedDocumentNode } from "@apollo/client";
+import { apolloLoader } from "./apollo";
+
+// TypedDocumentNode definition with types
+const GET_USER: TypedDocumentNode<
+ { user: { id: string; name: string; email: string } },
+ { id: string }
+> = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+export const loader = apolloLoader()(({ preloadQuery }) => {
+ const userQueryRef = preloadQuery(GET_USER, {
+ variables: { id: "1" },
+ });
+
+ return {
+ userQueryRef,
+ };
+});
+
+export default function UserPage() {
+ const { userQueryRef } = useLoaderData();
+ const { data } = useReadQuery(userQueryRef);
+
+ return (
+
+
{data.user.name}
+
{data.user.email}
+
+ );
+}
+```
+
+> **Important:** To provide better TypeScript support, `apolloLoader` is a method that you need to call twice: `apolloLoader()(loader)`
+
+### Multiple Queries in a Loader
+
+You can preload multiple queries in a single loader:
+
+```typescript
+import { gql } from "@apollo/client";
+import { useReadQuery } from "@apollo/client/react";
+import { useLoaderData } from "react-router";
+import type { Route } from "./+types/my-route";
+import { apolloLoader } from "./apollo";
+
+// TypedDocumentNode definitions omitted for brevity
+
+export const loader = apolloLoader()(({ preloadQuery }) => {
+ const userQueryRef = preloadQuery(GET_USER, {
+ variables: { id: "1" },
+ });
+
+ const postsQueryRef = preloadQuery(GET_POSTS, {
+ variables: { userId: "1" },
+ });
+
+ return {
+ userQueryRef,
+ postsQueryRef,
+ };
+});
+
+export default function UserPage() {
+ const { userQueryRef, postsQueryRef } = useLoaderData();
+ const { data: userData } = useReadQuery(userQueryRef);
+ const { data: postsData } = useReadQuery(postsQueryRef);
+
+ return (
+
+
{userData.user.name}
+
Posts
+
+ {postsData.posts.map((post) => (
+
{post.title}
+ ))}
+
+
+ );
+}
+```
+
+## Important Considerations
+
+1. **Import ApolloClient from Integration Package:** Always import `ApolloClient` from `@apollo/client-integration-react-router`, not from `@apollo/client`, to ensure proper SSR hydration.
+
+2. **TypeScript Support:** The `apolloLoader` function requires double invocation for proper TypeScript type inference: `apolloLoader()(loader)`.
+
+3. **Request Context:** The `makeClient` function receives the `Request` object during SSR and in loaders, but not in the browser. Use this to set up auth headers or other request-specific configuration.
+
+4. **Streaming SSR:** The integration fully supports React's streaming SSR capabilities. Place `Suspense` boundaries strategically for optimal user experience.
+
+5. **Cache Hydration:** The `ApolloHydrationHelper` component ensures that data loaded on the server is properly hydrated on the client, preventing unnecessary refetches.
diff --git a/.agents/skills/apollo-client/references/integration-tanstack-start.md b/.agents/skills/apollo-client/references/integration-tanstack-start.md
new file mode 100644
index 000000000..a3ecbfeae
--- /dev/null
+++ b/.agents/skills/apollo-client/references/integration-tanstack-start.md
@@ -0,0 +1,365 @@
+# Apollo Client Integration with TanStack Start
+
+This guide covers integrating Apollo Client in a TanStack Start application with support for modern streaming SSR.
+
+> **Note:** When using `npx create-tsrouter-app` to create a new TanStack Start application, you can choose Apollo Client in the setup wizard to have all of this configuration automatically set up for you.
+
+## Installation
+
+Install Apollo Client and the TanStack Start integration package:
+
+```bash
+npm install @apollo/client-integration-tanstack-start @apollo/client graphql rxjs
+```
+
+> **TypeScript users:** For type-safe GraphQL operations, see the [TypeScript Code Generation guide](typescript-codegen.md).
+
+## Setup
+
+### Step 1: Configure Root Route with Context
+
+In your `routes/__root.tsx`, change from `createRootRoute` to `createRootRouteWithContext` to provide the right context type:
+
+```typescript
+import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
+import {
+ createRootRouteWithContext,
+ Outlet,
+} from "@tanstack/react-router";
+
+export const Route = createRootRouteWithContext()({
+ component: RootComponent,
+});
+
+function RootComponent() {
+ return (
+
+
+
+
+ My App
+
+
+
+
+
+ );
+}
+```
+
+### Step 2: Set Up Apollo Client in Router
+
+In your `router.tsx`, set up your Apollo Client instance and run `routerWithApolloClient`:
+
+```typescript
+import { routerWithApolloClient, ApolloClient, InMemoryCache } from "@apollo/client-integration-tanstack-start";
+import { HttpLink } from "@apollo/client";
+import { createRouter } from "@tanstack/react-router";
+import { routeTree } from "./routeTree.gen";
+
+export function getRouter() {
+ const apolloClient = new ApolloClient({
+ cache: new InMemoryCache(),
+ link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
+ });
+
+ const router = createRouter({
+ routeTree,
+ context: {
+ ...routerWithApolloClient.defaultContext,
+ },
+ });
+
+ return routerWithApolloClient(router, apolloClient);
+}
+```
+
+> **Important:** `ApolloClient` and `InMemoryCache` must be imported from `@apollo/client-integration-tanstack-start`, not from `@apollo/client`.
+
+## Usage
+
+### Option 1: Loader with preloadQuery and useReadQuery
+
+Use the `preloadQuery` function in your route loader to preload data during navigation:
+
+```typescript
+import { gql } from "@apollo/client";
+import { useReadQuery } from "@apollo/client/react";
+import { createFileRoute } from "@tanstack/react-router";
+import type { TypedDocumentNode } from "@apollo/client";
+
+// TypedDocumentNode definition with types
+const GET_USER: TypedDocumentNode<
+ { user: { id: string; name: string; email: string } },
+ { id: string }
+> = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+export const Route = createFileRoute("/user/$userId")({
+ component: RouteComponent,
+ loader: ({ context: { preloadQuery }, params }) => {
+ const queryRef = preloadQuery(GET_USER, {
+ variables: { id: params.userId },
+ });
+
+ return {
+ queryRef,
+ };
+ },
+});
+
+function RouteComponent() {
+ const { queryRef } = Route.useLoaderData();
+ const { data } = useReadQuery(queryRef);
+
+ return (
+
+
{data.user.name}
+
{data.user.email}
+
+ );
+}
+```
+
+### Option 2: Direct useSuspenseQuery in Component
+
+You can also use Apollo Client's suspenseful hooks directly in your component without a loader:
+
+```typescript
+import { gql, useSuspenseQuery } from "@apollo/client/react";
+import { createFileRoute } from "@tanstack/react-router";
+import type { TypedDocumentNode } from "@apollo/client";
+
+// TypedDocumentNode definition with types
+const GET_POSTS: TypedDocumentNode<{
+ posts: Array<{ id: string; title: string; content: string }>;
+}> = gql`
+ query GetPosts {
+ posts {
+ id
+ title
+ content
+ }
+ }
+`;
+
+export const Route = createFileRoute("/posts")({
+ component: RouteComponent,
+});
+
+function RouteComponent() {
+ const { data } = useSuspenseQuery(GET_POSTS);
+
+ return (
+
+ );
+}
+```
+
+### Using useQueryRefHandlers for Refetching
+
+When using `useReadQuery`, you can get refetch functionality from `useQueryRefHandlers`:
+
+> **Important:** Always call `useQueryRefHandlers` before `useReadQuery`. These two hooks interact with the same `queryRef`, and calling them in the wrong order could cause subtle bugs.
+
+```typescript
+import { useReadQuery, useQueryRefHandlers, QueryRef } from "@apollo/client/react";
+
+function UserComponent({ queryRef }: { queryRef: QueryRef }) {
+ const { refetch } = useQueryRefHandlers(queryRef);
+ const { data } = useReadQuery(queryRef);
+
+ return (
+
+
{data.user.name}
+
+
+ );
+}
+```
+
+## Important Considerations
+
+1. **Import from Integration Package:** Always import `ApolloClient` and `InMemoryCache` from `@apollo/client-integration-tanstack-start`, not from `@apollo/client`, to ensure proper SSR hydration.
+
+2. **Context Type:** Use `createRootRouteWithContext()` to provide proper TypeScript types for the `preloadQuery` function in loaders.
+
+3. **Loader vs Component Queries:**
+ - Use `preloadQuery` in loaders when you want to start fetching data before the component renders
+ - Use `useSuspenseQuery` directly in components for simpler cases or when data fetching can wait until render
+
+4. **Streaming SSR:** The integration fully supports React's streaming SSR capabilities. Place `Suspense` boundaries strategically for optimal user experience.
+
+5. **Cache Management:** The Apollo Client instance is shared across all routes, so cache updates from one route will be reflected in all routes that use the same data.
+
+6. **Authentication:** Use Apollo Client's `SetContextLink` for dynamic auth tokens.
+
+## Advanced Configuration
+
+### Adding Authentication
+
+For authentication in TanStack Start with SSR support, you need to handle both server and client environments differently. Use `createIsomorphicFn` to provide environment-specific implementations:
+
+```typescript
+import { ApolloClient, InMemoryCache, routerWithApolloClient } from "@apollo/client-integration-tanstack-start";
+import { ApolloLink, HttpLink } from "@apollo/client";
+import { SetContextLink } from "@apollo/client/link/context";
+import { createIsomorphicFn } from "@tanstack/react-start";
+import { createRouter } from "@tanstack/react-router";
+import { getSession, getCookie } from "@tanstack/react-start/server";
+import { routeTree } from "./routeTree.gen";
+
+// Create isomorphic link that uses different implementations per environment
+const createAuthLink = createIsomorphicFn()
+ .server(() => {
+ // Server-only: Can access server-side functions like `getCookies`, `getCookie`, `getSession`, etc. exported from `"@tanstack/react-start/server"`
+ return new SetContextLink(async (prevContext) => {
+ return {
+ headers: {
+ ...prevContext.headers,
+ authorization: getCookie("Authorization"),
+ },
+ };
+ });
+ })
+ .client(() => {
+ // Client-only: Can access `localStorage` or other browser APIs
+ return new SetContextLink((prevContext) => {
+ return {
+ headers: {
+ ...prevContext.headers,
+ authorization: localStorage.getItem("authToken") ?? "",
+ },
+ };
+ });
+ });
+
+export function getRouter() {
+ const httpLink = new HttpLink({
+ uri: "https://your-graphql-endpoint.com/graphql",
+ });
+
+ const apolloClient = new ApolloClient({
+ cache: new InMemoryCache(),
+ link: ApolloLink.from([createAuthLink(), httpLink]),
+ });
+
+ const router = createRouter({
+ routeTree,
+ context: {
+ ...routerWithApolloClient.defaultContext,
+ },
+ });
+
+ return routerWithApolloClient(router, apolloClient);
+}
+```
+
+> **Important:** The `getRouter` function is called both on the server and client, so it must not contain environment-specific code. Use `createIsomorphicFn` to provide different implementations:
+>
+> - **Server:** Can access server-only functions like `getSession`, `getCookies`, `getCookie` from `@tanstack/react-start/server` to access authentication information in request or session data
+> - **Client:** Can use `localStorage` or other browser APIs to access auth tokens (if setting `credentials: "include"` is sufficient, try to prefer that over manually setting auth headers client-side)
+>
+> This ensures your authentication works correctly in both SSR and browser contexts.
+
+### Custom Cache Configuration
+
+```typescript
+import { ApolloClient, InMemoryCache } from "@apollo/client-integration-tanstack-start";
+import { HttpLink } from "@apollo/client";
+import { createRouter } from "@tanstack/react-router";
+import { routeTree } from "./routeTree.gen";
+import { routerWithApolloClient } from "@apollo/client-integration-tanstack-start";
+
+export function getRouter() {
+ const apolloClient = new ApolloClient({
+ cache: new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ posts: {
+ merge(existing = [], incoming) {
+ return [...existing, ...incoming];
+ },
+ },
+ },
+ },
+ },
+ }),
+ link: new HttpLink({ uri: "https://your-graphql-endpoint.com/graphql" }),
+ });
+
+ const router = createRouter({
+ routeTree,
+ context: {
+ ...routerWithApolloClient.defaultContext,
+ },
+ });
+
+ return routerWithApolloClient(router, apolloClient);
+}
+```
diff --git a/.agents/skills/apollo-client/references/mutations.md b/.agents/skills/apollo-client/references/mutations.md
new file mode 100644
index 000000000..8d9b65535
--- /dev/null
+++ b/.agents/skills/apollo-client/references/mutations.md
@@ -0,0 +1,540 @@
+# Mutations Reference
+
+## Table of Contents
+
+- [useMutation Hook](#usemutation-hook)
+- [Mutation Variables](#mutation-variables)
+- [Loading and Error States](#loading-and-error-states)
+- [Optimistic UI](#optimistic-ui)
+- [Cache Updates](#cache-updates)
+- [Refetch Queries](#refetch-queries)
+- [Error Handling](#error-handling)
+
+## useMutation Hook
+
+The `useMutation` hook is used to execute GraphQL mutations.
+
+### Basic Usage
+
+```tsx
+import { gql } from "@apollo/client";
+import { useMutation } from "@apollo/client/react";
+
+const ADD_TODO = gql`
+ mutation AddTodo($text: String!) {
+ addTodo(text: $text) {
+ id
+ text
+ completed
+ }
+ }
+`;
+
+function AddTodo() {
+ const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);
+
+ return (
+
+ );
+}
+```
+
+### Return Tuple
+
+```typescript
+const [
+ mutateFunction, // Function to call to execute mutation
+ {
+ data, // Mutation result data
+ loading, // True while mutation is in flight
+ error, // ApolloError if mutation failed
+ called, // True if mutation has been called
+ reset, // Reset mutation state
+ client, // Apollo Client instance
+ },
+] = useMutation(MUTATION);
+```
+
+## Mutation Variables
+
+### Variables in Options
+
+```tsx
+const [createUser] = useMutation(CREATE_USER, {
+ variables: {
+ input: {
+ name: "Default User",
+ email: "default@example.com",
+ },
+ },
+});
+
+// Call with default variables
+await createUser();
+
+// Override variables
+await createUser({
+ variables: {
+ input: {
+ name: "Custom User",
+ email: "custom@example.com",
+ },
+ },
+});
+```
+
+### TypeScript Types
+
+Use `TypedDocumentNode` instead of generic type parameters:
+
+```typescript
+import { gql, TypedDocumentNode } from "@apollo/client";
+
+interface CreateUserData {
+ createUser: {
+ id: string;
+ name: string;
+ email: string;
+ };
+}
+
+interface CreateUserVariables {
+ input: {
+ name: string;
+ email: string;
+ };
+}
+
+const CREATE_USER: TypedDocumentNode = gql`
+ mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+const [createUser, { data, loading }] = useMutation(CREATE_USER);
+
+const { data } = await createUser({
+ variables: {
+ input: { name: "John", email: "john@example.com" },
+ },
+});
+
+// data.createUser is fully typed
+```
+
+## Loading and Error States
+
+### Handling in UI
+
+```tsx
+function CreatePost() {
+ const [createPost, { loading, error, data, reset }] = useMutation(CREATE_POST);
+
+ if (data) {
+ return (
+
+
Post created: {data.createPost.title}
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+```
+
+### Async/Await Pattern
+
+If you only need the promise without using the hook's loading/data state, use `client.mutate` instead:
+
+```tsx
+import { useApolloClient } from "@apollo/client/react";
+
+function CreatePost() {
+ const client = useApolloClient();
+
+ async function handleSubmit(formData: FormData) {
+ try {
+ const { data } = await client.mutate({
+ mutation: CREATE_POST,
+ variables: {
+ input: {
+ title: formData.get("title"),
+ content: formData.get("content"),
+ },
+ },
+ });
+ console.log("Created:", data.createPost);
+ router.push(`/posts/${data.createPost.id}`);
+ } catch (error) {
+ console.error("Failed to create post:", error);
+ }
+ }
+
+ return (
+
+ );
+}
+```
+
+If you do use the hook's state, e.g. because you want to render the `loading` state, errors or returned `data`, you can also use the `useMutation` hook with `async..await` in your handler:
+
+```tsx
+function CreatePost() {
+ const [createPost, { loading }] = useMutation(CREATE_POST);
+
+ async function handleSubmit(formData: FormData) {
+ try {
+ const { data } = await createPost({
+ variables: {
+ input: {
+ title: formData.get("title"),
+ content: formData.get("content"),
+ },
+ },
+ });
+ console.log("Created:", data.createPost);
+ router.push(`/posts/${data.createPost.id}`);
+ } catch (error) {
+ console.error("Failed to create post:", error);
+ }
+ }
+
+ return (
+
+ );
+}
+```
+
+## Optimistic UI
+
+Optimistic UI immediately reflects the expected result of a mutation before the server responds.
+
+### Basic Optimistic Response
+
+**Important**: `optimisticResponse` needs to be a full valid response for the mutation. A partial result might result in subtle errors.
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ optimisticResponse: {
+ addTodo: {
+ __typename: "Todo",
+ id: "temp-id",
+ text: "New todo",
+ completed: false,
+ },
+ },
+});
+```
+
+### Dynamic Optimistic Response
+
+```tsx
+function TodoList() {
+ const [addTodo] = useMutation(ADD_TODO);
+
+ const handleAdd = (text: string) => {
+ addTodo({
+ variables: { text },
+ optimisticResponse: {
+ addTodo: {
+ __typename: "Todo",
+ id: `temp-${Date.now()}`,
+ text,
+ completed: false,
+ },
+ },
+ });
+ };
+
+ return ;
+}
+```
+
+### Optimistic Response with Cache Update
+
+```tsx
+const [toggleTodo] = useMutation(TOGGLE_TODO, {
+ optimisticResponse: ({ id }) => ({
+ toggleTodo: {
+ __typename: "Todo",
+ id,
+ completed: true, // Assume success
+ },
+ }),
+ update: (cache, { data }) => {
+ // This runs twice: once with optimistic data, once with server data
+ cache.modify({
+ id: cache.identify(data.toggleTodo),
+ fields: {
+ completed: () => data.toggleTodo.completed,
+ },
+ });
+ },
+});
+```
+
+## Cache Updates
+
+### Using update Function
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ update: (cache, { data }) => {
+ // Read existing todos from cache
+ const existingTodos = cache.readQuery<{ todos: Todo[] }>({
+ query: GET_TODOS,
+ });
+
+ // Write updated list back to cache
+ cache.writeQuery({
+ query: GET_TODOS,
+ data: {
+ todos: [...(existingTodos?.todos ?? []), data.addTodo],
+ },
+ });
+ },
+});
+```
+
+### cache.modify
+
+```tsx
+const [deleteTodo] = useMutation(DELETE_TODO, {
+ update: (cache, { data }) => {
+ cache.modify({
+ fields: {
+ todos: (existingTodos: Reference[], { readField }) => {
+ return existingTodos.filter((todoRef) => readField("id", todoRef) !== data.deleteTodo.id);
+ },
+ },
+ });
+ },
+});
+```
+
+### cache.evict
+
+```tsx
+const [deleteUser] = useMutation(DELETE_USER, {
+ update: (cache, { data }) => {
+ // Remove the user object from cache entirely
+ cache.evict({ id: cache.identify(data.deleteUser) });
+ // Clean up dangling references
+ cache.gc();
+ },
+});
+```
+
+### Updating Related Queries
+
+```tsx
+const [createPost] = useMutation(CREATE_POST, {
+ update: (cache, { data }) => {
+ // Update author's post count
+ cache.modify({
+ id: cache.identify({ __typename: "User", id: data.createPost.authorId }),
+ fields: {
+ postCount: (existing) => existing + 1,
+ posts: (existing, { toReference }) => [...existing, toReference(data.createPost)],
+ },
+ });
+
+ // Add to feed
+ cache.modify({
+ fields: {
+ feed: (existing, { toReference }) => [toReference(data.createPost), ...existing],
+ },
+ });
+ },
+});
+```
+
+## Refetch Queries
+
+### Basic Refetch
+
+There are three refetch notations:
+
+- **String**: `refetchQueries: ['getTodos']` - refetches all active `getTodos` queries
+- **Query document**: `refetchQueries: [GET_TODOS]` - refetches all active queries using this document
+- **Object**: `refetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }]` - **fetches** the query, regardless if it's actively used in the UI
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ // Refetch all active GET_TODOS queries
+ refetchQueries: ["getTodos"],
+ // Or: refetchQueries: [GET_TODOS],
+});
+
+// Fetch specific query with variables (even if not active)
+const [addTodo] = useMutation(ADD_TODO, {
+ refetchQueries: [{ query: GET_TODOS }, { query: GET_TODO_COUNT }],
+});
+```
+
+### Conditional Refetch
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ refetchQueries: (result) => {
+ if (result.data?.addTodo.priority === "HIGH") {
+ return [{ query: GET_HIGH_PRIORITY_TODOS }];
+ }
+ return [{ query: GET_TODOS }];
+ },
+});
+```
+
+### Refetch Active Queries
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ refetchQueries: "active", // Refetch all active queries
+ // Or: 'all' to refetch all queries (including inactive)
+});
+```
+
+### awaitRefetchQueries
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ refetchQueries: [{ query: GET_TODOS }],
+ awaitRefetchQueries: true, // Wait for refetch before resolving mutation
+});
+```
+
+### onQueryUpdated
+
+Returning `true` from `onQueryUpdated` causes a refetch. Don't call `refetch()` manually inside `onQueryUpdated`, as it won't retain the query and might cancel it early.
+
+```tsx
+const [addTodo] = useMutation(ADD_TODO, {
+ update: (cache, { data }) => {
+ // Update cache...
+ },
+ onQueryUpdated: (observableQuery) => {
+ // Called for each query affected by cache update
+ console.log(`Query ${observableQuery.queryName} was updated`);
+ // Return true to refetch
+ return true;
+ },
+});
+```
+
+## Error Handling
+
+### Error Policy
+
+```tsx
+const [createUser, { loading }] = useMutation(CREATE_USER, {
+ errorPolicy: "all", // Return both data and errors
+});
+
+const { data, errors } = await createUser({
+ variables: { input },
+});
+
+// Handle partial success
+if (data?.createUser) {
+ console.log("User created:", data.createUser);
+}
+if (errors) {
+ console.warn("Some errors occurred:", errors);
+}
+```
+
+### onError Callback
+
+```tsx
+const [createUser] = useMutation(CREATE_USER, {
+ onError: (error) => {
+ // Handle error globally
+ toast.error(`Failed to create user: ${error.message}`);
+
+ // Log to error tracking service
+ Sentry.captureException(error);
+ },
+ onCompleted: (data) => {
+ toast.success(`User ${data.createUser.name} created!`);
+ },
+});
+```
+
+### Field-Level Errors
+
+```tsx
+const [createUser] = useMutation(CREATE_USER, {
+ errorPolicy: "all",
+});
+
+const handleSubmit = async (input: CreateUserInput) => {
+ const { data, errors } = await createUser({
+ variables: { input },
+ });
+
+ // Handle GraphQL validation errors
+ const fieldErrors = errors?.reduce(
+ (acc, error) => {
+ const field = error.extensions?.field as string;
+ if (field) {
+ acc[field] = error.message;
+ }
+ return acc;
+ },
+ {} as Record,
+ );
+
+ if (fieldErrors?.email) {
+ setEmailError(fieldErrors.email);
+ }
+};
+```
diff --git a/.agents/skills/apollo-client/references/queries.md b/.agents/skills/apollo-client/references/queries.md
new file mode 100644
index 000000000..81c177cc7
--- /dev/null
+++ b/.agents/skills/apollo-client/references/queries.md
@@ -0,0 +1,421 @@
+# Queries Reference
+
+> **Note**: In most applications, there should only be one use of a query hook per page. Use fragment-reading hooks (`useFragment`, `useSuspenseFragment`) with component-colocated fragments and data masking for the rest of the page components.
+
+## Table of Contents
+
+- [useQuery Hook](#usequery-hook)
+- [Query Variables](#query-variables)
+- [Loading and Error States](#loading-and-error-states)
+- [useLazyQuery](#uselazyquery)
+- [Polling and Refetching](#polling-and-refetching)
+- [Fetch Policies](#fetch-policies)
+- [Conditional Queries](#conditional-queries)
+
+## useQuery Hook
+
+The `useQuery` hook is the primary way to fetch data in Apollo Client in non-suspenseful applications. It returns loading and error states that must be handled.
+
+> **Note**: In suspenseful applications, use `useSuspenseQuery` or `useBackgroundQuery` instead. See the [Suspense Hooks reference](suspense-hooks.md) for more details.
+
+### Basic Usage
+
+```tsx
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+
+const GET_DOGS = gql`
+ query GetDogs {
+ dogs {
+ id
+ breed
+ displayImage
+ }
+ }
+`;
+
+function Dogs() {
+ const { loading, error, data } = useQuery(GET_DOGS);
+
+ if (loading) return
Loading...
;
+ if (error) return
Error: {error.message}
;
+
+ return (
+
+ {data.dogs.map((dog) => (
+
{dog.breed}
+ ))}
+
+ );
+}
+```
+
+### Return Object
+
+```typescript
+const {
+ data, // Query result data
+ loading, // True during initial load
+ error, // ApolloError if request failed
+ networkStatus, // Detailed network state (1-8)
+ dataState, // For TypeScript type narrowing (AC 4.x)
+ refetch, // Function to re-execute query
+ fetchMore, // Function for pagination
+ startPolling, // Start polling at interval
+ stopPolling, // Stop polling
+ subscribeToMore, // Add subscription to query
+ updateQuery, // Manually update query result
+ client, // Apollo Client instance
+ called, // True if query has been executed
+ previousData, // Previous data (useful during loading)
+} = useQuery(QUERY);
+```
+
+## Query Variables
+
+### Basic Variables
+
+```tsx
+const GET_DOG = gql`
+ query GetDog($breed: String!) {
+ dog(breed: $breed) {
+ id
+ displayImage
+ }
+ }
+`;
+
+function DogPhoto({ breed }: { breed: string }) {
+ const { loading, error, data } = useQuery(GET_DOG, {
+ variables: { breed },
+ });
+
+ if (loading) return null;
+ if (error) return
+ >
+ );
+}
+```
+
+## useLazyQuery
+
+Use `useLazyQuery` when you want to execute a query in response to a user-triggered event (like a button click) rather than on component mount.
+
+**Important**: `useLazyQuery` doesn't guarantee a network request - it only sets variables. If data is already in the cache, this isn't a "refetch". Only use `useLazyQuery` if you consume the second tuple value (loading, data, error states) to synchronize cache data with the component. If you only need the promise, use `client.query` directly instead.
+
+### Basic Usage
+
+```tsx
+import { gql } from "@apollo/client";
+import { useLazyQuery } from "@apollo/client/react";
+
+const GET_DOG_PHOTO = gql`
+ query GetDogPhoto($breed: String!) {
+ dog(breed: $breed) {
+ id
+ displayImage
+ }
+ }
+`;
+
+function DelayedQuery() {
+ const [getDog, { loading, error, data, called }] = useLazyQuery(GET_DOG_PHOTO);
+
+ if (called && loading) return
Loading...
;
+ if (error) return
Error: {error.message}
;
+
+ return (
+
+ {data?.dog && }
+
+
+ );
+}
+```
+
+### When to Use client.query Instead
+
+If you only need the promise result and don't consume the loading/error/data states from the hook, use `client.query` instead:
+
+```tsx
+import { useApolloClient } from "@apollo/client/react";
+
+function SearchDogs() {
+ const client = useApolloClient();
+ const [search, setSearch] = useState("");
+
+ const handleSearch = async () => {
+ try {
+ const { data } = await client.query({
+ query: SEARCH_DOGS,
+ variables: { query: search },
+ });
+ console.log("Found dogs:", data.searchDogs);
+ } catch (error) {
+ console.error("Search failed:", error);
+ }
+ };
+
+ return (
+
;
+}
+```
+
+> **Note**: Using `skipToken` is preferred over `skip` as it avoids TypeScript issues with required variables and the non-null assertion operator.
+
+### SSR Skip
+
+```tsx
+// Skip during server-side rendering
+const { data } = useQuery(GET_USER_LOCATION, {
+ skip: typeof window === "undefined",
+ ssr: false,
+});
+```
diff --git a/.agents/skills/apollo-client/references/state-management.md b/.agents/skills/apollo-client/references/state-management.md
new file mode 100644
index 000000000..5bcd6f7a5
--- /dev/null
+++ b/.agents/skills/apollo-client/references/state-management.md
@@ -0,0 +1,412 @@
+# State Management Reference
+
+## Table of Contents
+
+- [Reactive Variables](#reactive-variables)
+- [Local-Only Fields](#local-only-fields)
+- [Type Policies for Local State](#type-policies-for-local-state)
+- [Combining Remote and Local State](#combining-remote-and-local-state)
+- [useReactiveVar Hook](#usereactivevar-hook)
+
+## Reactive Variables
+
+Reactive variables are a way to store local state outside of the Apollo Client cache while still triggering reactive updates.
+
+**Important**: Reactive variables store a single value that notifies `ApolloClient` instances when changed. They do not have separate values per ApolloClient instance. In multi-user environments like SSR, global or module-level reactive variables could be shared between users and cause data leaks. In frameworks that use SSR, always avoid storing reactive variables as globals.
+
+### Creating Reactive Variables
+
+```typescript
+import { makeVar } from "@apollo/client";
+
+// Simple reactive variable
+export const isLoggedInVar = makeVar(false);
+
+// Object reactive variable
+export const cartItemsVar = makeVar([]);
+
+// Complex state
+interface AppState {
+ theme: "light" | "dark";
+ sidebarOpen: boolean;
+ notifications: Notification[];
+}
+
+export const appStateVar = makeVar({
+ theme: "light",
+ sidebarOpen: true,
+ notifications: [],
+});
+```
+
+### Reading Reactive Variables
+
+```tsx
+// Direct read (non-reactive)
+const isLoggedIn = isLoggedInVar();
+
+// Reactive read in component
+import { useReactiveVar } from "@apollo/client/react";
+
+function AuthButton() {
+ const isLoggedIn = useReactiveVar(isLoggedInVar);
+
+ return isLoggedIn ? (
+
+ ) : (
+
+ );
+}
+```
+
+### Updating Reactive Variables
+
+```typescript
+// Set new value
+isLoggedInVar(true);
+
+// Update based on current value
+cartItemsVar([...cartItemsVar(), newItem]);
+
+// Update object state
+appStateVar({
+ ...appStateVar(),
+ theme: "dark",
+});
+
+// Helper function pattern
+export function toggleSidebar() {
+ const current = appStateVar();
+ appStateVar({ ...current, sidebarOpen: !current.sidebarOpen });
+}
+
+export function addNotification(notification: Notification) {
+ const current = appStateVar();
+ appStateVar({
+ ...current,
+ notifications: [...current.notifications, notification],
+ });
+}
+```
+
+## Local-Only Fields
+
+Local-only fields are fields defined in queries but resolved entirely on the client using the `@client` directive.
+
+**Important**: To use any `@client` fields, you need to add `LocalState` to the `ApolloClient` initialization:
+
+```typescript
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+import { LocalState } from "@apollo/client/local-state";
+
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ localState: new LocalState({}),
+ // ... other options
+});
+```
+
+> **Note**: `LocalState` is an Apollo Client 4.x concept and did not exist as a class in previous versions. In previous versions, a `localState` option was not necessary, and local resolvers (if used) could be passed directly to the `ApolloClient` constructor.
+
+### Basic @client Fields
+
+```tsx
+const GET_USER_WITH_LOCAL = gql`
+ query GetUserWithLocal($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ # Local-only fields
+ isSelected @client
+ displayName @client
+ }
+ }
+`;
+
+function UserCard({ userId }: { userId: string }) {
+ const { data } = useQuery(GET_USER_WITH_LOCAL, {
+ variables: { id: userId },
+ });
+
+ return (
+
+
{data?.user.displayName}
+
{data?.user.email}
+
+ );
+}
+```
+
+### Local Field Read Functions (Type Policies)
+
+Local field `read` functions are defined in entity-level type policies. You can use reactive variables inside these `read` functions, along with other calculations or derived values:
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ User: {
+ fields: {
+ // Simple local field from reactive variable
+ isSelected: {
+ read(_, { readField }) {
+ const id = readField("id");
+ return selectedUsersVar().includes(id);
+ },
+ },
+
+ // Computed local field (derived value)
+ displayName: {
+ read(_, { readField }) {
+ const name = readField("name");
+ const email = readField("email");
+ return name || email?.split("@")[0] || "Anonymous";
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+## LocalState Resolvers
+
+### Query-Level Local Resolvers
+
+Query-level local fields can be defined using `LocalState` resolvers. **Note**: Do not read reactive variables inside LocalState resolvers - this is not a documented/tested feature. It might not behave as expected.
+
+```typescript
+import { LocalState } from "@apollo/client/local-state";
+
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ localState: new LocalState({
+ resolvers: {
+ Query: {
+ // Read from localStorage
+ theme: () => {
+ if (typeof window !== "undefined") {
+ return localStorage.getItem("theme") || "light";
+ }
+ return "light";
+ },
+
+ // Read from cache
+ currentUser: (_, __, { cache }) => {
+ const userId = localStorage.getItem("currentUserId");
+ if (!userId) return null;
+ return cache.readFragment({
+ id: cache.identify({ __typename: "User", id: userId }),
+ fragment: gql`
+ fragment CurrentUser on User {
+ id
+ name
+ email
+ }
+ `,
+ });
+ },
+
+ // Compute value
+ isOnline: () => {
+ if (typeof navigator !== "undefined") {
+ return navigator.onLine;
+ }
+ return true;
+ },
+ },
+ },
+ }),
+});
+```
+
+### Using Local Query Fields
+
+```tsx
+const GET_AUTH_STATE = gql`
+ query GetAuthState {
+ isLoggedIn @client
+ currentUser @client {
+ id
+ name
+ email
+ }
+ }
+`;
+
+function AuthStatus() {
+ const { data } = useQuery(GET_AUTH_STATE);
+
+ if (!data?.isLoggedIn) {
+ return ;
+ }
+
+ return ;
+}
+```
+
+## Combining Remote and Local State
+
+### Mixing Remote and Local Fields
+
+```tsx
+const GET_PRODUCTS = gql`
+ query GetProducts {
+ products {
+ id
+ name
+ price
+ # Local fields
+ quantity @client
+ isInCart @client
+ }
+ }
+`;
+
+const cache = new InMemoryCache({
+ typePolicies: {
+ Product: {
+ fields: {
+ quantity: {
+ read(_, { readField }) {
+ const id = readField("id");
+ const cartItem = cartItemsVar().find((item) => item.productId === id);
+ return cartItem?.quantity ?? 0;
+ },
+ },
+
+ isInCart: {
+ read(_, { readField }) {
+ const id = readField("id");
+ return cartItemsVar().some((item) => item.productId === id);
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### Local Mutations
+
+```tsx
+import { LocalState } from "@apollo/client/local-state";
+
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ localState: new LocalState({
+ resolvers: {
+ Mutation: {
+ addToCart: (_, { productId, quantity }, { cache }) => {
+ // Read current cart from cache
+ const { cart } = cache.readQuery({ query: GET_CART }) || { cart: [] };
+
+ const existing = cart.find((item) => item.productId === productId);
+
+ const updatedCart = existing
+ ? cart.map((item) =>
+ item.productId === productId ? { ...item, quantity: item.quantity + quantity } : item,
+ )
+ : [...cart, { productId, quantity, __typename: "CartItem" }];
+
+ // Write updated cart back to cache
+ cache.writeQuery({
+ query: GET_CART,
+ data: { cart: updatedCart },
+ });
+
+ return true;
+ },
+ },
+ },
+ }),
+});
+
+const ADD_TO_CART = gql`
+ mutation AddToCart($productId: ID!, $quantity: Int!) {
+ addToCart(productId: $productId, quantity: $quantity) @client
+ }
+`;
+```
+
+### Persisting Local State
+
+```typescript
+// Create a helper function to permanently subscribe to reactive variable changes, without creating memory leaks
+function subscribeToVariable(weakRef: WeakRef>, listener: ReactiveListener) {
+ weakRef.deref()?.onNextChange((value) => {
+ listener(value);
+ subscribeToVariable(weakRef, listener);
+ });
+}
+
+// Create reactive variable with persistence
+const persistentCartVar = makeVar(
+ typeof window !== "undefined" && localStorage.getItem("cart") ? JSON.parse(localStorage.getItem("cart")!) : [],
+);
+
+// Save to localStorage when reactive variable changes
+subscribeToVariable(new WeakRef(persistentCartVar), (items) => {
+ try {
+ if (typeof window !== "undefined") {
+ localStorage.setItem("cart", JSON.stringify(items));
+ }
+ } catch (error) {
+ console.error("Failed to persist cart:", error);
+ }
+});
+```
+
+## useReactiveVar Hook
+
+The `useReactiveVar` hook subscribes a component to reactive variable updates.
+
+### Basic Usage
+
+```tsx
+import { useReactiveVar } from "@apollo/client/react";
+
+function ThemeToggle() {
+ const theme = useReactiveVar(themeVar);
+
+ return ;
+}
+```
+
+### With Derived State
+
+```tsx
+function CartSummary() {
+ const cartItems = useReactiveVar(cartItemsVar);
+
+ // Derived values are computed on each render
+ const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
+ const totalPrice = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
+
+ return (
+
+ );
+}
+```
diff --git a/.agents/skills/apollo-client/references/suspense-hooks.md b/.agents/skills/apollo-client/references/suspense-hooks.md
new file mode 100644
index 000000000..d5499a14f
--- /dev/null
+++ b/.agents/skills/apollo-client/references/suspense-hooks.md
@@ -0,0 +1,764 @@
+# Suspense Hooks Reference
+
+> **Note**: Suspense hooks are the recommended approach for data fetching in modern React applications (React 18+). They provide cleaner code, better loading state handling, and enable streaming SSR.
+
+## Table of Contents
+
+- [useSuspenseQuery Hook](#usesuspensequery-hook)
+- [useBackgroundQuery and useReadQuery](#usebackgroundquery-and-usereadquery)
+- [useLoadableQuery](#useloadablequery)
+- [createQueryPreloader](#createquerypreloader)
+- [useQueryRefHandlers](#usequeryrefhandlers)
+- [Distinguishing Queries with queryKey](#distinguishing-queries-with-querykey)
+- [Suspense Boundaries and Error Handling](#suspense-boundaries-and-error-handling)
+- [Transitions](#transitions)
+- [Avoiding Request Waterfalls](#avoiding-request-waterfalls)
+- [Fetch Policies](#fetch-policies)
+- [Streaming SSR or React Server Components](#streaming-ssr-or-react-server-components)
+- [Conditional Queries](#conditional-queries)
+
+## useSuspenseQuery Hook
+
+The `useSuspenseQuery` hook is the Suspense-ready replacement for `useQuery`. It initiates a network request and causes the component calling it to suspend while the request is made. Unlike `useQuery`, it does not return `loading` states—these are handled by React's Suspense boundaries and error boundaries.
+
+### Basic Usage
+
+```tsx
+import { Suspense } from "react";
+import { useSuspenseQuery } from "@apollo/client/react";
+import { GET_DOG } from "./queries.generated";
+
+function App() {
+ return (
+ Loading...}>
+
+
+ );
+}
+
+function Dog({ id }: { id: string }) {
+ const { data } = useSuspenseQuery(GET_DOG, {
+ variables: { id },
+ });
+
+ // data is always defined when this component renders
+ return
Name: {data.dog.name}
;
+}
+```
+
+### Return Object
+
+```typescript
+const {
+ data, // Query result data
+ dataState, // With default options: "complete" | "streaming"
+ // With returnPartialData: also "partial"
+ // With errorPolicy "all" or "ignore": also "empty"
+ error, // ApolloError (only when errorPolicy is "all" or "ignore")
+ networkStatus, // NetworkStatus.ready, NetworkStatus.loading, etc.
+ client, // Apollo Client instance
+ refetch, // Function to re-execute query
+ fetchMore, // Function for pagination
+} = useSuspenseQuery(QUERY, options);
+```
+
+### Key Differences from useQuery
+
+- **No `loading` boolean**: Component suspends instead of returning `loading: true`
+- **Error handling**: With default `errorPolicy` (`none`), errors are thrown and caught by error boundaries. With `errorPolicy: "all"` or `"ignore"`, the `error` property is returned and `data` may be `undefined`.
+- **`data` availability**: With default `errorPolicy` (`none`), `data` is guaranteed to be present when the component renders. With `errorPolicy: "all"` or `"ignore"`, when `dataState` is `empty`, `data` may be `undefined`.
+- **Suspense boundaries**: Must wrap component with `` to handle loading state
+
+### Changing Variables
+
+When variables change, `useSuspenseQuery` automatically re-runs the query. If the data is not in the cache, the component suspends again.
+
+```tsx
+import { useState } from "react";
+import { GET_DOGS } from "./queries.generated";
+
+function DogSelector() {
+ const { data } = useSuspenseQuery(GET_DOGS);
+ const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
+
+ return (
+ <>
+
+ Loading...}>
+
+
+ >
+ );
+}
+
+function Dog({ id }: { id: string }) {
+ const { data } = useSuspenseQuery(GET_DOG, {
+ variables: { id },
+ });
+
+ return (
+ <>
+
Name: {data.dog.name}
+
Breed: {data.dog.breed}
+ >
+ );
+}
+```
+
+### Rendering Partial Data
+
+Use `returnPartialData` to render immediately with partial cache data instead of suspending. The component will still suspend if there is no data in the cache.
+
+```tsx
+function Dog({ id }: { id: string }) {
+ const { data } = useSuspenseQuery(GET_DOG, {
+ variables: { id },
+ returnPartialData: true,
+ });
+
+ return (
+ <>
+
Name: {data.dog?.name ?? "Unknown"}
+ {data.dog?.breed &&
Breed: {data.dog.breed}
}
+ >
+ );
+}
+```
+
+## useBackgroundQuery and useReadQuery
+
+Use `useBackgroundQuery` with `useReadQuery` to avoid request waterfalls by starting a query in a parent component and reading the result in a child component. This pattern enables the parent to start fetching data before the child component renders.
+
+### Basic Usage
+
+```tsx
+import { Suspense } from "react";
+import { useBackgroundQuery, useReadQuery } from "@apollo/client/react";
+
+function Parent() {
+ // Start fetching immediately
+ const [queryRef] = useBackgroundQuery(GET_DOG, {
+ variables: { id: "3" },
+ });
+
+ return (
+ Loading...}>
+
+
+ );
+}
+
+function Child({ queryRef }: { queryRef: QueryRef }) {
+ // Read the query result
+ const { data } = useReadQuery(queryRef);
+
+ return
Name: {data.dog.name}
;
+}
+```
+
+### When to Use
+
+- **Avoiding waterfalls**: Start fetching data in a parent (preferably above a suspense boundary) before child components render
+- **Preloading data**: Begin fetching before the component that needs the data is ready
+- **Parallel queries**: Start multiple queries at once in a parent component
+
+### Return Values
+
+`useBackgroundQuery` returns a tuple:
+
+```typescript
+const [
+ queryRef, // QueryRef to pass to useReadQuery
+ { refetch, fetchMore, subscribeToMore }, // Helper functions
+] = useBackgroundQuery(QUERY, options);
+```
+
+`useReadQuery` returns the query result:
+
+```typescript
+const {
+ data, // Query result data (always defined)
+ dataState, // "complete" | "streaming" | "partial" | "empty"
+ error, // ApolloError (if errorPolicy allows)
+ networkStatus, // Detailed network state (1-8)
+} = useReadQuery(queryRef);
+```
+
+## useLoadableQuery
+
+Use `useLoadableQuery` to imperatively load a query in response to a user interaction (like a button click) instead of on component mount.
+
+### Basic Usage
+
+```tsx
+import { Suspense } from "react";
+import { useLoadableQuery, useReadQuery } from "@apollo/client/react";
+import { GET_GREETING } from "./queries.generated";
+
+function App() {
+ const [loadGreeting, queryRef] = useLoadableQuery(GET_GREETING);
+
+ return (
+ <>
+
+ Loading...}>{queryRef && }
+ >
+ );
+}
+
+function Greeting({ queryRef }: { queryRef: QueryRef }) {
+ const { data } = useReadQuery(queryRef);
+
+ return
{data.greeting.message}
;
+}
+```
+
+### Return Values
+
+```typescript
+const [
+ loadQuery, // Function to load the query
+ queryRef, // QueryRef (null until loadQuery is called)
+ { refetch, fetchMore, subscribeToMore, reset }, // Helper functions
+] = useLoadableQuery(QUERY, options);
+```
+
+### When to Use
+
+- **User-triggered fetching**: Load data in response to user actions
+- **Lazy loading**: Defer data fetching until it's actually needed
+- **Progressive disclosure**: Load data for UI elements that may not be initially visible
+
+## createQueryPreloader
+
+The `createQueryPreloader` function creates a `preloadQuery` function that can be used to initiate queries outside of React components. This is useful for preloading data before a component renders, such as in route loaders or event handlers.
+
+### Basic Usage
+
+```tsx
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+import { createQueryPreloader } from "@apollo/client/react";
+
+const client = new ApolloClient({
+ uri: "https://your-graphql-endpoint.com/graphql",
+ cache: new InMemoryCache(),
+});
+
+// Create a preload function
+export const preloadQuery = createQueryPreloader(client);
+```
+
+### Using preloadQuery with Route Loaders
+
+> **Note**: This example applies to React Router in non-framework mode. For React Router framework mode, see [setup-react-router.md](./setup-react-router.md).
+
+Use the preload function with React Router's `loader` function to begin loading data during route transitions:
+
+```tsx
+import { preloadQuery } from "@/lib/apollo-client";
+import { GET_DOG } from "./queries.generated";
+
+// React Router loader function
+export async function loader({ params }: { params: { id: string } }) {
+ return preloadQuery({
+ query: GET_DOG,
+ variables: { id: params.id },
+ });
+}
+
+// Route component
+export default function DogRoute() {
+ const queryRef = useLoaderData();
+
+ return (
+ Loading...}>
+
+
+ );
+}
+
+function DogDetails({ queryRef }: { queryRef: QueryRef }) {
+ const { data } = useReadQuery(queryRef);
+
+ return (
+
+
{data.dog.name}
+
Breed: {data.dog.breed}
+
+ );
+}
+```
+
+### Preventing Route Transitions Until Query Loads
+
+Use the `toPromise()` method to prevent route transitions until the query finishes loading:
+
+```tsx
+export async function loader({ params }: { params: { id: string } }) {
+ const queryRef = preloadQuery({
+ query: GET_DOG,
+ variables: { id: params.id },
+ });
+
+ // Wait for the query to complete before transitioning
+ return queryRef.toPromise();
+}
+```
+
+When `toPromise()` is used, the route transition waits for the query to complete, and the data renders immediately without showing a loading fallback.
+
+> **Note**: `toPromise()` resolves with the `queryRef` itself (not the data) to encourage using `useReadQuery` for cache updates. If you need raw query data in your loader, use `client.query()` directly.
+
+### With Next.js Server Components
+
+> **Note**: For Next.js App Router, use the `PreloadQuery` component from `@apollo/client-integration-nextjs` instead. See [setup-nextjs.md](./setup-nextjs.md) for details.
+
+## useQueryRefHandlers
+
+The `useQueryRefHandlers` hook provides access to `refetch` and `fetchMore` functions for queries initiated with `preloadQuery`, `useBackgroundQuery`, or `useLoadableQuery`. This is useful when you need to refetch or paginate data in components where the `queryRef` is passed through.
+
+> **Important:** Always call `useQueryRefHandlers` before `useReadQuery`. These two hooks interact with the same `queryRef`, and calling them in the wrong order could cause subtle bugs.
+
+### Basic Usage
+
+```tsx
+import { useQueryRefHandlers } from "@apollo/client/react";
+
+function Breeds({ queryRef }: { queryRef: QueryRef }) {
+ const { refetch } = useQueryRefHandlers(queryRef);
+ const { data } = useReadQuery(queryRef);
+ const [isPending, startTransition] = useTransition();
+
+ return (
+
+ );
+}
+```
+
+### When to Use
+
+- **Preloaded queries**: Access refetch/fetchMore for queries initiated with `preloadQuery`
+- **Background queries**: Use in child components receiving `queryRef` from `useBackgroundQuery`
+- **Loadable queries**: Refetch or paginate queries initiated with `useLoadableQuery`
+- **React transitions**: Integrate with transitions to avoid showing loading fallbacks during refetches
+
+## Distinguishing Queries with queryKey
+
+Apollo Client uses the combination of `query` and `variables` to uniquely identify each query. When multiple components use the same `query` and `variables`, they share the same identity and suspend at the same time, regardless of which component initiates the request.
+
+Use the `queryKey` option to ensure each hook has a unique identity:
+
+```tsx
+function UserProfile() {
+ // First query with unique key
+ const { data: userData } = useSuspenseQuery(GET_USER, {
+ variables: { id: "1" },
+ queryKey: ["user-profile"],
+ });
+
+ // Second query with same query and variables but different key
+ const { data: userPreview } = useSuspenseQuery(GET_USER, {
+ variables: { id: "1" },
+ queryKey: ["user-preview"],
+ });
+
+ return (
+
+
+
+
+ );
+}
+```
+
+### When to Use
+
+- **Multiple instances**: When rendering multiple components that use the same query and variables
+- **Preventing shared suspension**: When you want independent control over when each query suspends
+- **Separate cache entries**: When you need to maintain separate cache states for the same query
+
+> **Note**: Each item in the `queryKey` array must be a stable identifier to prevent infinite fetches.
+
+## Suspense Boundaries and Error Handling
+
+### Suspense Boundaries
+
+Wrap components that use Suspense hooks with `` boundaries to handle loading states. Place boundaries strategically to control the granularity of loading indicators.
+
+```tsx
+function App() {
+ return (
+ <>
+ {/* Top-level loading for entire page */}
+ }>
+
+
+
+ >
+ );
+}
+
+function Content() {
+ return (
+ <>
+
+ {/* Granular loading for sidebar */}
+ }>
+
+
+ >
+ );
+}
+```
+
+### Error Boundaries
+
+Suspense hooks throw errors to React error boundaries instead of returning them. Use error boundaries to handle GraphQL errors.
+
+```tsx
+import { ErrorBoundary } from "react-error-boundary";
+
+function App() {
+ return (
+ (
+
+
Something went wrong
+
{error.message}
+
+ )}
+ >
+ Loading...}>
+
+
+
+ );
+}
+```
+
+### Custom Error Policies
+
+Use `errorPolicy` to control how errors are handled:
+
+```tsx
+function Dog({ id }: { id: string }) {
+ const { data, error } = useSuspenseQuery(GET_DOG, {
+ variables: { id },
+ errorPolicy: "all", // Return both data and errors
+ });
+
+ return (
+ <>
+
Name: {data?.dog?.name ?? "Unknown"}
+ {error &&
Warning: {error.message}
}
+ >
+ );
+}
+```
+
+## Transitions
+
+Use React transitions to avoid showing loading UI when updating state. Transitions keep the previous UI visible while new data is fetching.
+
+### Using startTransition
+
+```tsx
+import { useState, Suspense, startTransition } from "react";
+
+function DogSelector() {
+ const { data } = useSuspenseQuery(GET_DOGS);
+ const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
+
+ return (
+ <>
+
+ Loading...}>
+
+
+ >
+ );
+}
+```
+
+### Using useTransition
+
+Use `useTransition` to get an `isPending` flag for visual feedback during transitions.
+
+```tsx
+import { useState, Suspense, useTransition } from "react";
+
+function DogSelector() {
+ const [isPending, startTransition] = useTransition();
+ const { data } = useSuspenseQuery(GET_DOGS);
+ const [selectedDog, setSelectedDog] = useState(data.dogs[0].id);
+
+ return (
+ <>
+
+ Loading...}>
+
+
+ >
+ );
+}
+```
+
+## Avoiding Request Waterfalls
+
+Request waterfalls occur when a child component waits for the parent to finish rendering before it can start fetching its own data. Use `useBackgroundQuery` to start fetching child data earlier in the component tree.
+
+> **Note**: When one query depends on the result of another query (e.g., the child query needs an ID from the parent query), the waterfall is unavoidable. The best solution is to restructure your schema to fetch all needed data in a single nested query.
+
+### Example: Independent Queries
+
+When queries don't depend on each other, use `useBackgroundQuery` to start them in parallel:
+
+```tsx
+const GET_USER = gql`
+ query GetUser($id: String!) {
+ user(id: $id) {
+ id
+ name
+ }
+ }
+`;
+
+const GET_POSTS = gql`
+ query GetPosts {
+ posts {
+ id
+ title
+ }
+ }
+`;
+
+function Parent() {
+ // Both queries start immediately - no waterfall
+ const [userRef] = useBackgroundQuery(GET_USER, {
+ variables: { id: "1" },
+ });
+
+ const [postsRef] = useBackgroundQuery(GET_POSTS);
+
+ return (
+ Loading...}>
+
+
+
+ );
+}
+
+function UserProfile({ queryRef }: { queryRef: QueryRef }) {
+ const { data } = useReadQuery(queryRef);
+
+ return
+ );
+}
+```
+
+## Fetch Policies
+
+Suspense hooks support most of the same fetch policies as `useQuery`, controlling how the query interacts with the cache. Note that `cache-only` and `standby` are not supported by Suspense hooks.
+
+| Policy | Description |
+| ------------------- | ---------------------------------------------------------- |
+| `cache-first` | Return cached data if available, otherwise fetch (default) |
+| `cache-and-network` | Return cached data immediately, then fetch and update |
+| `network-only` | Always fetch, update cache, ignore cached data |
+| `no-cache` | Always fetch, never read or write cache |
+
+### Usage Examples
+
+```tsx
+// Always fetch fresh data
+const { data } = useSuspenseQuery(GET_NOTIFICATIONS, {
+ fetchPolicy: "network-only",
+});
+
+// Prefer cached data
+const { data } = useSuspenseQuery(GET_CATEGORIES, {
+ fetchPolicy: "cache-first",
+});
+
+// Show cached data while fetching fresh data
+const { data } = useSuspenseQuery(GET_POSTS, {
+ fetchPolicy: "cache-and-network",
+});
+```
+
+## Streaming SSR or React Server Components
+
+Apollo Client integrates with modern React frameworks that support Streaming SSR and React Server Components. For detailed setup instructions specific to your framework, see:
+
+- **Next.js App Router**: [setup-nextjs.md](./setup-nextjs.md) - Includes React Server Components, PreloadQuery component, and streaming SSR
+- **React Router**: [setup-react-router.md](./setup-react-router.md) - Framework mode with SSR support
+- **TanStack Start**: [setup-tanstack-start.md](./setup-tanstack-start.md) - Full-stack React framework with SSR
+
+These guides cover:
+
+- Framework-specific client setup and configuration
+- Preloading queries for optimal performance
+- Streaming SSR with `useBackgroundQuery` and Suspense
+- Error handling in server-rendered environments
+
+## Conditional Queries
+
+### Using skipToken
+
+Use `skipToken` to conditionally skip queries without TypeScript issues. When `skipToken` is used, the component won't suspend and `data` will be `undefined`.
+
+```tsx
+import { skipToken } from "@apollo/client";
+
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+`;
+
+function UserProfile({ userId }: { userId: string | null }) {
+ const { data, dataState } = useSuspenseQuery(
+ GET_USER,
+ !userId
+ ? skipToken
+ : {
+ variables: { id: userId },
+ },
+ );
+
+ if (dataState !== "complete") {
+ return
Select a user
;
+ }
+
+ return ;
+}
+```
+
+### Conditional Rendering
+
+Alternatively, use conditional rendering to control when Suspense hooks are called. This provides better type safety and clearer component logic.
+
+```tsx
+function UserProfile({ userId }: { userId: string | null }) {
+ if (!userId) {
+ return
Select a user
;
+ }
+
+ return (
+ Loading...}>
+
+
+ );
+}
+
+function UserDetails({ userId }: { userId: string }) {
+ const { data } = useSuspenseQuery(GET_USER, {
+ variables: { id: userId },
+ });
+
+ return ;
+}
+```
+
+> **Note**: Using conditional rendering with `skipToken` provides better type safety and avoids issues with required variables. The `skip` option is deprecated in favor of `skipToken`.
diff --git a/.agents/skills/apollo-client/references/troubleshooting.md b/.agents/skills/apollo-client/references/troubleshooting.md
new file mode 100644
index 000000000..888020b74
--- /dev/null
+++ b/.agents/skills/apollo-client/references/troubleshooting.md
@@ -0,0 +1,491 @@
+# Troubleshooting Reference
+
+## Table of Contents
+
+- [Setup Issues](#setup-issues)
+- [Cache Issues](#cache-issues)
+- [TypeScript Issues](#typescript-issues)
+- [Performance Issues](#performance-issues)
+- [DevTools Usage](#devtools-usage)
+- [Common Error Messages](#common-error-messages)
+
+## Setup Issues
+
+### Provider Not Found
+
+**Error:** `Could not find "client" in the context or passed in as an option`
+
+**Cause:** Component is not wrapped with `ApolloProvider`.
+
+**Solution:**
+
+```tsx
+// Ensure ApolloProvider wraps your app
+import { ApolloProvider } from "@apollo/client";
+
+function App() {
+ return (
+
+
+
+ );
+}
+```
+
+### Multiple Apollo Clients
+
+**Problem:** Unintended cache isolation or conflicting states.
+
+**Solution:** Use a single client instance or explicitly manage multiple clients:
+
+```tsx
+// Single client (recommended)
+const client = new ApolloClient({
+ /* ... */
+});
+
+export function App() {
+ return (
+
+
+
+ );
+}
+
+// Multiple clients (rare use case)
+const publicClient = new ApolloClient({
+ uri: "/public/graphql",
+ cache: new InMemoryCache(),
+});
+const adminClient = new ApolloClient({
+ uri: "/admin/graphql",
+ cache: new InMemoryCache(),
+});
+
+function AdminSection() {
+ return (
+
+
+
+ );
+}
+```
+
+### Client Created in Component
+
+**Problem:** New client on every render causes cache loss.
+
+**Solution:** Create client outside component or use a ref pattern:
+
+```tsx
+// Bad - new client on every render
+function App() {
+ const client = new ApolloClient({
+ /* ... */
+ }); // Don't do this!
+ return ...;
+}
+
+// Module-level client definition
+// Okay if there is a 100% guarantee this application will never use SSR
+const client = new ApolloClient({
+ /* ... */
+});
+function App() {
+ return ...;
+}
+
+// Good - store Apollo Client in a ref that is initialized once
+function useApolloClient(makeApolloClient: () => ApolloClient): ApolloClient {
+ const storeRef = useRef(null);
+ if (!storeRef.current) {
+ storeRef.current = makeApolloClient();
+ }
+ return storeRef.current;
+}
+
+// Better - singleton global in non-SSR environments to survive unmounts
+const singleton = Symbol.for("ApolloClientSingleton");
+declare global {
+ interface Window {
+ [singleton]?: ApolloClient;
+ }
+}
+
+function useApolloClient(makeApolloClient: () => ApolloClient): ApolloClient {
+ const storeRef = useRef(null);
+ if (!storeRef.current) {
+ if (typeof window === "undefined") {
+ storeRef.current = makeApolloClient();
+ } else {
+ window[singleton] ??= makeApolloClient();
+ storeRef.current = window[singleton];
+ }
+ }
+ return storeRef.current;
+}
+// Note: this second option might need manual removal between tests
+```
+
+## Cache Issues
+
+### Stale Data Not Updating
+
+**Problem:** UI doesn't reflect mutations or other updates.
+
+**Solution 1:** Verify cache key identification:
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ // Ensure proper identification
+ Product: {
+ keyFields: ["id"], // or ['sku'] if no id field
+ },
+ },
+});
+```
+
+**Solution 2:** Update cache after mutations:
+
+```tsx
+const [deleteProduct] = useMutation(DELETE_PRODUCT, {
+ update: (cache, { data }) => {
+ cache.evict({ id: cache.identify(data.deleteProduct) });
+ cache.gc();
+ },
+});
+```
+
+**Solution 3:** Use appropriate fetch policy:
+
+```tsx
+const { data } = useQuery(GET_PRODUCTS, {
+ fetchPolicy: "cache-and-network", // Always fetch fresh data
+});
+```
+
+### Missing Cache Updates After Mutation
+
+**Problem:** New items don't appear in lists after creation.
+
+**Solution:** Manually update the cache:
+
+```tsx
+const [createProduct] = useMutation(CREATE_PRODUCT, {
+ update: (cache, { data }) => {
+ const existing = cache.readQuery<{ products: Product[] }>({
+ query: GET_PRODUCTS,
+ });
+
+ cache.writeQuery({
+ query: GET_PRODUCTS,
+ data: {
+ products: [...(existing?.products ?? []), data.createProduct],
+ },
+ });
+ },
+});
+```
+
+### Pagination Cache Issues
+
+**Problem:** Paginated data not merging correctly.
+
+**Solution:** Configure proper type policies:
+
+```typescript
+const cache = new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ products: {
+ keyArgs: ["category"], // Only category creates new cache entries
+ merge(existing = [], incoming) {
+ return [...existing, ...incoming];
+ },
+ },
+ },
+ },
+ },
+});
+```
+
+### Cache Normalization Problems
+
+**Problem:** Objects with same ID showing different data in different queries.
+
+**Debug:** Check cache contents:
+
+```typescript
+// In DevTools console or component
+console.log(client.cache.extract());
+```
+
+**Solution:** Ensure consistent `__typename` and `id` fields:
+
+```graphql
+query GetUsers {
+ users {
+ id # Always include id
+ name
+ }
+}
+```
+
+## TypeScript Issues
+
+### Type Generation Setup
+
+**Problem:** No type safety for GraphQL operations.
+
+**Solution:** Set up GraphQL Code Generator with the [recommended starter configuration](https://www.apollographql.com/docs/react/development-testing/graphql-codegen#recommended-starter-configuration), as described in the [Skill](../SKILL.md).
+
+### Using Generated Types
+
+```tsx
+import { useQuery } from "@apollo/client/react";
+import { GetUsersDocument, GetUsersQuery } from "./generated/graphql";
+
+function UserList() {
+ // Fully typed without manual type annotations
+ const { data, loading, error } = useQuery(GetUsersDocument);
+
+ // data.users is automatically typed as GetUsersQuery['users']
+ return (
+
+ {data?.users.map((user) => (
+
{user.name}
+ ))}
+
+ );
+}
+```
+
+## Performance Issues
+
+### Over-Fetching
+
+**Problem:** Fetching more data than needed.
+
+**Solution:** Select only required fields:
+
+```graphql
+# Bad - fetching everything
+query GetUsers {
+ users {
+ id
+ name
+ email
+ profile { ... }
+ posts { ... }
+ friends { ... }
+ }
+}
+
+# Good - fetch what's needed
+query GetUserNames {
+ users {
+ id
+ name
+ }
+}
+```
+
+### N+1 Queries
+
+**Problem:** Multiple network requests for related data.
+
+**Solution:** Structure queries to batch requests. Best practice: use query colocation and compose queries from fragments defined on child components.
+
+```graphql
+# Bad - separate queries
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+}
+query GetUserPosts($userId: ID!) {
+ posts(userId: $userId) {
+ id
+ title
+ }
+}
+
+# Good - single query
+query GetUserWithPosts($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ posts {
+ id
+ title
+ }
+ }
+}
+```
+
+### Unnecessary Re-Renders
+
+**Problem:** Components re-render when unrelated cache data changes.
+
+**Solution:** Use `useFragment` and data masking for selective field reading. If that is not possible, `useQuery` with `@nonreactive` directives might be an alternative.
+
+```tsx
+// Prefer useFragment with data masking
+const { data } = useFragment({
+ fragment: USER_FRAGMENT,
+ from: { __typename: "User", id },
+});
+
+// Alternative: use @nonreactive directive
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ # This field won't trigger re-renders when it changes
+ metadata @nonreactive {
+ lastSeen
+ preferences
+ }
+ }
+ }
+`;
+
+const { data } = useQuery(GET_USER, {
+ variables: { id },
+});
+```
+
+### Cache Misses
+
+**Debug:** Use Apollo DevTools to inspect cache.
+
+```typescript
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ // DevTools are enabled by default in development
+ // Only configure this when you need to enable them in production
+ devtools: {
+ enabled: true,
+ },
+});
+```
+
+## DevTools Usage
+
+### Installing Apollo DevTools
+
+Install the browser extension:
+
+- [Chrome](https://chrome.google.com/webstore/detail/apollo-client-devtools/jdkknkkbebbapilgoeccciglkfbmbnfm)
+- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/apollo-developer-tools/)
+
+### Enabling DevTools
+
+DevTools are enabled by default in development. Only configure this setting if you need to enable them in production:
+
+```typescript
+const client = new ApolloClient({
+ cache: new InMemoryCache(),
+ devtools: {
+ enabled: true, // Set to true to enable in production
+ },
+});
+```
+
+### DevTools Features
+
+1. **Cache Inspector**: View normalized cache contents
+2. **Queries**: See active queries and their states
+3. **Mutations**: Track mutation history
+4. **Explorer**: Build and test queries against your schema
+5. **Memoization Limits**: Monitor and track cache memoization
+6. **Cache Writes**: Track all writes to the cache
+
+### Debugging Cache
+
+```typescript
+// Log cache contents
+console.log(JSON.stringify(client.cache.extract(), null, 2));
+
+// Check specific object using cache.identify
+console.log(
+ client.cache.readFragment({
+ id: cache.identify({ __typename: "User", id: 1 }),
+ fragment: gql`
+ fragment _ on User {
+ id
+ name
+ email
+ }
+ `,
+ }),
+);
+```
+
+## Common Error Messages
+
+### "Missing field 'X' in {...}"
+
+**Cause:** Query doesn't include required field for cache normalization.
+
+**Solution:** Include `id` and `__typename`:
+
+```graphql
+query GetUsers {
+ users {
+ id # Required for caching
+ __typename # Usually added automatically
+ name
+ }
+}
+```
+
+**Additional advice**: Read the full error message thoroughly.
+
+### "Store reset while query was in flight"
+
+**Cause:** `client.resetStore()` called during active queries.
+
+**Solution:** Wait for queries to complete or use `clearStore()`:
+
+```typescript
+// Option 1: Clear without refetching
+await client.clearStore();
+
+// Option 2: Reset and refetch active queries
+await client.resetStore();
+```
+
+### "Invariant Violation: X"
+
+**Cause:** Various configuration or usage errors.
+
+**Common fixes:**
+
+- Ensure `ApolloProvider` wraps the component tree
+- Check that `gql` tagged templates are valid GraphQL
+- Verify cache configuration matches your schema
+
+### "Cannot read property 'X' of undefined"
+
+**Cause:** Accessing data before query completes.
+
+**Solution:** Check `dataState` for proper type narrowing:
+
+```tsx
+const { data, dataState } = useQuery(GET_USER);
+
+// dataState can be "complete", "partial", "streaming", or "empty"
+// It describes the completeness of the data, not a loading state
+if (dataState === "empty") return ;
+
+// Now data is guaranteed to exist
+return
{data.user.name}
;
+
+// Or use optional chaining
+return
{data?.user?.name}
;
+```
diff --git a/.agents/skills/apollo-client/references/typescript-codegen.md b/.agents/skills/apollo-client/references/typescript-codegen.md
new file mode 100644
index 000000000..25ea76872
--- /dev/null
+++ b/.agents/skills/apollo-client/references/typescript-codegen.md
@@ -0,0 +1,130 @@
+# TypeScript Code Generation
+
+This guide covers setting up GraphQL Code Generator for type-safe Apollo Client usage with TypeScript.
+
+## Installation
+
+```bash
+npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
+```
+
+## Configuration
+
+Create a `codegen.ts` file in your project root:
+
+```typescript
+// codegen.ts
+import { CodegenConfig } from "@graphql-codegen/cli";
+
+const config: CodegenConfig = {
+ overwrite: true,
+ schema: "",
+ // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
+ documents: ["src/**/*.{ts,tsx}"],
+ // Don't exit with non-zero status when there are no documents
+ ignoreNoDocuments: true,
+ generates: {
+ // Use a path that works the best for the structure of your application
+ "./src/types/__generated__/graphql.ts": {
+ plugins: ["typescript", "typescript-operations", "typed-document-node"],
+ config: {
+ avoidOptionals: {
+ // Use `null` for nullable fields instead of optionals
+ field: true,
+ // Allow nullable input fields to remain unspecified
+ inputValue: false,
+ },
+ // Use `unknown` instead of `any` for unconfigured scalars
+ defaultScalarType: "unknown",
+ // Apollo Client always includes `__typename` fields
+ nonOptionalTypename: true,
+ // Apollo Client doesn't add the `__typename` field to root types so
+ // don't generate a type for the `__typename` for root operation types.
+ skipTypeNameForRoot: true,
+ },
+ },
+ },
+};
+
+export default config;
+```
+
+## Enable Data Masking
+
+To enable data masking with GraphQL Code Generator, create a type declaration file to inform Apollo Client about the generated types:
+
+```typescript
+// apollo-client.d.ts
+import { GraphQLCodegenDataMasking } from "@apollo/client/masking";
+
+declare module "@apollo/client" {
+ export interface TypeOverrides extends GraphQLCodegenDataMasking.TypeOverrides {}
+}
+```
+
+## Running Code Generation
+
+Add a script to your `package.json`:
+
+```json
+{
+ "scripts": {
+ "codegen": "graphql-codegen"
+ }
+}
+```
+
+Run code generation:
+
+```bash
+npm run codegen
+```
+
+## Usage with Apollo Client
+
+The typed-document-node plugin generates `TypedDocumentNode` types that Apollo Client hooks automatically infer.
+
+### Defining Operations
+
+Define your operations inline with the `if (false)` pattern. This allows GraphQL Code Generator to detect and extract operations without executing the code at runtime (bundlers omit this dead code during minification):
+
+```typescript
+import { gql } from "@apollo/client";
+
+// This query will never be consumed in runtime code, so it is wrapped in `if (false)` so the bundler can omit it when bundling.
+if (false) {
+ gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+ }
+ `;
+}
+```
+
+### Using Generated Types
+
+After running `npm run codegen`, import the generated `TypedDocumentNode`:
+
+```typescript
+import { useQuery } from "@apollo/client/react";
+import { GetUserDocument } from "./queries.generated";
+
+function UserProfile({ userId }: { userId: string }) {
+ // Types are automatically inferred from GetUserDocument
+ const { data } = useQuery(GetUserDocument, {
+ variables: { id: userId },
+ });
+
+ return
{data.user.name}
;
+}
+```
+
+## Important Notes
+
+- The typed-document-node plugin might have a bundle size tradeoff but can prevent inconsistencies and is best suited for usage with LLMs, so it is recommended for most applications.
+- See the [GraphQL Code Generator documentation](https://www.apollographql.com/docs/react/development-testing/graphql-codegen#recommended-starter-configuration) for other recommended configuration patterns if required.
+- Apollo Client hooks automatically infer types from `TypedDocumentNode` - never use manual generics like `useQuery()`.
diff --git a/.agents/skills/apollo-mcp-server/SKILL.md b/.agents/skills/apollo-mcp-server/SKILL.md
new file mode 100644
index 000000000..e1cfa6904
--- /dev/null
+++ b/.agents/skills/apollo-mcp-server/SKILL.md
@@ -0,0 +1,306 @@
+---
+name: apollo-mcp-server
+description: >
+ Guide for using Apollo MCP Server to connect AI agents with GraphQL APIs.
+ Use this skill when: (1) setting up or configuring Apollo MCP Server,
+ (2) defining MCP tools from GraphQL operations, (3) using introspection
+ tools (introspect, search, validate, execute), (4) troubleshooting
+ MCP server connectivity or tool execution issues.
+license: MIT
+compatibility: Works with Claude Code, Claude Desktop, Cursor.
+metadata:
+ author: apollographql
+ version: "1.1.0"
+allowed-tools: Bash(rover:*) Bash(npx:*) Read Write Edit Glob Grep
+---
+
+# Apollo MCP Server Guide
+
+Apollo MCP Server exposes GraphQL operations as MCP tools, enabling AI agents to interact with GraphQL APIs through the Model Context Protocol.
+
+## Quick Start
+
+### Step 1: Install
+
+```bash
+# Linux / MacOS
+curl -sSL https://mcp.apollo.dev/download/nix/latest | sh
+
+# Windows
+iwr 'https://mcp.apollo.dev/download/win/latest' | iex
+```
+
+### Step 2: Configure
+
+Create `config.yaml` in your project root:
+
+```yaml
+# config.yaml
+transport:
+ type: streamable_http
+schema:
+ source: local
+ path: ./schema.graphql
+operations:
+ source: local
+ paths:
+ - ./operations/
+introspection:
+ introspect:
+ enabled: true
+ search:
+ enabled: true
+ validate:
+ enabled: true
+ execute:
+ enabled: true
+```
+
+Start the server:
+```bash
+apollo-mcp-server ./config.yaml
+```
+
+The MCP endpoint is available at `http://127.0.0.1:8000/mcp` (streamable_http defaults: address `127.0.0.1`, port `8000`). The GraphQL endpoint defaults to `http://localhost:4000/` — override with the `endpoint` key if your API runs elsewhere.
+
+### Step 3: Connect
+
+Add to your MCP client configuration:
+
+**Streamable HTTP (recommended):**
+
+Claude Desktop (`claude_desktop_config.json`):
+```json
+{
+ "mcpServers": {
+ "graphql-api": {
+ "command": "npx",
+ "args": ["mcp-remote", "http://127.0.0.1:8000/mcp"]
+ }
+ }
+}
+```
+
+Claude Code:
+```bash
+claude mcp add graphql-api -- npx mcp-remote http://127.0.0.1:8000/mcp
+```
+
+**Stdio (client launches the server directly):**
+
+Claude Desktop (`claude_desktop_config.json`) or Claude Code (`.mcp.json`):
+```json
+{
+ "mcpServers": {
+ "graphql-api": {
+ "command": "./apollo-mcp-server",
+ "args": ["./config.yaml"]
+ }
+ }
+}
+```
+
+## Built-in Tools
+
+Apollo MCP Server provides four introspection tools:
+
+| Tool | Purpose | When to Use |
+|------|---------|-------------|
+| `introspect` | Explore schema types in detail | Need type definitions, fields, relationships |
+| `search` | Find types in schema | Looking for specific types or fields |
+| `validate` | Check operation validity | Before executing operations |
+| `execute` | Run ad-hoc GraphQL operations | Testing or one-off queries |
+
+## Defining Custom Tools
+
+MCP tools are created from GraphQL operations. Three methods:
+
+### 1. Operation Files (Recommended)
+
+```yaml
+operations:
+ source: local
+ paths:
+ - ./operations/
+```
+
+```graphql
+# operations/users.graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+}
+
+mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ name
+ }
+}
+```
+
+Each named operation becomes an MCP tool.
+
+### 2. Operation Collections
+
+```yaml
+operations:
+ source: collection
+ id: your-collection-id
+```
+
+Use GraphOS Studio to manage operations collaboratively.
+
+### 3. Persisted Queries
+
+```yaml
+operations:
+ source: manifest
+ path: ./persisted-query-manifest.json
+```
+
+For production environments with pre-approved operations.
+
+## Reference Files
+
+Detailed documentation for specific topics:
+
+- [Tools](references/tools.md) - Introspection tools and minify notation
+- [Configuration](references/configuration.md) - All configuration options
+- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions
+
+## Key Rules
+
+### Security
+
+- **Never expose sensitive operations** without authentication
+- Use `headers` configuration for API keys and tokens
+- Disable introspection tools in production (they are disabled by default)
+- Set `overrides.mutation_mode: explicit` to require confirmation for mutations
+
+### Authentication
+
+```yaml
+# Static header
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+
+# Dynamic header forwarding
+forward_headers:
+ - x-forwarded-token
+
+# OAuth (streamable_http transport)
+transport:
+ type: streamable_http
+ auth:
+ servers:
+ - https://auth.example.com/.well-known/openid-configuration
+ audiences:
+ - https://api.example.com
+```
+
+### Token Optimization
+
+Enable minification to reduce token usage:
+
+```yaml
+introspection:
+ introspect:
+ minify: true
+ search:
+ minify: true
+```
+
+Minified output uses compact notation:
+- **T** = type, **I** = input, **E** = enum
+- **s** = String, **i** = Int, **b** = Boolean, **f** = Float, **d** = ID
+- **!** = required, **[]** = list
+
+### Mutations
+
+Control mutation behavior via the `overrides` section:
+
+```yaml
+overrides:
+ mutation_mode: all # Execute mutations directly
+ # mutation_mode: explicit # Require explicit confirmation
+ # mutation_mode: none # Block all mutations (default)
+```
+
+## Common Patterns
+
+### GraphOS Cloud Schema
+
+```yaml
+# schema.source defaults to uplink — can be omitted when graphos is configured
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: my-graph@production
+```
+
+### Local Development
+
+```yaml
+transport:
+ type: streamable_http
+schema:
+ source: local
+ path: ./schema.graphql
+introspection:
+ introspect:
+ enabled: true
+ search:
+ enabled: true
+ validate:
+ enabled: true
+ execute:
+ enabled: true
+overrides:
+ mutation_mode: all
+```
+
+### Production Setup
+
+```yaml
+transport:
+ type: streamable_http
+endpoint: https://api.production.com/graphql
+operations:
+ source: manifest
+ path: ./persisted-query-manifest.json
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: ${env.APOLLO_GRAPH_REF}
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+health_check:
+ enabled: true
+```
+
+### Docker
+
+```yaml
+transport:
+ type: streamable_http
+ address: 0.0.0.0
+ port: 8000
+endpoint: ${env.GRAPHQL_ENDPOINT}
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: ${env.APOLLO_GRAPH_REF}
+health_check:
+ enabled: true
+```
+
+## Ground Rules
+
+- ALWAYS configure authentication before exposing to AI agents
+- ALWAYS use `mutation_mode: explicit` or `mutation_mode: none` in shared environments
+- NEVER expose introspection tools with write access to production data
+- PREFER operation files over ad-hoc execute for predictable behavior
+- PREFER streamable_http transport for remote and multi-client deployments
+- USE stdio only when the MCP client launches the server process directly
+- USE GraphOS Studio collections for team collaboration
diff --git a/.agents/skills/apollo-mcp-server/references/configuration.md b/.agents/skills/apollo-mcp-server/references/configuration.md
new file mode 100644
index 000000000..fdbe63945
--- /dev/null
+++ b/.agents/skills/apollo-mcp-server/references/configuration.md
@@ -0,0 +1,485 @@
+# Apollo MCP Server Configuration Reference
+
+## Table of Contents
+
+- [Configuration File](#configuration-file)
+- [Core Settings](#core-settings)
+ - [endpoint](#endpoint)
+ - [schema](#schema)
+ - [operations](#operations)
+- [Transport](#transport)
+- [Headers](#headers)
+- [Introspection](#introspection)
+- [Overrides](#overrides)
+- [GraphOS Integration](#graphos-integration)
+- [Advanced Settings](#advanced-settings)
+- [Environment Variables](#environment-variables)
+- [Configuration Examples](#configuration-examples)
+
+---
+
+## Configuration File
+
+Apollo MCP Server uses YAML configuration. Pass the config file path as an argument:
+
+```bash
+apollo-mcp-server ./path/to/config.yaml
+```
+
+---
+
+## Core Settings
+
+### endpoint
+
+The GraphQL API endpoint URL. Defaults to `http://localhost:4000/`.
+
+```yaml
+endpoint: https://api.example.com/graphql
+```
+
+### schema
+
+Schema source configuration. Two options available:
+
+#### Local File
+
+```yaml
+schema:
+ source: local
+ path: ./schema.graphql
+```
+
+#### GraphOS Uplink (Default)
+
+`uplink` is the default schema source. When using uplink, you can omit the `schema` section entirely if `graphos` credentials are configured.
+
+```yaml
+schema:
+ source: uplink
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: my-graph@production
+```
+
+### operations
+
+Define which GraphQL operations become MCP tools. Defaults to `infer` (auto-discovers from schema).
+
+#### Infer (Default)
+
+```yaml
+operations:
+ source: infer
+```
+
+#### Local Files
+
+```yaml
+operations:
+ source: local
+ paths:
+ - ./operations/**/*.graphql
+```
+
+#### GraphOS Collection
+
+```yaml
+operations:
+ source: collection
+ id: abc123-collection-id
+```
+
+#### Persisted Query Manifest
+
+```yaml
+operations:
+ source: manifest
+ path: ./persisted-query-manifest.json
+```
+
+#### GraphOS Uplink
+
+```yaml
+operations:
+ source: uplink
+```
+
+---
+
+## Transport
+
+Configure how the MCP server communicates.
+
+### Streamable HTTP
+
+HTTP server for network access and multi-client deployments:
+
+```yaml
+transport:
+ type: streamable_http
+```
+
+Defaults: `address: 127.0.0.1`, `port: 8000`. The MCP endpoint is served at `http://127.0.0.1:8000/mcp`.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `address` | `127.0.0.1` | Bind address |
+| `port` | `8000` | Listen port |
+| `stateful_mode` | - | Session handling mode |
+
+#### Host Validation
+
+Controls which `Host` header values are accepted (streamable_http only):
+
+```yaml
+transport:
+ type: streamable_http
+ host_validation:
+ enabled: true
+ allowed_hosts:
+ - "example.com"
+ - "*.example.com"
+```
+
+#### Auth
+
+OAuth-based authentication for streamable_http transport:
+
+```yaml
+transport:
+ type: streamable_http
+ auth:
+ servers:
+ - https://auth.example.com/.well-known/openid-configuration
+ audiences:
+ - https://api.example.com
+ scopes:
+ - read
+ - write
+ scope_mode: any # any | all
+```
+
+### Stdio (Default)
+
+Standard input/output for direct CLI integration. This is the default transport when no `transport` section is specified:
+
+```yaml
+transport:
+ type: stdio
+```
+
+> **Note:** SSE transport was removed in v1.5.0. Use `streamable_http` instead.
+
+---
+
+## Headers
+
+Configure HTTP headers for GraphQL requests.
+
+### Static Headers
+
+```yaml
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+ X-API-Key: ${env.API_KEY}
+```
+
+### Dynamic Header Forwarding
+
+Forward headers from MCP client requests to the upstream GraphQL API:
+
+```yaml
+forward_headers:
+ - x-forwarded-user-token
+ - x-request-id
+```
+
+### Combined
+
+```yaml
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+forward_headers:
+ - x-user-context
+ - x-request-id
+```
+
+---
+
+## Introspection
+
+Control built-in introspection tools. All tools are disabled by default.
+
+```yaml
+introspection:
+ introspect:
+ enabled: true
+ minify: true
+ search:
+ enabled: true
+ minify: true
+ validate:
+ enabled: true
+ execute:
+ enabled: true
+```
+
+---
+
+## Overrides
+
+Control mutation behavior and other global settings.
+
+```yaml
+overrides:
+ mutation_mode: explicit # all | explicit | none
+```
+
+### Mutation Modes
+
+| Mode | Description |
+|------|-------------|
+| `all` | Execute mutations directly |
+| `explicit` | Require user confirmation |
+| `none` | Block all mutations (default) |
+
+---
+
+## GraphOS Integration
+
+Connect to Apollo GraphOS for managed schemas and operations.
+
+```yaml
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: my-graph@production
+```
+
+### With Uplink Schema
+
+```yaml
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: ${env.APOLLO_GRAPH_REF}
+```
+
+---
+
+## Advanced Settings
+
+### Custom Scalars
+
+Define how custom scalars are described to AI agents via an external JSON file:
+
+```yaml
+custom_scalars: ./scalars.json
+```
+
+```json
+// scalars.json
+{
+ "DateTime": "ISO 8601 date-time string (e.g. 2024-01-15T10:30:00Z)",
+ "JSON": "Arbitrary JSON object",
+ "UUID": "UUID v4 string (e.g. 550e8400-e29b-41d4-a716-446655440000)"
+}
+```
+
+### CORS (Streamable HTTP Transport)
+
+```yaml
+cors:
+ enabled: true
+ origins:
+ - http://localhost:3000
+ - https://app.example.com
+ # OR use match_origins for regex pattern matching:
+ # match_origins:
+ # - "^https://([a-z0-9]+[.])*example.com$"
+ # OR allow all origins (cannot be used with allow_credentials):
+ # allow_any_origin: true
+ allow_credentials: true
+ allow_methods:
+ - GET
+ - POST
+ allow_headers:
+ - accept
+ - content-type
+ - mcp-protocol-version
+ - mcp-session-id
+ expose_headers:
+ - mcp-session-id
+ max_age: 7200
+```
+
+### Health Check
+
+Health check is disabled by default. Applies to streamable_http transport only.
+
+```yaml
+health_check:
+ enabled: true
+ path: /health # default
+```
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `enabled` | `false` | Enable health endpoint |
+| `path` | `/health` | Health check path |
+
+Endpoints:
+- `GET /health` - Overall health: `{"status": "UP"}`
+- `GET /health?live` - Liveness probe
+- `GET /health?ready` - Readiness probe
+
+### Logging
+
+```yaml
+logging:
+ level: info # debug | info | warn | error
+ path: ./logs/mcp-server.log
+ rotation: daily
+```
+
+### Telemetry
+
+```yaml
+telemetry:
+ exporters:
+ metrics:
+ otlp:
+ endpoint: http://localhost:4317
+ tracing:
+ otlp:
+ endpoint: http://localhost:4317
+ service_name: graphql-mcp-server
+```
+
+---
+
+## Environment Variables
+
+### Config File Expansion
+
+Use `${env.VAR_NAME}` syntax inside YAML config files to reference environment variables:
+
+```yaml
+endpoint: ${env.GRAPHQL_ENDPOINT}
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+```
+
+### Environment Variable Overrides
+
+Any config option can be overridden via environment variables using the `APOLLO_MCP_` prefix with `__` (double underscore) as the nesting separator:
+
+```bash
+# Override transport type
+export APOLLO_MCP_TRANSPORT__TYPE=streamable_http
+
+# Override transport port
+export APOLLO_MCP_TRANSPORT__PORT=9000
+
+# Override logging level
+export APOLLO_MCP_LOGGING__LEVEL=debug
+```
+
+### GraphOS Variables
+
+| Variable | Description |
+|----------|-------------|
+| `APOLLO_KEY` | GraphOS API key |
+| `APOLLO_GRAPH_REF` | Graph reference (graph@variant) |
+
+---
+
+## Configuration Examples
+
+### Minimal Local Development
+
+```yaml
+schema:
+ source: local
+ path: ./schema.graphql
+introspection:
+ introspect:
+ enabled: true
+ search:
+ enabled: true
+ validate:
+ enabled: true
+ execute:
+ enabled: true
+overrides:
+ mutation_mode: all
+```
+
+### Production with GraphOS
+
+```yaml
+transport:
+ type: streamable_http
+endpoint: ${env.GRAPHQL_ENDPOINT}
+operations:
+ source: manifest
+ path: ./persisted-query-manifest.json
+graphos:
+ apollo_key: ${env.APOLLO_KEY}
+ apollo_graph_ref: ${env.APOLLO_GRAPH_REF}
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+health_check:
+ enabled: true
+```
+
+### Team Development
+
+```yaml
+transport:
+ type: streamable_http
+endpoint: https://dev-api.example.com/graphql
+schema:
+ source: local
+ path: ./schema.graphql
+operations:
+ source: local
+ paths:
+ - ./operations/**/*.graphql
+headers:
+ Authorization: "Bearer ${env.DEV_API_TOKEN}"
+introspection:
+ introspect:
+ enabled: true
+ minify: true
+ search:
+ enabled: true
+ minify: true
+ validate:
+ enabled: true
+ execute:
+ enabled: true
+overrides:
+ mutation_mode: explicit
+```
+
+### Read-Only Analytics
+
+```yaml
+endpoint: https://analytics.example.com/graphql
+schema:
+ source: local
+ path: ./analytics-schema.graphql
+operations:
+ source: local
+ paths:
+ - ./queries/**/*.graphql
+introspection:
+ introspect:
+ enabled: true
+ search:
+ enabled: true
+ validate:
+ enabled: true
+```
diff --git a/.agents/skills/apollo-mcp-server/references/tools.md b/.agents/skills/apollo-mcp-server/references/tools.md
new file mode 100644
index 000000000..979551876
--- /dev/null
+++ b/.agents/skills/apollo-mcp-server/references/tools.md
@@ -0,0 +1,302 @@
+# Apollo MCP Server Tools Reference
+
+## Table of Contents
+
+- [Introspection Tools](#introspection-tools)
+ - [introspect](#introspect)
+ - [search](#search)
+ - [validate](#validate)
+ - [execute](#execute)
+- [Minify Notation](#minify-notation)
+- [Custom Tools](#custom-tools)
+
+---
+
+## Introspection Tools
+
+Apollo MCP Server provides four built-in tools for schema exploration and operation execution. All tools are disabled by default and must be enabled in configuration.
+
+Each introspection tool supports an optional `hint` config option for providing custom instructions to the AI agent about when and how to use the tool.
+
+### introspect
+
+Explore schema types in detail with configurable depth.
+
+**Parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `type` | String | required | Type name to introspect |
+| `depth` | Int | 1 | Recursion depth for related types |
+| `minify` | Boolean | false | Use compact notation |
+
+**Examples:**
+
+```
+# Basic type introspection
+introspect(type: "User")
+
+# Deep introspection with related types
+introspect(type: "User", depth: 3)
+
+# Minified output for token efficiency
+introspect(type: "User", minify: true)
+```
+
+**Output (normal):**
+```graphql
+type User {
+ id: ID!
+ name: String!
+ email: String
+ posts: [Post!]!
+ createdAt: DateTime!
+}
+```
+
+**Output (minified):**
+```
+T User { id:d! name:s! email:s posts:[Post!]! createdAt:DateTime! }
+```
+
+**Depth Behavior:**
+
+- `depth: 1` - Only the requested type
+- `depth: 2` - Requested type + directly referenced types
+- `depth: 3` - Two levels of related types
+- Maximum recommended: 5
+
+### search
+
+Find types in the schema matching a query.
+
+**Parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `query` | String | required | Search term |
+| `leafDepth` | Int | 1 | Depth for leaf type expansion |
+| `minify` | Boolean | false | Use compact notation |
+
+**Config Options:**
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `index_memory_bytes` | `50000000` | Memory budget for the search index |
+| `leaf_depth` | `1` | Default leaf type expansion depth |
+
+**Behavior:**
+
+- Returns maximum 5 matching results
+- Searches type names, field names, and descriptions
+- Case-insensitive matching
+
+**Examples:**
+
+```
+# Find user-related types
+search(query: "user")
+
+# Search with expanded leaf types
+search(query: "product", leafDepth: 2)
+```
+
+**Output:**
+```
+Found 3 types matching "user":
+- User (type)
+- UserInput (input)
+- UserConnection (type)
+```
+
+### validate
+
+Check if a GraphQL operation is valid against the schema.
+
+**Parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `operation` | String | required | GraphQL operation to validate |
+
+**Validates:**
+
+- Syntax correctness
+- Schema compliance (fields exist, types match)
+- Variable definitions
+- Fragment validity
+
+**Examples:**
+
+```
+validate(operation: """
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ nonExistentField
+ }
+ }
+""")
+```
+
+**Output (error):**
+```
+Validation failed:
+- Field "nonExistentField" not found on type "User"
+```
+
+**Output (success):**
+```
+Operation is valid.
+Variables required: { id: ID! }
+```
+
+### execute
+
+Run ad-hoc GraphQL operations against the endpoint.
+
+**Parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `operation` | String | required | GraphQL operation |
+| `variables` | Object | {} | Operation variables |
+
+**Mutation Mode:**
+
+Behavior depends on `overrides.mutation_mode` configuration:
+
+| Mode | Query | Mutation |
+|------|-------|----------|
+| `all` | Execute | Execute |
+| `explicit` | Execute | Require confirmation |
+| `none` | Execute | Block |
+
+**Examples:**
+
+```
+# Query execution
+execute(
+ operation: "query { users { id name } }"
+)
+
+# With variables
+execute(
+ operation: """
+ query GetUser($id: ID!) {
+ user(id: $id) { id name }
+ }
+ """,
+ variables: { id: "123" }
+)
+
+# Mutation (requires appropriate mutation_mode)
+execute(
+ operation: """
+ mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) { id }
+ }
+ """,
+ variables: { input: { name: "Alice", email: "alice@example.com" } }
+)
+```
+
+---
+
+## Minify Notation
+
+Compact notation reduces token usage by 40-60%. Enable globally or per-request.
+
+### Type Abbreviations
+
+| Symbol | Meaning |
+|--------|---------|
+| `T` | type |
+| `I` | input |
+| `E` | enum |
+| `U` | union |
+| `F` | interface |
+
+### Scalar Abbreviations
+
+| Symbol | Meaning |
+|--------|---------|
+| `s` | String |
+| `i` | Int |
+| `f` | Float |
+| `b` | Boolean |
+| `d` | ID |
+
+### Modifiers
+
+| Symbol | Meaning |
+|--------|---------|
+| **!** | Non-null (required) |
+| **[]** | List |
+| **[!]** | List of non-null |
+| **[]!** | Non-null list |
+| **[!]!** | Non-null list of non-null |
+| `@D` | Deprecated |
+| `<>` | Implements |
+
+### Examples
+
+**Normal:**
+```graphql
+type Product {
+ id: ID!
+ name: String!
+ price: Float!
+ description: String
+ tags: [String!]!
+ variants: [ProductVariant!]
+}
+```
+
+**Minified:**
+```
+T Product { id:d! name:s! price:f! description:s tags:[s!]! variants:[ProductVariant!] }
+```
+
+---
+
+## Custom Tools
+
+Each GraphQL operation becomes an MCP tool with:
+
+- **Tool name**: Operation name (e.g., `GetUser`, `CreateProduct`)
+- **Parameters**: Operation variables become tool parameters
+- **Description**: Generated from operation or custom via directive
+
+### Tool Naming
+
+```graphql
+# Tool name: GetUserById
+query GetUserById($id: ID!) {
+ user(id: $id) { id name }
+}
+
+# Tool name: CreateProduct
+mutation CreateProduct($input: ProductInput!) {
+ createProduct(input: $input) { id }
+}
+```
+
+### Adding Descriptions
+
+Use comments for tool descriptions:
+
+```graphql
+# Fetches a user by their unique identifier.
+# Returns user profile including name and email.
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+}
+```
+
+The comment becomes the MCP tool description, helping AI agents understand when to use each tool.
diff --git a/.agents/skills/apollo-mcp-server/references/troubleshooting.md b/.agents/skills/apollo-mcp-server/references/troubleshooting.md
new file mode 100644
index 000000000..7779407dc
--- /dev/null
+++ b/.agents/skills/apollo-mcp-server/references/troubleshooting.md
@@ -0,0 +1,309 @@
+# Apollo MCP Server Troubleshooting
+
+## Table of Contents
+
+- [Debugging with MCP Inspector](#debugging-with-mcp-inspector)
+- [Connection Issues](#connection-issues)
+- [Authentication Problems](#authentication-problems)
+- [Schema Issues](#schema-issues)
+- [Tool Execution Errors](#tool-execution-errors)
+- [Health Check](#health-check)
+
+---
+
+## Debugging with MCP Inspector
+
+MCP Inspector provides visual debugging for MCP servers.
+
+### Installation
+
+```bash
+npx @modelcontextprotocol/inspector
+```
+
+### Usage
+
+```bash
+# Start inspector with your MCP server using the Streamable HTTP transport
+npx @modelcontextprotocol/inspector --transport http --server-url http://localhost:8000/mcp
+```
+
+```bash
+# Start inspector with your MCP server using the STDIO transport
+npx @modelcontextprotocol/inspector apollo-mcp-server ./config.yaml
+```
+
+### Inspector Features
+
+- View available tools and their schemas
+- Test tool invocations interactively
+- Inspect request/response payloads
+- Monitor server logs in real-time
+
+---
+
+## Connection Issues
+
+### Server Won't Start
+
+**Symptoms:** Server exits immediately or hangs
+
+**Solutions:**
+
+1. Check config file syntax:
+```bash
+# Validate YAML
+npx yaml-lint config.yaml
+```
+
+2. Enable debug logging:
+```bash
+APOLLO_MCP_LOGGING__LEVEL=debug apollo-mcp-server ./config.yaml
+```
+
+### Client Can't Connect
+
+**Symptoms:** MCP client shows "server not found" or timeout
+
+**Solutions:**
+
+**Streamable HTTP (recommended for remote/multi-client):**
+
+Configure your client to connect via `npx mcp-remote`:
+
+```json
+{
+ "mcpServers": {
+ "graphql": {
+ "command": "npx",
+ "args": ["mcp-remote", "http://127.0.0.1:8000/mcp"]
+ }
+ }
+}
+```
+
+**Stdio (client launches the server process):**
+
+```json
+{
+ "mcpServers": {
+ "graphql": {
+ "command": "./apollo-mcp-server",
+ "args": ["/absolute/path/to/config.yaml"]
+ }
+ }
+}
+```
+
+Test server manually:
+```bash
+apollo-mcp-server ./config.yaml
+# Should output JSON-RPC initialization
+```
+
+Check binary is installed:
+```bash
+which apollo-mcp-server
+```
+
+---
+
+## Authentication Problems
+
+### 401 Unauthorized
+
+**Symptoms:** All requests return 401
+
+**Solutions:**
+
+1. Verify API token is set:
+```bash
+echo $API_TOKEN # Should not be empty
+```
+
+2. Check header configuration:
+```yaml
+headers:
+ Authorization: "Bearer ${env.API_TOKEN}"
+```
+
+3. For dynamic token forwarding:
+```yaml
+forward_headers:
+ - x-forwarded-authorization
+```
+
+### Token Security Best Practices
+
+- **Never commit tokens** to version control
+- Use environment variables or secrets management
+- Rotate tokens regularly
+- Use minimum required permissions
+
+### OAuth Authentication
+
+For streamable_http transport, use `transport.auth` for OAuth:
+
+```yaml
+transport:
+ type: streamable_http
+ auth:
+ servers:
+ - https://auth.example.com/.well-known/openid-configuration
+ audiences:
+ - https://api.example.com
+ scopes:
+ - read
+```
+
+For forwarding user tokens to the upstream GraphQL API:
+
+```yaml
+forward_headers:
+ - authorization
+```
+
+**Security Warning:** Forwarding OAuth tokens exposes them to the MCP server. Ensure:
+- Server runs in trusted environment
+- Transport is encrypted (HTTPS)
+- Tokens have minimal scope
+
+---
+
+## Schema Issues
+
+### Schema Not Found
+
+**Symptoms:** "Schema file not found" error
+
+**Solutions:**
+
+1. Check file path (use absolute paths):
+```yaml
+schema:
+ source: local
+ path: /absolute/path/to/schema.graphql
+```
+
+2. Verify file exists:
+```bash
+ls -la ./schema.graphql
+```
+
+### GraphOS Uplink Errors
+
+**Symptoms:** "Failed to fetch schema from uplink"
+
+**Solutions:**
+
+1. Verify GraphOS credentials:
+```bash
+echo $APOLLO_KEY
+echo $APOLLO_GRAPH_REF
+```
+
+2. Check graph reference format:
+```yaml
+graphos:
+ apollo_graph_ref: my-graph@production # Format: graph@variant
+```
+
+---
+
+## Tool Execution Errors
+
+### Operation Validation Failed
+
+**Symptoms:** "Field X not found on type Y"
+
+**Solutions:**
+
+1. Ensure schema is up to date
+2. Use `validate` tool before `execute`:
+```
+validate(operation: "query { user { id name } }")
+```
+
+### Mutation Blocked
+
+**Symptoms:** "Mutations are disabled"
+
+**Solutions:**
+
+Check mutation mode in config:
+```yaml
+overrides:
+ mutation_mode: all # Or 'explicit' for confirmation
+```
+
+### Variable Type Mismatch
+
+**Symptoms:** "Variable $id expected ID!, got String"
+
+**Solutions:**
+
+Ensure variable types match operation:
+```graphql
+# Operation expects ID!
+query GetUser($id: ID!) { ... }
+
+# Correct invocation
+execute(variables: { id: "123" }) # String coerced to ID
+```
+
+---
+
+## Health Check
+
+For streamable_http transport, health endpoints help diagnose issues.
+
+### Endpoints
+
+| Endpoint | Purpose |
+|----------|---------|
+| `/health` | Overall health status |
+| `/health?live` | Liveness probe (is server running?) |
+| `/health?ready` | Readiness probe (can server handle requests?) |
+
+### Status Codes
+
+| Code | Meaning |
+|------|---------|
+| 200 | Healthy |
+| 503 | Unhealthy |
+
+### Example Check
+
+```bash
+curl http://localhost:8000/health
+# {"status": "UP"}
+
+curl http://localhost:8000/health?ready
+# {"status": "UP"}
+```
+
+### Common Health Issues
+
+**Schema check failing:**
+- Schema file missing or invalid
+- GraphOS uplink unreachable
+
+**Endpoint check failing:**
+- GraphQL endpoint unreachable
+- Network/firewall issues
+- Authentication problems
+
+---
+
+## Getting Help
+
+If issues persist:
+
+1. Enable debug logging:
+```bash
+APOLLO_MCP_LOGGING__LEVEL=debug apollo-mcp-server ./config.yaml
+```
+
+2. Check Apollo documentation: https://apollographql.com/docs
+
+3. Report issues: https://github.com/apollographql/apollo-mcp-server/issues
diff --git a/.agents/skills/apollo-server/SKILL.md b/.agents/skills/apollo-server/SKILL.md
new file mode 100644
index 000000000..f20bb6a89
--- /dev/null
+++ b/.agents/skills/apollo-server/SKILL.md
@@ -0,0 +1,294 @@
+---
+name: apollo-server
+description: >
+ Guide for building GraphQL servers with Apollo Server 5.x. Use this skill when:
+ (1) setting up a new Apollo Server project,
+ (2) writing resolvers or defining GraphQL schemas,
+ (3) implementing authentication or authorization,
+ (4) creating plugins or custom data sources,
+ (5) troubleshooting Apollo Server errors or performance issues.
+license: MIT
+compatibility: Node.js v20+, TypeScript 4.7+. Works with Express v4/v5, standalone, Fastify, and serverless.
+metadata:
+ author: apollographql
+ version: "1.0.0"
+allowed-tools: Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
+---
+
+# Apollo Server 5.x Guide
+
+Apollo Server is an open-source GraphQL server that works with any GraphQL schema. Apollo Server 5 is framework-agnostic and runs standalone or integrates with Express, Fastify, and serverless environments.
+
+## Quick Start
+
+### Step 1: Install
+
+```bash
+npm install @apollo/server graphql
+```
+
+For Express integration:
+
+```bash
+npm install @apollo/server @as-integrations/express5 express graphql cors
+```
+
+### Step 2: Define Schema
+
+```typescript
+const typeDefs = `#graphql
+ type Book {
+ title: String
+ author: String
+ }
+
+ type Query {
+ books: [Book]
+ }
+`;
+```
+
+### Step 3: Write Resolvers
+
+```typescript
+const resolvers = {
+ Query: {
+ books: () => [
+ { title: "The Great Gatsby", author: "F. Scott Fitzgerald" },
+ { title: "1984", author: "George Orwell" },
+ ],
+ },
+};
+```
+
+### Step 4: Start Server
+
+**Standalone (Recommended for prototyping):**
+
+The standalone server is great for prototyping, but for production services, we recommend integrating Apollo Server with a more fully-featured web framework such as Express, Koa, or Fastify. Swapping from the standalone server to a web framework later is straightforward.
+
+```typescript
+import { ApolloServer } from "@apollo/server";
+import { startStandaloneServer } from "@apollo/server/standalone";
+
+const server = new ApolloServer({ typeDefs, resolvers });
+
+const { url } = await startStandaloneServer(server, {
+ listen: { port: 4000 },
+});
+
+console.log(`Server ready at ${url}`);
+```
+
+**Express:**
+
+```typescript
+import { ApolloServer } from "@apollo/server";
+import { expressMiddleware } from "@as-integrations/express5";
+import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
+import express from "express";
+import http from "http";
+import cors from "cors";
+
+const app = express();
+const httpServer = http.createServer(app);
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+});
+
+await server.start();
+
+app.use(
+ "/graphql",
+ cors(),
+ express.json(),
+ expressMiddleware(server, {
+ context: async ({ req }) => ({ token: req.headers.authorization }),
+ }),
+);
+
+await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
+console.log("Server ready at http://localhost:4000/graphql");
+```
+
+## Schema Definition
+
+### Scalar Types
+
+- `Int` - 32-bit integer
+- `Float` - Double-precision floating-point
+- `String` - UTF-8 string
+- `Boolean` - true/false
+- `ID` - Unique identifier (serialized as String)
+
+### Type Definitions
+
+```graphql
+type User {
+ id: ID!
+ name: String!
+ email: String
+ posts: [Post!]!
+}
+
+type Post {
+ id: ID!
+ title: String!
+ content: String
+ author: User!
+}
+
+input CreatePostInput {
+ title: String!
+ content: String
+}
+
+type Query {
+ user(id: ID!): User
+ users: [User!]!
+}
+
+type Mutation {
+ createPost(input: CreatePostInput!): Post!
+}
+```
+
+### Enums and Interfaces
+
+```graphql
+enum Status {
+ DRAFT
+ PUBLISHED
+ ARCHIVED
+}
+
+interface Node {
+ id: ID!
+}
+
+type Article implements Node {
+ id: ID!
+ title: String!
+}
+```
+
+## Resolvers Overview
+
+Resolvers follow the signature: `(parent, args, contextValue, info)`
+
+- **parent**: Result from parent resolver (root resolvers receive undefined)
+- **args**: Arguments passed to the field
+- **contextValue**: Shared context object (auth, dataSources, etc.)
+- **info**: Field-specific info and schema details (rarely used)
+
+```typescript
+const resolvers = {
+ Query: {
+ user: async (_, { id }, { dataSources }) => {
+ return dataSources.usersAPI.getUser(id);
+ },
+ },
+ User: {
+ posts: async (parent, _, { dataSources }) => {
+ return dataSources.postsAPI.getPostsByAuthor(parent.id);
+ },
+ },
+ Mutation: {
+ createPost: async (_, { input }, { dataSources, user }) => {
+ if (!user) throw new GraphQLError("Not authenticated");
+ return dataSources.postsAPI.create({ ...input, authorId: user.id });
+ },
+ },
+};
+```
+
+## Context Setup
+
+Context is created per-request and passed to all resolvers.
+
+```typescript
+interface MyContext {
+ token?: string;
+ user?: User;
+ dataSources: {
+ usersAPI: UsersDataSource;
+ postsAPI: PostsDataSource;
+ };
+}
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+});
+
+// Standalone
+const { url } = await startStandaloneServer(server, {
+ context: async ({ req }) => ({
+ token: req.headers.authorization || "",
+ user: await getUser(req.headers.authorization || ""),
+ dataSources: {
+ usersAPI: new UsersDataSource(),
+ postsAPI: new PostsDataSource(),
+ },
+ }),
+});
+
+// Express middleware
+expressMiddleware(server, {
+ context: async ({ req, res }) => ({
+ token: req.headers.authorization,
+ user: await getUser(req.headers.authorization),
+ dataSources: {
+ usersAPI: new UsersDataSource(),
+ postsAPI: new PostsDataSource(),
+ },
+ }),
+});
+```
+
+## Reference Files
+
+Detailed documentation for specific topics:
+
+- [Resolvers](references/resolvers.md) - Resolver patterns and best practices
+- [Context and Auth](references/context-and-auth.md) - Authentication and authorization
+- [Plugins](references/plugins.md) - Server and request lifecycle hooks
+- [Data Sources](references/data-sources.md) - RESTDataSource and DataLoader
+- [Error Handling](references/error-handling.md) - GraphQLError and error formatting
+- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions
+
+## Key Rules
+
+### Schema Design
+
+- Use **!** (non-null) for fields that always have values
+- Prefer input types for mutations over inline arguments
+- Use interfaces for polymorphic types
+- Keep schema descriptions for documentation
+
+### Resolver Best Practices
+
+- Keep resolvers thin - delegate to services/data sources
+- Always handle errors explicitly
+- Use DataLoader for batching related queries
+- Return partial data when possible (GraphQL's strength)
+
+### Performance
+
+- Use `@defer` and `@stream` for large responses
+- Implement DataLoader to solve N+1 queries
+- Consider persisted queries for production
+- Use caching headers and CDN where appropriate
+
+## Ground Rules
+
+- ALWAYS use Apollo Server 5.x patterns (not v4 or earlier)
+- ALWAYS type your context with TypeScript generics
+- ALWAYS use `GraphQLError` from `graphql` package for errors
+- NEVER expose stack traces in production errors
+- PREFER `startStandaloneServer` for prototyping only
+- USE an integration with a server framework like Express, Koa, Fastify, Next, etc. for production apps
+- IMPLEMENT authentication in context, authorization in resolvers
diff --git a/.agents/skills/apollo-server/references/context-and-auth.md b/.agents/skills/apollo-server/references/context-and-auth.md
new file mode 100644
index 000000000..ed0388a97
--- /dev/null
+++ b/.agents/skills/apollo-server/references/context-and-auth.md
@@ -0,0 +1,477 @@
+# Context and Authentication Reference
+
+## Table of Contents
+
+- [Context Function](#context-function)
+- [TypeScript Context Typing](#typescript-context-typing)
+- [Authentication Patterns](#authentication-patterns)
+- [Authorization Patterns](#authorization-patterns)
+- [Data Sources in Context](#data-sources-in-context)
+- [Security Best Practices](#security-best-practices)
+
+## Context Function
+
+The context function runs for every request and returns an object shared across all resolvers.
+
+### Standalone Server
+
+```typescript
+import { ApolloServer } from "@apollo/server";
+import { startStandaloneServer } from "@apollo/server/standalone";
+
+const server = new ApolloServer({ typeDefs, resolvers });
+
+const { url } = await startStandaloneServer(server, {
+ context: async ({ req, res }) => {
+ // req: IncomingMessage
+ // res: ServerResponse
+ return {
+ token: req.headers.authorization,
+ };
+ },
+});
+```
+
+### Express Middleware
+
+```typescript
+import { expressMiddleware } from "@as-integrations/express5";
+
+app.use(
+ "/graphql",
+ cors(),
+ express.json(),
+ expressMiddleware(server, {
+ context: async ({ req, res }) => {
+ // req: express.Request
+ // res: express.Response
+ return {
+ token: req.headers.authorization,
+ ip: req.ip,
+ };
+ },
+ }),
+);
+```
+
+### Context Initialization Order
+
+```typescript
+const context = async ({ req }) => {
+ // 1. Extract credentials
+ const token = req.headers.authorization?.replace("Bearer ", "");
+
+ // 2. Validate and decode (fail fast)
+ let user = null;
+ if (token) {
+ try {
+ user = await verifyToken(token);
+ } catch (e) {
+ // Don't throw - let resolvers handle auth
+ console.warn("Invalid token:", e.message);
+ }
+ }
+
+ // 3. Initialize data sources
+ const dataSources = {
+ usersAPI: new UsersDataSource(),
+ postsAPI: new PostsDataSource(),
+ };
+
+ // 4. Return context object
+ return { token, user, dataSources };
+};
+```
+
+## TypeScript Context Typing
+
+Define and use a typed context for type safety:
+
+```typescript
+import { ApolloServer } from "@apollo/server";
+
+// Define context type
+interface MyContext {
+ token?: string;
+ user?: {
+ id: string;
+ email: string;
+ roles: string[];
+ };
+ dataSources: {
+ usersAPI: UsersDataSource;
+ postsAPI: PostsDataSource;
+ };
+}
+
+// Pass to ApolloServer
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+});
+
+// Resolvers get typed context
+const resolvers = {
+ Query: {
+ me: async (_, __, context: MyContext) => {
+ if (!context.user) {
+ throw new GraphQLError("Not authenticated");
+ }
+ return context.dataSources.usersAPI.getById(context.user.id);
+ },
+ },
+};
+```
+
+## Authentication Patterns
+
+### JWT Authentication
+
+```typescript
+import jwt from "jsonwebtoken";
+
+interface JwtPayload {
+ userId: string;
+ email: string;
+ roles: string[];
+}
+
+const context = async ({ req }) => {
+ const token = req.headers.authorization?.replace("Bearer ", "");
+
+ let user = null;
+ if (token) {
+ try {
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
+ user = {
+ id: decoded.userId,
+ email: decoded.email,
+ roles: decoded.roles,
+ };
+ } catch (e) {
+ // Token invalid or expired - user remains null
+ }
+ }
+
+ return { user };
+};
+```
+
+### Session Authentication
+
+```typescript
+import session from "express-session";
+
+// Express setup
+app.use(
+ session({
+ secret: process.env.SESSION_SECRET!,
+ resave: false,
+ saveUninitialized: false,
+ }),
+);
+
+app.use(
+ "/graphql",
+ cors(),
+ express.json(),
+ expressMiddleware(server, {
+ context: async ({ req }) => ({
+ user: req.session.user,
+ session: req.session,
+ }),
+ }),
+);
+
+// Login mutation
+const resolvers = {
+ Mutation: {
+ login: async (_, { email, password }, { session, dataSources }) => {
+ const user = await dataSources.usersAPI.authenticate(email, password);
+ if (!user) {
+ throw new GraphQLError("Invalid credentials");
+ }
+ session.user = user;
+ return user;
+ },
+
+ logout: async (_, __, { session }) => {
+ return new Promise((resolve, reject) => {
+ session.destroy((err) => {
+ if (err) reject(err);
+ else resolve(true);
+ });
+ });
+ },
+ },
+};
+```
+
+### API Key Authentication
+
+```typescript
+const context = async ({ req }) => {
+ const apiKey = req.headers["x-api-key"];
+
+ let client = null;
+ if (apiKey) {
+ client = await db.apiKeys.findOne({ key: apiKey, active: true });
+ }
+
+ return {
+ client,
+ isAuthenticated: !!client,
+ };
+};
+```
+
+## Authorization Patterns
+
+### Field-Level Authorization
+
+```typescript
+import { GraphQLError } from "graphql";
+
+const resolvers = {
+ User: {
+ email: (parent, _, { user }) => {
+ // Only return email to the user themselves or admins
+ if (user?.id === parent.id || user?.roles.includes("admin")) {
+ return parent.email;
+ }
+ return null;
+ },
+
+ privateData: (parent, _, { user }) => {
+ if (!user) {
+ throw new GraphQLError("Not authenticated", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+ if (user.id !== parent.id) {
+ throw new GraphQLError("Not authorized", {
+ extensions: { code: "FORBIDDEN" },
+ });
+ }
+ return parent.privateData;
+ },
+ },
+};
+```
+
+### Role-Based Authorization
+
+```typescript
+// Helper function
+function requireRole(user: User | null, roles: string[]): void {
+ if (!user) {
+ throw new GraphQLError("Not authenticated", {
+ extensions: { code: "UNAUTHENTICATED" },
+ });
+ }
+
+ const hasRole = roles.some((role) => user.roles.includes(role));
+ if (!hasRole) {
+ throw new GraphQLError(`Requires one of: ${roles.join(", ")}`, {
+ extensions: { code: "FORBIDDEN" },
+ });
+ }
+}
+
+const resolvers = {
+ Mutation: {
+ deleteUser: async (_, { id }, { user, dataSources }) => {
+ requireRole(user, ["admin"]);
+ return dataSources.usersAPI.delete(id);
+ },
+
+ updatePost: async (_, { id, input }, { user, dataSources }) => {
+ requireRole(user, ["admin", "editor"]);
+ return dataSources.postsAPI.update(id, input);
+ },
+ },
+};
+```
+
+### Directive-Based Authorization
+
+```typescript
+import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils";
+import { defaultFieldResolver } from "graphql";
+
+// Schema directive
+const typeDefs = `#graphql
+ directive @auth(requires: Role = USER) on FIELD_DEFINITION
+
+ enum Role {
+ ADMIN
+ USER
+ GUEST
+ }
+
+ type Query {
+ users: [User!]! @auth(requires: ADMIN)
+ me: User @auth
+ }
+`;
+
+// Transform schema
+function authDirectiveTransformer(schema) {
+ return mapSchema(schema, {
+ [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
+ const authDirective = getDirective(schema, fieldConfig, "auth")?.[0];
+
+ if (authDirective) {
+ const { requires } = authDirective;
+ const { resolve = defaultFieldResolver } = fieldConfig;
+
+ fieldConfig.resolve = async (source, args, context, info) => {
+ if (!context.user) {
+ throw new GraphQLError("Not authenticated");
+ }
+
+ if (requires && !context.user.roles.includes(requires)) {
+ throw new GraphQLError(`Requires ${requires} role`);
+ }
+
+ return resolve(source, args, context, info);
+ };
+ }
+
+ return fieldConfig;
+ },
+ });
+}
+```
+
+## Data Sources in Context
+
+### Creating Data Sources
+
+```typescript
+interface MyContext {
+ dataSources: {
+ usersAPI: UsersDataSource;
+ postsAPI: PostsDataSource;
+ };
+}
+
+const context = async ({ req }): Promise => {
+ return {
+ dataSources: {
+ usersAPI: new UsersDataSource(),
+ postsAPI: new PostsDataSource(),
+ },
+ };
+};
+```
+
+### Passing User to Data Sources
+
+```typescript
+class AuthenticatedDataSource extends RESTDataSource {
+ private user?: User;
+
+ setUser(user: User) {
+ this.user = user;
+ }
+
+ override willSendRequest(path: string, request: AugmentedRequest) {
+ if (this.user) {
+ request.headers["x-user-id"] = this.user.id;
+ }
+ }
+}
+
+const context = async ({ req }) => {
+ const user = await getUser(req.headers.authorization);
+
+ const usersAPI = new UsersDataSource();
+ if (user) {
+ usersAPI.setUser(user);
+ }
+
+ return { user, dataSources: { usersAPI } };
+};
+```
+
+## Security Best Practices
+
+### Never Trust Client Input
+
+```typescript
+const context = async ({ req }) => {
+ // Bad - trusting client header
+ const userId = req.headers["x-user-id"];
+
+ // Good - verify token server-side
+ const token = req.headers.authorization?.replace("Bearer ", "");
+ const user = token ? await verifyToken(token) : null;
+
+ return { user };
+};
+```
+
+### Don't Expose Internal Errors
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ formatError: (formattedError, error) => {
+ // Log full error internally
+ console.error(error);
+
+ // Don't expose internal errors to clients
+ if (formattedError.extensions?.code === "INTERNAL_SERVER_ERROR") {
+ return {
+ message: "Internal server error",
+ extensions: { code: "INTERNAL_SERVER_ERROR" },
+ };
+ }
+
+ return formattedError;
+ },
+});
+```
+
+### Rate Limiting
+
+```typescript
+import rateLimit from "express-rate-limit";
+
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100, // limit each IP to 100 requests per window
+});
+
+app.use("/graphql", limiter);
+```
+
+### Depth Limiting
+
+```typescript
+import depthLimit from "graphql-depth-limit";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [depthLimit(10)],
+});
+```
+
+### Query Complexity
+
+```typescript
+import { createComplexityLimitRule } from "graphql-validation-complexity";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [
+ createComplexityLimitRule(1000, {
+ onCost: (cost) => console.log("Query cost:", cost),
+ }),
+ ],
+});
+```
diff --git a/.agents/skills/apollo-server/references/data-sources.md b/.agents/skills/apollo-server/references/data-sources.md
new file mode 100644
index 000000000..07371826e
--- /dev/null
+++ b/.agents/skills/apollo-server/references/data-sources.md
@@ -0,0 +1,403 @@
+# Data Sources Reference
+
+## Table of Contents
+
+- [RESTDataSource](#restdatasource)
+- [DataLoader](#dataloader)
+- [Custom Data Sources](#custom-data-sources)
+- [Best Practices](#best-practices)
+
+## RESTDataSource
+
+`@apollo/datasource-rest` provides a class for fetching data from REST APIs with built-in caching and request deduplication.
+
+### Installation
+
+```bash
+npm install @apollo/datasource-rest
+```
+
+### Basic Usage
+
+```typescript
+import { RESTDataSource } from "@apollo/datasource-rest";
+
+class UsersAPI extends RESTDataSource {
+ override baseURL = "https://api.example.com/";
+
+ async getUser(id: string): Promise {
+ return this.get(`users/${id}`);
+ }
+
+ async getUsers(): Promise {
+ return this.get("users");
+ }
+
+ async createUser(input: CreateUserInput): Promise {
+ return this.post("users", { body: input });
+ }
+
+ async updateUser(id: string, input: UpdateUserInput): Promise {
+ return this.put(`users/${id}`, { body: input });
+ }
+
+ async deleteUser(id: string): Promise {
+ return this.delete(`users/${id}`);
+ }
+}
+```
+
+### Context Integration
+
+```typescript
+interface MyContext {
+ dataSources: {
+ usersAPI: UsersAPI;
+ postsAPI: PostsAPI;
+ };
+}
+
+const server = new ApolloServer({ typeDefs, resolvers });
+
+const { url } = await startStandaloneServer(server, {
+ context: async () => ({
+ dataSources: {
+ usersAPI: new UsersAPI(),
+ postsAPI: new PostsAPI(),
+ },
+ }),
+});
+```
+
+### Request Customization
+
+```typescript
+class AuthenticatedAPI extends RESTDataSource {
+ override baseURL = "https://api.example.com/";
+
+ private token: string;
+
+ constructor(token: string) {
+ super();
+ this.token = token;
+ }
+
+ // Add headers to every request
+ override willSendRequest(path: string, request: AugmentedRequest) {
+ request.headers["authorization"] = `Bearer ${this.token}`;
+ request.headers["x-request-id"] = crypto.randomUUID();
+ }
+
+ // Transform response data
+ override async didReceiveResponse(response: Response, request: Request): Promise {
+ const body = await super.didReceiveResponse(response, request);
+ // Add metadata or transform
+ return body;
+ }
+}
+```
+
+### Caching
+
+RESTDataSource supports caching based on HTTP cache headers:
+
+```typescript
+class CachedAPI extends RESTDataSource {
+ override baseURL = "https://api.example.com/";
+
+ // Override cache options per request
+ async getUser(id: string): Promise {
+ return this.get(`users/${id}`, {
+ cacheOptions: { ttl: 300 }, // 5 minutes
+ });
+ }
+
+ // Disable caching for specific requests
+ async getUserFresh(id: string): Promise {
+ return this.get(`users/${id}`, {
+ cacheOptions: { ttl: 0 },
+ });
+ }
+}
+```
+
+### Error Handling
+
+```typescript
+import { GraphQLError } from "graphql";
+
+class UsersAPI extends RESTDataSource {
+ override baseURL = "https://api.example.com/";
+
+ async getUser(id: string): Promise {
+ try {
+ return await this.get(`users/${id}`);
+ } catch (error) {
+ if (error.extensions?.response?.status === 404) {
+ throw new GraphQLError("User not found", {
+ extensions: { code: "NOT_FOUND" },
+ });
+ }
+ throw error;
+ }
+ }
+}
+```
+
+## DataLoader
+
+DataLoader batches and caches individual loads within a single request, solving the N+1 problem.
+
+### Installation
+
+```bash
+npm install dataloader
+```
+
+### Basic Usage
+
+```typescript
+import DataLoader from "dataloader";
+
+// Batch function receives array of keys, returns array of results in same order
+const userLoader = new DataLoader(async (ids) => {
+ const users = await db.query("SELECT * FROM users WHERE id IN (?)", [ids]);
+
+ // IMPORTANT: Return results in same order as input ids
+ const userMap = new Map(users.map((u) => [u.id, u]));
+ return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
+});
+
+// Usage
+const user1 = await userLoader.load("1");
+const user2 = await userLoader.load("2");
+
+// Both loads are batched into single query
+```
+
+### Context Integration
+
+Create new DataLoader instances per request to prevent caching across requests:
+
+```typescript
+import DataLoader from "dataloader";
+
+interface MyContext {
+ loaders: {
+ userLoader: DataLoader;
+ postsByAuthorLoader: DataLoader;
+ };
+}
+
+const context = async (): Promise => ({
+ loaders: {
+ userLoader: new DataLoader(async (ids) => {
+ const users = await db.users.findByIds(ids);
+ const userMap = new Map(users.map((u) => [u.id, u]));
+ return ids.map((id) => userMap.get(id) ?? null);
+ }),
+
+ postsByAuthorLoader: new DataLoader(async (authorIds) => {
+ const posts = await db.posts.findByAuthorIds(authorIds);
+ return authorIds.map((id) => posts.filter((p) => p.authorId === id));
+ }),
+ },
+});
+
+// Resolvers
+const resolvers = {
+ Post: {
+ author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
+ },
+ User: {
+ posts: (user, _, { loaders }) => loaders.postsByAuthorLoader.load(user.id),
+ },
+};
+```
+
+### Options
+
+```typescript
+const loader = new DataLoader(batchFn, {
+ // Maximum batch size (default: Infinity)
+ maxBatchSize: 100,
+
+ // Use object keys (default: false)
+ cacheKeyFn: (key) => JSON.stringify(key),
+
+ // Disable caching (default: true)
+ cache: false,
+
+ // Custom cache implementation
+ cacheMap: new Map(),
+
+ // Batch scheduling function
+ batchScheduleFn: (callback) => setTimeout(callback, 10),
+});
+```
+
+### Priming the Cache
+
+```typescript
+// Pre-populate cache after batch fetch
+const users = await db.users.findAll();
+users.forEach((user) => userLoader.prime(user.id, user));
+
+// Clear specific key
+userLoader.clear("1");
+
+// Clear all
+userLoader.clearAll();
+```
+
+## Custom Data Sources
+
+Create custom data sources for databases, queues, or other services:
+
+```typescript
+class DatabaseSource {
+ private db: Database;
+
+ constructor(db: Database) {
+ this.db = db;
+ }
+
+ async getUser(id: string): Promise {
+ return this.db.users.findUnique({ where: { id } });
+ }
+
+ async createUser(input: CreateUserInput): Promise {
+ return this.db.users.create({ data: input });
+ }
+
+ async getUsersWithPosts(ids: string[]): Promise {
+ return this.db.users.findMany({
+ where: { id: { in: ids } },
+ include: { posts: true },
+ });
+ }
+}
+
+// Context
+const context = async () => ({
+ dataSources: {
+ db: new DatabaseSource(prisma),
+ },
+});
+```
+
+### With Connection Pooling
+
+```typescript
+class PooledDataSource {
+ private pool: Pool;
+
+ constructor(pool: Pool) {
+ this.pool = pool;
+ }
+
+ async query(sql: string, params: unknown[]): Promise {
+ const client = await this.pool.connect();
+ try {
+ const result = await client.query(sql, params);
+ return result.rows;
+ } finally {
+ client.release();
+ }
+ }
+
+ async getUser(id: string): Promise {
+ const rows = await this.query("SELECT * FROM users WHERE id = $1", [id]);
+ return rows[0] ?? null;
+ }
+}
+```
+
+## Best Practices
+
+### Create Data Sources Per Request
+
+```typescript
+// Good - new instance per request
+context: async () => ({
+ dataSources: {
+ usersAPI: new UsersAPI(),
+ },
+});
+
+// Bad - shared instance across requests
+const usersAPI = new UsersAPI();
+context: async () => ({
+ dataSources: { usersAPI },
+});
+```
+
+### Combine DataLoader with RESTDataSource
+
+```typescript
+import DataLoader from "dataloader";
+
+class UsersAPI extends RESTDataSource {
+ override baseURL = "https://api.example.com/";
+
+ private batchLoader = new DataLoader(async (ids) => {
+ const users = await this.get("users", {
+ params: { ids: ids.join(",") },
+ });
+ const userMap = new Map(users.map((u) => [u.id, u]));
+ return ids.map((id) => userMap.get(id) ?? new Error(`Not found: ${id}`));
+ });
+
+ async getUser(id: string): Promise {
+ return this.batchLoader.load(id);
+ }
+
+ async getUsers(ids: string[]): Promise {
+ return this.batchLoader.loadMany(ids);
+ }
+}
+```
+
+### Handle Partial Failures
+
+```typescript
+const userLoader = new DataLoader(async (ids) => {
+ const users = await fetchUsers(ids);
+ const userMap = new Map(users.map((u) => [u.id, u]));
+
+ // Return Error for missing items, not null
+ return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
+});
+
+// In resolver, handle the error
+const resolvers = {
+ Post: {
+ author: async (post, _, { loaders }) => {
+ try {
+ return await loaders.userLoader.load(post.authorId);
+ } catch (e) {
+ // Return null for nullable field
+ return null;
+ }
+ },
+ },
+};
+```
+
+### Avoid Over-fetching
+
+```typescript
+// Bad - always fetches all relations
+async getUser(id: string): Promise {
+ return this.get(`users/${id}?include=posts,followers,following`);
+}
+
+// Good - separate methods for different needs
+async getUser(id: string): Promise {
+ return this.get(`users/${id}`);
+}
+
+async getUserWithPosts(id: string): Promise {
+ return this.get(`users/${id}?include=posts`);
+}
+```
diff --git a/.agents/skills/apollo-server/references/error-handling.md b/.agents/skills/apollo-server/references/error-handling.md
new file mode 100644
index 000000000..53b4937be
--- /dev/null
+++ b/.agents/skills/apollo-server/references/error-handling.md
@@ -0,0 +1,447 @@
+# Error Handling Reference
+
+## Table of Contents
+
+- [GraphQLError](#graphqlerror)
+- [Error Codes](#error-codes)
+- [formatError Option](#formaterror-option)
+- [Production Error Handling](#production-error-handling)
+
+## GraphQLError
+
+Apollo Server 4 uses `GraphQLError` from the `graphql` package. Always import from `graphql`, not from Apollo Server.
+
+### Basic Usage
+
+```typescript
+import { GraphQLError } from "graphql";
+
+const resolvers = {
+ Query: {
+ user: async (_, { id }, { dataSources }) => {
+ const user = await dataSources.usersAPI.getById(id);
+
+ if (!user) {
+ throw new GraphQLError("User not found", {
+ extensions: {
+ code: "NOT_FOUND",
+ argumentName: "id",
+ },
+ });
+ }
+
+ return user;
+ },
+ },
+};
+```
+
+### Error Response Format
+
+```json
+{
+ "errors": [
+ {
+ "message": "User not found",
+ "locations": [{ "line": 2, "column": 3 }],
+ "path": ["user"],
+ "extensions": {
+ "code": "NOT_FOUND",
+ "argumentName": "id"
+ }
+ }
+ ],
+ "data": {
+ "user": null
+ }
+}
+```
+
+### GraphQLError Options
+
+```typescript
+new GraphQLError(message, {
+ // Custom error code
+ extensions: {
+ code: "CUSTOM_CODE",
+ // Any additional metadata
+ http: { status: 400 },
+ },
+
+ // Original error (for stack traces)
+ originalError: caughtError,
+
+ // AST node(s) associated with the error
+ nodes: fieldNode,
+
+ // Source location
+ positions: [15],
+
+ // Path to the field that caused the error
+ path: ["user", "email"],
+});
+```
+
+## Error Codes
+
+### Built-in Codes
+
+| Code | Description | HTTP Status |
+| ------------------------------- | --------------------------------- | ----------- |
+| `GRAPHQL_PARSE_FAILED` | Syntax error in query | 400 |
+| `GRAPHQL_VALIDATION_FAILED` | Query doesn't match schema | 400 |
+| `BAD_USER_INPUT` | Invalid argument value | 400 |
+| `UNAUTHENTICATED` | Missing or invalid authentication | 401 |
+| `FORBIDDEN` | Not authorized for this operation | 403 |
+| `PERSISTED_QUERY_NOT_FOUND` | APQ hash not found | 404 |
+| `PERSISTED_QUERY_NOT_SUPPORTED` | Server doesn't support APQ | 400 |
+| `OPERATION_RESOLUTION_FAILURE` | Operation couldn't be determined | 400 |
+| `BAD_REQUEST` | Invalid request format | 400 |
+| `INTERNAL_SERVER_ERROR` | Unexpected server error | 500 |
+
+### Custom Error Classes
+
+```typescript
+import { GraphQLError } from "graphql";
+
+export class NotFoundError extends GraphQLError {
+ constructor(resource: string, id: string) {
+ super(`${resource} with id '${id}' not found`, {
+ extensions: {
+ code: "NOT_FOUND",
+ resource,
+ id,
+ },
+ });
+ }
+}
+
+export class AuthenticationError extends GraphQLError {
+ constructor(message = "Not authenticated") {
+ super(message, {
+ extensions: {
+ code: "UNAUTHENTICATED",
+ http: { status: 401 },
+ },
+ });
+ }
+}
+
+export class ForbiddenError extends GraphQLError {
+ constructor(message = "Not authorized") {
+ super(message, {
+ extensions: {
+ code: "FORBIDDEN",
+ http: { status: 403 },
+ },
+ });
+ }
+}
+
+export class ValidationError extends GraphQLError {
+ constructor(message: string, field: string) {
+ super(message, {
+ extensions: {
+ code: "BAD_USER_INPUT",
+ field,
+ },
+ });
+ }
+}
+
+// Usage
+throw new NotFoundError("User", id);
+throw new AuthenticationError();
+throw new ForbiddenError("Admin access required");
+throw new ValidationError("Email is invalid", "email");
+```
+
+### Setting HTTP Status
+
+```typescript
+throw new GraphQLError("Not authenticated", {
+ extensions: {
+ code: "UNAUTHENTICATED",
+ http: {
+ status: 401,
+ headers: new Map([["WWW-Authenticate", "Bearer"]]),
+ },
+ },
+});
+```
+
+## formatError Option
+
+Use `formatError` to transform errors before sending to clients.
+
+### Basic Formatting
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ formatError: (formattedError, error) => {
+ // formattedError: already formatted GraphQL error
+ // error: original error (may be wrapped)
+
+ console.error("GraphQL Error:", error);
+
+ return formattedError;
+ },
+});
+```
+
+### Masking Internal Errors
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ formatError: (formattedError, error) => {
+ // Log full error for debugging
+ console.error(error);
+
+ // Don't expose internal server errors
+ if (formattedError.extensions?.code === "INTERNAL_SERVER_ERROR") {
+ return {
+ message: "An internal error occurred",
+ extensions: {
+ code: "INTERNAL_SERVER_ERROR",
+ },
+ };
+ }
+
+ // Remove stacktrace in production
+ if (process.env.NODE_ENV === "production") {
+ delete formattedError.extensions?.stacktrace;
+ }
+
+ return formattedError;
+ },
+});
+```
+
+### Adding Request Context
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ formatError: (formattedError, error) => {
+ // Add request ID for support
+ return {
+ ...formattedError,
+ extensions: {
+ ...formattedError.extensions,
+ requestId: crypto.randomUUID(),
+ timestamp: new Date().toISOString(),
+ },
+ };
+ },
+});
+```
+
+### Error Classification
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ formatError: (formattedError, error) => {
+ const code = formattedError.extensions?.code;
+
+ // Client errors - return as-is
+ const clientCodes = ["BAD_USER_INPUT", "UNAUTHENTICATED", "FORBIDDEN", "NOT_FOUND"];
+
+ if (clientCodes.includes(code as string)) {
+ return formattedError;
+ }
+
+ // Server errors - mask details
+ return {
+ message: "Something went wrong",
+ extensions: {
+ code: "INTERNAL_SERVER_ERROR",
+ },
+ };
+ },
+});
+```
+
+## Production Error Handling
+
+### Logging Strategy
+
+```typescript
+import { ApolloServerPlugin } from "@apollo/server";
+
+const errorLoggingPlugin: ApolloServerPlugin = {
+ async requestDidStart() {
+ return {
+ async didEncounterErrors({ errors, request, contextValue }) {
+ for (const error of errors) {
+ const code = error.extensions?.code;
+
+ // Log all errors with context
+ const logEntry = {
+ message: error.message,
+ code,
+ path: error.path,
+ operationName: request.operationName,
+ userId: contextValue.user?.id,
+ timestamp: new Date().toISOString(),
+ };
+
+ // Different log levels based on error type
+ if (code === "INTERNAL_SERVER_ERROR") {
+ console.error("Server Error:", logEntry, error.originalError);
+ } else if (code === "UNAUTHENTICATED" || code === "FORBIDDEN") {
+ console.warn("Auth Error:", logEntry);
+ } else {
+ console.info("Client Error:", logEntry);
+ }
+ }
+ },
+ };
+ },
+};
+```
+
+### Error Reporting Service
+
+```typescript
+import * as Sentry from "@sentry/node";
+
+const sentryPlugin: ApolloServerPlugin = {
+ async requestDidStart({ request, contextValue }) {
+ return {
+ async didEncounterErrors({ errors }) {
+ for (const error of errors) {
+ // Only report server errors
+ if (error.extensions?.code === "INTERNAL_SERVER_ERROR") {
+ Sentry.withScope((scope) => {
+ scope.setTag("kind", "graphql");
+ scope.setExtra("query", request.query);
+ scope.setExtra("variables", request.variables);
+ scope.setUser({ id: contextValue.user?.id });
+
+ Sentry.captureException(error.originalError ?? error);
+ });
+ }
+ }
+ },
+ };
+ },
+};
+```
+
+### Partial Data with Errors
+
+GraphQL can return partial data alongside errors:
+
+```typescript
+// Schema
+type Query {
+ user(id: ID!): User
+ posts: [Post!]!
+}
+
+// Query
+query {
+ user(id: "1") { name }
+ posts { title }
+}
+
+// If user resolver throws but posts succeeds:
+{
+ "errors": [
+ {
+ "message": "User not found",
+ "path": ["user"]
+ }
+ ],
+ "data": {
+ "user": null,
+ "posts": [
+ { "title": "Post 1" },
+ { "title": "Post 2" }
+ ]
+ }
+}
+```
+
+### Error Recovery Patterns
+
+```typescript
+const resolvers = {
+ Query: {
+ // Return null for optional fields instead of throwing
+ user: async (_, { id }, { dataSources }) => {
+ try {
+ return await dataSources.usersAPI.getById(id);
+ } catch (e) {
+ console.error("Failed to fetch user:", e);
+ return null;
+ }
+ },
+
+ // Return empty array for list fields
+ users: async (_, __, { dataSources }) => {
+ try {
+ return await dataSources.usersAPI.getAll();
+ } catch (e) {
+ console.error("Failed to fetch users:", e);
+ return [];
+ }
+ },
+ },
+
+ User: {
+ // Handle failures in field resolvers gracefully
+ posts: async (parent, _, { dataSources }) => {
+ try {
+ return await dataSources.postsAPI.getByAuthor(parent.id);
+ } catch (e) {
+ console.error("Failed to fetch posts:", e);
+ return [];
+ }
+ },
+ },
+};
+```
+
+### Validation Errors
+
+```typescript
+import { z } from "zod";
+
+const CreateUserSchema = z.object({
+ email: z.string().email(),
+ name: z.string().min(1).max(100),
+ age: z.number().int().positive().optional(),
+});
+
+const resolvers = {
+ Mutation: {
+ createUser: async (_, { input }, { dataSources }) => {
+ const result = CreateUserSchema.safeParse(input);
+
+ if (!result.success) {
+ const errors = result.error.issues.map((issue) => ({
+ field: issue.path.join("."),
+ message: issue.message,
+ }));
+
+ throw new GraphQLError("Validation failed", {
+ extensions: {
+ code: "BAD_USER_INPUT",
+ validationErrors: errors,
+ },
+ });
+ }
+
+ return dataSources.usersAPI.create(result.data);
+ },
+ },
+};
+```
diff --git a/.agents/skills/apollo-server/references/plugins.md b/.agents/skills/apollo-server/references/plugins.md
new file mode 100644
index 000000000..35b8cf2b6
--- /dev/null
+++ b/.agents/skills/apollo-server/references/plugins.md
@@ -0,0 +1,438 @@
+# Plugins Reference
+
+## Table of Contents
+
+- [Plugin Structure](#plugin-structure)
+- [Server Lifecycle Hooks](#server-lifecycle-hooks)
+- [Request Lifecycle Hooks](#request-lifecycle-hooks)
+- [Common Plugin Patterns](#common-plugin-patterns)
+- [Built-in Plugins](#built-in-plugins)
+
+## Plugin Structure
+
+Plugins are objects that implement lifecycle hooks:
+
+```typescript
+import { ApolloServerPlugin } from "@apollo/server";
+
+const myPlugin: ApolloServerPlugin = {
+ // Server lifecycle
+ async serverWillStart(service) {
+ console.log("Server starting");
+ return {
+ async drainServer() {
+ console.log("Server draining");
+ },
+ async serverWillStop() {
+ console.log("Server stopping");
+ },
+ };
+ },
+
+ // Request lifecycle
+ async requestDidStart(requestContext) {
+ console.log("Request started");
+ return {
+ async parsingDidStart() {
+ return async (err) => {
+ if (err) console.log("Parsing error:", err);
+ };
+ },
+ async validationDidStart() {
+ return async (errs) => {
+ if (errs) console.log("Validation errors:", errs);
+ };
+ },
+ async didResolveOperation(requestContext) {
+ console.log("Operation:", requestContext.operationName);
+ },
+ async executionDidStart() {
+ return {
+ willResolveField({ info }) {
+ const start = Date.now();
+ return () => {
+ console.log(`${info.fieldName}: ${Date.now() - start}ms`);
+ };
+ },
+ };
+ },
+ async willSendResponse(requestContext) {
+ console.log("Sending response");
+ },
+ };
+ },
+};
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [myPlugin],
+});
+```
+
+## Server Lifecycle Hooks
+
+### serverWillStart
+
+Called when `server.start()` is invoked, before the server begins accepting requests.
+
+```typescript
+const plugin: ApolloServerPlugin = {
+ async serverWillStart(service) {
+ // service.schema - the GraphQL schema
+ // service.apollo - Apollo config (if using Apollo Studio)
+
+ console.log("Schema types:", Object.keys(service.schema.getTypeMap()));
+
+ // Return object with cleanup hooks
+ return {
+ async drainServer() {
+ // Called when server.stop() begins
+ // Use to stop accepting new requests
+ await closeConnections();
+ },
+
+ async serverWillStop() {
+ // Called after drainServer, before server fully stops
+ // Use for final cleanup
+ await db.disconnect();
+ },
+
+ schemaDidLoadOrUpdate(schemaContext) {
+ // Called when schema changes (e.g., with gateway)
+ console.log("Schema updated");
+ },
+ };
+ },
+};
+```
+
+### drainServer Pattern
+
+```typescript
+import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
+import http from "http";
+
+const httpServer = http.createServer(app);
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ ApolloServerPluginDrainHttpServer({ httpServer }),
+ // Custom drain plugin
+ {
+ async serverWillStart() {
+ return {
+ async drainServer() {
+ // Stop background jobs
+ await scheduler.stop();
+ // Close database connections
+ await db.end();
+ },
+ };
+ },
+ },
+ ],
+});
+```
+
+## Request Lifecycle Hooks
+
+Request lifecycle flows in this order:
+
+```
+requestDidStart
+ └─> didResolveSource
+ └─> parsingDidStart / parsingDidEnd
+ └─> validationDidStart / validationDidEnd
+ └─> didResolveOperation
+ └─> responseForOperation (can short-circuit)
+ └─> executionDidStart
+ └─> willResolveField (per field)
+ └─> didEncounterErrors (if errors)
+ └─> willSendResponse
+```
+
+### Full Request Lifecycle
+
+```typescript
+const plugin: ApolloServerPlugin = {
+ async requestDidStart(requestContext) {
+ const start = Date.now();
+ const { request, contextValue } = requestContext;
+
+ console.log("Query:", request.query);
+ console.log("Variables:", request.variables);
+
+ return {
+ async didResolveSource(requestContext) {
+ // Source (query string) has been resolved
+ },
+
+ async parsingDidStart(requestContext) {
+ // Return end hook for when parsing completes
+ return async (err) => {
+ if (err) {
+ console.error("Parse error:", err);
+ }
+ };
+ },
+
+ async validationDidStart(requestContext) {
+ return async (errs) => {
+ if (errs?.length) {
+ console.error("Validation errors:", errs);
+ }
+ };
+ },
+
+ async didResolveOperation(requestContext) {
+ // Operation name and type are now available
+ console.log("Operation:", requestContext.operationName);
+ console.log("Type:", requestContext.operation?.operation);
+ },
+
+ async responseForOperation(requestContext) {
+ // Return response to skip execution (e.g., cached response)
+ // Return null to continue normal execution
+ return null;
+ },
+
+ async executionDidStart(requestContext) {
+ return {
+ willResolveField({ source, args, contextValue, info }) {
+ const fieldStart = Date.now();
+ return (error, result) => {
+ const duration = Date.now() - fieldStart;
+ if (duration > 100) {
+ console.log(`Slow field ${info.fieldName}: ${duration}ms`);
+ }
+ };
+ },
+ };
+ },
+
+ async didEncounterErrors(requestContext) {
+ for (const error of requestContext.errors) {
+ console.error("GraphQL Error:", error);
+ }
+ },
+
+ async willSendResponse(requestContext) {
+ console.log(`Request completed in ${Date.now() - start}ms`);
+ },
+ };
+ },
+};
+```
+
+## Common Plugin Patterns
+
+### Logging Plugin
+
+```typescript
+const loggingPlugin: ApolloServerPlugin = {
+ async requestDidStart({ request, contextValue }) {
+ const start = Date.now();
+ const requestId = crypto.randomUUID();
+
+ console.log(
+ JSON.stringify({
+ type: "request_start",
+ requestId,
+ operationName: request.operationName,
+ userId: contextValue.user?.id,
+ }),
+ );
+
+ return {
+ async willSendResponse({ response }) {
+ console.log(
+ JSON.stringify({
+ type: "request_end",
+ requestId,
+ duration: Date.now() - start,
+ errors: response.body.kind === "single" ? (response.body.singleResult.errors?.length ?? 0) : 0,
+ }),
+ );
+ },
+ };
+ },
+};
+```
+
+### Timing Plugin
+
+```typescript
+const timingPlugin: ApolloServerPlugin = {
+ async requestDidStart() {
+ const fieldTimes: Map = new Map();
+
+ return {
+ async executionDidStart() {
+ return {
+ willResolveField({ info }) {
+ const start = process.hrtime.bigint();
+ return () => {
+ const duration = Number(process.hrtime.bigint() - start) / 1e6;
+ const key = `${info.parentType.name}.${info.fieldName}`;
+ const times = fieldTimes.get(key) ?? [];
+ times.push(duration);
+ fieldTimes.set(key, times);
+ };
+ },
+ };
+ },
+
+ async willSendResponse({ response }) {
+ if (response.body.kind === "single") {
+ response.body.singleResult.extensions = {
+ ...response.body.singleResult.extensions,
+ timing: Object.fromEntries(fieldTimes),
+ };
+ }
+ },
+ };
+ },
+};
+```
+
+### Error Tracking Plugin
+
+```typescript
+const errorTrackingPlugin: ApolloServerPlugin = {
+ async requestDidStart({ request, contextValue }) {
+ return {
+ async didEncounterErrors({ errors, request }) {
+ for (const error of errors) {
+ // Skip client errors
+ if (error.extensions?.code === "BAD_USER_INPUT") continue;
+
+ // Report to error tracking service
+ await errorTracker.captureException(error.originalError ?? error, {
+ extra: {
+ operationName: request.operationName,
+ query: request.query,
+ variables: request.variables,
+ userId: contextValue.user?.id,
+ },
+ });
+ }
+ },
+ };
+ },
+};
+```
+
+### Caching Plugin
+
+```typescript
+const cachingPlugin: ApolloServerPlugin = {
+ async requestDidStart({ request }) {
+ const cacheKey = createHash("sha256")
+ .update(request.query ?? "")
+ .update(JSON.stringify(request.variables ?? {}))
+ .digest("hex");
+
+ return {
+ async responseForOperation() {
+ const cached = await cache.get(cacheKey);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+ return null;
+ },
+
+ async willSendResponse({ response }) {
+ if (response.body.kind === "single" && !response.body.singleResult.errors) {
+ await cache.set(cacheKey, JSON.stringify(response.body.singleResult), {
+ ttl: 300,
+ });
+ }
+ },
+ };
+ },
+};
+```
+
+## Built-in Plugins
+
+### ApolloServerPluginDrainHttpServer
+
+Gracefully shuts down HTTP server during `server.stop()`:
+
+```typescript
+import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
+});
+```
+
+### ApolloServerPluginLandingPageLocalDefault
+
+Shows Apollo Sandbox for local development:
+
+```typescript
+import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
+});
+```
+
+### ApolloServerPluginLandingPageProductionDefault
+
+Shows production landing page:
+
+```typescript
+import { ApolloServerPluginLandingPageProductionDefault } from "@apollo/server/plugin/landingPage/default";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ process.env.NODE_ENV === "production"
+ ? ApolloServerPluginLandingPageProductionDefault()
+ : ApolloServerPluginLandingPageLocalDefault({ embed: true }),
+ ],
+});
+```
+
+### ApolloServerPluginUsageReporting
+
+Reports metrics to Apollo Studio:
+
+```typescript
+import { ApolloServerPluginUsageReporting } from "@apollo/server/plugin/usageReporting";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ ApolloServerPluginUsageReporting({
+ sendVariableValues: { all: true },
+ sendHeaders: { all: true },
+ }),
+ ],
+});
+```
+
+### ApolloServerPluginInlineTrace
+
+Includes trace data in responses (for federated graphs):
+
+```typescript
+import { ApolloServerPluginInlineTrace } from "@apollo/server/plugin/inlineTrace";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginInlineTrace()],
+});
+```
diff --git a/.agents/skills/apollo-server/references/resolvers.md b/.agents/skills/apollo-server/references/resolvers.md
new file mode 100644
index 000000000..f89226da7
--- /dev/null
+++ b/.agents/skills/apollo-server/references/resolvers.md
@@ -0,0 +1,321 @@
+# Resolvers Reference
+
+## Table of Contents
+
+- [Resolver Signature](#resolver-signature)
+- [Resolver Map Structure](#resolver-map-structure)
+- [Async Resolvers](#async-resolvers)
+- [Field Resolvers](#field-resolvers)
+- [Default Resolvers](#default-resolvers)
+- [N+1 Problem](#n1-problem)
+- [Best Practices](#best-practices)
+
+## Resolver Signature
+
+Every resolver receives four positional arguments:
+
+```typescript
+(parent, args, contextValue, info) => result;
+```
+
+### Arguments
+
+| Argument | Description |
+| -------------- | ----------------------------------------------------------------------------------------- |
+| `parent` | Return value of the parent resolver. For root types (Query, Mutation), this is undefined. |
+| `args` | Object containing all arguments passed to the field. |
+| `contextValue` | Shared object for all resolvers in a request. Contains auth, dataSources, etc. |
+| `info` | Contains field name, path, schema, and AST information. Rarely needed. |
+
+### TypeScript Typing
+
+```typescript
+import { GraphQLResolveInfo } from "graphql";
+
+type Resolver = (
+ parent: TParent,
+ args: TArgs,
+ context: TContext,
+ info: GraphQLResolveInfo,
+) => TResult | Promise;
+
+// Example
+const userResolver: Resolver = async (
+ _,
+ { id },
+ { dataSources },
+) => {
+ return dataSources.usersAPI.getUser(id);
+};
+```
+
+## Resolver Map Structure
+
+Resolvers are organized by type and field name:
+
+```typescript
+const resolvers = {
+ Query: {
+ users: (_, __, { dataSources }) => dataSources.usersAPI.getAll(),
+ user: (_, { id }, { dataSources }) => dataSources.usersAPI.getById(id),
+ },
+
+ Mutation: {
+ createUser: (_, { input }, { dataSources }) => dataSources.usersAPI.create(input),
+ updateUser: (_, { id, input }, { dataSources }) => dataSources.usersAPI.update(id, input),
+ deleteUser: (_, { id }, { dataSources }) => dataSources.usersAPI.delete(id),
+ },
+
+ Subscription: {
+ userCreated: {
+ subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(["USER_CREATED"]),
+ },
+ },
+
+ User: {
+ posts: (parent, _, { dataSources }) => dataSources.postsAPI.getByAuthor(parent.id),
+ fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
+ },
+
+ // Custom scalar
+ DateTime: new GraphQLScalarType({
+ name: "DateTime",
+ serialize: (value) => value.toISOString(),
+ parseValue: (value) => new Date(value),
+ }),
+
+ // Enum mapping (when values differ from schema)
+ Status: {
+ DRAFT: 0,
+ PUBLISHED: 1,
+ ARCHIVED: 2,
+ },
+};
+```
+
+## Async Resolvers
+
+Resolvers can return Promises. Apollo Server awaits them automatically.
+
+```typescript
+const resolvers = {
+ Query: {
+ // Async/await pattern (recommended)
+ user: async (_, { id }, { dataSources }) => {
+ const user = await dataSources.usersAPI.getById(id);
+ if (!user) {
+ throw new GraphQLError("User not found", {
+ extensions: { code: "NOT_FOUND" },
+ });
+ }
+ return user;
+ },
+
+ // Promise pattern
+ users: (_, __, { dataSources }) => {
+ return dataSources.usersAPI.getAll();
+ },
+
+ // Parallel fetching
+ dashboard: async (_, __, { dataSources }) => {
+ const [users, posts, stats] = await Promise.all([
+ dataSources.usersAPI.getAll(),
+ dataSources.postsAPI.getRecent(),
+ dataSources.analyticsAPI.getStats(),
+ ]);
+ return { users, posts, stats };
+ },
+ },
+};
+```
+
+## Field Resolvers
+
+Field resolvers compute derived values or fetch related data:
+
+```typescript
+const resolvers = {
+ User: {
+ // Computed field
+ fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
+
+ // Related data (one-to-many)
+ posts: async (parent, _, { dataSources }) => {
+ return dataSources.postsAPI.getByAuthorId(parent.id);
+ },
+
+ // With arguments
+ posts: async (parent, { limit, offset }, { dataSources }) => {
+ return dataSources.postsAPI.getByAuthorId(parent.id, { limit, offset });
+ },
+
+ // Conditional fetching
+ privateData: async (parent, _, { user }) => {
+ if (user?.id !== parent.id) {
+ return null;
+ }
+ return parent.privateData;
+ },
+ },
+
+ Post: {
+ // Related data (many-to-one)
+ author: async (parent, _, { dataSources }) => {
+ return dataSources.usersAPI.getById(parent.authorId);
+ },
+ },
+};
+```
+
+## Default Resolvers
+
+Apollo Server provides default resolvers that return `parent[fieldName]`:
+
+```typescript
+// Schema
+type User {
+ id: ID!
+ name: String!
+ email: String
+}
+
+// Resolvers - you don't need to write these
+const resolvers = {
+ User: {
+ id: (parent) => parent.id, // Default resolver handles this
+ name: (parent) => parent.name, // Default resolver handles this
+ email: (parent) => parent.email, // Default resolver handles this
+ },
+};
+
+// Only define resolvers when:
+// 1. Field name differs from data property
+// 2. Data needs transformation
+// 3. Field requires fetching related data
+const resolvers = {
+ User: {
+ fullName: (parent) => `${parent.first_name} ${parent.last_name}`,
+ },
+};
+```
+
+## N+1 Problem
+
+The N+1 problem occurs when fetching related data triggers separate queries for each item.
+
+### Problem Example
+
+```typescript
+// Query: { users { posts { title } } }
+// This causes:
+// 1 query for users
+// N queries for posts (one per user)
+
+const resolvers = {
+ Query: {
+ users: () => db.query("SELECT * FROM users"), // 1 query
+ },
+ User: {
+ posts: (parent) => db.query("SELECT * FROM posts WHERE author_id = ?", [parent.id]), // N queries
+ },
+};
+```
+
+### Solution: DataLoader
+
+```typescript
+import DataLoader from "dataloader";
+
+// Create loader per request (in context)
+const context = async () => ({
+ loaders: {
+ postsByAuthor: new DataLoader(async (authorIds) => {
+ const posts = await db.query("SELECT * FROM posts WHERE author_id IN (?)", [authorIds]);
+ // Return posts grouped by author_id in same order as input
+ return authorIds.map((id) => posts.filter((p) => p.author_id === id));
+ }),
+ },
+});
+
+// Use loader in resolver
+const resolvers = {
+ User: {
+ posts: (parent, _, { loaders }) => loaders.postsByAuthor.load(parent.id),
+ },
+};
+```
+
+## Best Practices
+
+### Keep Resolvers Thin
+
+```typescript
+// Bad - business logic in resolver
+const resolvers = {
+ Mutation: {
+ createOrder: async (_, { input }, { dataSources, user }) => {
+ const items = await dataSources.inventory.checkStock(input.items);
+ const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
+ const discount = user.isPremium ? total * 0.1 : 0;
+ // ... more logic
+ },
+ },
+};
+
+// Good - delegate to service
+const resolvers = {
+ Mutation: {
+ createOrder: async (_, { input }, { services, user }) => {
+ return services.orders.create(input, user);
+ },
+ },
+};
+```
+
+### Handle Null Appropriately
+
+```typescript
+const resolvers = {
+ Query: {
+ user: async (_, { id }, { dataSources }) => {
+ // Return null for not found (matches nullable schema)
+ return dataSources.usersAPI.getById(id);
+ },
+
+ userRequired: async (_, { id }, { dataSources }) => {
+ const user = await dataSources.usersAPI.getById(id);
+ if (!user) {
+ throw new GraphQLError("User not found");
+ }
+ return user;
+ },
+ },
+};
+```
+
+### Avoid Over-fetching in Parent
+
+```typescript
+// Bad - fetching all data upfront
+const resolvers = {
+ Query: {
+ user: async (_, { id }) => {
+ const user = await db.user.findById(id);
+ const posts = await db.posts.findByAuthor(id);
+ const followers = await db.followers.findByUser(id);
+ return { ...user, posts, followers };
+ },
+ },
+};
+
+// Good - fetch only when requested
+const resolvers = {
+ Query: {
+ user: (_, { id }) => db.user.findById(id),
+ },
+ User: {
+ posts: (parent) => db.posts.findByAuthor(parent.id),
+ followers: (parent) => db.followers.findByUser(parent.id),
+ },
+};
+```
diff --git a/.agents/skills/apollo-server/references/troubleshooting.md b/.agents/skills/apollo-server/references/troubleshooting.md
new file mode 100644
index 000000000..0645583dd
--- /dev/null
+++ b/.agents/skills/apollo-server/references/troubleshooting.md
@@ -0,0 +1,494 @@
+# Troubleshooting Reference
+
+## Table of Contents
+
+- [Setup Issues](#setup-issues)
+- [Schema Issues](#schema-issues)
+- [Runtime Errors](#runtime-errors)
+- [Performance Issues](#performance-issues)
+- [Integration Issues](#integration-issues)
+- [Debugging Tips](#debugging-tips)
+
+## Setup Issues
+
+### Module Not Found Errors
+
+**Error:** `Cannot find module '@apollo/server'`
+
+```bash
+# Ensure correct packages are installed
+npm install @apollo/server graphql
+
+# For Express integration
+npm install @apollo/server express graphql cors
+
+# Clear node_modules and reinstall if issues persist
+rm -rf node_modules package-lock.json
+npm install
+```
+
+**Error:** `Cannot find module '@apollo/server/standalone'`
+
+This is a subpath export. Ensure:
+
+- Node.js v18+ (for native ESM subpath exports)
+- TypeScript `moduleResolution` is `bundler`, `node16`, or `nodenext`
+
+### TypeScript Configuration
+
+**Error:** `Cannot use import statement outside a module`
+
+```json
+// package.json
+{
+ "type": "module"
+}
+
+// tsconfig.json
+{
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "esModuleInterop": true,
+ "target": "ES2022"
+ }
+}
+```
+
+**Error:** `Property 'xxx' does not exist on type 'BaseContext'`
+
+```typescript
+// Define and use typed context
+interface MyContext {
+ user?: User;
+ dataSources: DataSources;
+}
+
+const server = new ApolloServer({ typeDefs, resolvers });
+```
+
+### CommonJS Compatibility
+
+Apollo Server 4 is ESM-first. For CommonJS projects:
+
+```typescript
+// Use dynamic import
+const { ApolloServer } = await import('@apollo/server');
+
+// Or configure tsconfig for interop
+{
+ "compilerOptions": {
+ "module": "CommonJS",
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true
+ }
+}
+```
+
+## Schema Issues
+
+### Unknown Type Error
+
+**Error:** `Unknown type "User". Did you mean...`
+
+```typescript
+// Ensure all types are defined in typeDefs
+const typeDefs = `#graphql
+ type Query {
+ user(id: ID!): User # User must be defined
+ }
+
+ type User { # Define the type
+ id: ID!
+ name: String!
+ }
+`;
+```
+
+### Missing Resolver
+
+**Error:** `Cannot return null for non-nullable field Query.user`
+
+```typescript
+// Schema declares non-null
+type Query {
+ user(id: ID!): User! # ! means non-null
+}
+
+// Resolver must return a value
+const resolvers = {
+ Query: {
+ user: async (_, { id }, { dataSources }) => {
+ const user = await dataSources.usersAPI.getById(id);
+ if (!user) {
+ throw new GraphQLError('User not found'); // Throw, don't return null
+ }
+ return user;
+ },
+ },
+};
+```
+
+### Enum Mismatch
+
+**Error:** `Enum "Status" cannot represent value: "draft"`
+
+```typescript
+// Schema defines uppercase
+enum Status {
+ DRAFT
+ PUBLISHED
+}
+
+// But database returns lowercase
+// Solution: Map enum values
+const resolvers = {
+ Status: {
+ DRAFT: 'draft',
+ PUBLISHED: 'published',
+ },
+ // Or transform in resolver
+ Post: {
+ status: (parent) => parent.status.toUpperCase(),
+ },
+};
+```
+
+### Input Type Errors
+
+**Error:** `Variable "$input" got invalid value`
+
+```graphql
+# Schema
+input CreateUserInput {
+ email: String!
+ name: String!
+}
+
+# Query - ensure variable matches input type exactly
+mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) { id }
+}
+
+# Variables - must match schema structure
+{
+ "input": {
+ "email": "test@example.com",
+ "name": "Test User"
+ }
+}
+```
+
+## Runtime Errors
+
+### Context Undefined
+
+**Error:** `Cannot read properties of undefined (reading 'user')`
+
+```typescript
+// Ensure context function returns complete object
+const context = async ({ req }) => {
+ // Always return all expected properties
+ return {
+ user: (await getUser(req.headers.authorization)) ?? null,
+ dataSources: {
+ usersAPI: new UsersAPI(),
+ },
+ };
+};
+```
+
+### Async/Await Issues
+
+**Error:** `[object Promise]` returned instead of actual data
+
+```typescript
+// Bad - missing await
+const resolvers = {
+ Query: {
+ user: (_, { id }, { dataSources }) => {
+ dataSources.usersAPI.getById(id); // Missing return/await
+ },
+ },
+};
+
+// Good - return promise or use async/await
+const resolvers = {
+ Query: {
+ user: (_, { id }, { dataSources }) => {
+ return dataSources.usersAPI.getById(id); // Return promise
+ },
+ // OR
+ user: async (_, { id }, { dataSources }) => {
+ return await dataSources.usersAPI.getById(id); // Async/await
+ },
+ },
+};
+```
+
+### Circular Reference
+
+**Error:** `Converting circular structure to JSON`
+
+```typescript
+// Avoid returning raw ORM objects with circular refs
+const resolvers = {
+ Query: {
+ user: async (_, { id }) => {
+ const user = await prisma.user.findUnique({
+ where: { id },
+ include: { posts: { include: { author: true } } }, // Circular!
+ });
+
+ // Transform to plain object
+ return {
+ id: user.id,
+ name: user.name,
+ posts: user.posts.map((p) => ({ id: p.id, title: p.title })),
+ };
+ },
+ },
+};
+```
+
+## Performance Issues
+
+### N+1 Queries
+
+**Symptom:** Slow queries, many database calls
+
+```typescript
+// Problem: Each user triggers separate posts query
+const resolvers = {
+ User: {
+ posts: (parent) => db.posts.findByAuthor(parent.id), // N queries
+ },
+};
+
+// Solution: Use DataLoader
+import DataLoader from "dataloader";
+
+const context = async () => ({
+ loaders: {
+ postsByAuthor: new DataLoader(async (authorIds) => {
+ const posts = await db.posts.findByAuthorIds(authorIds); // 1 query
+ return authorIds.map((id) => posts.filter((p) => p.authorId === id));
+ }),
+ },
+});
+
+const resolvers = {
+ User: {
+ posts: (parent, _, { loaders }) => loaders.postsByAuthor.load(parent.id),
+ },
+};
+```
+
+### Memory Leaks
+
+**Symptom:** Memory usage grows over time
+
+```typescript
+// Problem: Shared data source instances
+const sharedAPI = new UsersAPI(); // Wrong!
+const context = async () => ({ dataSources: { usersAPI: sharedAPI } });
+
+// Solution: Create per-request instances
+const context = async () => ({
+ dataSources: {
+ usersAPI: new UsersAPI(), // New instance per request
+ },
+});
+
+// Problem: DataLoader created once
+const loader = new DataLoader(batchFn); // Wrong - caches forever!
+
+// Solution: Create per-request DataLoaders
+const context = async () => ({
+ loaders: {
+ userLoader: new DataLoader(batchFn), // New instance per request
+ },
+});
+```
+
+### Large Response Handling
+
+**Symptom:** Timeout or memory errors on large queries
+
+```typescript
+// Add pagination
+type Query {
+ users(limit: Int = 10, offset: Int = 0): [User!]!
+}
+
+// Limit query depth
+import depthLimit from 'graphql-depth-limit';
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [depthLimit(5)],
+});
+
+// Add query complexity limit
+import { createComplexityLimitRule } from 'graphql-validation-complexity';
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [createComplexityLimitRule(1000)],
+});
+```
+
+## Integration Issues
+
+### CORS Errors
+
+**Error:** `Access-Control-Allow-Origin` header missing
+
+```typescript
+import cors from "cors";
+
+// Express integration - add cors before middleware
+app.use(
+ "/graphql",
+ cors({
+ origin: ["http://localhost:3000", "https://myapp.com"],
+ credentials: true,
+ }),
+ express.json(),
+ expressMiddleware(server),
+);
+
+// Standalone - configure cors option
+const { url } = await startStandaloneServer(server, {
+ listen: { port: 4000 },
+ context: async ({ req }) => ({
+ /* ... */
+ }),
+ // Standalone has basic CORS enabled by default
+});
+```
+
+### Body Parser Issues
+
+**Error:** `req.body` is undefined or empty
+
+```typescript
+// Express - ensure json middleware is before Apollo
+app.use(express.json()); // Must come before expressMiddleware
+
+app.use(
+ "/graphql",
+ cors(),
+ express.json(), // JSON parser is required
+ expressMiddleware(server),
+);
+```
+
+### WebSocket / Subscriptions
+
+**Error:** Subscriptions not working
+
+```typescript
+// Apollo Server 4 doesn't include subscription support by default
+// Use graphql-ws package
+import { WebSocketServer } from "ws";
+import { useServer } from "graphql-ws/lib/use/ws";
+import { makeExecutableSchema } from "@graphql-tools/schema";
+
+const schema = makeExecutableSchema({ typeDefs, resolvers });
+
+const wsServer = new WebSocketServer({
+ server: httpServer,
+ path: "/graphql",
+});
+
+useServer({ schema }, wsServer);
+```
+
+## Debugging Tips
+
+### Enable Debug Logging
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ {
+ async requestDidStart({ request }) {
+ console.log("Query:", request.query);
+ console.log("Variables:", JSON.stringify(request.variables, null, 2));
+
+ return {
+ async willSendResponse({ response }) {
+ console.log("Response:", JSON.stringify(response.body, null, 2));
+ },
+ };
+ },
+ },
+ ],
+});
+```
+
+### Inspect Context
+
+```typescript
+const resolvers = {
+ Query: {
+ debug: (_, __, context) => {
+ console.log("Context keys:", Object.keys(context));
+ console.log("User:", context.user);
+ return "Check server logs";
+ },
+ },
+};
+```
+
+### Test Resolvers Directly
+
+```typescript
+// Unit test resolvers without server
+import { resolvers } from "./resolvers";
+
+describe("Query.user", () => {
+ it("returns user by id", async () => {
+ const mockDataSources = {
+ usersAPI: {
+ getById: jest.fn().mockResolvedValue({ id: "1", name: "Test" }),
+ },
+ };
+
+ const result = await resolvers.Query.user(undefined, { id: "1" }, { dataSources: mockDataSources });
+
+ expect(result).toEqual({ id: "1", name: "Test" });
+ });
+});
+```
+
+### Check Schema
+
+```typescript
+import { printSchema } from "graphql";
+import { makeExecutableSchema } from "@graphql-tools/schema";
+
+const schema = makeExecutableSchema({ typeDefs, resolvers });
+console.log(printSchema(schema));
+```
+
+### Apollo Sandbox
+
+Enable Apollo Sandbox for interactive debugging:
+
+```typescript
+import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
+});
+```
+
+Open `http://localhost:4000/graphql` in browser to access Sandbox.
diff --git a/.agents/skills/graphql-operations/SKILL.md b/.agents/skills/graphql-operations/SKILL.md
new file mode 100644
index 000000000..536902219
--- /dev/null
+++ b/.agents/skills/graphql-operations/SKILL.md
@@ -0,0 +1,244 @@
+---
+name: graphql-operations
+description: >
+ Guide for writing GraphQL operations (queries, mutations, fragments) following best practices. Use this skill when:
+ (1) writing GraphQL queries or mutations,
+ (2) organizing operations with fragments,
+ (3) optimizing data fetching patterns,
+ (4) setting up type generation or linting,
+ (5) reviewing operations for efficiency.
+license: MIT
+compatibility: Any GraphQL client (Apollo Client, urql, Relay, etc.)
+metadata:
+ author: apollographql
+ version: "1.0.0"
+allowed-tools: Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep
+---
+
+# GraphQL Operations Guide
+
+This guide covers best practices for writing GraphQL operations (queries, mutations, subscriptions) as a client developer. Well-written operations are efficient, type-safe, and maintainable.
+
+## Operation Basics
+
+### Query Structure
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+}
+```
+
+### Mutation Structure
+
+```graphql
+mutation CreatePost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ createdAt
+ }
+}
+```
+
+### Subscription Structure
+
+```graphql
+subscription OnMessageReceived($channelId: ID!) {
+ messageReceived(channelId: $channelId) {
+ id
+ content
+ sender {
+ id
+ name
+ }
+ }
+}
+```
+
+## Quick Reference
+
+### Operation Naming
+
+| Pattern | Example |
+| ------------ | ------------------------------------------- |
+| Query | `GetUser`, `ListPosts`, `SearchProducts` |
+| Mutation | `CreateUser`, `UpdatePost`, `DeleteComment` |
+| Subscription | `OnMessageReceived`, `OnUserStatusChanged` |
+
+### Variable Syntax
+
+```graphql
+# Required variable
+query GetUser($id: ID!) { ... }
+
+# Optional variable with default
+query ListPosts($first: Int = 20) { ... }
+
+# Multiple variables
+query SearchPosts($query: String!, $status: PostStatus, $first: Int = 10) { ... }
+```
+
+### Fragment Syntax
+
+```graphql
+# Define fragment
+fragment UserBasicInfo on User {
+ id
+ name
+ avatarUrl
+}
+
+# Use fragment
+query GetUser($id: ID!) {
+ user(id: $id) {
+ ...UserBasicInfo
+ email
+ }
+}
+```
+
+### Directives
+
+```graphql
+query GetUser($id: ID!, $includeEmail: Boolean!) {
+ user(id: $id) {
+ id
+ name
+ email @include(if: $includeEmail)
+ }
+}
+
+query GetPosts($skipDrafts: Boolean!) {
+ posts {
+ id
+ title
+ draft @skip(if: $skipDrafts)
+ }
+}
+```
+
+## Key Principles
+
+### 1. Request Only What You Need
+
+```graphql
+# Good: Specific fields
+query GetUserName($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+}
+
+# Avoid: Over-fetching
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ bio
+ posts {
+ id
+ title
+ content
+ comments {
+ id
+ }
+ }
+ followers {
+ id
+ name
+ }
+ # ... many unused fields
+ }
+}
+```
+
+### 2. Name All Operations
+
+```graphql
+# Good: Named operation
+query GetUserPosts($userId: ID!) {
+ user(id: $userId) {
+ posts {
+ id
+ title
+ }
+ }
+}
+
+# Avoid: Anonymous operation
+query {
+ user(id: "123") {
+ posts {
+ id
+ title
+ }
+ }
+}
+```
+
+### 3. Use Variables, Not Inline Values
+
+```graphql
+# Good: Variables
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+}
+
+# Avoid: Hardcoded values
+query {
+ user(id: "123") {
+ id
+ name
+ }
+}
+```
+
+### 4. Colocate Fragments with Components
+
+```tsx
+// UserAvatar.tsx
+export const USER_AVATAR_FRAGMENT = gql`
+ fragment UserAvatar on User {
+ id
+ name
+ avatarUrl
+ }
+`;
+
+function UserAvatar({ user }) {
+ return ;
+}
+```
+
+## Reference Files
+
+Detailed documentation for specific topics:
+
+- [Queries](references/queries.md) - Query patterns and optimization
+- [Mutations](references/mutations.md) - Mutation patterns and error handling
+- [Fragments](references/fragments.md) - Fragment organization and reuse
+- [Variables](references/variables.md) - Variable usage and types
+- [Tooling](references/tooling.md) - Code generation and linting
+
+## Ground Rules
+
+- ALWAYS name your operations (no anonymous queries/mutations)
+- ALWAYS use variables for dynamic values
+- ALWAYS request only the fields you need
+- ALWAYS include `id` field for cacheable types
+- NEVER hardcode values in operations
+- NEVER duplicate field selections across files
+- PREFER fragments for reusable field selections
+- PREFER colocating fragments with components
+- USE descriptive operation names that reflect purpose
+- USE `@include`/`@skip` for conditional fields
diff --git a/.agents/skills/graphql-operations/references/fragments.md b/.agents/skills/graphql-operations/references/fragments.md
new file mode 100644
index 000000000..db9b1c6e1
--- /dev/null
+++ b/.agents/skills/graphql-operations/references/fragments.md
@@ -0,0 +1,536 @@
+# Fragment Patterns
+
+This reference covers patterns for organizing and using GraphQL fragments effectively.
+
+## Table of Contents
+
+- [Fragment Basics](#fragment-basics)
+- [Fragment Colocation](#fragment-colocation)
+- [Fragment Reuse](#fragment-reuse)
+- [Inline Fragments](#inline-fragments)
+- [Type Conditions](#type-conditions)
+- [Fragment Composition](#fragment-composition)
+- [Anti-Patterns](#anti-patterns)
+
+## Fragment Basics
+
+### Defining Fragments
+
+```graphql
+fragment UserBasicInfo on User {
+ id
+ name
+ avatarUrl
+}
+```
+
+### Using Fragments
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ ...UserBasicInfo
+ email
+ }
+}
+
+fragment UserBasicInfo on User {
+ id
+ name
+ avatarUrl
+}
+```
+
+### Fragment Spread
+
+The `...` operator spreads fragment fields:
+
+```graphql
+query GetPost($id: ID!) {
+ post(id: $id) {
+ id
+ title
+ author {
+ ...UserBasicInfo # Spreads id, name, avatarUrl
+ }
+ }
+}
+```
+
+## Fragment Colocation
+
+### Colocate with Components
+
+Keep fragments next to the components that use them:
+
+```
+src/
+ components/
+ UserAvatar/
+ UserAvatar.tsx
+ UserAvatar.fragment.graphql
+ UserCard/
+ UserCard.tsx
+ UserCard.fragment.graphql
+ PostList/
+ PostList.tsx
+ PostList.query.graphql
+```
+
+### Component Owns Its Data
+
+```tsx
+// UserAvatar.tsx
+import { gql } from "@apollo/client";
+
+export const USER_AVATAR_FRAGMENT = gql`
+ fragment UserAvatar on User {
+ id
+ name
+ avatarUrl
+ }
+`;
+
+interface UserAvatarProps {
+ user: UserAvatarFragment;
+}
+
+export function UserAvatar({ user }: UserAvatarProps) {
+ return ;
+}
+```
+
+### Parent Composes Fragments
+
+```tsx
+// UserCard.tsx
+import { gql } from "@apollo/client";
+import { USER_AVATAR_FRAGMENT, UserAvatar } from "./UserAvatar";
+
+export const USER_CARD_FRAGMENT = gql`
+ fragment UserCard on User {
+ id
+ name
+ bio
+ ...UserAvatar
+ }
+ ${USER_AVATAR_FRAGMENT}
+`;
+
+export function UserCard({ user }: { user: UserCardFragment }) {
+ return (
+
+ );
+}
+```
+
+## Fragment Reuse
+
+### Shared Fragments
+
+For common patterns used across many components:
+
+```graphql
+# fragments/common.graphql
+
+fragment Timestamps on Node {
+ createdAt
+ updatedAt
+}
+
+fragment PageInfoFields on PageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+}
+```
+
+### Domain-Specific Fragments
+
+```graphql
+# fragments/user.graphql
+
+fragment UserSummary on User {
+ id
+ name
+ avatarUrl
+}
+
+fragment UserProfile on User {
+ ...UserSummary
+ bio
+ location
+ website
+ socialLinks {
+ platform
+ url
+ }
+}
+
+fragment UserWithStats on User {
+ ...UserSummary
+ followerCount
+ followingCount
+ postCount
+}
+```
+
+### Using Shared Fragments
+
+```graphql
+query GetPost($id: ID!) {
+ post(id: $id) {
+ id
+ title
+ ...Timestamps
+ author {
+ ...UserSummary
+ }
+ }
+}
+```
+
+## Inline Fragments
+
+### Anonymous Inline Fragments
+
+For grouping fields with directives:
+
+```graphql
+query GetUser($id: ID!, $includeDetails: Boolean!) {
+ user(id: $id) {
+ id
+ name
+ ... @include(if: $includeDetails) {
+ email
+ bio
+ website
+ }
+ }
+}
+```
+
+### Inline Fragments on Interfaces
+
+```graphql
+query GetNodes($ids: [ID!]!) {
+ nodes(ids: $ids) {
+ id
+ ... on User {
+ name
+ email
+ }
+ ... on Post {
+ title
+ content
+ }
+ }
+}
+```
+
+## Type Conditions
+
+### Fragments on Union Types
+
+```graphql
+query Search($query: String!) {
+ search(query: $query) {
+ ... on User {
+ id
+ name
+ avatarUrl
+ }
+ ... on Post {
+ id
+ title
+ excerpt
+ }
+ ... on Comment {
+ id
+ body
+ post {
+ id
+ title
+ }
+ }
+ }
+}
+```
+
+### Named Fragments for Unions
+
+```graphql
+query Search($query: String!) {
+ search(query: $query) {
+ ...SearchResultUser
+ ...SearchResultPost
+ ...SearchResultComment
+ }
+}
+
+fragment SearchResultUser on User {
+ id
+ name
+ avatarUrl
+}
+
+fragment SearchResultPost on Post {
+ id
+ title
+ excerpt
+ author {
+ name
+ }
+}
+
+fragment SearchResultComment on Comment {
+ id
+ body
+ post {
+ id
+ title
+ }
+}
+```
+
+### Handling \_\_typename
+
+```typescript
+function SearchResult({ result }) {
+ switch (result.__typename) {
+ case 'User':
+ return ;
+ case 'Post':
+ return ;
+ case 'Comment':
+ return ;
+ }
+}
+```
+
+## Fragment Composition
+
+### Building Up Fragments
+
+```graphql
+# Base fragment
+fragment PostCore on Post {
+ id
+ title
+ slug
+}
+
+# Extended fragment
+fragment PostPreview on Post {
+ ...PostCore
+ excerpt
+ featuredImage {
+ url
+ }
+}
+
+# Full fragment
+fragment PostFull on Post {
+ ...PostPreview
+ content
+ publishedAt
+ author {
+ ...UserSummary
+ }
+ tags {
+ id
+ name
+ }
+}
+```
+
+### Fragments in Fragments
+
+```graphql
+fragment CommentWithAuthor on Comment {
+ id
+ body
+ createdAt
+ author {
+ ...UserSummary
+ }
+}
+
+fragment PostWithComments on Post {
+ id
+ title
+ comments(first: 10) {
+ edges {
+ node {
+ ...CommentWithAuthor
+ }
+ }
+ }
+}
+```
+
+### Fragment Spread Order
+
+Order doesn't matter - fields are merged:
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ ...UserProfile
+ ...UserStats
+ # Both fragments' fields are included
+ }
+}
+```
+
+## Anti-Patterns
+
+### Avoid Giant Fragments
+
+```graphql
+# Bad: Too many fields, not all needed everywhere
+fragment UserEverything on User {
+ id
+ name
+ email
+ bio
+ avatarUrl
+ coverImage
+ website
+ location
+ socialLinks { ... }
+ posts { ... }
+ followers { ... }
+ following { ... }
+ # ... 50 more fields
+}
+
+# Good: Focused fragments for specific uses
+fragment UserAvatar on User {
+ id
+ name
+ avatarUrl
+}
+
+fragment UserProfile on User {
+ id
+ name
+ bio
+ avatarUrl
+ website
+ location
+}
+```
+
+### Avoid Unused Fragment Fields
+
+```graphql
+# Bad: Component only uses name and avatarUrl
+fragment UserInfo on User {
+ id
+ name
+ email # unused
+ avatarUrl
+ bio # unused
+ website # unused
+}
+
+# Good: Only request what's needed
+fragment UserInfo on User {
+ id
+ name
+ avatarUrl
+}
+```
+
+### Avoid Deeply Nested Fragments
+
+```graphql
+# Bad: Hard to understand what's being fetched
+fragment Level1 on User {
+ ...Level2
+}
+fragment Level2 on User {
+ ...Level3
+}
+fragment Level3 on User {
+ ...Level4
+}
+# ... continues
+
+# Good: Keep nesting shallow
+fragment UserWithPosts on User {
+ id
+ name
+ posts {
+ ...PostPreview
+ }
+}
+```
+
+### Avoid Circular Fragment Dependencies
+
+```graphql
+# Bad: Circular reference (won't work)
+fragment UserWithPosts on User {
+ posts {
+ ...PostWithAuthor
+ }
+}
+
+fragment PostWithAuthor on Post {
+ author {
+ ...UserWithPosts # Circular!
+ }
+}
+
+# Good: Break the cycle
+fragment UserWithPosts on User {
+ posts {
+ ...PostPreview
+ }
+}
+
+fragment PostWithAuthor on Post {
+ author {
+ ...UserSummary # Different fragment, no cycle
+ }
+}
+```
diff --git a/.agents/skills/graphql-operations/references/mutations.md b/.agents/skills/graphql-operations/references/mutations.md
new file mode 100644
index 000000000..cc5c09d94
--- /dev/null
+++ b/.agents/skills/graphql-operations/references/mutations.md
@@ -0,0 +1,435 @@
+# Mutation Patterns
+
+This reference covers patterns for writing effective GraphQL mutations.
+
+## Table of Contents
+
+- [Mutation Structure](#mutation-structure)
+- [Input Patterns](#input-patterns)
+- [Response Selection](#response-selection)
+- [Error Handling](#error-handling)
+- [Optimistic Updates](#optimistic-updates)
+- [Mutation Naming](#mutation-naming)
+
+## Mutation Structure
+
+### Basic Mutation
+
+```graphql
+mutation CreatePost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ createdAt
+ }
+}
+```
+
+Variables:
+
+```json
+{
+ "input": {
+ "title": "My Post",
+ "content": "Post content..."
+ }
+}
+```
+
+### Mutation with Multiple Arguments
+
+```graphql
+mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
+ updatePost(id: $id, input: $input) {
+ id
+ title
+ updatedAt
+ }
+}
+```
+
+### Multiple Mutations
+
+Execute multiple mutations in one request (sequential execution):
+
+```graphql
+mutation SetupUserProfile($userId: ID!, $profileInput: ProfileInput!, $settingsInput: SettingsInput!) {
+ updateProfile(userId: $userId, input: $profileInput) {
+ id
+ bio
+ }
+ updateSettings(userId: $userId, input: $settingsInput) {
+ id
+ theme
+ notifications
+ }
+}
+```
+
+## Input Patterns
+
+### Single Input Object
+
+Recommended pattern - single input argument:
+
+```graphql
+mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ email
+ }
+}
+```
+
+```json
+{
+ "input": {
+ "email": "user@example.com",
+ "name": "John Doe",
+ "password": "secret123"
+ }
+}
+```
+
+### Nested Input Objects
+
+```graphql
+mutation CreateOrder($input: CreateOrderInput!) {
+ createOrder(input: $input) {
+ id
+ total
+ }
+}
+```
+
+```json
+{
+ "input": {
+ "items": [
+ { "productId": "prod_1", "quantity": 2 },
+ { "productId": "prod_2", "quantity": 1 }
+ ],
+ "shippingAddress": {
+ "street": "123 Main St",
+ "city": "New York",
+ "country": "US"
+ }
+ }
+}
+```
+
+### Optional Fields
+
+```graphql
+mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
+ updateUser(id: $id, input: $input) {
+ id
+ name
+ bio
+ }
+}
+```
+
+```json
+{
+ "id": "user_123",
+ "input": {
+ "name": "New Name"
+ // bio not included - won't be changed
+ }
+}
+```
+
+## Response Selection
+
+### Return the Modified Object
+
+Always return the mutated object with updated fields:
+
+```graphql
+mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
+ updatePost(id: $id, input: $input) {
+ id
+ title
+ content
+ updatedAt # Server-set field
+ }
+}
+```
+
+### Return Related Objects
+
+If mutation affects related data, include it:
+
+```graphql
+mutation AddComment($input: AddCommentInput!) {
+ addComment(input: $input) {
+ id
+ body
+ post {
+ id
+ commentCount # Updated count
+ }
+ author {
+ id
+ name
+ }
+ }
+}
+```
+
+### Return for Cache Updates
+
+Select fields needed to update your cache:
+
+```graphql
+mutation DeletePost($id: ID!) {
+ deletePost(id: $id) {
+ id # Needed to remove from cache
+ author {
+ id
+ postCount # May need to decrement
+ }
+ }
+}
+```
+
+### Return Connections for List Updates
+
+```graphql
+mutation CreatePost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ createdAt
+ author {
+ id
+ posts(first: 1) {
+ edges {
+ node {
+ id
+ }
+ }
+ totalCount
+ }
+ }
+ }
+}
+```
+
+## Error Handling
+
+### Query Result Unions
+
+When schema uses union types for errors:
+
+```graphql
+mutation CreateUser($input: CreateUserInput!) {
+ createUser(input: $input) {
+ ... on CreateUserSuccess {
+ user {
+ id
+ email
+ }
+ }
+ ... on ValidationError {
+ message
+ field
+ }
+ ... on EmailAlreadyExists {
+ message
+ existingUserId
+ }
+ }
+}
+```
+
+### Handle All Cases
+
+```typescript
+const result = await client.mutate({
+ mutation: CREATE_USER,
+ variables: { input },
+});
+
+const { createUser } = result.data;
+
+switch (createUser.__typename) {
+ case "CreateUserSuccess":
+ // Handle success
+ return createUser.user;
+ case "ValidationError":
+ // Handle validation error
+ throw new ValidationError(createUser.field, createUser.message);
+ case "EmailAlreadyExists":
+ // Handle specific business error
+ throw new EmailExistsError(createUser.existingUserId);
+}
+```
+
+### GraphQL Errors
+
+Handle network and GraphQL errors:
+
+```typescript
+try {
+ const result = await client.mutate({
+ mutation: CREATE_POST,
+ variables: { input },
+ });
+ return result.data.createPost;
+} catch (error) {
+ if (error.graphQLErrors?.length) {
+ // Handle GraphQL errors
+ const gqlError = error.graphQLErrors[0];
+ if (gqlError.extensions?.code === "UNAUTHENTICATED") {
+ // Redirect to login
+ }
+ }
+ if (error.networkError) {
+ // Handle network error
+ }
+ throw error;
+}
+```
+
+## Optimistic Updates
+
+### Select Fields for Optimistic Response
+
+Include all fields that will display immediately:
+
+```graphql
+mutation LikePost($postId: ID!) {
+ likePost(postId: $postId) {
+ id
+ likeCount
+ isLikedByViewer
+ }
+}
+```
+
+```typescript
+client.mutate({
+ mutation: LIKE_POST,
+ variables: { postId: "post_123" },
+ optimisticResponse: {
+ likePost: {
+ __typename: "Post",
+ id: "post_123",
+ likeCount: currentCount + 1,
+ isLikedByViewer: true,
+ },
+ },
+});
+```
+
+### Include Created IDs
+
+For create mutations, use temporary IDs:
+
+```graphql
+mutation AddComment($input: AddCommentInput!) {
+ addComment(input: $input) {
+ id
+ body
+ createdAt
+ author {
+ id
+ name
+ avatarUrl
+ }
+ }
+}
+```
+
+```typescript
+client.mutate({
+ mutation: ADD_COMMENT,
+ variables: { input: { postId, body } },
+ optimisticResponse: {
+ addComment: {
+ __typename: "Comment",
+ id: `temp-${Date.now()}`, // Temporary ID
+ body,
+ createdAt: new Date().toISOString(),
+ author: {
+ __typename: "User",
+ id: currentUser.id,
+ name: currentUser.name,
+ avatarUrl: currentUser.avatarUrl,
+ },
+ },
+ },
+});
+```
+
+## Mutation Naming
+
+### Naming Conventions
+
+| Operation | Pattern | Examples |
+| ------------ | -------------------- | ------------------------------- |
+| Create | `Create{Type}` | `CreateUser`, `CreatePost` |
+| Update | `Update{Type}` | `UpdateUser`, `UpdatePost` |
+| Delete | `Delete{Type}` | `DeleteUser`, `DeletePost` |
+| Action | `{Verb}{Type}` | `PublishPost`, `ArchiveProject` |
+| Relationship | `{Add/Remove}{Type}` | `AddTeamMember`, `RemoveTag` |
+
+### Good Names
+
+```graphql
+mutation CreateUser($input: CreateUserInput!) { ... }
+mutation UpdateUserProfile($userId: ID!, $input: ProfileInput!) { ... }
+mutation DeletePost($id: ID!) { ... }
+mutation PublishArticle($id: ID!) { ... }
+mutation ArchiveProject($id: ID!) { ... }
+mutation AddItemToCart($input: AddItemInput!) { ... }
+mutation RemoveTeamMember($teamId: ID!, $userId: ID!) { ... }
+mutation FollowUser($userId: ID!) { ... }
+mutation MarkNotificationAsRead($id: ID!) { ... }
+```
+
+### Operation Name Matches Server
+
+Match client operation name to server mutation:
+
+```graphql
+# Server schema
+type Mutation {
+ createPost(input: CreatePostInput!): Post!
+}
+
+# Client operation - name reflects the action
+mutation CreatePost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ }
+}
+```
+
+### Context-Specific Names
+
+Add context when same mutation is used differently:
+
+```graphql
+# For creating a draft
+mutation CreateDraftPost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ status
+ }
+}
+
+# For creating and publishing immediately
+mutation CreateAndPublishPost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ title
+ status
+ publishedAt
+ }
+}
+```
diff --git a/.agents/skills/graphql-operations/references/queries.md b/.agents/skills/graphql-operations/references/queries.md
new file mode 100644
index 000000000..cddf3e59b
--- /dev/null
+++ b/.agents/skills/graphql-operations/references/queries.md
@@ -0,0 +1,504 @@
+# Query Patterns
+
+This reference covers patterns for writing effective GraphQL queries.
+
+## Table of Contents
+
+- [Query Structure](#query-structure)
+- [Field Selection](#field-selection)
+- [Aliases](#aliases)
+- [Directives](#directives)
+- [Query Naming](#query-naming)
+- [Query Organization](#query-organization)
+- [Performance Optimization](#performance-optimization)
+
+## Query Structure
+
+### Basic Query
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+}
+```
+
+Components:
+
+- `query` - Operation type
+- `GetUser` - Operation name
+- `($id: ID!)` - Variable definitions
+- `user(id: $id)` - Field with argument
+- `{ id name email }` - Selection set
+
+### Query with Multiple Root Fields
+
+```graphql
+query GetDashboardData($userId: ID!) {
+ user(id: $userId) {
+ id
+ name
+ }
+ notifications(first: 5) {
+ id
+ message
+ }
+ stats {
+ totalPosts
+ totalComments
+ }
+}
+```
+
+### Nested Queries
+
+```graphql
+query GetUserWithPosts($userId: ID!) {
+ user(id: $userId) {
+ id
+ name
+ posts(first: 10) {
+ edges {
+ node {
+ id
+ title
+ comments(first: 3) {
+ edges {
+ node {
+ id
+ body
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## Field Selection
+
+### Request Only Needed Fields
+
+```graphql
+# For a user card component
+query GetUserCard($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ avatarUrl
+ # Don't request email, bio, etc. if not displayed
+ }
+}
+```
+
+### Always Include ID Fields
+
+Include `id` for any type you might cache or refetch:
+
+```graphql
+query GetPost($id: ID!) {
+ post(id: $id) {
+ id # Always include for caching
+ title
+ author {
+ id # Include for author cache entry
+ name
+ }
+ }
+}
+```
+
+### Selecting Connections
+
+For paginated data, request what you need:
+
+```graphql
+query GetUserPosts($userId: ID!, $first: Int!, $after: String) {
+ user(id: $userId) {
+ id
+ posts(first: $first, after: $after) {
+ edges {
+ node {
+ id
+ title
+ excerpt
+ }
+ cursor # Only if implementing infinite scroll
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ totalCount # Only if displaying total
+ }
+ }
+}
+```
+
+## Aliases
+
+### Basic Alias
+
+Rename fields in the response:
+
+```graphql
+query GetUserNames($id: ID!) {
+ user(id: $id) {
+ userId: id
+ displayName: name
+ }
+}
+
+# Response: { user: { userId: "123", displayName: "John" } }
+```
+
+### Query Same Field Multiple Times
+
+```graphql
+query GetMultipleUsers {
+ admin: user(id: "1") {
+ id
+ name
+ }
+ moderator: user(id: "2") {
+ id
+ name
+ }
+ currentUser: user(id: "3") {
+ id
+ name
+ }
+}
+```
+
+### Alias with Different Arguments
+
+```graphql
+query GetPostsByStatus($userId: ID!) {
+ user(id: $userId) {
+ id
+ publishedPosts: posts(status: PUBLISHED, first: 5) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ draftPosts: posts(status: DRAFT, first: 5) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+}
+```
+
+## Directives
+
+### @include Directive
+
+Include field only if condition is true:
+
+```graphql
+query GetUser($id: ID!, $includeEmail: Boolean!) {
+ user(id: $id) {
+ id
+ name
+ email @include(if: $includeEmail)
+ }
+}
+
+# Variables: { id: "123", includeEmail: true }
+# Returns email field
+
+# Variables: { id: "123", includeEmail: false }
+# Does not return email field
+```
+
+### @skip Directive
+
+Skip field if condition is true:
+
+```graphql
+query GetPost($id: ID!, $isPreview: Boolean!) {
+ post(id: $id) {
+ id
+ title
+ content @skip(if: $isPreview)
+ excerpt
+ }
+}
+```
+
+### Directives on Fragments
+
+```graphql
+query GetUser($id: ID!, $expanded: Boolean!) {
+ user(id: $id) {
+ id
+ name
+ ...UserDetails @include(if: $expanded)
+ }
+}
+
+fragment UserDetails on User {
+ bio
+ website
+ socialLinks {
+ platform
+ url
+ }
+}
+```
+
+### Combining Directives
+
+```graphql
+query GetPost($id: ID!, $showComments: Boolean!, $hideAuthor: Boolean!) {
+ post(id: $id) {
+ id
+ title
+ author @skip(if: $hideAuthor) {
+ id
+ name
+ }
+ comments(first: 10) @include(if: $showComments) {
+ edges {
+ node {
+ id
+ body
+ }
+ }
+ }
+ }
+}
+```
+
+## Query Naming
+
+### Naming Conventions
+
+| Purpose | Pattern | Examples |
+| --------------------- | ------------------ | ------------------------------------ |
+| Fetch single item | `Get{Type}` | `GetUser`, `GetPost` |
+| Fetch list | `List{Types}` | `ListUsers`, `ListPosts` |
+| Search | `Search{Types}` | `SearchUsers`, `SearchProducts` |
+| Fetch for specific UI | `Get{Feature}Data` | `GetDashboardData`, `GetProfilePage` |
+
+### Good Names
+
+```graphql
+query GetUserProfile($id: ID!) { ... }
+query ListRecentPosts($first: Int!) { ... }
+query SearchProducts($query: String!) { ... }
+query GetOrderDetails($orderId: ID!) { ... }
+query GetHomeFeed($userId: ID!) { ... }
+```
+
+### Avoid Generic Names
+
+```graphql
+# Avoid
+query Data { ... }
+query Query1 { ... }
+query FetchStuff { ... }
+
+# Prefer
+query GetCurrentUser { ... }
+query ListActiveProjects { ... }
+query SearchCustomers($query: String!) { ... }
+```
+
+## Query Organization
+
+### One Query Per File
+
+```
+src/
+ graphql/
+ queries/
+ GetUser.graphql
+ ListPosts.graphql
+ SearchProducts.graphql
+```
+
+```graphql
+# GetUser.graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ email
+ }
+}
+```
+
+### Colocate with Components
+
+```
+src/
+ components/
+ UserProfile/
+ UserProfile.tsx
+ UserProfile.graphql
+ UserProfile.test.tsx
+```
+
+### Import and Use
+
+```typescript
+// With graphql-tag
+import { gql } from "@apollo/client";
+
+export const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+ }
+`;
+
+// With .graphql files (requires loader)
+import { GetUserDocument } from "./UserProfile.generated";
+```
+
+## Performance Optimization
+
+### Avoid Over-fetching
+
+Only request fields used by your component:
+
+```graphql
+# For a list view - minimal fields
+query ListPostsForIndex {
+ posts(first: 20) {
+ edges {
+ node {
+ id
+ title
+ excerpt
+ author { name }
+ }
+ }
+ }
+}
+
+# For detail view - more fields
+query GetPostDetail($id: ID!) {
+ post(id: $id) {
+ id
+ title
+ content
+ publishedAt
+ author {
+ id
+ name
+ bio
+ avatarUrl
+ }
+ comments(first: 10) { ... }
+ }
+}
+```
+
+### Use Pagination
+
+Never fetch unbounded lists:
+
+```graphql
+# Avoid
+query GetAllPosts {
+ posts {
+ # Could return thousands
+ id
+ title
+ }
+}
+
+# Prefer
+query GetPosts($first: Int = 20, $after: String) {
+ posts(first: $first, after: $after) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}
+```
+
+### Batch Related Queries
+
+Fetch related data in one request:
+
+```graphql
+# Instead of multiple queries
+query GetDashboard($userId: ID!) {
+ user(id: $userId) {
+ id
+ name
+ }
+ recentPosts: posts(first: 5, orderBy: { field: CREATED_AT, direction: DESC }) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ notifications(first: 10, unreadOnly: true) {
+ edges {
+ node {
+ id
+ message
+ }
+ }
+ }
+}
+```
+
+### Use Fragments for Repeated Selections
+
+```graphql
+query GetPostsWithAuthors {
+ posts(first: 10) {
+ edges {
+ node {
+ id
+ title
+ author {
+ ...AuthorInfo
+ }
+ }
+ }
+ }
+ featuredPost {
+ id
+ title
+ author {
+ ...AuthorInfo
+ }
+ }
+}
+
+fragment AuthorInfo on User {
+ id
+ name
+ avatarUrl
+}
+```
diff --git a/.agents/skills/graphql-operations/references/tooling.md b/.agents/skills/graphql-operations/references/tooling.md
new file mode 100644
index 000000000..4fb4e6180
--- /dev/null
+++ b/.agents/skills/graphql-operations/references/tooling.md
@@ -0,0 +1,404 @@
+# Tooling
+
+This reference covers tools for working with GraphQL operations, including code generation and linting.
+
+## Table of Contents
+
+- [GraphQL Code Generator](#graphql-code-generator)
+- [ESLint GraphQL](#eslint-graphql)
+- [IDE Extensions](#ide-extensions)
+- [Operation Validation](#operation-validation)
+
+## GraphQL Code Generator
+
+### Overview
+
+GraphQL Code Generator generates TypeScript types from your schema and operations, ensuring type safety throughout your application.
+
+### Installation
+
+```bash
+npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
+```
+
+### Basic Configuration
+
+Create `codegen.ts`:
+
+```typescript
+// codegen.ts
+import { CodegenConfig } from "@graphql-codegen/cli";
+
+const config: CodegenConfig = {
+ overwrite: true,
+ schema: "",
+ // This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
+ documents: ["src/**/*.{ts,tsx}"],
+ // Don't exit with non-zero status when there are no documents
+ ignoreNoDocuments: true,
+ generates: {
+ // Use a path that works the best for the structure of your application
+ "./src/types/__generated__/graphql.ts": {
+ plugins: ["typescript", "typescript-operations", "typed-document-node"],
+ config: {
+ avoidOptionals: {
+ // Use `null` for nullable fields instead of optionals
+ field: true,
+ // Allow nullable input fields to remain unspecified
+ inputValue: false,
+ },
+ // Use `unknown` instead of `any` for unconfigured scalars
+ defaultScalarType: "unknown",
+ // Apollo Client always includes `__typename` fields
+ nonOptionalTypename: true,
+ // Apollo Client doesn't add the `__typename` field to root types so
+ // don't generate a type for the `__typename` for root operation types.
+ skipTypeNameForRoot: true,
+ },
+ },
+ },
+};
+
+export default config;
+```
+
+### Run Generation
+
+```bash
+# One-time generation
+npx graphql-codegen
+
+# Watch mode for development
+npx graphql-codegen --watch
+```
+
+### Package Scripts
+
+```json
+{
+ "scripts": {
+ "codegen": "graphql-codegen",
+ "codegen:watch": "graphql-codegen --watch"
+ }
+}
+```
+
+### Generated Types Usage
+
+```tsx
+// Before: Manual typing
+const GET_USER = gql`
+ query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+ }
+`;
+
+// Manually typed
+interface GetUserData {
+ user: {
+ id: string;
+ name: string;
+ } | null;
+}
+
+const { data } = useQuery(GET_USER, { variables: { id } });
+
+// After: Generated types
+import { useGetUserQuery } from "./generated/graphql";
+
+const { data } = useGetUserQuery({ variables: { id } });
+// data.user is fully typed!
+```
+
+### Near-Operation File Generation
+
+Generate types next to operations:
+
+```typescript
+const config: CodegenConfig = {
+ schema: "http://localhost:4000/graphql",
+ documents: ["src/**/*.graphql"],
+ generates: {
+ "src/": {
+ preset: "near-operation-file",
+ presetConfig: {
+ extension: ".generated.ts",
+ baseTypesPath: "generated/graphql.ts",
+ },
+ plugins: ["typescript-operations", "typescript-react-apollo"],
+ },
+ "src/generated/graphql.ts": {
+ plugins: ["typescript"],
+ },
+ },
+};
+```
+
+Results in:
+
+```
+src/
+ components/
+ UserCard/
+ UserCard.graphql
+ UserCard.generated.ts # Generated types for this file
+```
+
+### Fragment Types
+
+```tsx
+// UserAvatar.graphql
+// fragment UserAvatar on User {
+// id
+// name
+// avatarUrl
+// }
+
+import { UserAvatarFragment } from "./UserAvatar.generated";
+
+interface UserAvatarProps {
+ user: UserAvatarFragment;
+}
+
+export function UserAvatar({ user }: UserAvatarProps) {
+ return ;
+}
+```
+
+## ESLint GraphQL
+
+### Installation
+
+```bash
+npm install -D @graphql-eslint/eslint-plugin
+```
+
+### Configuration
+
+```javascript
+// eslint.config.js (flat config)
+import graphqlPlugin from "@graphql-eslint/eslint-plugin";
+
+export default [
+ {
+ files: ["**/*.graphql"],
+ languageOptions: {
+ parser: graphqlPlugin.parser,
+ },
+ plugins: {
+ "@graphql-eslint": graphqlPlugin,
+ },
+ rules: {
+ "@graphql-eslint/known-type-names": "error",
+ "@graphql-eslint/no-anonymous-operations": "error",
+ "@graphql-eslint/no-duplicate-fields": "error",
+ "@graphql-eslint/naming-convention": [
+ "error",
+ {
+ OperationDefinition: {
+ style: "PascalCase",
+ forbiddenPrefixes: ["Query", "Mutation", "Subscription"],
+ },
+ FragmentDefinition: {
+ style: "PascalCase",
+ },
+ },
+ ],
+ },
+ },
+];
+```
+
+### Recommended Rules
+
+```javascript
+rules: {
+ // Syntax and validity
+ '@graphql-eslint/known-type-names': 'error',
+ '@graphql-eslint/known-fragment-names': 'error',
+ '@graphql-eslint/no-undefined-variables': 'error',
+ '@graphql-eslint/no-unused-variables': 'error',
+ '@graphql-eslint/no-unused-fragments': 'error',
+ '@graphql-eslint/unique-operation-name': 'error',
+ '@graphql-eslint/unique-fragment-name': 'error',
+
+ // Best practices
+ '@graphql-eslint/no-anonymous-operations': 'error',
+ '@graphql-eslint/no-duplicate-fields': 'error',
+ '@graphql-eslint/require-id-when-available': 'warn',
+
+ // Naming
+ '@graphql-eslint/naming-convention': ['error', { ... }],
+}
+```
+
+### Schema-Aware Rules
+
+Provide schema for advanced validation:
+
+```javascript
+{
+ files: ['**/*.graphql'],
+ languageOptions: {
+ parser: graphqlPlugin.parser,
+ parserOptions: {
+ schema: './schema.graphql',
+ // or
+ schema: 'http://localhost:4000/graphql',
+ },
+ },
+}
+```
+
+## IDE Extensions
+
+### VS Code
+
+**GraphQL: Language Feature Support** (GraphQL Foundation)
+
+- Syntax highlighting
+- Autocomplete for schema types
+- Go to definition
+- Hover documentation
+- Validation against schema
+
+Configuration (`.graphqlrc.yml`):
+
+```yaml
+schema: "http://localhost:4000/graphql"
+documents: "src/**/*.{graphql,ts,tsx}"
+```
+
+**Apollo GraphQL** (Apollo)
+
+- Apollo-specific features
+- Schema registry integration
+- Performance insights
+
+### JetBrains IDEs
+
+**GraphQL** plugin:
+
+- Syntax highlighting
+- Schema-aware completion
+- Validation
+- Navigate to definition
+
+Configuration (`.graphqlconfig`):
+
+```json
+{
+ "schemaPath": "./schema.graphql",
+ "includes": ["src/**/*.graphql"]
+}
+```
+
+### Configuration Files
+
+Common configuration file names:
+
+- `.graphqlrc` (JSON)
+- `.graphqlrc.yml` (YAML)
+- `.graphqlrc.json` (JSON)
+- `graphql.config.js` (JavaScript)
+
+```yaml
+# .graphqlrc.yml
+schema: "http://localhost:4000/graphql"
+documents: "src/**/*.graphql"
+extensions:
+ codegen:
+ generates:
+ ./src/generated/graphql.ts:
+ plugins:
+ - typescript
+ - typescript-operations
+```
+
+## Operation Validation
+
+### Validate Against Schema
+
+```bash
+# Using graphql-inspector
+npx graphql-inspector validate ./src/**/*.graphql ./schema.graphql
+```
+
+### CI Integration
+
+```yaml
+# .github/workflows/graphql.yml
+name: GraphQL Validation
+
+on: [push, pull_request]
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Download schema
+ run: npx graphql-inspector introspect http://localhost:4000/graphql --write schema.graphql
+
+ - name: Validate operations
+ run: npx graphql-inspector validate './src/**/*.graphql' schema.graphql
+
+ - name: Check for breaking changes
+ run: npx graphql-inspector diff schema.graphql http://localhost:4000/graphql
+```
+
+### Pre-commit Hook
+
+```json
+// package.json
+{
+ "lint-staged": {
+ "*.graphql": ["eslint --fix", "graphql-inspector validate ./schema.graphql"]
+ }
+}
+```
+
+### Operation Complexity Check
+
+```bash
+# Check query complexity
+npx graphql-query-complexity-checker \
+ --schema ./schema.graphql \
+ --query ./src/queries/GetUser.graphql \
+ --max-complexity 100
+```
+
+### Persisted Queries Extraction
+
+Generate persisted queries for production:
+
+```typescript
+// codegen.ts
+const config: CodegenConfig = {
+ generates: {
+ "./persisted-queries.json": {
+ plugins: ["graphql-codegen-persisted-query-ids"],
+ config: {
+ output: "client",
+ algorithm: "sha256",
+ },
+ },
+ },
+};
+```
+
+Output:
+
+```json
+{
+ "abc123...": "query GetUser($id: ID!) { user(id: $id) { id name } }",
+ "def456...": "mutation CreatePost($input: CreatePostInput!) { ... }"
+}
+```
diff --git a/.agents/skills/graphql-operations/references/variables.md b/.agents/skills/graphql-operations/references/variables.md
new file mode 100644
index 000000000..c1d321603
--- /dev/null
+++ b/.agents/skills/graphql-operations/references/variables.md
@@ -0,0 +1,440 @@
+# Variable Patterns
+
+This reference covers patterns for using variables in GraphQL operations.
+
+## Table of Contents
+
+- [Variable Basics](#variable-basics)
+- [Variable Types](#variable-types)
+- [Default Values](#default-values)
+- [Complex Inputs](#complex-inputs)
+- [Best Practices](#best-practices)
+
+## Variable Basics
+
+### Declaring Variables
+
+Variables are declared in the operation definition:
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+}
+```
+
+### Using Variables
+
+Variables are referenced with `$` prefix:
+
+```graphql
+query GetUser($id: ID!) {
+ user(id: $id) {
+ # $id used here
+ id
+ name
+ }
+}
+```
+
+### Passing Variables
+
+Variables are passed as a separate JSON object:
+
+```typescript
+const { data } = await client.query({
+ query: GET_USER,
+ variables: {
+ id: "user_123",
+ },
+});
+```
+
+### Multiple Variables
+
+```graphql
+query SearchPosts($query: String!, $status: PostStatus, $first: Int!, $after: String) {
+ searchPosts(query: $query, status: $status, first: $first, after: $after) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "query": "graphql",
+ "status": "PUBLISHED",
+ "first": 10,
+ "after": "cursor_abc"
+}
+```
+
+## Variable Types
+
+### Scalar Types
+
+```graphql
+query Example(
+ $id: ID!
+ $name: String!
+ $count: Int!
+ $price: Float!
+ $active: Boolean!
+) {
+ # ...
+}
+```
+
+### Custom Scalar Types
+
+```graphql
+query Example(
+ $date: DateTime!
+ $email: Email!
+ $url: URL!
+) {
+ # ...
+}
+```
+
+### Enum Types
+
+```graphql
+query GetPosts($status: PostStatus!) {
+ posts(status: $status) {
+ id
+ title
+ }
+}
+```
+
+```json
+{
+ "status": "PUBLISHED"
+}
+```
+
+### List Types
+
+```graphql
+query GetUsers($ids: [ID!]!) {
+ users(ids: $ids) {
+ id
+ name
+ }
+}
+```
+
+```json
+{
+ "ids": ["user_1", "user_2", "user_3"]
+}
+```
+
+### Input Object Types
+
+```graphql
+mutation CreatePost($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ }
+}
+```
+
+```json
+{
+ "input": {
+ "title": "My Post",
+ "content": "Post content...",
+ "tags": ["graphql", "api"]
+ }
+}
+```
+
+### Required vs Optional
+
+```graphql
+query Example(
+ $required: String! # Must be provided, cannot be null
+ $optional: String # Can be omitted or null
+ $requiredList: [String!]! # List required, items required
+ $optionalList: [String] # List optional, items optional
+) {
+ # ...
+}
+```
+
+## Default Values
+
+### Simple Defaults
+
+```graphql
+query GetPosts($first: Int = 10, $status: PostStatus = PUBLISHED) {
+ posts(first: $first, status: $status) {
+ id
+ title
+ }
+}
+```
+
+If not provided, uses defaults:
+
+```json
+{}
+// Equivalent to: { "first": 10, "status": "PUBLISHED" }
+```
+
+Override defaults:
+
+```json
+{
+ "first": 20
+}
+// Uses first: 20, status: PUBLISHED (default)
+```
+
+### Defaults with Optional Variables
+
+```graphql
+# Variable is optional (no !) but has default
+query GetPosts($first: Int = 10) {
+ posts(first: $first) {
+ id
+ }
+}
+```
+
+### Defaults for Complex Types
+
+```graphql
+query GetPosts($orderBy: PostOrderInput = { field: CREATED_AT, direction: DESC }) {
+ posts(orderBy: $orderBy) {
+ id
+ title
+ }
+}
+```
+
+### When to Use Defaults
+
+Use defaults for:
+
+- Pagination limits (`first: Int = 20`)
+- Sort order (`direction: SortDirection = DESC`)
+- Common filter values (`status: Status = ACTIVE`)
+- Feature flags (`includeArchived: Boolean = false`)
+
+## Complex Inputs
+
+### Nested Input Objects
+
+```graphql
+mutation CreateOrder($input: CreateOrderInput!) {
+ createOrder(input: $input) {
+ id
+ total
+ }
+}
+```
+
+```json
+{
+ "input": {
+ "customer": {
+ "email": "customer@example.com",
+ "name": "John Doe"
+ },
+ "items": [
+ { "productId": "prod_1", "quantity": 2 },
+ { "productId": "prod_2", "quantity": 1 }
+ ],
+ "shippingAddress": {
+ "street": "123 Main St",
+ "city": "New York",
+ "state": "NY",
+ "zipCode": "10001",
+ "country": "US"
+ }
+ }
+}
+```
+
+### Lists of Input Objects
+
+```graphql
+mutation BulkCreateUsers($inputs: [CreateUserInput!]!) {
+ bulkCreateUsers(inputs: $inputs) {
+ id
+ email
+ }
+}
+```
+
+```json
+{
+ "inputs": [
+ { "email": "user1@example.com", "name": "User 1" },
+ { "email": "user2@example.com", "name": "User 2" },
+ { "email": "user3@example.com", "name": "User 3" }
+ ]
+}
+```
+
+### Filter Inputs
+
+```graphql
+query SearchProducts($filter: ProductFilter!) {
+ products(filter: $filter) {
+ id
+ name
+ price
+ }
+}
+```
+
+```json
+{
+ "filter": {
+ "category": "ELECTRONICS",
+ "priceRange": {
+ "min": 100,
+ "max": 500
+ },
+ "inStock": true,
+ "tags": ["featured", "sale"]
+ }
+}
+```
+
+## Best Practices
+
+### Always Use Variables for Dynamic Values
+
+```graphql
+# Good: Uses variable
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ name
+ }
+}
+
+# Bad: Hardcoded value
+query GetUser {
+ user(id: "123") {
+ id
+ name
+ }
+}
+```
+
+### Match Variable Names to Arguments
+
+```graphql
+# Good: Clear relationship
+query GetUser($userId: ID!) {
+ user(id: $userId) {
+ id
+ }
+}
+
+# Also good: Same name
+query GetUser($id: ID!) {
+ user(id: $id) {
+ id
+ }
+}
+
+# Bad: Confusing names
+query GetUser($x: ID!) {
+ user(id: $x) {
+ id
+ }
+}
+```
+
+### Use Descriptive Variable Names
+
+```graphql
+# Good
+query SearchPosts(
+ $searchQuery: String!
+ $authorId: ID
+ $publishedAfter: DateTime
+ $maxResults: Int = 20
+) {
+ searchPosts(
+ query: $searchQuery
+ author: $authorId
+ after: $publishedAfter
+ first: $maxResults
+ ) {
+ # ...
+ }
+}
+
+# Bad
+query SearchPosts($q: String!, $a: ID, $d: DateTime, $n: Int) {
+ # ...
+}
+```
+
+### Group Related Variables
+
+```typescript
+// Good: Variables object mirrors input structure
+const variables = {
+ input: {
+ title: formData.title,
+ content: formData.content,
+ tags: formData.tags,
+ },
+};
+
+// Less clear: Flat variables
+const variables = {
+ title: formData.title,
+ content: formData.content,
+ tags: formData.tags,
+};
+```
+
+### Validate Variables Client-Side
+
+```typescript
+function createPost(input: CreatePostInput) {
+ // Validate before sending
+ if (!input.title?.trim()) {
+ throw new Error("Title is required");
+ }
+ if (input.title.length > 200) {
+ throw new Error("Title too long");
+ }
+
+ return client.mutate({
+ mutation: CREATE_POST,
+ variables: { input },
+ });
+}
+```
+
+### Type Variables with TypeScript
+
+```typescript
+// Generated types from schema
+interface GetUserQueryVariables {
+ id: string;
+}
+
+// Use with Apollo Client
+const { data } = useQuery(GET_USER, {
+ variables: { id: userId }, // Type-checked
+});
+```
diff --git a/.agents/skills/graphql-schema/SKILL.md b/.agents/skills/graphql-schema/SKILL.md
new file mode 100644
index 000000000..46315dbff
--- /dev/null
+++ b/.agents/skills/graphql-schema/SKILL.md
@@ -0,0 +1,172 @@
+---
+name: graphql-schema
+description: >
+ Guide for designing GraphQL schemas following industry best practices. Use this skill when:
+ (1) designing a new GraphQL schema or API,
+ (2) reviewing existing schema for improvements,
+ (3) deciding on type structures or nullability,
+ (4) implementing pagination or error patterns,
+ (5) ensuring security in schema design.
+license: MIT
+compatibility: Any GraphQL implementation (Apollo Server, graphql-js, Yoga, etc.)
+metadata:
+ author: apollographql
+ version: "1.0.0"
+allowed-tools: Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep
+---
+
+# GraphQL Schema Design Guide
+
+This guide covers best practices for designing GraphQL schemas that are intuitive, performant, and maintainable. Schema design is primarily a server-side concern that directly impacts API usability.
+
+## Schema Design Principles
+
+### 1. Design for Client Needs
+
+- Think about what queries clients will write
+- Organize types around use cases, not database tables
+- Expose capabilities, not implementation details
+
+### 2. Be Explicit
+
+- Use clear, descriptive names
+- Make nullability intentional
+- Document with descriptions
+
+### 3. Design for Evolution
+
+- Plan for backwards compatibility
+- Use deprecation before removal
+- Avoid breaking changes
+
+## Quick Reference
+
+### Type Definition Syntax
+
+```graphql
+"""
+A user in the system.
+"""
+type User {
+ id: ID!
+ email: String!
+ name: String
+ posts(first: Int = 10, after: String): PostConnection!
+ createdAt: DateTime!
+}
+```
+
+### Nullability Rules
+
+| Pattern | Meaning |
+|---------|---------|
+| String | Nullable - may be null |
+| String! | Non-null - always has value |
+| [String] | Nullable list, nullable items |
+| [String!] | Nullable list, non-null items |
+| [String]! | Non-null list, nullable items |
+| [String!]! | Non-null list, non-null items |
+
+**Best Practice:** Use **[Type!]!** for lists - empty list over null, no null items.
+
+### Input vs Output Types
+
+```graphql
+# Output type - what clients receive
+type User {
+ id: ID!
+ email: String!
+ createdAt: DateTime!
+}
+
+# Input type - what clients send
+input CreateUserInput {
+ email: String!
+ name: String
+}
+
+# Mutation using input type
+type Mutation {
+ createUser(input: CreateUserInput!): User!
+}
+```
+
+### Interface Pattern
+
+```graphql
+interface Node {
+ id: ID!
+}
+
+type User implements Node {
+ id: ID!
+ email: String!
+}
+
+type Post implements Node {
+ id: ID!
+ title: String!
+}
+```
+
+### Union Pattern
+
+```graphql
+union SearchResult = User | Post | Comment
+
+type Query {
+ search(query: String!): [SearchResult!]!
+}
+```
+
+## Reference Files
+
+Detailed documentation for specific topics:
+
+- [Types](references/types.md) - Type design patterns, interfaces, unions, and custom scalars
+- [Naming](references/naming.md) - Naming conventions for types, fields, and arguments
+- [Pagination](references/pagination.md) - Connection pattern and cursor-based pagination
+- [Errors](references/errors.md) - Error modeling and result types
+- [Security](references/security.md) - Security best practices for schema design
+
+## Key Rules
+
+### Type Design
+
+- Define types based on domain concepts, not data storage
+- Use interfaces for shared fields across types
+- Use unions for mutually exclusive types
+- Keep types focused (single responsibility)
+- Avoid deep nesting - flatten when possible
+
+### Field Design
+
+- Fields should be named from client's perspective
+- Return the most specific type possible
+- Make expensive fields explicit (consider arguments)
+- Use arguments for filtering, sorting, pagination
+
+### Mutation Design
+
+- Use single input argument pattern: `mutation(input: InputType!)`
+- Return affected objects in mutation responses
+- Model mutations around business operations, not CRUD
+- Consider returning a union of success/error types
+
+### ID Strategy
+
+- Use globally unique IDs when possible
+- Implement `Node` interface for refetchability
+- Base64-encode compound IDs if needed
+
+## Ground Rules
+
+- ALWAYS add descriptions to types and fields
+- ALWAYS use non-null (**!**) for fields that cannot be null
+- ALWAYS use **[Type!]!** pattern for lists
+- NEVER expose database internals in schema
+- NEVER break backwards compatibility without deprecation
+- PREFER dedicated input types over many arguments
+- PREFER enums over arbitrary strings for fixed values
+- USE `ID` type for identifiers, not `String` or `Int`
+- USE custom scalars for domain-specific values (DateTime, Email, URL)
diff --git a/.agents/skills/graphql-schema/references/errors.md b/.agents/skills/graphql-schema/references/errors.md
new file mode 100644
index 000000000..f7acf864c
--- /dev/null
+++ b/.agents/skills/graphql-schema/references/errors.md
@@ -0,0 +1,388 @@
+# Error Design Patterns
+
+This reference covers error handling patterns in GraphQL schema design.
+
+## Table of Contents
+
+- [GraphQL Error Model](#graphql-error-model)
+- [When to Use Each Pattern](#when-to-use-each-pattern)
+- [Union-Based Error Pattern](#union-based-error-pattern)
+- [Interface-Based Errors](#interface-based-errors)
+- [Error Codes](#error-codes)
+- [Partial Success](#partial-success)
+
+## GraphQL Error Model
+
+GraphQL has a built-in error system with errors in the response:
+
+```json
+{
+ "data": { "user": null },
+ "errors": [
+ {
+ "message": "User not found",
+ "path": ["user"],
+ "extensions": {
+ "code": "NOT_FOUND"
+ }
+ }
+ ]
+}
+```
+
+### Built-in Errors: Good For
+
+- Unexpected errors (bugs, infrastructure issues)
+- Authentication failures (401-level)
+- Authorization failures (403-level)
+- Validation of query itself
+
+### Built-in Errors: Bad For
+
+- Expected business errors (item out of stock)
+- Multiple error types for one operation
+- Errors that need rich data
+
+## When to Use Each Pattern
+
+| Scenario | Pattern |
+|----------|---------|
+| Unexpected server error | Built-in errors |
+| Authentication required | Built-in errors |
+| User input validation | Union result types |
+| Business rule violation | Union result types |
+| Partial success possible | Union or nullable fields |
+| Multiple error types | Union result types |
+
+## Union-Based Error Pattern
+
+### Basic Result Type
+
+```graphql
+type Mutation {
+ createUser(input: CreateUserInput!): CreateUserResult!
+}
+
+union CreateUserResult = CreateUserSuccess | ValidationError
+
+type CreateUserSuccess {
+ user: User!
+}
+
+type ValidationError {
+ message: String!
+ field: String
+}
+```
+
+### Multiple Error Types
+
+```graphql
+union CreateOrderResult =
+ | CreateOrderSuccess
+ | ValidationError
+ | InsufficientInventory
+ | PaymentFailed
+
+type CreateOrderSuccess {
+ order: Order!
+}
+
+type ValidationError {
+ message: String!
+ field: String
+}
+
+type InsufficientInventory {
+ message: String!
+ unavailableItems: [OrderItem!]!
+}
+
+type PaymentFailed {
+ message: String!
+ reason: PaymentFailureReason!
+ retryable: Boolean!
+}
+
+enum PaymentFailureReason {
+ CARD_DECLINED
+ INSUFFICIENT_FUNDS
+ EXPIRED_CARD
+ FRAUD_SUSPECTED
+}
+```
+
+### Client Usage
+
+```graphql
+mutation CreateOrder($input: CreateOrderInput!) {
+ createOrder(input: $input) {
+ ... on CreateOrderSuccess {
+ order {
+ id
+ total
+ }
+ }
+ ... on ValidationError {
+ message
+ field
+ }
+ ... on InsufficientInventory {
+ message
+ unavailableItems {
+ productId
+ requestedQuantity
+ availableQuantity
+ }
+ }
+ ... on PaymentFailed {
+ message
+ reason
+ retryable
+ }
+ }
+}
+```
+
+### Benefits of Union Pattern
+
+1. **Type safety** - Clients know all possible outcomes
+2. **Rich error data** - Each error type has specific fields
+3. **Self-documenting** - Schema shows what can go wrong
+4. **Forced handling** - Clients must handle each case
+
+## Interface-Based Errors
+
+### Error Interface
+
+```graphql
+interface Error {
+ message: String!
+}
+
+type ValidationError implements Error {
+ message: String!
+ field: String!
+}
+
+type NotFoundError implements Error {
+ message: String!
+ resourceType: String!
+ resourceId: ID!
+}
+
+type PermissionError implements Error {
+ message: String!
+ requiredPermission: String!
+}
+
+union UpdateUserResult = User | ValidationError | NotFoundError | PermissionError
+```
+
+### Using Interface for Queries
+
+```graphql
+type Query {
+ user(id: ID!): UserResult!
+}
+
+union UserResult = User | NotFoundError | PermissionError
+
+# Client query:
+query GetUser($id: ID!) {
+ user(id: $id) {
+ ... on User {
+ id
+ name
+ }
+ ... on Error {
+ message
+ }
+ }
+}
+```
+
+## Error Codes
+
+### Standardize Error Codes
+
+```graphql
+enum ErrorCode {
+ # Validation errors
+ VALIDATION_FAILED
+ INVALID_INPUT
+ REQUIRED_FIELD_MISSING
+
+ # Authentication/Authorization
+ UNAUTHENTICATED
+ UNAUTHORIZED
+ TOKEN_EXPIRED
+
+ # Resource errors
+ NOT_FOUND
+ ALREADY_EXISTS
+ CONFLICT
+
+ # Business logic
+ INSUFFICIENT_FUNDS
+ LIMIT_EXCEEDED
+ OPERATION_NOT_ALLOWED
+
+ # System errors
+ INTERNAL_ERROR
+ SERVICE_UNAVAILABLE
+ RATE_LIMITED
+}
+
+type MutationError {
+ code: ErrorCode!
+ message: String!
+ field: String
+ details: JSON
+}
+```
+
+### Error with Code Pattern
+
+```graphql
+type ValidationError {
+ code: ErrorCode!
+ message: String!
+ field: String
+}
+
+type CreateUserSuccess {
+ user: User!
+}
+
+union CreateUserResult = CreateUserSuccess | ValidationError
+
+# Usage enables consistent error handling:
+# if (result.__typename === 'ValidationError') {
+# switch (result.code) {
+# case 'ALREADY_EXISTS': ...
+# case 'INVALID_INPUT': ...
+# }
+# }
+```
+
+## Partial Success
+
+### Batch Operations
+
+For operations on multiple items:
+
+```graphql
+input BulkUpdateInput {
+ items: [UpdateItemInput!]!
+}
+
+type BulkUpdateResult {
+ successful: [Item!]!
+ failed: [BulkUpdateError!]!
+}
+
+type BulkUpdateError {
+ index: Int!
+ itemId: ID
+ error: UpdateError!
+}
+
+union UpdateError = ValidationError | NotFoundError | PermissionError
+
+type Mutation {
+ bulkUpdateItems(input: BulkUpdateInput!): BulkUpdateResult!
+}
+```
+
+### Client Usage for Batch
+
+```graphql
+mutation BulkUpdate($input: BulkUpdateInput!) {
+ bulkUpdateItems(input: $input) {
+ successful {
+ id
+ name
+ }
+ failed {
+ index
+ itemId
+ error {
+ ... on ValidationError {
+ message
+ field
+ }
+ ... on NotFoundError {
+ message
+ resourceId
+ }
+ }
+ }
+ }
+}
+```
+
+### Warnings Pattern
+
+Return success with warnings:
+
+```graphql
+type ImportResult {
+ imported: [Record!]!
+ skipped: [SkippedRecord!]!
+ warnings: [ImportWarning!]!
+}
+
+type SkippedRecord {
+ row: Int!
+ reason: String!
+ data: JSON
+}
+
+type ImportWarning {
+ row: Int
+ message: String!
+ severity: WarningSeverity!
+}
+
+enum WarningSeverity {
+ INFO
+ WARNING
+ ERROR
+}
+```
+
+### Nullable Fields for Partial Data
+
+```graphql
+type UserWithExternalData {
+ id: ID!
+ name: String!
+ # These might fail independently
+ profileImage: Image # External service
+ socialConnections: [Social] # External service
+ # Errors for each
+ profileImageError: String
+ socialConnectionsError: String
+}
+```
+
+Alternative with explicit result types:
+
+```graphql
+type UserWithExternalData {
+ id: ID!
+ name: String!
+ profileImage: ImageResult!
+ socialConnections: SocialConnectionsResult!
+}
+
+union ImageResult = Image | FetchError
+union SocialConnectionsResult = SocialConnectionList | FetchError
+
+type FetchError {
+ message: String!
+ service: String!
+ retryable: Boolean!
+}
+```
diff --git a/.agents/skills/graphql-schema/references/naming.md b/.agents/skills/graphql-schema/references/naming.md
new file mode 100644
index 000000000..d07b50101
--- /dev/null
+++ b/.agents/skills/graphql-schema/references/naming.md
@@ -0,0 +1,400 @@
+# Naming Conventions
+
+This reference covers naming conventions for GraphQL schemas. Consistent naming makes APIs intuitive and self-documenting.
+
+## Table of Contents
+
+- [General Principles](#general-principles)
+- [Types](#types)
+- [Fields](#fields)
+- [Arguments](#arguments)
+- [Enums](#enums)
+- [Mutations](#mutations)
+- [Input Types](#input-types)
+- [Anti-Patterns](#anti-patterns)
+
+## General Principles
+
+1. **Be Descriptive** - Names should clearly indicate purpose
+2. **Be Consistent** - Follow the same patterns throughout
+3. **Use Domain Language** - Match business terminology
+4. **Avoid Abbreviations** - Prefer `createdAt` over `crtAt`
+5. **Think Client-Side** - Name from consumer's perspective
+
+## Types
+
+### Object Types: PascalCase
+
+Use singular nouns in PascalCase:
+
+```graphql
+# Correct
+type User { ... }
+type BlogPost { ... }
+type ShoppingCart { ... }
+type PaymentMethod { ... }
+
+# Incorrect
+type user { ... } # lowercase
+type Users { ... } # plural
+type blog_post { ... } # snake_case
+```
+
+### Interface Types: PascalCase
+
+Use adjectives or nouns describing capability:
+
+```graphql
+interface Node { ... }
+interface Timestamped { ... }
+interface Searchable { ... }
+interface Commentable { ... }
+```
+
+### Union Types: PascalCase
+
+Use nouns or compound names:
+
+```graphql
+union SearchResult = User | Post | Comment
+union MediaContent = Image | Video | Audio
+union NotificationTarget = User | Group | Channel
+```
+
+## Fields
+
+### Object Fields: camelCase
+
+Use camelCase, typically nouns or noun phrases:
+
+```graphql
+type User {
+ id: ID!
+ firstName: String!
+ lastName: String!
+ emailAddress: String!
+ createdAt: DateTime!
+ isActive: Boolean!
+}
+```
+
+### Boolean Fields
+
+Prefix with `is`, `has`, `can`, or `should`:
+
+```graphql
+type User {
+ isActive: Boolean!
+ isVerified: Boolean!
+ hasSubscription: Boolean!
+ canEdit: Boolean!
+ shouldNotify: Boolean!
+}
+
+type Post {
+ isPublished: Boolean!
+ isArchived: Boolean!
+ hasFeaturedImage: Boolean!
+}
+```
+
+### Collection Fields
+
+Use plural nouns:
+
+```graphql
+type User {
+ posts: [Post!]!
+ followers: [User!]!
+ notifications: [Notification!]!
+}
+
+type Query {
+ users: [User!]!
+ allPosts: [Post!]!
+}
+```
+
+### Relationship Fields
+
+Name based on the relationship:
+
+```graphql
+type Post {
+ author: User! # Not: user, createdBy
+ comments: [Comment!]!
+ tags: [Tag!]!
+}
+
+type Comment {
+ post: Post! # Parent reference
+ author: User!
+ replies: [Comment!]! # Child reference
+}
+```
+
+### Computed Fields
+
+Name by what they return, not how they're computed:
+
+```graphql
+type User {
+ fullName: String! # Not: getFullName, computedName
+ postCount: Int! # Not: calculatePostCount
+ recentActivity: [Activity!]!
+}
+```
+
+## Arguments
+
+### Argument Names: camelCase
+
+```graphql
+type Query {
+ user(id: ID!): User
+ users(first: Int, after: String): UserConnection!
+ search(query: String!, filters: SearchFilters): [SearchResult!]!
+}
+```
+
+### Common Argument Patterns
+
+```graphql
+# Single item lookup
+user(id: ID!): User
+post(slug: String!): Post
+
+# Filtering
+users(role: Role, isActive: Boolean): [User!]!
+
+# Pagination
+posts(first: Int, after: String): PostConnection!
+posts(last: Int, before: String): PostConnection!
+
+# Sorting
+posts(orderBy: PostOrderBy): [Post!]!
+
+# Search
+search(query: String!): [SearchResult!]!
+```
+
+### Avoid Generic Names
+
+```graphql
+# Avoid
+posts(filter: JSON)
+users(options: Options)
+
+# Prefer
+posts(status: PostStatus, authorId: ID)
+users(role: Role, createdAfter: DateTime)
+```
+
+## Enums
+
+### Enum Names: PascalCase
+
+```graphql
+enum UserRole { ... }
+enum OrderStatus { ... }
+enum SortDirection { ... }
+```
+
+### Enum Values: SCREAMING_SNAKE_CASE
+
+```graphql
+enum UserRole {
+ ADMIN
+ MODERATOR
+ MEMBER
+ GUEST
+}
+
+enum OrderStatus {
+ PENDING_PAYMENT
+ PAYMENT_RECEIVED
+ PROCESSING
+ SHIPPED
+ DELIVERED
+ CANCELLED
+ REFUNDED
+}
+
+enum SortDirection {
+ ASC
+ DESC
+}
+```
+
+## Mutations
+
+### Mutation Names: verbNoun
+
+Use action verbs followed by the subject:
+
+```graphql
+type Mutation {
+ # Create operations
+ createUser(input: CreateUserInput!): User!
+ createPost(input: CreatePostInput!): Post!
+
+ # Update operations
+ updateUser(id: ID!, input: UpdateUserInput!): User!
+ updatePost(id: ID!, input: UpdatePostInput!): Post!
+
+ # Delete operations
+ deleteUser(id: ID!): DeleteUserPayload!
+ deletePost(id: ID!): DeletePostPayload!
+
+ # Domain-specific operations
+ publishPost(id: ID!): Post!
+ archivePost(id: ID!): Post!
+
+ sendMessage(input: SendMessageInput!): Message!
+
+ addItemToCart(input: AddItemInput!): Cart!
+ removeItemFromCart(itemId: ID!): Cart!
+
+ followUser(userId: ID!): FollowPayload!
+ unfollowUser(userId: ID!): UnfollowPayload!
+}
+```
+
+### Common Verb Patterns
+
+| Operation | Verbs |
+|-----------|-------|
+| Create | `create`, `add`, `register`, `submit` |
+| Read | `get`, `fetch`, `load` (avoid in mutations) |
+| Update | `update`, `edit`, `modify`, `set` |
+| Delete | `delete`, `remove`, `archive` |
+| State change | `publish`, `approve`, `reject`, `cancel` |
+| Relationships | `add`, `remove`, `link`, `unlink` |
+| Actions | `send`, `invite`, `follow`, `like` |
+
+## Input Types
+
+### Input Type Names
+
+Use the mutation name + `Input`:
+
+```graphql
+input CreateUserInput {
+ email: String!
+ name: String!
+}
+
+input UpdateUserInput {
+ email: String
+ name: String
+}
+
+input SendMessageInput {
+ recipientId: ID!
+ body: String!
+}
+```
+
+### Payload Types
+
+Use the mutation name + `Payload` or `Result`:
+
+```graphql
+type DeleteUserPayload {
+ success: Boolean!
+ deletedUserId: ID
+}
+
+type FollowUserPayload {
+ follower: User!
+ followee: User!
+}
+```
+
+## Anti-Patterns
+
+### Don't Use Hungarian Notation
+
+```graphql
+# Avoid
+type TUser { ... }
+type UserType { ... }
+strName: String!
+intAge: Int!
+
+# Prefer
+type User { ... }
+name: String!
+age: Int!
+```
+
+### Don't Use Redundant Prefixes
+
+```graphql
+# Avoid
+type User {
+ userId: ID!
+ userName: String!
+ userEmail: String!
+}
+
+# Prefer
+type User {
+ id: ID!
+ name: String!
+ email: String!
+}
+```
+
+### Don't Expose Implementation Details
+
+```graphql
+# Avoid
+type User {
+ mysql_id: Int!
+ redis_cache_key: String!
+ getDerivedStateFromProps: JSON!
+}
+
+# Prefer
+type User {
+ id: ID!
+ # Internal details should not appear in schema
+}
+```
+
+### Don't Use Vague Names
+
+```graphql
+# Avoid
+type Query {
+ getData: JSON
+ getInfo(type: String): JSON
+ fetch(params: JSON): JSON
+}
+
+# Prefer
+type Query {
+ userProfile(userId: ID!): UserProfile
+ orderHistory(first: Int): OrderConnection!
+ searchProducts(query: String!): [Product!]!
+}
+```
+
+### Don't Mix Conventions
+
+```graphql
+# Avoid: inconsistent naming
+type User {
+ firstName: String! # camelCase
+ last_name: String! # snake_case
+ EmailAddress: String! # PascalCase
+}
+
+# Prefer: consistent camelCase
+type User {
+ firstName: String!
+ lastName: String!
+ emailAddress: String!
+}
+```
diff --git a/.agents/skills/graphql-schema/references/pagination.md b/.agents/skills/graphql-schema/references/pagination.md
new file mode 100644
index 000000000..28be6803a
--- /dev/null
+++ b/.agents/skills/graphql-schema/references/pagination.md
@@ -0,0 +1,396 @@
+# Pagination Design
+
+This reference covers pagination patterns for GraphQL schemas, with focus on the cursor-based Connection pattern.
+
+## Table of Contents
+
+- [Pagination Approaches](#pagination-approaches)
+- [Offset vs Cursor](#offset-vs-cursor)
+- [Connection Pattern](#connection-pattern)
+- [Building Connections](#building-connections)
+- [Sorting and Filtering](#sorting-and-filtering)
+- [Performance Considerations](#performance-considerations)
+
+## Pagination Approaches
+
+### Simple List (No Pagination)
+
+Only use for small, bounded collections:
+
+```graphql
+type User {
+ # OK: Users typically have few roles
+ roles: [Role!]!
+
+ # OK: Limited enum values
+ permissions: [Permission!]!
+}
+```
+
+### Offset-Based
+
+Straightforward approach with limitations:
+
+```graphql
+type Query {
+ posts(offset: Int = 0, limit: Int = 20): [Post!]!
+}
+```
+
+### Cursor-Based (Connection Pattern)
+
+Recommended for most cases:
+
+```graphql
+type Query {
+ posts(first: Int, after: String): PostConnection!
+}
+```
+
+## Offset vs Cursor
+
+### Offset-Based Pagination
+
+```graphql
+type Query {
+ posts(offset: Int = 0, limit: Int = 20): PostsPage!
+}
+
+type PostsPage {
+ items: [Post!]!
+ totalCount: Int!
+ hasMore: Boolean!
+}
+```
+
+**Pros:**
+- Straightforward to build
+- Allows jumping to specific page
+- Familiar to REST developers
+
+**Cons:**
+- Inconsistent with real-time data (items shift)
+- Poor performance on large offsets
+- Duplicate or missing items when data changes
+
+### Cursor-Based Pagination
+
+```graphql
+type Query {
+ posts(first: Int, after: String): PostConnection!
+}
+```
+
+**Pros:**
+- Stable pagination (cursor points to specific item)
+- Efficient for large datasets
+- Works well with real-time updates
+- Industry standard (Relay specification)
+
+**Cons:**
+- Can't jump to arbitrary page
+- Requires more code to build
+- Opaque cursors require explanation
+
+## Connection Pattern
+
+### Relay Connection Specification
+
+The Connection pattern is defined by the Relay specification and widely adopted:
+
+```graphql
+type Query {
+ posts(
+ first: Int
+ after: String
+ last: Int
+ before: String
+ ): PostConnection!
+}
+
+type PostConnection {
+ edges: [PostEdge!]!
+ pageInfo: PageInfo!
+ totalCount: Int
+}
+
+type PostEdge {
+ node: Post!
+ cursor: String!
+}
+
+type PageInfo {
+ hasNextPage: Boolean!
+ hasPreviousPage: Boolean!
+ startCursor: String
+ endCursor: String
+}
+```
+
+### Connection Arguments
+
+| Argument | Purpose |
+|----------|---------|
+| `first` | Number of items from the start |
+| `after` | Cursor to start after (forward pagination) |
+| `last` | Number of items from the end |
+| `before` | Cursor to start before (backward pagination) |
+
+**Usage patterns:**
+- Forward: `first` + `after`
+- Backward: `last` + `before`
+- Don't mix forward and backward in same request
+
+### Edge Type
+
+Edges contain:
+- `node`: The actual item
+- `cursor`: Opaque cursor for this item
+- Additional edge-specific data (optional)
+
+```graphql
+type PostEdge {
+ node: Post!
+ cursor: String!
+ # Edge-specific metadata
+ addedAt: DateTime
+ addedBy: User
+}
+```
+
+### PageInfo Type
+
+```graphql
+type PageInfo {
+ hasNextPage: Boolean! # More items forward?
+ hasPreviousPage: Boolean! # More items backward?
+ startCursor: String # Cursor of first item
+ endCursor: String # Cursor of last item
+}
+```
+
+## Building Connections
+
+### Basic Connection Query
+
+```graphql
+type Query {
+ # Simple connection
+ posts(first: Int, after: String): PostConnection!
+
+ # Connection with filters
+ userPosts(
+ userId: ID!
+ first: Int
+ after: String
+ status: PostStatus
+ ): PostConnection!
+}
+```
+
+### Connection for Relationships
+
+```graphql
+type User {
+ id: ID!
+ name: String!
+
+ # Paginated relationship
+ posts(first: Int, after: String): PostConnection!
+ followers(first: Int, after: String): UserConnection!
+ following(first: Int, after: String): UserConnection!
+}
+```
+
+### Cursor Design
+
+Cursors should be:
+- **Opaque**: Clients shouldn't parse them
+- **Stable**: Same cursor = same position
+- **Serializable**: Usually base64-encoded
+
+```typescript
+// Common cursor strategies:
+
+// 1. Encoded ID (simple)
+const cursor = base64(`id:${post.id}`);
+
+// 2. Encoded timestamp + ID (for sorted lists)
+const cursor = base64(`${post.createdAt}:${post.id}`);
+
+// 3. Encoded offset (simpler, but less stable)
+const cursor = base64(`offset:${index}`);
+```
+
+### Default Page Size
+
+Always set sensible defaults and limits:
+
+```graphql
+type Query {
+ posts(
+ first: Int = 20 # Default page size
+ after: String
+ ): PostConnection!
+}
+```
+
+In resolver, enforce maximum:
+```typescript
+const resolvers = {
+ Query: {
+ posts: (_, { first = 20, after }) => {
+ const limit = Math.min(first, 100); // Cap at 100
+ // ...
+ }
+ }
+};
+```
+
+## Sorting and Filtering
+
+### Sorting Arguments
+
+```graphql
+enum PostOrderField {
+ CREATED_AT
+ UPDATED_AT
+ TITLE
+ POPULARITY
+}
+
+input PostOrder {
+ field: PostOrderField!
+ direction: OrderDirection!
+}
+
+enum OrderDirection {
+ ASC
+ DESC
+}
+
+type Query {
+ posts(
+ first: Int
+ after: String
+ orderBy: PostOrder = { field: CREATED_AT, direction: DESC }
+ ): PostConnection!
+}
+```
+
+### Filtering Arguments
+
+```graphql
+input PostFilter {
+ status: PostStatus
+ authorId: ID
+ createdAfter: DateTime
+ createdBefore: DateTime
+ tags: [String!]
+}
+
+type Query {
+ posts(
+ first: Int
+ after: String
+ filter: PostFilter
+ orderBy: PostOrder
+ ): PostConnection!
+}
+```
+
+### Combined Example
+
+```graphql
+type Query {
+ posts(
+ first: Int = 20
+ after: String
+ last: Int
+ before: String
+ filter: PostFilter
+ orderBy: PostOrder
+ ): PostConnection!
+}
+
+# Usage:
+# query {
+# posts(
+# first: 10
+# filter: { status: PUBLISHED, tags: ["graphql"] }
+# orderBy: { field: CREATED_AT, direction: DESC }
+# ) {
+# edges {
+# node { id title }
+# cursor
+# }
+# pageInfo {
+# hasNextPage
+# endCursor
+# }
+# }
+# }
+```
+
+## Performance Considerations
+
+### Avoid COUNT(*) for totalCount
+
+`totalCount` can be expensive. Options:
+
+1. Make it nullable and skip when expensive
+2. Return an estimate
+3. Use a separate query for count
+
+```graphql
+type PostConnection {
+ edges: [PostEdge!]!
+ pageInfo: PageInfo!
+ totalCount: Int # Nullable - may not always be computed
+}
+```
+
+### Use Efficient Cursors
+
+Cursor-based pagination should use indexed columns:
+
+```sql
+-- Efficient: Uses index on created_at
+SELECT * FROM posts
+WHERE created_at < $cursor_timestamp
+ORDER BY created_at DESC
+LIMIT 20;
+
+-- Inefficient: Full table scan
+SELECT * FROM posts
+LIMIT 20 OFFSET 10000;
+```
+
+### Limit Maximum Page Size
+
+Prevent clients from requesting too many items:
+
+```typescript
+const MAX_PAGE_SIZE = 100;
+
+function resolveConnection(first: number | null) {
+ const limit = Math.min(first ?? 20, MAX_PAGE_SIZE);
+ // ...
+}
+```
+
+### Index Cursor Columns
+
+Ensure database indexes exist for cursor columns:
+
+```sql
+CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
+CREATE INDEX idx_posts_author_created ON posts(author_id, created_at DESC);
+```
+
+### Consider Denormalization
+
+For very large datasets, consider:
+- Materialized views
+- Denormalized count columns
+- Cached aggregations
diff --git a/.agents/skills/graphql-schema/references/security.md b/.agents/skills/graphql-schema/references/security.md
new file mode 100644
index 000000000..b8ffd5138
--- /dev/null
+++ b/.agents/skills/graphql-schema/references/security.md
@@ -0,0 +1,484 @@
+# Security Best Practices
+
+This reference covers security considerations for GraphQL schema design.
+
+## Table of Contents
+
+- [Introspection](#introspection)
+- [Query Complexity](#query-complexity)
+- [Depth Limiting](#depth-limiting)
+- [Rate Limiting](#rate-limiting)
+- [Field-Level Authorization](#field-level-authorization)
+- [Input Validation](#input-validation)
+- [Persisted Queries](#persisted-queries)
+- [Information Disclosure](#information-disclosure)
+
+## Introspection
+
+### Disable in Production
+
+Introspection reveals your entire schema. Disable it in production:
+
+```typescript
+// Apollo Server
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ introspection: process.env.NODE_ENV !== 'production',
+});
+```
+
+### Allow for Development
+
+Keep introspection enabled for:
+- Development environments
+- Internal tools
+- Authorized clients (with authentication)
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ introspection: process.env.NODE_ENV === 'development' ||
+ process.env.ENABLE_INTROSPECTION === 'true',
+});
+```
+
+## Query Complexity
+
+### Why Limit Complexity
+
+A single query can request enormous amounts of data:
+
+```graphql
+# Potentially very expensive
+query {
+ users(first: 1000) {
+ posts(first: 1000) {
+ comments(first: 1000) {
+ author {
+ posts(first: 1000) {
+ # ... could go deeper
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### Complexity Calculation
+
+Assign costs to fields and limit total cost:
+
+```graphql
+type Query {
+ users(first: Int): [User!]! @cost(complexity: 10, multipliers: ["first"])
+}
+
+type User {
+ posts(first: Int): [Post!]! @cost(complexity: 5, multipliers: ["first"])
+}
+```
+
+### Implementation with graphql-query-complexity
+
+```typescript
+import { createComplexityLimitRule } from 'graphql-validation-complexity';
+
+const complexityLimitRule = createComplexityLimitRule(1000, {
+ scalarCost: 1,
+ objectCost: 10,
+ listFactor: 10,
+});
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [complexityLimitRule],
+});
+```
+
+### Cost Estimation in Schema
+
+Document expected costs:
+
+```graphql
+"""
+Returns user's posts.
+Cost: Base 5 + (first * 2)
+"""
+type User {
+ posts(
+ first: Int = 20 @cost(weight: 2)
+ ): PostConnection! @cost(complexity: 5)
+}
+```
+
+## Depth Limiting
+
+### Why Limit Depth
+
+Prevent deeply nested queries:
+
+```graphql
+# Depth: 10+ levels deep
+query {
+ user {
+ friends {
+ friends {
+ friends {
+ friends {
+ # ...
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+### Implementation
+
+```typescript
+import depthLimit from 'graphql-depth-limit';
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ validationRules: [depthLimit(10)],
+});
+```
+
+### Recommended Limits
+
+| Application Type | Max Depth |
+|-----------------|-----------|
+| Simple API | 5-7 |
+| Complex API | 7-10 |
+| Internal tools | 10-15 |
+
+## Rate Limiting
+
+### Query-Based Rate Limiting
+
+Limit queries per time window:
+
+```typescript
+// Example with express-rate-limit
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 1000, // limit each IP to 1000 requests per window
+});
+
+app.use('/graphql', limiter);
+```
+
+### Complexity-Based Rate Limiting
+
+Limit based on query cost, not just count:
+
+```typescript
+// Track complexity per user
+const userComplexityBudget = new Map();
+
+const complexityPlugin = {
+ requestDidStart() {
+ return {
+ didResolveOperation({ request, document, context }) {
+ const complexity = calculateComplexity(document);
+ const userId = context.user?.id || request.http?.headers.get('x-forwarded-for');
+
+ const current = userComplexityBudget.get(userId) || 0;
+ if (current + complexity > MAX_COMPLEXITY_PER_MINUTE) {
+ throw new GraphQLError('Rate limit exceeded');
+ }
+ userComplexityBudget.set(userId, current + complexity);
+ }
+ };
+ }
+};
+```
+
+### Field-Specific Rate Limiting
+
+Limit expensive fields specifically:
+
+```graphql
+type Query {
+ """
+ Rate limited to 10 requests per minute
+ """
+ expensiveAnalytics: Analytics! @rateLimit(max: 10, window: "1m")
+}
+```
+
+## Field-Level Authorization
+
+### Schema Design for Authorization
+
+Don't expose unauthorized fields in schema:
+
+```graphql
+# User-facing schema
+type User {
+ id: ID!
+ name: String!
+ publicProfile: PublicProfile!
+}
+
+# Admin-only schema (separate schema or schema stitching)
+type User {
+ id: ID!
+ name: String!
+ email: String! # Admin only
+ internalNotes: String # Admin only
+}
+```
+
+### Resolver-Level Authorization
+
+Check permissions in resolvers:
+
+```typescript
+const resolvers = {
+ User: {
+ email: (user, args, context) => {
+ if (!context.user || context.user.id !== user.id) {
+ if (!context.user?.isAdmin) {
+ return null; // or throw error
+ }
+ }
+ return user.email;
+ },
+ internalNotes: (user, args, context) => {
+ if (!context.user?.isAdmin) {
+ throw new GraphQLError('Not authorized', {
+ extensions: { code: 'UNAUTHORIZED' }
+ });
+ }
+ return user.internalNotes;
+ }
+ }
+};
+```
+
+### Directive-Based Authorization
+
+```graphql
+directive @auth(requires: Role!) on FIELD_DEFINITION
+
+enum Role {
+ USER
+ ADMIN
+ SUPER_ADMIN
+}
+
+type User {
+ id: ID!
+ name: String!
+ email: String! @auth(requires: USER) # Own data or admin
+ ssn: String @auth(requires: SUPER_ADMIN)
+}
+```
+
+## Input Validation
+
+### Schema-Level Validation
+
+Use custom scalars for validation:
+
+```graphql
+scalar Email # Validates email format
+scalar URL # Validates URL format
+scalar DateTime # Validates ISO 8601 format
+
+type Mutation {
+ createUser(
+ email: Email!
+ website: URL
+ birthDate: DateTime!
+ ): User!
+}
+```
+
+### Input Constraints
+
+Document and enforce constraints:
+
+```graphql
+input CreatePostInput {
+ """
+ Title of the post.
+ Min length: 1
+ Max length: 200
+ """
+ title: String!
+
+ """
+ Post content.
+ Max length: 50000
+ """
+ content: String!
+
+ """
+ Tags for the post.
+ Max items: 10
+ """
+ tags: [String!]
+}
+```
+
+### Resolver Validation
+
+Always validate in resolvers:
+
+```typescript
+const resolvers = {
+ Mutation: {
+ createPost: (_, { input }) => {
+ if (input.title.length > 200) {
+ throw new GraphQLError('Title too long', {
+ extensions: { code: 'VALIDATION_ERROR', field: 'title' }
+ });
+ }
+ if (input.content.length > 50000) {
+ throw new GraphQLError('Content too long', {
+ extensions: { code: 'VALIDATION_ERROR', field: 'content' }
+ });
+ }
+ if (input.tags?.length > 10) {
+ throw new GraphQLError('Too many tags', {
+ extensions: { code: 'VALIDATION_ERROR', field: 'tags' }
+ });
+ }
+ // ... create post
+ }
+ }
+};
+```
+
+## Persisted Queries
+
+### What Are Persisted Queries?
+
+Map query hashes to pre-approved queries:
+
+```
+Client sends: { "extensions": { "persistedQuery": { "sha256Hash": "abc123..." }}}
+Server looks up: abc123... → "query GetUser($id: ID!) { user(id: $id) { id name }}"
+```
+
+### Benefits
+
+1. **Security**: Only allow approved queries
+2. **Performance**: No parsing overhead
+3. **Bandwidth**: Smaller payloads
+4. **CDN**: Queries can be cached
+
+### Implementation
+
+```typescript
+// Apollo Server with automatic persisted queries
+import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ persistedQueries: {
+ cache: new KeyValueCache(), // Your cache implementation
+ },
+});
+```
+
+### Strict Mode
+
+In production, reject non-persisted queries:
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ persistedQueries: {
+ cache: persistedQueryCache,
+ },
+ allowBatchedHttpRequests: false,
+ plugins: [
+ {
+ async requestDidStart() {
+ return {
+ async didResolveOperation({ request }) {
+ if (!request.extensions?.persistedQuery) {
+ throw new GraphQLError('Only persisted queries allowed');
+ }
+ }
+ };
+ }
+ }
+ ]
+});
+```
+
+## Information Disclosure
+
+### Error Messages
+
+Don't leak internal details:
+
+```typescript
+// Bad: Exposes internal implementation
+throw new Error(`Database error: SQLSTATE[23000]: duplicate key 'users_email_unique'`);
+
+// Good: User-friendly message
+throw new GraphQLError('Email already registered', {
+ extensions: { code: 'EMAIL_EXISTS' }
+});
+```
+
+### Stack Traces
+
+Disable in production:
+
+```typescript
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ includeStacktraceInErrorResponses: process.env.NODE_ENV === 'development',
+});
+```
+
+### Schema Descriptions
+
+Don't expose sensitive information in descriptions:
+
+```graphql
+# Bad: Exposes internal details
+"""
+User entity. Stored in PostgreSQL users table.
+Synced with Salesforce via nightly cron job.
+"""
+type User { ... }
+
+# Good: Public-facing description
+"""
+A user account in the system.
+"""
+type User { ... }
+```
+
+### Field Nullability and Errors
+
+Consider what null vs error reveals:
+
+```graphql
+type User {
+ # Returns null if no permission - reveals existence
+ secretData: String
+
+ # Returns error if no permission - might reveal existence
+ secretData: String!
+}
+
+# Better: Same behavior whether exists or not
+# Query returns null/error whether user exists or not
+```
diff --git a/.agents/skills/graphql-schema/references/types.md b/.agents/skills/graphql-schema/references/types.md
new file mode 100644
index 000000000..c28844015
--- /dev/null
+++ b/.agents/skills/graphql-schema/references/types.md
@@ -0,0 +1,445 @@
+# Type Design Patterns
+
+This reference covers type design patterns for building well-structured GraphQL schemas.
+
+## Table of Contents
+
+- [Schema-First Design](#schema-first-design)
+- [Object Types](#object-types)
+- [Nullability Strategy](#nullability-strategy)
+- [ID Design](#id-design)
+- [Interfaces](#interfaces)
+- [Unions](#unions)
+- [Input Types](#input-types)
+- [Enums](#enums)
+- [Custom Scalars](#custom-scalars)
+
+## Schema-First Design
+
+Design your schema before implementing resolvers. This ensures:
+
+- API design focuses on client needs
+- Implementation details don't leak into schema
+- Team agreement on API contract
+
+```graphql
+# Start with what clients need
+type Query {
+ # Get a user's profile with recent activity
+ userProfile(id: ID!): UserProfile
+
+ # Search for content across the platform
+ search(query: String!, type: SearchType): SearchResults!
+}
+```
+
+## Object Types
+
+### Single Responsibility
+
+Each type should represent one clear concept:
+
+```graphql
+# Good: Focused types
+type User {
+ id: ID!
+ email: String!
+ profile: UserProfile!
+}
+
+type UserProfile {
+ displayName: String!
+ bio: String
+ avatarUrl: String
+}
+
+# Avoid: Overloaded type
+type User {
+ id: ID!
+ email: String!
+ displayName: String!
+ bio: String
+ avatarUrl: String
+ # ... mixing identity and profile concerns
+}
+```
+
+### Field Cohesion
+
+Group related fields. If fields always appear together, they belong together:
+
+```graphql
+type Address {
+ street: String!
+ city: String!
+ state: String!
+ postalCode: String!
+ country: String!
+}
+
+type Order {
+ id: ID!
+ shippingAddress: Address!
+ billingAddress: Address!
+}
+```
+
+### Computed vs Stored Fields
+
+Both computed and stored data should be fields. Clients don't care about storage:
+
+```graphql
+type Product {
+ id: ID!
+ name: String!
+ priceInCents: Int! # Stored
+ formattedPrice: String! # Computed
+ inStock: Boolean! # Computed from inventory
+}
+```
+
+## Nullability Strategy
+
+### Non-Null by Default
+
+Make fields non-null unless there's a reason for null:
+
+```graphql
+type User {
+ id: ID! # Always exists
+ email: String! # Required field
+ name: String # Optional - user might not set
+ deletedAt: DateTime # Null means not deleted
+}
+```
+
+### Valid Reasons for Nullable Fields
+
+1. **Optional data** - User hasn't provided it
+2. **Partial failure** - Resolver might fail for this field
+3. **Permission-based** - Hidden if no access
+4. **Semantic meaning** - Null means something (e.g., "not set")
+
+### List Nullability
+
+Always use **[Type!]!** for lists:
+
+```graphql
+type User {
+ # Correct: non-null list, non-null items
+ posts: [Post!]!
+
+ # Return empty list, not null
+ # Never return [null, post, null]
+}
+```
+
+### Nullable for External Dependencies
+
+If a field depends on an external service that might fail:
+
+```graphql
+type User {
+ id: ID!
+ email: String!
+
+ # Might fail if recommendation service is down
+ # Better to return null than fail entire query
+ recommendedPosts: [Post!]
+}
+```
+
+## ID Design
+
+### Global IDs
+
+Use globally unique IDs for entity identification:
+
+```graphql
+interface Node {
+ "Globally unique identifier"
+ id: ID!
+}
+
+type User implements Node {
+ id: ID! # e.g., "User:123" or base64("User:123")
+}
+```
+
+### Base64 Encoding Pattern
+
+Encode type and database ID together:
+
+```
+# Format: base64(TypeName:databaseId)
+User:123 → VXNlcjoxMjM=
+Post:456 → UG9zdDo0NTY=
+```
+
+Benefits:
+- Globally unique across types
+- Can determine type from ID
+- Opaque to clients (discourages assumptions)
+
+### Expose Database IDs Separately
+
+If clients need the original ID:
+
+```graphql
+type User implements Node {
+ id: ID! # Global ID: "VXNlcjoxMjM="
+ databaseId: Int! # Original: 123
+}
+```
+
+## Interfaces
+
+### When to Use Interfaces
+
+Use interfaces when types share common fields and behavior:
+
+```graphql
+interface Timestamped {
+ createdAt: DateTime!
+ updatedAt: DateTime!
+}
+
+type User implements Timestamped {
+ id: ID!
+ email: String!
+ createdAt: DateTime!
+ updatedAt: DateTime!
+}
+
+type Post implements Timestamped {
+ id: ID!
+ title: String!
+ createdAt: DateTime!
+ updatedAt: DateTime!
+}
+```
+
+### Node Interface for Refetching
+
+Implement `Node` for any type that can be fetched by ID:
+
+```graphql
+interface Node {
+ id: ID!
+}
+
+type Query {
+ node(id: ID!): Node
+ nodes(ids: [ID!]!): [Node]!
+}
+```
+
+### Multiple Interfaces
+
+Types can implement multiple interfaces:
+
+```graphql
+interface Node {
+ id: ID!
+}
+
+interface Timestamped {
+ createdAt: DateTime!
+ updatedAt: DateTime!
+}
+
+type Comment implements Node & Timestamped {
+ id: ID!
+ body: String!
+ createdAt: DateTime!
+ updatedAt: DateTime!
+}
+```
+
+## Unions
+
+### When to Use Unions
+
+Use unions for mutually exclusive types that don't share fields:
+
+```graphql
+union SearchResult = User | Post | Comment
+
+type Query {
+ search(query: String!): [SearchResult!]!
+}
+```
+
+### Unions vs Interfaces
+
+| Use Case | Choice |
+|----------|--------|
+| Types share common fields | Interface |
+| Types are mutually exclusive | Union |
+| Polymorphic field return | Either (depends on shared fields) |
+| Error handling patterns | Union (Result type) |
+
+### Result Type Pattern
+
+Use unions for operation results:
+
+```graphql
+type CreateUserSuccess {
+ user: User!
+}
+
+type ValidationError {
+ field: String!
+ message: String!
+}
+
+type EmailAlreadyExists {
+ existingUserId: ID!
+}
+
+union CreateUserResult = CreateUserSuccess | ValidationError | EmailAlreadyExists
+
+type Mutation {
+ createUser(input: CreateUserInput!): CreateUserResult!
+}
+```
+
+## Input Types
+
+### Structure Input Types
+
+Group related inputs:
+
+```graphql
+input CreatePostInput {
+ title: String!
+ body: String!
+ tags: [String!]
+ publishAt: DateTime
+}
+
+input UpdatePostInput {
+ title: String
+ body: String
+ tags: [String!]
+}
+
+type Mutation {
+ createPost(input: CreatePostInput!): Post!
+ updatePost(id: ID!, input: UpdatePostInput!): Post!
+}
+```
+
+### Optional Fields in Updates
+
+Make update input fields nullable to allow partial updates:
+
+```graphql
+input UpdateUserInput {
+ name: String # Pass to change, omit to keep
+ email: String # Pass to change, omit to keep
+ bio: String # Pass to change, omit to keep
+}
+```
+
+### Nested Input Types
+
+Create nested inputs for complex structures:
+
+```graphql
+input AddressInput {
+ street: String!
+ city: String!
+ state: String!
+ postalCode: String!
+ country: String!
+}
+
+input CreateOrderInput {
+ items: [OrderItemInput!]!
+ shippingAddress: AddressInput!
+ billingAddress: AddressInput
+}
+```
+
+## Enums
+
+### Use Enums for Fixed Values
+
+```graphql
+enum OrderStatus {
+ PENDING
+ CONFIRMED
+ SHIPPED
+ DELIVERED
+ CANCELLED
+}
+
+enum SortDirection {
+ ASC
+ DESC
+}
+```
+
+### Document Enum Values
+
+```graphql
+enum Role {
+ "Regular user with limited permissions"
+ USER
+
+ "Moderator with content management permissions"
+ MODERATOR
+
+ "Administrator with full system access"
+ ADMIN
+}
+```
+
+## Custom Scalars
+
+### Common Custom Scalars
+
+```graphql
+scalar DateTime # ISO 8601 date-time
+scalar Date # ISO 8601 date
+scalar Time # ISO 8601 time
+scalar URL # Valid URL string
+scalar Email # Valid email address
+scalar JSON # Arbitrary JSON (use sparingly)
+scalar UUID # UUID string
+scalar BigInt # Large integers beyond Int range
+```
+
+### When to Use Custom Scalars
+
+Use custom scalars when:
+- Built-in scalars don't capture the domain concept
+- Validation at the schema level is valuable
+- Serialization format matters (e.g., dates)
+
+```graphql
+type User {
+ email: Email! # Validated email format
+ website: URL # Validated URL format
+ createdAt: DateTime! # Consistent date format
+}
+```
+
+### Don't Overuse JSON Scalar
+
+Avoid `JSON` scalar except for truly dynamic data:
+
+```graphql
+# Avoid: Loses type safety
+type Config {
+ settings: JSON!
+}
+
+# Better: Define the structure
+type Config {
+ theme: Theme!
+ notifications: NotificationSettings!
+ privacy: PrivacySettings!
+}
+```
diff --git a/.agents/skills/typescript-advanced-types/SKILL.md b/.agents/skills/typescript-advanced-types/SKILL.md
new file mode 100644
index 000000000..7b603dfa6
--- /dev/null
+++ b/.agents/skills/typescript-advanced-types/SKILL.md
@@ -0,0 +1,717 @@
+---
+name: typescript-advanced-types
+description: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.
+---
+
+# TypeScript Advanced Types
+
+Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
+
+## When to Use This Skill
+
+- Building type-safe libraries or frameworks
+- Creating reusable generic components
+- Implementing complex type inference logic
+- Designing type-safe API clients
+- Building form validation systems
+- Creating strongly-typed configuration objects
+- Implementing type-safe state management
+- Migrating JavaScript codebases to TypeScript
+
+## Core Concepts
+
+### 1. Generics
+
+**Purpose:** Create reusable, type-flexible components while maintaining type safety.
+
+**Basic Generic Function:**
+
+```typescript
+function identity(value: T): T {
+ return value;
+}
+
+const num = identity(42); // Type: number
+const str = identity("hello"); // Type: string
+const auto = identity(true); // Type inferred: boolean
+```
+
+**Generic Constraints:**
+
+```typescript
+interface HasLength {
+ length: number;
+}
+
+function logLength(item: T): T {
+ console.log(item.length);
+ return item;
+}
+
+logLength("hello"); // OK: string has length
+logLength([1, 2, 3]); // OK: array has length
+logLength({ length: 10 }); // OK: object has length
+// logLength(42); // Error: number has no length
+```
+
+**Multiple Type Parameters:**
+
+```typescript
+function merge(obj1: T, obj2: U): T & U {
+ return { ...obj1, ...obj2 };
+}
+
+const merged = merge({ name: "John" }, { age: 30 });
+// Type: { name: string } & { age: number }
+```
+
+### 2. Conditional Types
+
+**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.
+
+**Basic Conditional Type:**
+
+```typescript
+type IsString = T extends string ? true : false;
+
+type A = IsString; // true
+type B = IsString; // false
+```
+
+**Extracting Return Types:**
+
+```typescript
+type ReturnType = T extends (...args: any[]) => infer R ? R : never;
+
+function getUser() {
+ return { id: 1, name: "John" };
+}
+
+type User = ReturnType;
+// Type: { id: number; name: string; }
+```
+
+**Distributive Conditional Types:**
+
+```typescript
+type ToArray = T extends any ? T[] : never;
+
+type StrOrNumArray = ToArray;
+// Type: string[] | number[]
+```
+
+**Nested Conditions:**
+
+```typescript
+type TypeName = T extends string
+ ? "string"
+ : T extends number
+ ? "number"
+ : T extends boolean
+ ? "boolean"
+ : T extends undefined
+ ? "undefined"
+ : T extends Function
+ ? "function"
+ : "object";
+
+type T1 = TypeName; // "string"
+type T2 = TypeName<() => void>; // "function"
+```
+
+### 3. Mapped Types
+
+**Purpose:** Transform existing types by iterating over their properties.
+
+**Basic Mapped Type:**
+
+```typescript
+type Readonly = {
+ readonly [P in keyof T]: T[P];
+};
+
+interface User {
+ id: number;
+ name: string;
+}
+
+type ReadonlyUser = Readonly;
+// Type: { readonly id: number; readonly name: string; }
+```
+
+**Optional Properties:**
+
+```typescript
+type Partial = {
+ [P in keyof T]?: T[P];
+};
+
+type PartialUser = Partial;
+// Type: { id?: number; name?: string; }
+```
+
+**Key Remapping:**
+
+```typescript
+type Getters = {
+ [K in keyof T as `get${Capitalize}`]: () => T[K];
+};
+
+interface Person {
+ name: string;
+ age: number;
+}
+
+type PersonGetters = Getters;
+// Type: { getName: () => string; getAge: () => number; }
+```
+
+**Filtering Properties:**
+
+```typescript
+type PickByType = {
+ [K in keyof T as T[K] extends U ? K : never]: T[K];
+};
+
+interface Mixed {
+ id: number;
+ name: string;
+ age: number;
+ active: boolean;
+}
+
+type OnlyNumbers = PickByType;
+// Type: { id: number; age: number; }
+```
+
+### 4. Template Literal Types
+
+**Purpose:** Create string-based types with pattern matching and transformation.
+
+**Basic Template Literal:**
+
+```typescript
+type EventName = "click" | "focus" | "blur";
+type EventHandler = `on${Capitalize}`;
+// Type: "onClick" | "onFocus" | "onBlur"
+```
+
+**String Manipulation:**
+
+```typescript
+type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
+type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
+type CapitalizedName = Capitalize<"john">; // "John"
+type UncapitalizedName = Uncapitalize<"John">; // "john"
+```
+
+**Path Building:**
+
+```typescript
+type Path = T extends object
+ ? {
+ [K in keyof T]: K extends string ? `${K}` | `${K}.${Path}` : never;
+ }[keyof T]
+ : never;
+
+interface Config {
+ server: {
+ host: string;
+ port: number;
+ };
+ database: {
+ url: string;
+ };
+}
+
+type ConfigPath = Path;
+// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
+```
+
+### 5. Utility Types
+
+**Built-in Utility Types:**
+
+```typescript
+// Partial - Make all properties optional
+type PartialUser = Partial;
+
+// Required - Make all properties required
+type RequiredUser = Required;
+
+// Readonly - Make all properties readonly
+type ReadonlyUser = Readonly;
+
+// Pick - Select specific properties
+type UserName = Pick;
+
+// Omit - Remove specific properties
+type UserWithoutPassword = Omit;
+
+// Exclude - Exclude types from union
+type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
+
+// Extract - Extract types from union
+type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
+
+// NonNullable - Exclude null and undefined
+type T3 = NonNullable; // string
+
+// Record - Create object type with keys K and values T
+type PageInfo = Record<"home" | "about", { title: string }>;
+```
+
+## Advanced Patterns
+
+### Pattern 1: Type-Safe Event Emitter
+
+```typescript
+type EventMap = {
+ "user:created": { id: string; name: string };
+ "user:updated": { id: string };
+ "user:deleted": { id: string };
+};
+
+class TypedEventEmitter> {
+ private listeners: {
+ [K in keyof T]?: Array<(data: T[K]) => void>;
+ } = {};
+
+ on(event: K, callback: (data: T[K]) => void): void {
+ if (!this.listeners[event]) {
+ this.listeners[event] = [];
+ }
+ this.listeners[event]!.push(callback);
+ }
+
+ emit(event: K, data: T[K]): void {
+ const callbacks = this.listeners[event];
+ if (callbacks) {
+ callbacks.forEach((callback) => callback(data));
+ }
+ }
+}
+
+const emitter = new TypedEventEmitter();
+
+emitter.on("user:created", (data) => {
+ console.log(data.id, data.name); // Type-safe!
+});
+
+emitter.emit("user:created", { id: "1", name: "John" });
+// emitter.emit("user:created", { id: "1" }); // Error: missing 'name'
+```
+
+### Pattern 2: Type-Safe API Client
+
+```typescript
+type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
+
+type EndpointConfig = {
+ "/users": {
+ GET: { response: User[] };
+ POST: { body: { name: string; email: string }; response: User };
+ };
+ "/users/:id": {
+ GET: { params: { id: string }; response: User };
+ PUT: { params: { id: string }; body: Partial; response: User };
+ DELETE: { params: { id: string }; response: void };
+ };
+};
+
+type ExtractParams = T extends { params: infer P } ? P : never;
+type ExtractBody = T extends { body: infer B } ? B : never;
+type ExtractResponse = T extends { response: infer R } ? R : never;
+
+class APIClient>> {
+ async request(
+ path: Path,
+ method: Method,
+ ...[options]: ExtractParams extends never
+ ? ExtractBody extends never
+ ? []
+ : [{ body: ExtractBody }]
+ : [
+ {
+ params: ExtractParams;
+ body?: ExtractBody;
+ },
+ ]
+ ): Promise> {
+ // Implementation here
+ return {} as any;
+ }
+}
+
+const api = new APIClient();
+
+// Type-safe API calls
+const users = await api.request("/users", "GET");
+// Type: User[]
+
+const newUser = await api.request("/users", "POST", {
+ body: { name: "John", email: "john@example.com" },
+});
+// Type: User
+
+const user = await api.request("/users/:id", "GET", {
+ params: { id: "123" },
+});
+// Type: User
+```
+
+### Pattern 3: Builder Pattern with Type Safety
+
+```typescript
+type BuilderState = {
+ [K in keyof T]: T[K] | undefined;
+};
+
+type RequiredKeys = {
+ [K in keyof T]-?: {} extends Pick ? never : K;
+}[keyof T];
+
+type OptionalKeys = {
+ [K in keyof T]-?: {} extends Pick ? K : never;
+}[keyof T];
+
+type IsComplete =
+ RequiredKeys extends keyof S
+ ? S[RequiredKeys] extends undefined
+ ? false
+ : true
+ : false;
+
+class Builder = {}> {
+ private state: S = {} as S;
+
+ set(key: K, value: T[K]): Builder> {
+ this.state[key] = value;
+ return this as any;
+ }
+
+ build(this: IsComplete extends true ? this : never): T {
+ return this.state as T;
+ }
+}
+
+interface User {
+ id: string;
+ name: string;
+ email: string;
+ age?: number;
+}
+
+const builder = new Builder();
+
+const user = builder
+ .set("id", "1")
+ .set("name", "John")
+ .set("email", "john@example.com")
+ .build(); // OK: all required fields set
+
+// const incomplete = builder
+// .set("id", "1")
+// .build(); // Error: missing required fields
+```
+
+### Pattern 4: Deep Readonly/Partial
+
+```typescript
+type DeepReadonly = {
+ readonly [P in keyof T]: T[P] extends object
+ ? T[P] extends Function
+ ? T[P]
+ : DeepReadonly
+ : T[P];
+};
+
+type DeepPartial = {
+ [P in keyof T]?: T[P] extends object
+ ? T[P] extends Array
+ ? Array>
+ : DeepPartial
+ : T[P];
+};
+
+interface Config {
+ server: {
+ host: string;
+ port: number;
+ ssl: {
+ enabled: boolean;
+ cert: string;
+ };
+ };
+ database: {
+ url: string;
+ pool: {
+ min: number;
+ max: number;
+ };
+ };
+}
+
+type ReadonlyConfig = DeepReadonly;
+// All nested properties are readonly
+
+type PartialConfig = DeepPartial;
+// All nested properties are optional
+```
+
+### Pattern 5: Type-Safe Form Validation
+
+```typescript
+type ValidationRule = {
+ validate: (value: T) => boolean;
+ message: string;
+};
+
+type FieldValidation = {
+ [K in keyof T]?: ValidationRule[];
+};
+
+type ValidationErrors = {
+ [K in keyof T]?: string[];
+};
+
+class FormValidator> {
+ constructor(private rules: FieldValidation) {}
+
+ validate(data: T): ValidationErrors | null {
+ const errors: ValidationErrors = {};
+ let hasErrors = false;
+
+ for (const key in this.rules) {
+ const fieldRules = this.rules[key];
+ const value = data[key];
+
+ if (fieldRules) {
+ const fieldErrors: string[] = [];
+
+ for (const rule of fieldRules) {
+ if (!rule.validate(value)) {
+ fieldErrors.push(rule.message);
+ }
+ }
+
+ if (fieldErrors.length > 0) {
+ errors[key] = fieldErrors;
+ hasErrors = true;
+ }
+ }
+ }
+
+ return hasErrors ? errors : null;
+ }
+}
+
+interface LoginForm {
+ email: string;
+ password: string;
+}
+
+const validator = new FormValidator({
+ email: [
+ {
+ validate: (v) => v.includes("@"),
+ message: "Email must contain @",
+ },
+ {
+ validate: (v) => v.length > 0,
+ message: "Email is required",
+ },
+ ],
+ password: [
+ {
+ validate: (v) => v.length >= 8,
+ message: "Password must be at least 8 characters",
+ },
+ ],
+});
+
+const errors = validator.validate({
+ email: "invalid",
+ password: "short",
+});
+// Type: { email?: string[]; password?: string[]; } | null
+```
+
+### Pattern 6: Discriminated Unions
+
+```typescript
+type Success = {
+ status: "success";
+ data: T;
+};
+
+type Error = {
+ status: "error";
+ error: string;
+};
+
+type Loading = {
+ status: "loading";
+};
+
+type AsyncState = Success | Error | Loading;
+
+function handleState(state: AsyncState): void {
+ switch (state.status) {
+ case "success":
+ console.log(state.data); // Type: T
+ break;
+ case "error":
+ console.log(state.error); // Type: string
+ break;
+ case "loading":
+ console.log("Loading...");
+ break;
+ }
+}
+
+// Type-safe state machine
+type State =
+ | { type: "idle" }
+ | { type: "fetching"; requestId: string }
+ | { type: "success"; data: any }
+ | { type: "error"; error: Error };
+
+type Event =
+ | { type: "FETCH"; requestId: string }
+ | { type: "SUCCESS"; data: any }
+ | { type: "ERROR"; error: Error }
+ | { type: "RESET" };
+
+function reducer(state: State, event: Event): State {
+ switch (state.type) {
+ case "idle":
+ return event.type === "FETCH"
+ ? { type: "fetching", requestId: event.requestId }
+ : state;
+ case "fetching":
+ if (event.type === "SUCCESS") {
+ return { type: "success", data: event.data };
+ }
+ if (event.type === "ERROR") {
+ return { type: "error", error: event.error };
+ }
+ return state;
+ case "success":
+ case "error":
+ return event.type === "RESET" ? { type: "idle" } : state;
+ }
+}
+```
+
+## Type Inference Techniques
+
+### 1. Infer Keyword
+
+```typescript
+// Extract array element type
+type ElementType = T extends (infer U)[] ? U : never;
+
+type NumArray = number[];
+type Num = ElementType; // number
+
+// Extract promise type
+type PromiseType = T extends Promise ? U : never;
+
+type AsyncNum = PromiseType>; // number
+
+// Extract function parameters
+type Parameters = T extends (...args: infer P) => any ? P : never;
+
+function foo(a: string, b: number) {}
+type FooParams = Parameters; // [string, number]
+```
+
+### 2. Type Guards
+
+```typescript
+function isString(value: unknown): value is string {
+ return typeof value === "string";
+}
+
+function isArrayOf(
+ value: unknown,
+ guard: (item: unknown) => item is T,
+): value is T[] {
+ return Array.isArray(value) && value.every(guard);
+}
+
+const data: unknown = ["a", "b", "c"];
+
+if (isArrayOf(data, isString)) {
+ data.forEach((s) => s.toUpperCase()); // Type: string[]
+}
+```
+
+### 3. Assertion Functions
+
+```typescript
+function assertIsString(value: unknown): asserts value is string {
+ if (typeof value !== "string") {
+ throw new Error("Not a string");
+ }
+}
+
+function processValue(value: unknown) {
+ assertIsString(value);
+ // value is now typed as string
+ console.log(value.toUpperCase());
+}
+```
+
+## Best Practices
+
+1. **Use `unknown` over `any`**: Enforce type checking
+2. **Prefer `interface` for object shapes**: Better error messages
+3. **Use `type` for unions and complex types**: More flexible
+4. **Leverage type inference**: Let TypeScript infer when possible
+5. **Create helper types**: Build reusable type utilities
+6. **Use const assertions**: Preserve literal types
+7. **Avoid type assertions**: Use type guards instead
+8. **Document complex types**: Add JSDoc comments
+9. **Use strict mode**: Enable all strict compiler options
+10. **Test your types**: Use type tests to verify type behavior
+
+## Type Testing
+
+```typescript
+// Type assertion tests
+type AssertEqual = [T] extends [U]
+ ? [U] extends [T]
+ ? true
+ : false
+ : false;
+
+type Test1 = AssertEqual; // true
+type Test2 = AssertEqual; // false
+type Test3 = AssertEqual; // false
+
+// Expect error helper
+type ExpectError = T;
+
+// Example usage
+type ShouldError = ExpectError>;
+```
+
+## Common Pitfalls
+
+1. **Over-using `any`**: Defeats the purpose of TypeScript
+2. **Ignoring strict null checks**: Can lead to runtime errors
+3. **Too complex types**: Can slow down compilation
+4. **Not using discriminated unions**: Misses type narrowing opportunities
+5. **Forgetting readonly modifiers**: Allows unintended mutations
+6. **Circular type references**: Can cause compiler errors
+7. **Not handling edge cases**: Like empty arrays or null values
+
+## Performance Considerations
+
+- Avoid deeply nested conditional types
+- Use simple types when possible
+- Cache complex type computations
+- Limit recursion depth in recursive types
+- Use build tools to skip type checking in production
diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md
new file mode 100644
index 000000000..a194a6183
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/AGENTS.md
@@ -0,0 +1,3373 @@
+# React Best Practices
+
+**Version 1.0.0**
+Vercel Engineering
+January 2026
+
+> **Note:**
+> This document is mainly for agents and LLMs to follow when maintaining,
+> generating, or refactoring React and Next.js codebases. Humans
+> may also find it useful, but guidance here is optimized for automation
+> and consistency by AI-assisted workflows.
+
+---
+
+## Abstract
+
+Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
+
+---
+
+## Table of Contents
+
+1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
+ - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
+ - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
+ - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
+ - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
+ - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
+2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
+ - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
+ - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
+ - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
+ - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
+ - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
+3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
+ - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
+ - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
+ - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
+ - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level)
+ - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries)
+ - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition)
+ - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache)
+ - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations)
+4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
+ - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
+ - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
+ - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
+ - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
+5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
+ - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
+ - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
+ - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
+ - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)
+ - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
+ - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)
+ - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)
+ - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)
+ - 5.9 [Split Combined Hook Computations](#59-split-combined-hook-computations)
+ - 5.10 [Subscribe to Derived State](#510-subscribe-to-derived-state)
+ - 5.11 [Use Functional setState Updates](#511-use-functional-setstate-updates)
+ - 5.12 [Use Lazy State Initialization](#512-use-lazy-state-initialization)
+ - 5.13 [Use Transitions for Non-Urgent Updates](#513-use-transitions-for-non-urgent-updates)
+ - 5.14 [Use useDeferredValue for Expensive Derived Renders](#514-use-usedeferredvalue-for-expensive-derived-renders)
+ - 5.15 [Use useRef for Transient Values](#515-use-useref-for-transient-values)
+6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
+ - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
+ - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
+ - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
+ - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
+ - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
+ - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
+ - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
+ - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)
+ - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)
+ - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)
+ - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)
+7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
+ - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
+ - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
+ - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
+ - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
+ - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
+ - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
+ - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
+ - 7.8 [Early Return from Functions](#78-early-return-from-functions)
+ - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
+ - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass)
+ - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort)
+ - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups)
+ - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability)
+8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
+ - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
+ - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
+ - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
+
+---
+
+## 1. Eliminating Waterfalls
+
+**Impact: CRITICAL**
+
+Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
+
+### 1.1 Defer Await Until Needed
+
+**Impact: HIGH (avoids blocking unused code paths)**
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect: blocks both branches**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct: only blocks when needed**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example: early return optimization**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
+
+### 1.2 Dependency-Based Parallelization
+
+**Impact: CRITICAL (2-10× improvement)**
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect: profile waits for config unnecessarily**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct: config and profile run in parallel**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+
+### 1.3 Prevent Waterfall Chains in API Routes
+
+**Impact: CRITICAL (2-10× improvement)**
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect: config waits for auth, data waits for both**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct: auth and config start immediately**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
+
+### 1.4 Promise.all() for Independent Operations
+
+**Impact: CRITICAL (2-10× improvement)**
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect: sequential execution, 3 round trips**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct: parallel execution, 1 round trip**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
+
+### 1.5 Strategic Suspense Boundaries
+
+**Impact: HIGH (faster initial paint)**
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect: wrapper blocked by data fetching**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct: wrapper shows immediately, data streams in**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return
{data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative: share promise across components**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+}
+
+function DataSummary({ dataPromise }: { dataPromise: Promise }) {
+ const data = use(dataPromise) // Reuses the same promise
+ return
{data.summary}
+}
+```
+
+Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
+
+**When NOT to use this pattern:**
+
+- Critical data needed for layout decisions (affects positioning)
+
+- SEO-critical content above the fold
+
+- Small, fast queries where suspense overhead isn't worth it
+
+- When you want to avoid layout shift (loading → content jump)
+
+**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
+
+---
+
+## 2. Bundle Size Optimization
+
+**Impact: CRITICAL**
+
+Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
+
+### 2.1 Avoid Barrel File Imports
+
+**Impact: CRITICAL (200-800ms import cost, slow builds)**
+
+Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
+
+Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
+
+**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
+
+**Incorrect: imports entire library**
+
+```tsx
+import { Check, X, Menu } from 'lucide-react'
+// Loads 1,583 modules, takes ~2.8s extra in dev
+// Runtime cost: 200-800ms on every cold start
+
+import { Button, TextField } from '@mui/material'
+// Loads 2,225 modules, takes ~4.2s extra in dev
+```
+
+**Correct: imports only what you need**
+
+```tsx
+import Check from 'lucide-react/dist/esm/icons/check'
+import X from 'lucide-react/dist/esm/icons/x'
+import Menu from 'lucide-react/dist/esm/icons/menu'
+// Loads only 3 modules (~2KB vs ~1MB)
+
+import Button from '@mui/material/Button'
+import TextField from '@mui/material/TextField'
+// Loads only what you use
+```
+
+**Alternative: Next.js 13.5+**
+
+```js
+// next.config.js - use optimizePackageImports
+module.exports = {
+ experimental: {
+ optimizePackageImports: ['lucide-react', '@mui/material']
+ }
+}
+
+// Then you can keep the ergonomic barrel imports:
+import { Check, X, Menu } from 'lucide-react'
+// Automatically transformed to direct imports at build time
+```
+
+Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
+
+Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
+
+Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+
+### 2.2 Conditional Module Loading
+
+**Impact: HIGH (loads large data only when needed)**
+
+Load large data or modules only when a feature is activated.
+
+**Example: lazy-load animation frames**
+
+```tsx
+function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) {
+ const [frames, setFrames] = useState(null)
+
+ useEffect(() => {
+ if (enabled && !frames && typeof window !== 'undefined') {
+ import('./animation-frames.js')
+ .then(mod => setFrames(mod.frames))
+ .catch(() => setEnabled(false))
+ }
+ }, [enabled, frames, setEnabled])
+
+ if (!frames) return
+ return
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
+
+### 2.3 Defer Non-Critical Third-Party Libraries
+
+**Impact: MEDIUM (loads after hydration)**
+
+Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
+
+**Incorrect: blocks initial bundle**
+
+```tsx
+import { Analytics } from '@vercel/analytics/react'
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+**Correct: loads after hydration**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const Analytics = dynamic(
+ () => import('@vercel/analytics/react').then(m => m.Analytics),
+ { ssr: false }
+)
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+### 2.4 Dynamic Imports for Heavy Components
+
+**Impact: CRITICAL (directly affects TTI and LCP)**
+
+Use `next/dynamic` to lazy-load large components not needed on initial render.
+
+**Incorrect: Monaco bundles with main chunk ~300KB**
+
+```tsx
+import { MonacoEditor } from './monaco-editor'
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
+
+**Correct: Monaco loads on demand**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const MonacoEditor = dynamic(
+ () => import('./monaco-editor').then(m => m.MonacoEditor),
+ { ssr: false }
+)
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
+
+### 2.5 Preload Based on User Intent
+
+**Impact: MEDIUM (reduces perceived latency)**
+
+Preload heavy bundles before they're needed to reduce perceived latency.
+
+**Example: preload on hover/focus**
+
+```tsx
+function EditorButton({ onClick }: { onClick: () => void }) {
+ const preload = () => {
+ if (typeof window !== 'undefined') {
+ void import('./monaco-editor')
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+**Example: preload when feature flag is enabled**
+
+```tsx
+function FlagsProvider({ children, flags }: Props) {
+ useEffect(() => {
+ if (flags.editorEnabled && typeof window !== 'undefined') {
+ void import('./monaco-editor').then(mod => mod.init())
+ }
+ }, [flags.editorEnabled])
+
+ return
+ {children}
+
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
+
+---
+
+## 3. Server-Side Performance
+
+**Impact: HIGH**
+
+Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
+
+### 3.1 Authenticate Server Actions Like API Routes
+
+**Impact: CRITICAL (prevents unauthorized access to server mutations)**
+
+Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
+
+Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
+
+**Incorrect: no authentication check**
+
+```typescript
+'use server'
+
+export async function deleteUser(userId: string) {
+ // Anyone can call this! No auth check
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**Correct: authentication inside the action**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { unauthorized } from '@/lib/errors'
+
+export async function deleteUser(userId: string) {
+ // Always check auth inside the action
+ const session = await verifySession()
+
+ if (!session) {
+ throw unauthorized('Must be logged in')
+ }
+
+ // Check authorization too
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
+ throw unauthorized('Cannot delete other users')
+ }
+
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**With input validation:**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { z } from 'zod'
+
+const updateProfileSchema = z.object({
+ userId: z.string().uuid(),
+ name: z.string().min(1).max(100),
+ email: z.string().email()
+})
+
+export async function updateProfile(data: unknown) {
+ // Validate input first
+ const validated = updateProfileSchema.parse(data)
+
+ // Then authenticate
+ const session = await verifySession()
+ if (!session) {
+ throw new Error('Unauthorized')
+ }
+
+ // Then authorize
+ if (session.user.id !== validated.userId) {
+ throw new Error('Can only update own profile')
+ }
+
+ // Finally perform the mutation
+ await db.user.update({
+ where: { id: validated.userId },
+ data: {
+ name: validated.name,
+ email: validated.email
+ }
+ })
+
+ return { success: true }
+}
+```
+
+Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
+
+### 3.2 Avoid Duplicate Serialization in RSC Props
+
+**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
+
+RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
+
+**Incorrect: duplicates array**
+
+```tsx
+// RSC: sends 6 strings (2 arrays × 3 items)
+
+```
+
+**Correct: sends 3 strings**
+
+```tsx
+// RSC: send once
+
+
+// Client: transform there
+'use client'
+const sorted = useMemo(() => [...usernames].sort(), [usernames])
+```
+
+**Nested deduplication behavior:**
+
+```tsx
+// string[] - duplicates everything
+usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
+
+// object[] - duplicates array structure only
+users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
+```
+
+Deduplication works recursively. Impact varies by data type:
+
+- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
+
+- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
+
+**Operations breaking deduplication: create new references**
+
+- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
+
+- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
+
+**More examples:**
+
+```tsx
+// ❌ Bad
+ u.active)} />
+
+
+// ✅ Good
+
+
+// Do filtering/destructuring in client
+```
+
+**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
+
+### 3.3 Cross-Request LRU Caching
+
+**Impact: HIGH (caches across requests)**
+
+`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
+
+**Implementation:**
+
+```typescript
+import { LRUCache } from 'lru-cache'
+
+const cache = new LRUCache({
+ max: 1000,
+ ttl: 5 * 60 * 1000 // 5 minutes
+})
+
+export async function getUser(id: string) {
+ const cached = cache.get(id)
+ if (cached) return cached
+
+ const user = await db.user.findUnique({ where: { id } })
+ cache.set(id, user)
+ return user
+}
+
+// Request 1: DB query, result cached
+// Request 2: cache hit, no DB query
+```
+
+Use when sequential user actions hit multiple endpoints needing the same data within seconds.
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
+
+**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
+
+Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+
+### 3.4 Hoist Static I/O to Module Level
+
+**Impact: HIGH (avoids repeated file/network I/O per request)**
+
+When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
+
+**Incorrect: reads font file on every request**
+
+**Correct: loads once at module initialization**
+
+**Alternative: synchronous file reads with Node.js fs**
+
+**General Node.js example: loading config or templates**
+
+**When to use this pattern:**
+
+- Loading fonts for OG image generation
+
+- Loading static logos, icons, or watermarks
+
+- Reading configuration files that don't change at runtime
+
+- Loading email templates or other static templates
+
+- Any static asset that's the same across all requests
+
+**When NOT to use this pattern:**
+
+- Assets that vary per request or user
+
+- Files that may change during runtime (use caching with TTL instead)
+
+- Large files that would consume too much memory if kept loaded
+
+- Sensitive data that shouldn't persist in memory
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
+
+**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
+
+### 3.5 Minimize Serialization at RSC Boundaries
+
+**Impact: HIGH (reduces data transfer size)**
+
+The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
+
+**Incorrect: serializes all 50 fields**
+
+```tsx
+async function Page() {
+ const user = await fetchUser() // 50 fields
+ return
+}
+
+'use client'
+function Profile({ user }: { user: User }) {
+ return
{user.name}
// uses 1 field
+}
+```
+
+**Correct: serializes only 1 field**
+
+```tsx
+async function Page() {
+ const user = await fetchUser()
+ return
+}
+
+'use client'
+function Profile({ name }: { name: string }) {
+ return
{name}
+}
+```
+
+### 3.6 Parallel Data Fetching with Component Composition
+
+**Impact: CRITICAL (eliminates server-side waterfalls)**
+
+React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
+
+**Incorrect: Sidebar waits for Page's fetch to complete**
+
+```tsx
+export default async function Page() {
+ const header = await fetchHeader()
+ return (
+
+
{header}
+
+
+ )
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return
+}
+```
+
+**Correct: both fetch simultaneously**
+
+```tsx
+async function Header() {
+ const data = await fetchHeader()
+ return
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
+
+### 5.2 Defer State Reads to Usage Point
+
+**Impact: MEDIUM (avoids unnecessary subscriptions)**
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect: subscribes to all searchParams changes**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+**Correct: reads on demand, no subscription**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
+
+**Impact: LOW-MEDIUM (wasted computation on every render)**
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+### 5.4 Don't Define Components Inside Components
+
+**Impact: HIGH (prevents remount on every render)**
+
+Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
+
+A common reason developers do this is to access parent variables without passing props. Always pass props instead.
+
+**Incorrect: remounts on every render**
+
+```tsx
+function UserProfile({ user, theme }) {
+ // Defined inside to access `theme` - BAD
+ const Avatar = () => (
+
+ )
+
+ // Defined inside to access `user` - BAD
+ const Stats = () => (
+
+ )
+}
+```
+
+**Symptoms of this bug:**
+
+- Input fields lose focus on every keystroke
+
+- Animations restart unexpectedly
+
+- `useEffect` cleanup/setup runs on every parent render
+
+- Scroll position resets inside the component
+
+### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+**Impact: MEDIUM (restores memoization by using a constant for default value)**
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect: `onClick` has different values on every rerender**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct: stable default value**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+### 5.6 Extract to Memoized Components
+
+**Impact: MEDIUM (enables early returns)**
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect: computes avatar even when loading**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return
{avatar}
+}
+```
+
+**Correct: skips computation when loading**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
+
+### 5.7 Narrow Effect Dependencies
+
+**Impact: LOW (minimizes effect re-runs)**
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect: re-runs on any user field change**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct: re-runs only when id changes**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
+
+### 5.8 Put Interaction Logic in Event Handlers
+
+**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect: event modeled as state + effect**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return
+}
+```
+
+**Correct: do it in the handler**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return
+}
+```
+
+Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
+
+### 5.9 Split Combined Hook Computations
+
+**Impact: MEDIUM (avoids recomputing independent steps)**
+
+When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.
+
+**Incorrect: changing `sortOrder` recomputes filtering**
+
+```tsx
+const sortedProducts = useMemo(() => {
+ const filtered = products.filter((p) => p.category === category)
+ const sorted = filtered.toSorted((a, b) =>
+ sortOrder === "asc" ? a.price - b.price : b.price - a.price
+ )
+ return sorted
+}, [products, category, sortOrder])
+```
+
+**Correct: filtering only recomputes when products or category change**
+
+```tsx
+const filteredProducts = useMemo(
+ () => products.filter((p) => p.category === category),
+ [products, category]
+)
+
+const sortedProducts = useMemo(
+ () =>
+ filteredProducts.toSorted((a, b) =>
+ sortOrder === "asc" ? a.price - b.price : b.price - a.price
+ ),
+ [filteredProducts, sortOrder]
+)
+```
+
+This pattern also applies to `useEffect` when combining unrelated side effects:
+
+**Incorrect: both effects run when either dependency changes**
+
+```tsx
+useEffect(() => {
+ analytics.trackPageView(pathname)
+ document.title = `${pageTitle} | My App`
+}, [pathname, pageTitle])
+```
+
+**Correct: effects run independently**
+
+```tsx
+useEffect(() => {
+ analytics.trackPageView(pathname)
+}, [pathname])
+
+useEffect(() => {
+ document.title = `${pageTitle} | My App`
+}, [pageTitle])
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.
+
+### 5.10 Subscribe to Derived State
+
+**Impact: MEDIUM (reduces re-render frequency)**
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect: re-renders on every pixel change**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct: re-renders only when boolean changes**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
+
+### 5.11 Use Functional setState Updates
+
+**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect: requires state as dependency**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct: stable callbacks, no stale closures**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+
+2. **No stale closures** - Always operates on the latest state value
+
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+
+- Inside useCallback/useMemo when state is needed
+
+- Event handlers that reference state
+
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+
+- Setting state from props/arguments only: `setName(newName)`
+
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
+
+### 5.12 Use Lazy State Initialization
+
+**Impact: MEDIUM (wasted computation on every render)**
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect: runs on every render**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct: runs only once**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
+
+### 5.13 Use Transitions for Non-Urgent Updates
+
+**Impact: MEDIUM (maintains UI responsiveness)**
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect: blocks UI on every scroll**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct: non-blocking updates**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+### 5.14 Use useDeferredValue for Expensive Derived Renders
+
+**Impact: MEDIUM (keeps input responsive during heavy computation)**
+
+When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.
+
+**Incorrect: input feels laggy while filtering**
+
+```tsx
+function Search({ items }: { items: Item[] }) {
+ const [query, setQuery] = useState('')
+ const filtered = items.filter(item => fuzzyMatch(item, query))
+
+ return (
+ <>
+ setQuery(e.target.value)} />
+
+ >
+ )
+}
+```
+
+**Correct: input stays snappy, results render when ready**
+
+```tsx
+function Search({ items }: { items: Item[] }) {
+ const [query, setQuery] = useState('')
+ const deferredQuery = useDeferredValue(query)
+ const filtered = useMemo(
+ () => items.filter(item => fuzzyMatch(item, deferredQuery)),
+ [items, deferredQuery]
+ )
+ const isStale = query !== deferredQuery
+
+ return (
+ <>
+ setQuery(e.target.value)} />
+
+
+
+ >
+ )
+}
+```
+
+**When to use:**
+
+- Filtering/searching large lists
+
+- Expensive visualizations (charts, graphs) reacting to input
+
+- Any derived state that causes noticeable render delays
+
+**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.
+
+Reference: [https://react.dev/reference/react/useDeferredValue](https://react.dev/reference/react/useDeferredValue)
+
+### 5.15 Use useRef for Transient Values
+
+**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect: renders every update**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct: no re-render for tracking**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+---
+
+## 6. Rendering Performance
+
+**Impact: MEDIUM**
+
+Optimizing the rendering process reduces the work the browser needs to do.
+
+### 6.1 Animate SVG Wrapper Instead of SVG Element
+
+**Impact: LOW (enables hardware acceleration)**
+
+Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
+
+### 6.4 Optimize SVG Precision
+
+**Impact: LOW (reduces file size)**
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect: excessive precision**
+
+```svg
+
+```
+
+**Correct: 1 decimal place**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
+
+### 6.5 Prevent Hydration Mismatch Without Flickering
+
+**Impact: MEDIUM (avoids visual flicker and hydration errors)**
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect: breaks SSR**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct: no flicker, no hydration mismatch**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
+
+### 6.6 Suppress Expected Hydration Mismatches
+
+**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect: known mismatch warnings**
+
+```tsx
+function Timestamp() {
+ return {new Date().toLocaleString()}
+}
+```
+
+**Correct: suppress expected mismatch only**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
+
+### 6.7 Use Activity Component for Show/Hide
+
+**Impact: MEDIUM (preserves state/DOM)**
+
+Use React's `` to preserve state/DOM for expensive components that frequently toggle visibility.
+
+**Usage:**
+
+```tsx
+import { Activity } from 'react'
+
+function Dropdown({ isOpen }: Props) {
+ return (
+
+
+
+ )
+}
+```
+
+Avoids expensive re-renders and state loss.
+
+### 6.8 Use defer or async on Script Tags
+
+**Impact: HIGH (eliminates render-blocking)**
+
+Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
+
+- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
+
+- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
+
+Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
+
+**Incorrect: blocks rendering**
+
+```tsx
+export default function Document() {
+ return (
+
+
+
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Correct: non-blocking**
+
+```tsx
+import Script from 'next/script'
+
+export default function Page() {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
+
+Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
+
+### 6.9 Use Explicit Conditional Rendering
+
+**Impact: LOW (prevents rendering 0 or NaN)**
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect: renders "0" when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count}}
+
+ )
+}
+
+// When count = 0, renders:
0
+// When count = 5, renders:
5
+```
+
+**Correct: renders nothing when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders:
5
+```
+
+### 6.10 Use React DOM Resource Hints
+
+**Impact: HIGH (reduces load time for critical resources)**
+
+React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
+
+- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
+
+- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
+
+- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
+
+- **`preloadModule(href)`**: Fetch an ES module you'll use soon
+
+- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
+
+- **`preinitModule(href)`**: Fetch and evaluate an ES module
+
+**Example: preconnect to third-party APIs**
+
+```tsx
+import { preconnect, prefetchDNS } from 'react-dom'
+
+export default function App() {
+ prefetchDNS('https://analytics.example.com')
+ preconnect('https://api.example.com')
+
+ return {/* content */}
+}
+```
+
+**Example: preload critical fonts and styles**
+
+```tsx
+import { preload, preinit } from 'react-dom'
+
+export default function RootLayout({ children }) {
+ // Preload font file
+ preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
+
+ // Fetch and apply critical stylesheet immediately
+ preinit('/styles/critical.css', { as: 'style' })
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Example: preload modules for code-split routes**
+
+```tsx
+import { preloadModule, preinitModule } from 'react-dom'
+
+function Navigation() {
+ const preloadDashboard = () => {
+ preloadModule('/dashboard.js', { as: 'script' })
+ }
+
+ return (
+
+ )
+}
+```
+
+**When to use each:**
+
+| API | Use case |
+
+|-----|----------|
+
+| `prefetchDNS` | Third-party domains you'll connect to later |
+
+| `preconnect` | APIs or CDNs you'll fetch from immediately |
+
+| `preload` | Critical resources needed for current page |
+
+| `preloadModule` | JS modules for likely next navigation |
+
+| `preinit` | Stylesheets/scripts that must execute early |
+
+| `preinitModule` | ES modules that must execute early |
+
+Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)
+
+### 6.11 Use useTransition Over Manual Loading States
+
+**Impact: LOW (reduces re-renders and improves code clarity)**
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect: manual loading state**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct: useTransition with built-in pending state**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+
+- **Error resilience**: Pending state correctly resets even if the transition throws
+
+- **Better responsiveness**: Keeps the UI responsive during updates
+
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
+
+---
+
+## 7. JavaScript Performance
+
+**Impact: LOW-MEDIUM**
+
+Micro-optimizations for hot paths can add up to meaningful improvements.
+
+### 7.1 Avoid Layout Thrashing
+
+**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
+
+Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
+
+**This is OK: browser batches style changes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Each line invalidates style, but browser batches the recalculation
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+}
+```
+
+**Incorrect: interleaved reads and writes force reflows**
+
+```typescript
+function layoutThrashing(element: HTMLElement) {
+ element.style.width = '100px'
+ const width = element.offsetWidth // Forces reflow
+ element.style.height = '200px'
+ const height = element.offsetHeight // Forces another reflow
+}
+```
+
+**Correct: batch writes, then read once**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Batch all writes together
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+
+ // Read after all writes are done (single reflow)
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Correct: batch reads, then writes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ element.classList.add('highlighted-box')
+
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Better: use CSS classes**
+
+**React example:**
+
+```tsx
+// Incorrect: interleaving style changes with layout queries
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (ref.current && isHighlighted) {
+ ref.current.style.width = '100px'
+ const width = ref.current.offsetWidth // Forces layout
+ ref.current.style.height = '200px'
+ }
+ }, [isHighlighted])
+
+ return
+}
+```
+
+**Why this matters in React:**
+
+1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
+
+2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
+
+**Browser support: fallback for older browsers**
+
+```typescript
+// Fallback for older browsers
+const sorted = [...items].sort((a, b) => a.value - b.value)
+```
+
+`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
+
+**Other immutable array methods:**
+
+- `.toSorted()` - immutable sort
+
+- `.toReversed()` - immutable reverse
+
+- `.toSpliced()` - immutable splice
+
+- `.with()` - immutable element replacement
+
+---
+
+## 8. Advanced Patterns
+
+**Impact: LOW**
+
+Advanced patterns for specific cases that require careful implementation.
+
+### 8.1 Initialize App Once, Not Per Mount
+
+**Impact: LOW-MEDIUM (avoids duplicate init in development)**
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect: runs twice in dev, re-runs on remount**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct: once per app load**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
+
+### 8.2 Store Event Handlers in Refs
+
+**Impact: LOW (stable subscriptions)**
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect: re-subscribes on every render**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct: stable subscription**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
+
+### 8.3 useEffectEvent for Stable Callback Refs
+
+**Impact: LOW (prevents effect re-runs)**
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect: effect re-runs on every callback change**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct: using React's useEffectEvent**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
+
+---
+
+## References
+
+1. [https://react.dev](https://react.dev)
+2. [https://nextjs.org](https://nextjs.org)
+3. [https://swr.vercel.app](https://swr.vercel.app)
+4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
diff --git a/.agents/skills/vercel-react-best-practices/README.md b/.agents/skills/vercel-react-best-practices/README.md
new file mode 100644
index 000000000..f283e1c0c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/README.md
@@ -0,0 +1,123 @@
+# React Best Practices
+
+A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
+
+## Structure
+
+- `rules/` - Individual rule files (one per rule)
+ - `_sections.md` - Section metadata (titles, impacts, descriptions)
+ - `_template.md` - Template for creating new rules
+ - `area-description.md` - Individual rule files
+- `src/` - Build scripts and utilities
+- `metadata.json` - Document metadata (version, organization, abstract)
+- __`AGENTS.md`__ - Compiled output (generated)
+- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
+
+## Getting Started
+
+1. Install dependencies:
+ ```bash
+ pnpm install
+ ```
+
+2. Build AGENTS.md from rules:
+ ```bash
+ pnpm build
+ ```
+
+3. Validate rule files:
+ ```bash
+ pnpm validate
+ ```
+
+4. Extract test cases:
+ ```bash
+ pnpm extract-tests
+ ```
+
+## Creating a New Rule
+
+1. Copy `rules/_template.md` to `rules/area-description.md`
+2. Choose the appropriate area prefix:
+ - `async-` for Eliminating Waterfalls (Section 1)
+ - `bundle-` for Bundle Size Optimization (Section 2)
+ - `server-` for Server-Side Performance (Section 3)
+ - `client-` for Client-Side Data Fetching (Section 4)
+ - `rerender-` for Re-render Optimization (Section 5)
+ - `rendering-` for Rendering Performance (Section 6)
+ - `js-` for JavaScript Performance (Section 7)
+ - `advanced-` for Advanced Patterns (Section 8)
+3. Fill in the frontmatter and content
+4. Ensure you have clear examples with explanations
+5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
+
+## Rule File Structure
+
+Each rule file should follow this structure:
+
+```markdown
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description
+tags: tag1, tag2, tag3
+---
+
+## Rule Title Here
+
+Brief explanation of the rule and why it matters.
+
+**Incorrect (description of what's wrong):**
+
+```typescript
+// Bad code example
+```
+
+**Correct (description of what's right):**
+
+```typescript
+// Good code example
+```
+
+Optional explanatory text after examples.
+
+Reference: [Link](https://example.com)
+
+## File Naming Convention
+
+- Files starting with `_` are special (excluded from build)
+- Rule files: `area-description.md` (e.g., `async-parallel.md`)
+- Section is automatically inferred from filename prefix
+- Rules are sorted alphabetically by title within each section
+- IDs (e.g., 1.1, 1.2) are auto-generated during build
+
+## Impact Levels
+
+- `CRITICAL` - Highest priority, major performance gains
+- `HIGH` - Significant performance improvements
+- `MEDIUM-HIGH` - Moderate-high gains
+- `MEDIUM` - Moderate performance improvements
+- `LOW-MEDIUM` - Low-medium gains
+- `LOW` - Incremental improvements
+
+## Scripts
+
+- `pnpm build` - Compile rules into AGENTS.md
+- `pnpm validate` - Validate all rule files
+- `pnpm extract-tests` - Extract test cases for LLM evaluation
+- `pnpm dev` - Build and validate
+
+## Contributing
+
+When adding or modifying rules:
+
+1. Use the correct filename prefix for your section
+2. Follow the `_template.md` structure
+3. Include clear bad/good examples with explanations
+4. Add appropriate tags
+5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
+6. Rules are automatically sorted by title - no need to manage numbers!
+
+## Acknowledgments
+
+Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
diff --git a/.agents/skills/vercel-react-best-practices/SKILL.md b/.agents/skills/vercel-react-best-practices/SKILL.md
new file mode 100644
index 000000000..4bdde580f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/SKILL.md
@@ -0,0 +1,143 @@
+---
+name: vercel-react-best-practices
+description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
+license: MIT
+metadata:
+ author: vercel
+ version: "1.0.0"
+---
+
+# Vercel React Best Practices
+
+Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 64 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new React components or Next.js pages
+- Implementing data fetching (client or server-side)
+- Reviewing code for performance issues
+- Refactoring existing React/Next.js code
+- Optimizing bundle size or load times
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix |
+|----------|----------|--------|--------|
+| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
+| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
+| 3 | Server-Side Performance | HIGH | `server-` |
+| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
+| 5 | Re-render Optimization | MEDIUM | `rerender-` |
+| 6 | Rendering Performance | MEDIUM | `rendering-` |
+| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
+| 8 | Advanced Patterns | LOW | `advanced-` |
+
+## Quick Reference
+
+### 1. Eliminating Waterfalls (CRITICAL)
+
+- `async-defer-await` - Move await into branches where actually used
+- `async-parallel` - Use Promise.all() for independent operations
+- `async-dependencies` - Use better-all for partial dependencies
+- `async-api-routes` - Start promises early, await late in API routes
+- `async-suspense-boundaries` - Use Suspense to stream content
+
+### 2. Bundle Size Optimization (CRITICAL)
+
+- `bundle-barrel-imports` - Import directly, avoid barrel files
+- `bundle-dynamic-imports` - Use next/dynamic for heavy components
+- `bundle-defer-third-party` - Load analytics/logging after hydration
+- `bundle-conditional` - Load modules only when feature is activated
+- `bundle-preload` - Preload on hover/focus for perceived speed
+
+### 3. Server-Side Performance (HIGH)
+
+- `server-auth-actions` - Authenticate server actions like API routes
+- `server-cache-react` - Use React.cache() for per-request deduplication
+- `server-cache-lru` - Use LRU cache for cross-request caching
+- `server-dedup-props` - Avoid duplicate serialization in RSC props
+- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
+- `server-serialization` - Minimize data passed to client components
+- `server-parallel-fetching` - Restructure components to parallelize fetches
+- `server-after-nonblocking` - Use after() for non-blocking operations
+
+### 4. Client-Side Data Fetching (MEDIUM-HIGH)
+
+- `client-swr-dedup` - Use SWR for automatic request deduplication
+- `client-event-listeners` - Deduplicate global event listeners
+- `client-passive-event-listeners` - Use passive listeners for scroll
+- `client-localstorage-schema` - Version and minimize localStorage data
+
+### 5. Re-render Optimization (MEDIUM)
+
+- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
+- `rerender-memo` - Extract expensive work into memoized components
+- `rerender-memo-with-default-value` - Hoist default non-primitive props
+- `rerender-dependencies` - Use primitive dependencies in effects
+- `rerender-derived-state` - Subscribe to derived booleans, not raw values
+- `rerender-derived-state-no-effect` - Derive state during render, not effects
+- `rerender-functional-setstate` - Use functional setState for stable callbacks
+- `rerender-lazy-state-init` - Pass function to useState for expensive values
+- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
+- `rerender-split-combined-hooks` - Split hooks with independent dependencies
+- `rerender-move-effect-to-event` - Put interaction logic in event handlers
+- `rerender-transitions` - Use startTransition for non-urgent updates
+- `rerender-use-deferred-value` - Defer expensive renders to keep input responsive
+- `rerender-use-ref-transient-values` - Use refs for transient frequent values
+- `rerender-no-inline-components` - Don't define components inside components
+
+### 6. Rendering Performance (MEDIUM)
+
+- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
+- `rendering-content-visibility` - Use content-visibility for long lists
+- `rendering-hoist-jsx` - Extract static JSX outside components
+- `rendering-svg-precision` - Reduce SVG coordinate precision
+- `rendering-hydration-no-flicker` - Use inline script for client-only data
+- `rendering-hydration-suppress-warning` - Suppress expected mismatches
+- `rendering-activity` - Use Activity component for show/hide
+- `rendering-conditional-render` - Use ternary, not && for conditionals
+- `rendering-usetransition-loading` - Prefer useTransition for loading state
+- `rendering-resource-hints` - Use React DOM resource hints for preloading
+- `rendering-script-defer-async` - Use defer or async on script tags
+
+### 7. JavaScript Performance (LOW-MEDIUM)
+
+- `js-batch-dom-css` - Group CSS changes via classes or cssText
+- `js-index-maps` - Build Map for repeated lookups
+- `js-cache-property-access` - Cache object properties in loops
+- `js-cache-function-results` - Cache function results in module-level Map
+- `js-cache-storage` - Cache localStorage/sessionStorage reads
+- `js-combine-iterations` - Combine multiple filter/map into one loop
+- `js-length-check-first` - Check array length before expensive comparison
+- `js-early-exit` - Return early from functions
+- `js-hoist-regexp` - Hoist RegExp creation outside loops
+- `js-min-max-loop` - Use loop for min/max instead of sort
+- `js-set-map-lookups` - Use Set/Map for O(1) lookups
+- `js-tosorted-immutable` - Use toSorted() for immutability
+- `js-flatmap-filter` - Use flatMap to map and filter in one pass
+
+### 8. Advanced Patterns (LOW)
+
+- `advanced-event-handler-refs` - Store event handlers in refs
+- `advanced-init-once` - Initialize app once per app load
+- `advanced-use-latest` - useLatest for stable callback refs
+
+## How to Use
+
+Read individual rule files for detailed explanations and code examples:
+
+```
+rules/async-parallel.md
+rules/bundle-barrel-imports.md
+```
+
+Each rule file contains:
+- Brief explanation of why it matters
+- Incorrect code example with explanation
+- Correct code example with explanation
+- Additional context and references
+
+## Full Compiled Document
+
+For the complete guide with all rules expanded: `AGENTS.md`
diff --git a/.agents/skills/vercel-react-best-practices/rules/_sections.md b/.agents/skills/vercel-react-best-practices/rules/_sections.md
new file mode 100644
index 000000000..4d20c144b
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/_sections.md
@@ -0,0 +1,46 @@
+# Sections
+
+This file defines all sections, their ordering, impact levels, and descriptions.
+The section ID (in parentheses) is the filename prefix used to group rules.
+
+---
+
+## 1. Eliminating Waterfalls (async)
+
+**Impact:** CRITICAL
+**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
+
+## 2. Bundle Size Optimization (bundle)
+
+**Impact:** CRITICAL
+**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
+
+## 3. Server-Side Performance (server)
+
+**Impact:** HIGH
+**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
+
+## 4. Client-Side Data Fetching (client)
+
+**Impact:** MEDIUM-HIGH
+**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
+
+## 5. Re-render Optimization (rerender)
+
+**Impact:** MEDIUM
+**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
+
+## 6. Rendering Performance (rendering)
+
+**Impact:** MEDIUM
+**Description:** Optimizing the rendering process reduces the work the browser needs to do.
+
+## 7. JavaScript Performance (js)
+
+**Impact:** LOW-MEDIUM
+**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
+
+## 8. Advanced Patterns (advanced)
+
+**Impact:** LOW
+**Description:** Advanced patterns for specific cases that require careful implementation.
diff --git a/.agents/skills/vercel-react-best-practices/rules/_template.md b/.agents/skills/vercel-react-best-practices/rules/_template.md
new file mode 100644
index 000000000..1e9e70703
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/_template.md
@@ -0,0 +1,28 @@
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description of impact (e.g., "20-50% improvement")
+tags: tag1, tag2
+---
+
+## Rule Title Here
+
+**Impact: MEDIUM (optional impact description)**
+
+Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
+
+**Incorrect (description of what's wrong):**
+
+```typescript
+// Bad code example here
+const bad = example()
+```
+
+**Correct (description of what's right):**
+
+```typescript
+// Good code example here
+const good = example()
+```
+
+Reference: [Link to documentation or resource](https://example.com)
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
new file mode 100644
index 000000000..97e7ade24
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
@@ -0,0 +1,55 @@
+---
+title: Store Event Handlers in Refs
+impact: LOW
+impactDescription: stable subscriptions
+tags: advanced, hooks, refs, event-handlers, optimization
+---
+
+## Store Event Handlers in Refs
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect (re-subscribes on every render):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct (stable subscription):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ const handlerRef = useRef(handler)
+ useEffect(() => {
+ handlerRef.current = handler
+ }, [handler])
+
+ useEffect(() => {
+ const listener = (e) => handlerRef.current(e)
+ window.addEventListener(event, listener)
+ return () => window.removeEventListener(event, listener)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
new file mode 100644
index 000000000..73ee38e5e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
@@ -0,0 +1,42 @@
+---
+title: Initialize App Once, Not Per Mount
+impact: LOW-MEDIUM
+impactDescription: avoids duplicate init in development
+tags: initialization, useEffect, app-startup, side-effects
+---
+
+## Initialize App Once, Not Per Mount
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect (runs twice in dev, re-runs on remount):**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct (once per app load):**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
new file mode 100644
index 000000000..9c7cb5016
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
@@ -0,0 +1,39 @@
+---
+title: useEffectEvent for Stable Callback Refs
+impact: LOW
+impactDescription: prevents effect re-runs
+tags: advanced, hooks, useEffectEvent, refs, optimization
+---
+
+## useEffectEvent for Stable Callback Refs
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect (effect re-runs on every callback change):**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct (using React's useEffectEvent):**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
new file mode 100644
index 000000000..6feda1ef0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
@@ -0,0 +1,38 @@
+---
+title: Prevent Waterfall Chains in API Routes
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: api-routes, server-actions, waterfalls, parallelization
+---
+
+## Prevent Waterfall Chains in API Routes
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect (config waits for auth, data waits for both):**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct (auth and config start immediately):**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
new file mode 100644
index 000000000..ea7082a36
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
@@ -0,0 +1,80 @@
+---
+title: Defer Await Until Needed
+impact: HIGH
+impactDescription: avoids blocking unused code paths
+tags: async, await, conditional, optimization
+---
+
+## Defer Await Until Needed
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect (blocks both branches):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct (only blocks when needed):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example (early return optimization):**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
new file mode 100644
index 000000000..0484ebab9
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
@@ -0,0 +1,51 @@
+---
+title: Dependency-Based Parallelization
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, dependencies, better-all
+---
+
+## Dependency-Based Parallelization
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect (profile waits for config unnecessarily):**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct (config and profile run in parallel):**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-parallel.md b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
new file mode 100644
index 000000000..64133f6c3
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
@@ -0,0 +1,28 @@
+---
+title: Promise.all() for Independent Operations
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, promises, waterfalls
+---
+
+## Promise.all() for Independent Operations
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect (sequential execution, 3 round trips):**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct (parallel execution, 1 round trip):**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
new file mode 100644
index 000000000..1fbc05b04
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
@@ -0,0 +1,99 @@
+---
+title: Strategic Suspense Boundaries
+impact: HIGH
+impactDescription: faster initial paint
+tags: async, suspense, streaming, layout-shift
+---
+
+## Strategic Suspense Boundaries
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect (wrapper blocked by data fetching):**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct (wrapper shows immediately, data streams in):**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return
{data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative (share promise across components):**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+ )
+}
+```
+
+Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
+
+See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
new file mode 100644
index 000000000..180f8ac8f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
@@ -0,0 +1,80 @@
+---
+title: Cache Repeated Function Calls
+impact: MEDIUM
+impactDescription: avoid redundant computation
+tags: javascript, cache, memoization, performance
+---
+
+## Cache Repeated Function Calls
+
+Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
+
+**Incorrect (redundant computation):**
+
+```typescript
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // slugify() called 100+ times for same project names
+ const slug = slugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
new file mode 100644
index 000000000..7e866f585
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
@@ -0,0 +1,40 @@
+---
+title: Use Explicit Conditional Rendering
+impact: LOW
+impactDescription: prevents rendering 0 or NaN
+tags: rendering, conditional, jsx, falsy-values
+---
+
+## Use Explicit Conditional Rendering
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect (renders "0" when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count}}
+
+ )
+}
+
+// When count = 0, renders:
0
+// When count = 5, renders:
5
+```
+
+**Correct (renders nothing when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders:
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
new file mode 100644
index 000000000..5cf0e79b6
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
@@ -0,0 +1,82 @@
+---
+title: Prevent Hydration Mismatch Without Flickering
+impact: MEDIUM
+impactDescription: avoids visual flicker and hydration errors
+tags: rendering, ssr, hydration, localStorage, flicker
+---
+
+## Prevent Hydration Mismatch Without Flickering
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect (breaks SSR):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct (no flicker, no hydration mismatch):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
new file mode 100644
index 000000000..24ba2513a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
@@ -0,0 +1,30 @@
+---
+title: Suppress Expected Hydration Mismatches
+impact: LOW-MEDIUM
+impactDescription: avoids noisy hydration warnings for known differences
+tags: rendering, hydration, ssr, nextjs
+---
+
+## Suppress Expected Hydration Mismatches
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect (known mismatch warnings):**
+
+```tsx
+function Timestamp() {
+ return {new Date().toLocaleString()}
+}
+```
+
+**Correct (suppress expected mismatch only):**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md b/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md
new file mode 100644
index 000000000..1290bef06
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md
@@ -0,0 +1,85 @@
+---
+title: Use React DOM Resource Hints
+impact: HIGH
+impactDescription: reduces load time for critical resources
+tags: rendering, preload, preconnect, prefetch, resource-hints
+---
+
+## Use React DOM Resource Hints
+
+**Impact: HIGH (reduces load time for critical resources)**
+
+React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
+
+- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
+- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
+- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
+- **`preloadModule(href)`**: Fetch an ES module you'll use soon
+- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
+- **`preinitModule(href)`**: Fetch and evaluate an ES module
+
+**Example (preconnect to third-party APIs):**
+
+```tsx
+import { preconnect, prefetchDNS } from 'react-dom'
+
+export default function App() {
+ prefetchDNS('https://analytics.example.com')
+ preconnect('https://api.example.com')
+
+ return {/* content */}
+}
+```
+
+**Example (preload critical fonts and styles):**
+
+```tsx
+import { preload, preinit } from 'react-dom'
+
+export default function RootLayout({ children }) {
+ // Preload font file
+ preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
+
+ // Fetch and apply critical stylesheet immediately
+ preinit('/styles/critical.css', { as: 'style' })
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Example (preload modules for code-split routes):**
+
+```tsx
+import { preloadModule, preinitModule } from 'react-dom'
+
+function Navigation() {
+ const preloadDashboard = () => {
+ preloadModule('/dashboard.js', { as: 'script' })
+ }
+
+ return (
+
+ )
+}
+```
+
+**When to use each:**
+
+| API | Use case |
+|-----|----------|
+| `prefetchDNS` | Third-party domains you'll connect to later |
+| `preconnect` | APIs or CDNs you'll fetch from immediately |
+| `preload` | Critical resources needed for current page |
+| `preloadModule` | JS modules for likely next navigation |
+| `preinit` | Stylesheets/scripts that must execute early |
+| `preinitModule` | ES modules that must execute early |
+
+Reference: [React DOM Resource Preloading APIs](https://react.dev/reference/react-dom#resource-preloading-apis)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md b/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md
new file mode 100644
index 000000000..ee275ed1c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md
@@ -0,0 +1,68 @@
+---
+title: Use defer or async on Script Tags
+impact: HIGH
+impactDescription: eliminates render-blocking
+tags: rendering, script, defer, async, performance
+---
+
+## Use defer or async on Script Tags
+
+**Impact: HIGH (eliminates render-blocking)**
+
+Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
+
+- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
+- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
+
+Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
+
+**Incorrect (blocks rendering):**
+
+```tsx
+export default function Document() {
+ return (
+
+
+
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Correct (non-blocking):**
+
+```tsx
+export default function Document() {
+ return (
+
+
+ {/* Independent script - use async */}
+
+ {/* DOM-dependent script - use defer */}
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
+
+```tsx
+import Script from 'next/script'
+
+export default function Page() {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+Reference: [MDN - Script element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
new file mode 100644
index 000000000..6d7712860
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
@@ -0,0 +1,28 @@
+---
+title: Optimize SVG Precision
+impact: LOW
+impactDescription: reduces file size
+tags: rendering, svg, optimization, svgo
+---
+
+## Optimize SVG Precision
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect (excessive precision):**
+
+```svg
+
+```
+
+**Correct (1 decimal place):**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
new file mode 100644
index 000000000..0c1b0b98e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
@@ -0,0 +1,75 @@
+---
+title: Use useTransition Over Manual Loading States
+impact: LOW
+impactDescription: reduces re-renders and improves code clarity
+tags: rendering, transitions, useTransition, loading, state
+---
+
+## Use useTransition Over Manual Loading States
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect (manual loading state):**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct (useTransition with built-in pending state):**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+- **Error resilience**: Pending state correctly resets even if the transition throws
+- **Better responsiveness**: Keeps the UI responsive during updates
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [useTransition](https://react.dev/reference/react/useTransition)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
new file mode 100644
index 000000000..e867c95f0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
@@ -0,0 +1,39 @@
+---
+title: Defer State Reads to Usage Point
+impact: MEDIUM
+impactDescription: avoids unnecessary subscriptions
+tags: rerender, searchParams, localStorage, optimization
+---
+
+## Defer State Reads to Usage Point
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect (subscribes to all searchParams changes):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
+
+**Correct (reads on demand, no subscription):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
new file mode 100644
index 000000000..47a4d9268
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
@@ -0,0 +1,45 @@
+---
+title: Narrow Effect Dependencies
+impact: LOW
+impactDescription: minimizes effect re-runs
+tags: rerender, useEffect, dependencies, optimization
+---
+
+## Narrow Effect Dependencies
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect (re-runs on any user field change):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct (re-runs only when id changes):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
new file mode 100644
index 000000000..3d9fe4050
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
@@ -0,0 +1,40 @@
+---
+title: Calculate Derived State During Rendering
+impact: MEDIUM
+impactDescription: avoids redundant renders and state drift
+tags: rerender, derived-state, useEffect, state
+---
+
+## Calculate Derived State During Rendering
+
+If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
+
+**Incorrect (redundant state and effect):**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const [fullName, setFullName] = useState('')
+
+ useEffect(() => {
+ setFullName(firstName + ' ' + lastName)
+ }, [firstName, lastName])
+
+ return
+}
+```
+
+References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
new file mode 100644
index 000000000..e5c899f6c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
@@ -0,0 +1,29 @@
+---
+title: Subscribe to Derived State
+impact: MEDIUM
+impactDescription: reduces re-render frequency
+tags: rerender, derived-state, media-query, optimization
+---
+
+## Subscribe to Derived State
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect (re-renders on every pixel change):**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct (re-renders only when boolean changes):**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
new file mode 100644
index 000000000..b004ef45e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
@@ -0,0 +1,74 @@
+---
+title: Use Functional setState Updates
+impact: MEDIUM
+impactDescription: prevents stale closures and unnecessary callback recreations
+tags: react, hooks, useState, useCallback, callbacks, closures
+---
+
+## Use Functional setState Updates
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect (requires state as dependency):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct (stable callbacks, no stale closures):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+2. **No stale closures** - Always operates on the latest state value
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+- Inside useCallback/useMemo when state is needed
+- Event handlers that reference state
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+- Setting state from props/arguments only: `setName(newName)`
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
new file mode 100644
index 000000000..4ecb350fb
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
@@ -0,0 +1,58 @@
+---
+title: Use Lazy State Initialization
+impact: MEDIUM
+impactDescription: wasted computation on every render
+tags: react, hooks, useState, performance, initialization
+---
+
+## Use Lazy State Initialization
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect (runs on every render):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct (runs only once):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
new file mode 100644
index 000000000..635704918
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
@@ -0,0 +1,38 @@
+---
+
+title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+impact: MEDIUM
+impactDescription: restores memoization by using a constant for default value
+tags: rerender, memo, optimization
+
+---
+
+## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect (`onClick` has different values on every rerender):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct (stable default value):**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
new file mode 100644
index 000000000..f8982ab61
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
@@ -0,0 +1,44 @@
+---
+title: Extract to Memoized Components
+impact: MEDIUM
+impactDescription: enables early returns
+tags: rerender, memo, useMemo, optimization
+---
+
+## Extract to Memoized Components
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect (computes avatar even when loading):**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return
{avatar}
+}
+```
+
+**Correct (skips computation when loading):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
new file mode 100644
index 000000000..dd58a1af0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
@@ -0,0 +1,45 @@
+---
+title: Put Interaction Logic in Event Handlers
+impact: MEDIUM
+impactDescription: avoids effect re-runs and duplicate side effects
+tags: rerender, useEffect, events, side-effects, dependencies
+---
+
+## Put Interaction Logic in Event Handlers
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect (event modeled as state + effect):**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return
+}
+```
+
+**Correct (do it in the handler):**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return
+}
+```
+
+Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md b/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md
new file mode 100644
index 000000000..d97592ace
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md
@@ -0,0 +1,82 @@
+---
+title: Don't Define Components Inside Components
+impact: HIGH
+impactDescription: prevents remount on every render
+tags: rerender, components, remount, performance
+---
+
+## Don't Define Components Inside Components
+
+**Impact: HIGH (prevents remount on every render)**
+
+Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
+
+A common reason developers do this is to access parent variables without passing props. Always pass props instead.
+
+**Incorrect (remounts on every render):**
+
+```tsx
+function UserProfile({ user, theme }) {
+ // Defined inside to access `theme` - BAD
+ const Avatar = () => (
+
+ )
+
+ // Defined inside to access `user` - BAD
+ const Stats = () => (
+
+ )
+}
+```
+
+**Symptoms of this bug:**
+- Input fields lose focus on every keystroke
+- Animations restart unexpectedly
+- `useEffect` cleanup/setup runs on every parent render
+- Scroll position resets inside the component
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
new file mode 100644
index 000000000..59dfab0f3
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
@@ -0,0 +1,35 @@
+---
+title: Do not wrap a simple expression with a primitive result type in useMemo
+impact: LOW-MEDIUM
+impactDescription: wasted computation on every render
+tags: rerender, useMemo, optimization
+---
+
+## Do not wrap a simple expression with a primitive result type in useMemo
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md b/.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md
new file mode 100644
index 000000000..89d805644
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md
@@ -0,0 +1,64 @@
+---
+title: Split Combined Hook Computations
+impact: MEDIUM
+impactDescription: avoids recomputing independent steps
+tags: rerender, useMemo, useEffect, dependencies, optimization
+---
+
+## Split Combined Hook Computations
+
+When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value.
+
+**Incorrect (changing `sortOrder` recomputes filtering):**
+
+```tsx
+const sortedProducts = useMemo(() => {
+ const filtered = products.filter((p) => p.category === category)
+ const sorted = filtered.toSorted((a, b) =>
+ sortOrder === "asc" ? a.price - b.price : b.price - a.price
+ )
+ return sorted
+}, [products, category, sortOrder])
+```
+
+**Correct (filtering only recomputes when products or category change):**
+
+```tsx
+const filteredProducts = useMemo(
+ () => products.filter((p) => p.category === category),
+ [products, category]
+)
+
+const sortedProducts = useMemo(
+ () =>
+ filteredProducts.toSorted((a, b) =>
+ sortOrder === "asc" ? a.price - b.price : b.price - a.price
+ ),
+ [filteredProducts, sortOrder]
+)
+```
+
+This pattern also applies to `useEffect` when combining unrelated side effects:
+
+**Incorrect (both effects run when either dependency changes):**
+
+```tsx
+useEffect(() => {
+ analytics.trackPageView(pathname)
+ document.title = `${pageTitle} | My App`
+}, [pathname, pageTitle])
+```
+
+**Correct (effects run independently):**
+
+```tsx
+useEffect(() => {
+ analytics.trackPageView(pathname)
+}, [pathname])
+
+useEffect(() => {
+ document.title = `${pageTitle} | My App`
+}, [pageTitle])
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
new file mode 100644
index 000000000..d99f43f76
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
@@ -0,0 +1,40 @@
+---
+title: Use Transitions for Non-Urgent Updates
+impact: MEDIUM
+impactDescription: maintains UI responsiveness
+tags: rerender, transitions, startTransition, performance
+---
+
+## Use Transitions for Non-Urgent Updates
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect (blocks UI on every scroll):**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct (non-blocking updates):**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md b/.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md
new file mode 100644
index 000000000..619c04b01
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md
@@ -0,0 +1,59 @@
+---
+title: Use useDeferredValue for Expensive Derived Renders
+impact: MEDIUM
+impactDescription: keeps input responsive during heavy computation
+tags: rerender, useDeferredValue, optimization, concurrent
+---
+
+## Use useDeferredValue for Expensive Derived Renders
+
+When user input triggers expensive computations or renders, use `useDeferredValue` to keep the input responsive. The deferred value lags behind, allowing React to prioritize the input update and render the expensive result when idle.
+
+**Incorrect (input feels laggy while filtering):**
+
+```tsx
+function Search({ items }: { items: Item[] }) {
+ const [query, setQuery] = useState('')
+ const filtered = items.filter(item => fuzzyMatch(item, query))
+
+ return (
+ <>
+ setQuery(e.target.value)} />
+
+ >
+ )
+}
+```
+
+**Correct (input stays snappy, results render when ready):**
+
+```tsx
+function Search({ items }: { items: Item[] }) {
+ const [query, setQuery] = useState('')
+ const deferredQuery = useDeferredValue(query)
+ const filtered = useMemo(
+ () => items.filter(item => fuzzyMatch(item, deferredQuery)),
+ [items, deferredQuery]
+ )
+ const isStale = query !== deferredQuery
+
+ return (
+ <>
+ setQuery(e.target.value)} />
+
+
+
+ >
+ )
+}
+```
+
+**When to use:**
+
+- Filtering/searching large lists
+- Expensive visualizations (charts, graphs) reacting to input
+- Any derived state that causes noticeable render delays
+
+**Note:** Wrap the expensive computation in `useMemo` with the deferred value as a dependency, otherwise it still runs on every render.
+
+Reference: [React useDeferredValue](https://react.dev/reference/react/useDeferredValue)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
new file mode 100644
index 000000000..cf04b81f8
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
@@ -0,0 +1,73 @@
+---
+title: Use useRef for Transient Values
+impact: MEDIUM
+impactDescription: avoids unnecessary re-renders on frequent updates
+tags: rerender, useref, state, performance
+---
+
+## Use useRef for Transient Values
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect (renders every update):**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct (no re-render for tracking):**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
new file mode 100644
index 000000000..e8f5b260f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
@@ -0,0 +1,73 @@
+---
+title: Use after() for Non-Blocking Operations
+impact: MEDIUM
+impactDescription: faster response times
+tags: server, async, logging, analytics, side-effects
+---
+
+## Use after() for Non-Blocking Operations
+
+Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
+
+**Incorrect (blocks response):**
+
+```tsx
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Logging blocks the response
+ const userAgent = request.headers.get('user-agent') || 'unknown'
+ await logUserAction({ userAgent })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+**Correct (non-blocking):**
+
+```tsx
+import { after } from 'next/server'
+import { headers, cookies } from 'next/headers'
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Log after response is sent
+ after(async () => {
+ const userAgent = (await headers()).get('user-agent') || 'unknown'
+ const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
+
+ logUserAction({ sessionCookie, userAgent })
+ })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+The response is sent immediately while logging happens in the background.
+
+**Common use cases:**
+
+- Analytics tracking
+- Audit logging
+- Sending notifications
+- Cache invalidation
+- Cleanup tasks
+
+**Important notes:**
+
+- `after()` runs even if the response fails or redirects
+- Works in Server Actions, Route Handlers, and Server Components
+
+Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
new file mode 100644
index 000000000..ee82c0442
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
@@ -0,0 +1,96 @@
+---
+title: Authenticate Server Actions Like API Routes
+impact: CRITICAL
+impactDescription: prevents unauthorized access to server mutations
+tags: server, server-actions, authentication, security, authorization
+---
+
+## Authenticate Server Actions Like API Routes
+
+**Impact: CRITICAL (prevents unauthorized access to server mutations)**
+
+Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
+
+Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
+
+**Incorrect (no authentication check):**
+
+```typescript
+'use server'
+
+export async function deleteUser(userId: string) {
+ // Anyone can call this! No auth check
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**Correct (authentication inside the action):**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { unauthorized } from '@/lib/errors'
+
+export async function deleteUser(userId: string) {
+ // Always check auth inside the action
+ const session = await verifySession()
+
+ if (!session) {
+ throw unauthorized('Must be logged in')
+ }
+
+ // Check authorization too
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
+ throw unauthorized('Cannot delete other users')
+ }
+
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**With input validation:**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { z } from 'zod'
+
+const updateProfileSchema = z.object({
+ userId: z.string().uuid(),
+ name: z.string().min(1).max(100),
+ email: z.string().email()
+})
+
+export async function updateProfile(data: unknown) {
+ // Validate input first
+ const validated = updateProfileSchema.parse(data)
+
+ // Then authenticate
+ const session = await verifySession()
+ if (!session) {
+ throw new Error('Unauthorized')
+ }
+
+ // Then authorize
+ if (session.user.id !== validated.userId) {
+ throw new Error('Can only update own profile')
+ }
+
+ // Finally perform the mutation
+ await db.user.update({
+ where: { id: validated.userId },
+ data: {
+ name: validated.name,
+ email: validated.email
+ }
+ })
+
+ return { success: true }
+}
+```
+
+Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
new file mode 100644
index 000000000..ef6938aa5
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
@@ -0,0 +1,41 @@
+---
+title: Cross-Request LRU Caching
+impact: HIGH
+impactDescription: caches across requests
+tags: server, cache, lru, cross-request
+---
+
+## Cross-Request LRU Caching
+
+`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
+
+**Implementation:**
+
+```typescript
+import { LRUCache } from 'lru-cache'
+
+const cache = new LRUCache({
+ max: 1000,
+ ttl: 5 * 60 * 1000 // 5 minutes
+})
+
+export async function getUser(id: string) {
+ const cached = cache.get(id)
+ if (cached) return cached
+
+ const user = await db.user.findUnique({ where: { id } })
+ cache.set(id, user)
+ return user
+}
+
+// Request 1: DB query, result cached
+// Request 2: cache hit, no DB query
+```
+
+Use when sequential user actions hit multiple endpoints needing the same data within seconds.
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
+
+**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
+
+Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
new file mode 100644
index 000000000..87c9ca331
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
@@ -0,0 +1,76 @@
+---
+title: Per-Request Deduplication with React.cache()
+impact: MEDIUM
+impactDescription: deduplicates within request
+tags: server, cache, react-cache, deduplication
+---
+
+## Per-Request Deduplication with React.cache()
+
+Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
+
+**Usage:**
+
+```typescript
+import { cache } from 'react'
+
+export const getCurrentUser = cache(async () => {
+ const session = await auth()
+ if (!session?.user?.id) return null
+ return await db.user.findUnique({
+ where: { id: session.user.id }
+ })
+})
+```
+
+Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
+
+**Avoid inline objects as arguments:**
+
+`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
+
+**Incorrect (always cache miss):**
+
+```typescript
+const getUser = cache(async (params: { uid: number }) => {
+ return await db.user.findUnique({ where: { id: params.uid } })
+})
+
+// Each call creates new object, never hits cache
+getUser({ uid: 1 })
+getUser({ uid: 1 }) // Cache miss, runs query again
+```
+
+**Correct (cache hit):**
+
+```typescript
+const getUser = cache(async (uid: number) => {
+ return await db.user.findUnique({ where: { id: uid } })
+})
+
+// Primitive args use value equality
+getUser(1)
+getUser(1) // Cache hit, returns cached result
+```
+
+If you must pass objects, pass the same reference:
+
+```typescript
+const params = { uid: 1 }
+getUser(params) // Query runs
+getUser(params) // Cache hit (same reference)
+```
+
+**Next.js-Specific Note:**
+
+In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
+
+- Database queries (Prisma, Drizzle, etc.)
+- Heavy computations
+- Authentication checks
+- File system operations
+- Any non-fetch async work
+
+Use `React.cache()` to deduplicate these operations across your component tree.
+
+Reference: [React.cache documentation](https://react.dev/reference/react/cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
new file mode 100644
index 000000000..fb24a2562
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
@@ -0,0 +1,65 @@
+---
+title: Avoid Duplicate Serialization in RSC Props
+impact: LOW
+impactDescription: reduces network payload by avoiding duplicate serialization
+tags: server, rsc, serialization, props, client-components
+---
+
+## Avoid Duplicate Serialization in RSC Props
+
+**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
+
+RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
+
+**Incorrect (duplicates array):**
+
+```tsx
+// RSC: sends 6 strings (2 arrays × 3 items)
+
+```
+
+**Correct (sends 3 strings):**
+
+```tsx
+// RSC: send once
+
+
+// Client: transform there
+'use client'
+const sorted = useMemo(() => [...usernames].sort(), [usernames])
+```
+
+**Nested deduplication behavior:**
+
+Deduplication works recursively. Impact varies by data type:
+
+- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
+- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
+
+```tsx
+// string[] - duplicates everything
+usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
+
+// object[] - duplicates array structure only
+users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
+```
+
+**Operations breaking deduplication (create new references):**
+
+- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
+- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
+
+**More examples:**
+
+```tsx
+// ❌ Bad
+ u.active)} />
+
+
+// ✅ Good
+
+
+// Do filtering/destructuring in client
+```
+
+**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md b/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md
new file mode 100644
index 000000000..5b642b69b
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md
@@ -0,0 +1,142 @@
+---
+title: Hoist Static I/O to Module Level
+impact: HIGH
+impactDescription: avoids repeated file/network I/O per request
+tags: server, io, performance, next.js, route-handlers, og-image
+---
+
+## Hoist Static I/O to Module Level
+
+**Impact: HIGH (avoids repeated file/network I/O per request)**
+
+When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
+
+**Incorrect: reads font file on every request**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+
+export async function GET(request: Request) {
+ // Runs on EVERY request - expensive!
+ const fontData = await fetch(
+ new URL('./fonts/Inter.ttf', import.meta.url)
+ ).then(res => res.arrayBuffer())
+
+ const logoData = await fetch(
+ new URL('./images/logo.png', import.meta.url)
+ ).then(res => res.arrayBuffer())
+
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: fontData }] }
+ )
+}
+```
+
+**Correct: loads once at module initialization**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+
+// Module-level: runs ONCE when module is first imported
+const fontData = fetch(
+ new URL('./fonts/Inter.ttf', import.meta.url)
+).then(res => res.arrayBuffer())
+
+const logoData = fetch(
+ new URL('./images/logo.png', import.meta.url)
+).then(res => res.arrayBuffer())
+
+export async function GET(request: Request) {
+ // Await the already-started promises
+ const [font, logo] = await Promise.all([fontData, logoData])
+
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: font }] }
+ )
+}
+```
+
+**Alternative: synchronous file reads with Node.js fs**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+import { readFileSync } from 'fs'
+import { join } from 'path'
+
+// Synchronous read at module level - blocks only during module init
+const fontData = readFileSync(
+ join(process.cwd(), 'public/fonts/Inter.ttf')
+)
+
+const logoData = readFileSync(
+ join(process.cwd(), 'public/images/logo.png')
+)
+
+export async function GET(request: Request) {
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: fontData }] }
+ )
+}
+```
+
+**General Node.js example: loading config or templates**
+
+```typescript
+// Incorrect: reads config on every call
+export async function processRequest(data: Data) {
+ const config = JSON.parse(
+ await fs.readFile('./config.json', 'utf-8')
+ )
+ const template = await fs.readFile('./template.html', 'utf-8')
+
+ return render(template, data, config)
+}
+
+// Correct: loads once at module level
+const configPromise = fs.readFile('./config.json', 'utf-8')
+ .then(JSON.parse)
+const templatePromise = fs.readFile('./template.html', 'utf-8')
+
+export async function processRequest(data: Data) {
+ const [config, template] = await Promise.all([
+ configPromise,
+ templatePromise
+ ])
+
+ return render(template, data, config)
+}
+```
+
+**When to use this pattern:**
+
+- Loading fonts for OG image generation
+- Loading static logos, icons, or watermarks
+- Reading configuration files that don't change at runtime
+- Loading email templates or other static templates
+- Any static asset that's the same across all requests
+
+**When NOT to use this pattern:**
+
+- Assets that vary per request or user
+- Files that may change during runtime (use caching with TTL instead)
+- Large files that would consume too much memory if kept loaded
+- Sensitive data that shouldn't persist in memory
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
+
+**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
new file mode 100644
index 000000000..1affc835a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
@@ -0,0 +1,83 @@
+---
+title: Parallel Data Fetching with Component Composition
+impact: CRITICAL
+impactDescription: eliminates server-side waterfalls
+tags: server, rsc, parallel-fetching, composition
+---
+
+## Parallel Data Fetching with Component Composition
+
+React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
+
+**Incorrect (Sidebar waits for Page's fetch to complete):**
+
+```tsx
+export default async function Page() {
+ const header = await fetchHeader()
+ return (
+
+ )
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-serialization.md b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
new file mode 100644
index 000000000..39c5c4164
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
@@ -0,0 +1,38 @@
+---
+title: Minimize Serialization at RSC Boundaries
+impact: HIGH
+impactDescription: reduces data transfer size
+tags: server, rsc, serialization, props
+---
+
+## Minimize Serialization at RSC Boundaries
+
+The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
+
+**Incorrect (serializes all 50 fields):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser() // 50 fields
+ return
+}
+
+'use client'
+function Profile({ user }: { user: User }) {
+ return
{user.name}
// uses 1 field
+}
+```
+
+**Correct (serializes only 1 field):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser()
+ return
+}
+
+'use client'
+function Profile({ name }: { name: string }) {
+ return