opencode-skills-collection 1.0.186 → 1.0.187

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +5 -1
  2. package/bundled-skills/3d-web-experience/SKILL.md +152 -37
  3. package/bundled-skills/agent-evaluation/SKILL.md +1088 -26
  4. package/bundled-skills/agent-memory-systems/SKILL.md +1037 -25
  5. package/bundled-skills/agent-tool-builder/SKILL.md +668 -16
  6. package/bundled-skills/ai-agents-architect/SKILL.md +271 -31
  7. package/bundled-skills/ai-product/SKILL.md +716 -26
  8. package/bundled-skills/ai-wrapper-product/SKILL.md +450 -44
  9. package/bundled-skills/algolia-search/SKILL.md +867 -15
  10. package/bundled-skills/autonomous-agents/SKILL.md +1033 -26
  11. package/bundled-skills/aws-serverless/SKILL.md +1046 -35
  12. package/bundled-skills/azure-functions/SKILL.md +1318 -19
  13. package/bundled-skills/browser-automation/SKILL.md +1065 -28
  14. package/bundled-skills/browser-extension-builder/SKILL.md +159 -32
  15. package/bundled-skills/bullmq-specialist/SKILL.md +347 -16
  16. package/bundled-skills/clerk-auth/SKILL.md +796 -15
  17. package/bundled-skills/computer-use-agents/SKILL.md +1870 -28
  18. package/bundled-skills/context-window-management/SKILL.md +271 -18
  19. package/bundled-skills/conversation-memory/SKILL.md +453 -24
  20. package/bundled-skills/crewai/SKILL.md +252 -46
  21. package/bundled-skills/discord-bot-architect/SKILL.md +1207 -34
  22. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  23. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  24. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  25. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  26. package/bundled-skills/docs/users/bundles.md +1 -1
  27. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  28. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  29. package/bundled-skills/docs/users/getting-started.md +1 -1
  30. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  31. package/bundled-skills/docs/users/usage.md +4 -4
  32. package/bundled-skills/docs/users/visual-guide.md +4 -4
  33. package/bundled-skills/email-systems/SKILL.md +646 -26
  34. package/bundled-skills/faf-expert/SKILL.md +221 -0
  35. package/bundled-skills/faf-wizard/SKILL.md +252 -0
  36. package/bundled-skills/file-uploads/SKILL.md +212 -11
  37. package/bundled-skills/firebase/SKILL.md +646 -16
  38. package/bundled-skills/gcp-cloud-run/SKILL.md +1117 -32
  39. package/bundled-skills/graphql/SKILL.md +1026 -27
  40. package/bundled-skills/hubspot-integration/SKILL.md +804 -19
  41. package/bundled-skills/idea-darwin/SKILL.md +120 -0
  42. package/bundled-skills/inngest/SKILL.md +431 -16
  43. package/bundled-skills/interactive-portfolio/SKILL.md +342 -44
  44. package/bundled-skills/langfuse/SKILL.md +296 -41
  45. package/bundled-skills/langgraph/SKILL.md +259 -50
  46. package/bundled-skills/micro-saas-launcher/SKILL.md +343 -44
  47. package/bundled-skills/neon-postgres/SKILL.md +572 -15
  48. package/bundled-skills/nextjs-supabase-auth/SKILL.md +269 -21
  49. package/bundled-skills/notion-template-business/SKILL.md +371 -44
  50. package/bundled-skills/personal-tool-builder/SKILL.md +537 -44
  51. package/bundled-skills/plaid-fintech/SKILL.md +825 -19
  52. package/bundled-skills/prompt-caching/SKILL.md +438 -25
  53. package/bundled-skills/rag-engineer/SKILL.md +271 -29
  54. package/bundled-skills/salesforce-development/SKILL.md +912 -19
  55. package/bundled-skills/satori/SKILL.md +54 -0
  56. package/bundled-skills/scroll-experience/SKILL.md +381 -44
  57. package/bundled-skills/segment-cdp/SKILL.md +817 -19
  58. package/bundled-skills/shopify-apps/SKILL.md +1475 -19
  59. package/bundled-skills/slack-bot-builder/SKILL.md +1162 -28
  60. package/bundled-skills/telegram-bot-builder/SKILL.md +152 -37
  61. package/bundled-skills/telegram-mini-app/SKILL.md +445 -44
  62. package/bundled-skills/trigger-dev/SKILL.md +916 -27
  63. package/bundled-skills/twilio-communications/SKILL.md +1310 -28
  64. package/bundled-skills/upstash-qstash/SKILL.md +898 -27
  65. package/bundled-skills/vercel-deployment/SKILL.md +637 -39
  66. package/bundled-skills/viral-generator-builder/SKILL.md +132 -37
  67. package/bundled-skills/voice-agents/SKILL.md +937 -27
  68. package/bundled-skills/voice-ai-development/SKILL.md +375 -46
  69. package/bundled-skills/workflow-automation/SKILL.md +982 -29
  70. package/bundled-skills/zapier-make-patterns/SKILL.md +772 -27
  71. package/package.json +1 -1
@@ -1,22 +1,39 @@
1
1
  ---
2
2
  name: graphql
3
- description: "You're a developer who has built GraphQL APIs at scale. You've seen the N+1 query problem bring down production servers. You've watched clients craft deeply nested queries that took minutes to resolve. You know that GraphQL's power is also its danger."
3
+ description: GraphQL gives clients exactly the data they need - no more, no
4
+ less. One endpoint, typed schema, introspection. But the flexibility that
5
+ makes it powerful also makes it dangerous. Without proper controls, clients
6
+ can craft queries that bring down your server.
4
7
  risk: safe
5
- source: "vibeship-spawner-skills (Apache 2.0)"
6
- date_added: "2026-02-27"
8
+ source: vibeship-spawner-skills (Apache 2.0)
9
+ date_added: 2026-02-27
7
10
  ---
8
11
 
9
12
  # GraphQL
10
13
 
11
- You're a developer who has built GraphQL APIs at scale. You've seen the
12
- N+1 query problem bring down production servers. You've watched clients
13
- craft deeply nested queries that took minutes to resolve. You know that
14
- GraphQL's power is also its danger.
14
+ GraphQL gives clients exactly the data they need - no more, no less. One
15
+ endpoint, typed schema, introspection. But the flexibility that makes it
16
+ powerful also makes it dangerous. Without proper controls, clients can
17
+ craft queries that bring down your server.
15
18
 
16
- Your hard-won lessons: The team that didn't use DataLoader had unusable
17
- APIs. The team that allowed unlimited query depth got DDoS'd by their
18
- own clients. The team that made everything nullable couldn't distinguish
19
- errors from empty data. You've l
19
+ This skill covers schema design, resolvers, DataLoader for N+1 prevention,
20
+ federation for microservices, and client integration with Apollo/urql.
21
+ Key insight: GraphQL is a contract. The schema is the API documentation.
22
+ Design it carefully.
23
+
24
+ 2025 lesson: GraphQL isn't always the answer. For simple CRUD, REST is
25
+ simpler. For high-performance public APIs, REST with caching wins. Use
26
+ GraphQL when you have complex data relationships and diverse client needs.
27
+
28
+ ## Principles
29
+
30
+ - Schema-first design - the schema is the contract
31
+ - Prevent N+1 queries with DataLoader
32
+ - Limit query depth and complexity
33
+ - Use fragments for reusable selections
34
+ - Mutations should be specific, not generic update operations
35
+ - Errors are data - use union types for expected failures
36
+ - Nullability is meaningful - design it intentionally
20
37
 
21
38
  ## Capabilities
22
39
 
@@ -30,44 +47,1026 @@ errors from empty data. You've l
30
47
  - apollo-client
31
48
  - urql
32
49
 
50
+ ## Scope
51
+
52
+ - database-queries -> postgres-wizard
53
+ - authentication -> authentication-oauth
54
+ - rest-api-design -> backend
55
+ - websocket-infrastructure -> backend
56
+
57
+ ## Tooling
58
+
59
+ ### Server
60
+
61
+ - @apollo/server - When: Apollo Server v4 Note: Most popular GraphQL server
62
+ - graphql-yoga - When: Lightweight alternative Note: Good for serverless
63
+ - mercurius - When: Fastify integration Note: Fast, uses JIT
64
+
65
+ ### Client
66
+
67
+ - @apollo/client - When: Full-featured client Note: Caching, state management
68
+ - urql - When: Lightweight alternative Note: Smaller, simpler
69
+ - graphql-request - When: Simple requests Note: Minimal, no caching
70
+
71
+ ### Tools
72
+
73
+ - graphql-codegen - When: Type generation Note: Essential for TypeScript
74
+ - dataloader - When: N+1 prevention Note: Batches and caches
75
+
33
76
  ## Patterns
34
77
 
35
78
  ### Schema Design
36
79
 
37
80
  Type-safe schema with proper nullability
38
81
 
82
+ **When to use**: Designing any GraphQL API
83
+
84
+ # SCHEMA DESIGN:
85
+
86
+ """
87
+ The schema is your API contract. Design nullability
88
+ intentionally - non-null fields must always resolve.
89
+ """
90
+
91
+ type Query {
92
+ # Non-null - will always return user or throw
93
+ user(id: ID!): User!
94
+
95
+ # Nullable - returns null if not found
96
+ userByEmail(email: String!): User
97
+
98
+ # Non-null list with non-null items
99
+ users(limit: Int = 10, offset: Int = 0): [User!]!
100
+
101
+ # Search with pagination
102
+ searchUsers(
103
+ query: String!
104
+ first: Int
105
+ after: String
106
+ ): UserConnection!
107
+ }
108
+
109
+ type Mutation {
110
+ # Input types for complex mutations
111
+ createUser(input: CreateUserInput!): CreateUserPayload!
112
+ updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
113
+ deleteUser(id: ID!): DeleteUserPayload!
114
+ }
115
+
116
+ type Subscription {
117
+ userCreated: User!
118
+ messageReceived(roomId: ID!): Message!
119
+ }
120
+
121
+ # Input types
122
+ input CreateUserInput {
123
+ email: String!
124
+ name: String!
125
+ role: Role = USER
126
+ }
127
+
128
+ input UpdateUserInput {
129
+ email: String
130
+ name: String
131
+ role: Role
132
+ }
133
+
134
+ # Payload types (for errors as data)
135
+ type CreateUserPayload {
136
+ user: User
137
+ errors: [Error!]!
138
+ }
139
+
140
+ union UpdateUserPayload = UpdateUserSuccess | NotFoundError | ValidationError
141
+
142
+ type UpdateUserSuccess {
143
+ user: User!
144
+ }
145
+
146
+ # Enums
147
+ enum Role {
148
+ USER
149
+ ADMIN
150
+ MODERATOR
151
+ }
152
+
153
+ # Types with relationships
154
+ type User {
155
+ id: ID!
156
+ email: String!
157
+ name: String!
158
+ role: Role!
159
+ posts(limit: Int = 10): [Post!]!
160
+ createdAt: DateTime!
161
+ }
162
+
163
+ type Post {
164
+ id: ID!
165
+ title: String!
166
+ content: String!
167
+ author: User!
168
+ comments: [Comment!]!
169
+ published: Boolean!
170
+ }
171
+
172
+ # Pagination (Relay-style)
173
+ type UserConnection {
174
+ edges: [UserEdge!]!
175
+ pageInfo: PageInfo!
176
+ totalCount: Int!
177
+ }
178
+
179
+ type UserEdge {
180
+ node: User!
181
+ cursor: String!
182
+ }
183
+
184
+ type PageInfo {
185
+ hasNextPage: Boolean!
186
+ hasPreviousPage: Boolean!
187
+ startCursor: String
188
+ endCursor: String
189
+ }
190
+
39
191
  ### DataLoader for N+1 Prevention
40
192
 
41
193
  Batch and cache database queries
42
194
 
195
+ **When to use**: Resolving relationships
196
+
197
+ # DATALOADER:
198
+
199
+ """
200
+ Without DataLoader, fetching 10 posts with authors
201
+ makes 11 queries (1 for posts + 10 for each author).
202
+ DataLoader batches into 2 queries.
203
+ """
204
+
205
+ import DataLoader from 'dataloader';
206
+
207
+ // Create loaders per request
208
+ function createLoaders(db) {
209
+ return {
210
+ userLoader: new DataLoader(async (ids) => {
211
+ // Single query for all users
212
+ const users = await db.user.findMany({
213
+ where: { id: { in: ids } }
214
+ });
215
+
216
+ // Return in same order as ids
217
+ const userMap = new Map(users.map(u => [u.id, u]));
218
+ return ids.map(id => userMap.get(id) || null);
219
+ }),
220
+
221
+ postsByAuthorLoader: new DataLoader(async (authorIds) => {
222
+ const posts = await db.post.findMany({
223
+ where: { authorId: { in: authorIds } }
224
+ });
225
+
226
+ // Group by author
227
+ const postsByAuthor = new Map();
228
+ posts.forEach(post => {
229
+ const existing = postsByAuthor.get(post.authorId) || [];
230
+ postsByAuthor.set(post.authorId, [...existing, post]);
231
+ });
232
+
233
+ return authorIds.map(id => postsByAuthor.get(id) || []);
234
+ })
235
+ };
236
+ }
237
+
238
+ // Attach to context
239
+ const server = new ApolloServer({
240
+ typeDefs,
241
+ resolvers,
242
+ });
243
+
244
+ app.use('/graphql', expressMiddleware(server, {
245
+ context: async ({ req }) => ({
246
+ db,
247
+ loaders: createLoaders(db),
248
+ user: req.user
249
+ })
250
+ }));
251
+
252
+ // Use in resolvers
253
+ const resolvers = {
254
+ Post: {
255
+ author: (post, _, { loaders }) => {
256
+ return loaders.userLoader.load(post.authorId);
257
+ }
258
+ },
259
+ User: {
260
+ posts: (user, _, { loaders }) => {
261
+ return loaders.postsByAuthorLoader.load(user.id);
262
+ }
263
+ }
264
+ };
265
+
43
266
  ### Apollo Client Caching
44
267
 
45
268
  Normalized cache with type policies
46
269
 
47
- ## Anti-Patterns
270
+ **When to use**: Client-side data management
271
+
272
+ # APOLLO CLIENT CACHING:
273
+
274
+ """
275
+ Apollo Client normalizes responses into a flat cache.
276
+ Configure type policies for custom cache behavior.
277
+ """
278
+
279
+ import { ApolloClient, InMemoryCache } from '@apollo/client';
280
+
281
+ const cache = new InMemoryCache({
282
+ typePolicies: {
283
+ Query: {
284
+ fields: {
285
+ // Paginated field
286
+ users: {
287
+ keyArgs: ['query'], // Cache separately per query
288
+ merge(existing = { edges: [] }, incoming, { args }) {
289
+ // Append for infinite scroll
290
+ if (args?.after) {
291
+ return {
292
+ ...incoming,
293
+ edges: [...existing.edges, ...incoming.edges]
294
+ };
295
+ }
296
+ return incoming;
297
+ }
298
+ }
299
+ }
300
+ },
301
+ User: {
302
+ keyFields: ['id'], // How to identify users
303
+ fields: {
304
+ fullName: {
305
+ read(_, { readField }) {
306
+ // Computed field
307
+ return `${readField('firstName')} ${readField('lastName')}`;
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ });
314
+
315
+ const client = new ApolloClient({
316
+ uri: '/graphql',
317
+ cache,
318
+ defaultOptions: {
319
+ watchQuery: {
320
+ fetchPolicy: 'cache-and-network'
321
+ }
322
+ }
323
+ });
324
+
325
+ // Queries with hooks
326
+ import { useQuery, useMutation } from '@apollo/client';
327
+
328
+ const GET_USER = gql`
329
+ query GetUser($id: ID!) {
330
+ user(id: $id) {
331
+ id
332
+ name
333
+ email
334
+ }
335
+ }
336
+ `;
337
+
338
+ function UserProfile({ userId }) {
339
+ const { data, loading, error } = useQuery(GET_USER, {
340
+ variables: { id: userId }
341
+ });
342
+
343
+ if (loading) return <Spinner />;
344
+ if (error) return <Error message={error.message} />;
345
+
346
+ return <div>{data.user.name}</div>;
347
+ }
348
+
349
+ // Mutations with cache updates
350
+ const CREATE_USER = gql`
351
+ mutation CreateUser($input: CreateUserInput!) {
352
+ createUser(input: $input) {
353
+ user {
354
+ id
355
+ name
356
+ email
357
+ }
358
+ errors {
359
+ field
360
+ message
361
+ }
362
+ }
363
+ }
364
+ `;
365
+
366
+ function CreateUserForm() {
367
+ const [createUser, { loading }] = useMutation(CREATE_USER, {
368
+ update(cache, { data: { createUser } }) {
369
+ // Update cache after mutation
370
+ if (createUser.user) {
371
+ cache.modify({
372
+ fields: {
373
+ users(existing = []) {
374
+ const newRef = cache.writeFragment({
375
+ data: createUser.user,
376
+ fragment: gql`
377
+ fragment NewUser on User {
378
+ id
379
+ name
380
+ email
381
+ }
382
+ `
383
+ });
384
+ return [...existing, newRef];
385
+ }
386
+ }
387
+ });
388
+ }
389
+ }
390
+ });
391
+ }
392
+
393
+ ### Code Generation
394
+
395
+ Type-safe operations from schema
396
+
397
+ **When to use**: TypeScript projects
398
+
399
+ # GRAPHQL CODEGEN:
400
+
401
+ """
402
+ Generate TypeScript types from your schema and operations.
403
+ No more manually typing query responses.
404
+ """
405
+
406
+ # Install
407
+ npm install -D @graphql-codegen/cli
408
+ npm install -D @graphql-codegen/typescript
409
+ npm install -D @graphql-codegen/typescript-operations
410
+ npm install -D @graphql-codegen/typescript-react-apollo
411
+
412
+ # codegen.ts
413
+ import type { CodegenConfig } from '@graphql-codegen/cli';
414
+
415
+ const config: CodegenConfig = {
416
+ schema: 'http://localhost:4000/graphql',
417
+ documents: ['src/**/*.graphql', 'src/**/*.tsx'],
418
+ generates: {
419
+ './src/generated/graphql.ts': {
420
+ plugins: [
421
+ 'typescript',
422
+ 'typescript-operations',
423
+ 'typescript-react-apollo'
424
+ ],
425
+ config: {
426
+ withHooks: true,
427
+ withComponent: false
428
+ }
429
+ }
430
+ }
431
+ };
432
+
433
+ export default config;
434
+
435
+ # Run generation
436
+ npx graphql-codegen
437
+
438
+ # Usage - fully typed!
439
+ import { useGetUserQuery, useCreateUserMutation } from './generated/graphql';
440
+
441
+ function UserProfile({ userId }: { userId: string }) {
442
+ const { data, loading } = useGetUserQuery({
443
+ variables: { id: userId } // Type-checked!
444
+ });
445
+
446
+ // data.user is fully typed
447
+ return <div>{data?.user?.name}</div>;
448
+ }
449
+
450
+ ### Error Handling with Unions
451
+
452
+ Expected errors as data, not exceptions
453
+
454
+ **When to use**: Operations that can fail in expected ways
455
+
456
+ # ERRORS AS DATA:
457
+
458
+ """
459
+ Use union types for expected failure cases.
460
+ GraphQL errors are for unexpected failures.
461
+ """
462
+
463
+ # Schema
464
+ type Mutation {
465
+ login(email: String!, password: String!): LoginResult!
466
+ }
467
+
468
+ union LoginResult = LoginSuccess | InvalidCredentials | AccountLocked
469
+
470
+ type LoginSuccess {
471
+ user: User!
472
+ token: String!
473
+ }
474
+
475
+ type InvalidCredentials {
476
+ message: String!
477
+ }
478
+
479
+ type AccountLocked {
480
+ message: String!
481
+ unlockAt: DateTime
482
+ }
483
+
484
+ # Resolver
485
+ const resolvers = {
486
+ Mutation: {
487
+ login: async (_, { email, password }, { db }) => {
488
+ const user = await db.user.findByEmail(email);
489
+
490
+ if (!user || !await verifyPassword(password, user.hash)) {
491
+ return {
492
+ __typename: 'InvalidCredentials',
493
+ message: 'Invalid email or password'
494
+ };
495
+ }
496
+
497
+ if (user.lockedUntil && user.lockedUntil > new Date()) {
498
+ return {
499
+ __typename: 'AccountLocked',
500
+ message: 'Account temporarily locked',
501
+ unlockAt: user.lockedUntil
502
+ };
503
+ }
504
+
505
+ return {
506
+ __typename: 'LoginSuccess',
507
+ user,
508
+ token: generateToken(user)
509
+ };
510
+ }
511
+ },
512
+
513
+ LoginResult: {
514
+ __resolveType(obj) {
515
+ return obj.__typename;
516
+ }
517
+ }
518
+ };
519
+
520
+ # Client query
521
+ const LOGIN = gql`
522
+ mutation Login($email: String!, $password: String!) {
523
+ login(email: $email, password: $password) {
524
+ ... on LoginSuccess {
525
+ user { id name }
526
+ token
527
+ }
528
+ ... on InvalidCredentials {
529
+ message
530
+ }
531
+ ... on AccountLocked {
532
+ message
533
+ unlockAt
534
+ }
535
+ }
536
+ }
537
+ `;
538
+
539
+ // Handle all cases
540
+ const result = data.login;
541
+ switch (result.__typename) {
542
+ case 'LoginSuccess':
543
+ setToken(result.token);
544
+ redirect('/dashboard');
545
+ break;
546
+ case 'InvalidCredentials':
547
+ setError(result.message);
548
+ break;
549
+ case 'AccountLocked':
550
+ setError(`${result.message}. Try again at ${result.unlockAt}`);
551
+ break;
552
+ }
553
+
554
+ ## Sharp Edges
555
+
556
+ ### Each resolver makes separate database queries
557
+
558
+ Severity: CRITICAL
559
+
560
+ Situation: You write resolvers that fetch data individually. A query for
561
+ 10 posts with authors makes 11 database queries. For 100 posts,
562
+ that's 101 queries. Response time becomes seconds.
563
+
564
+ Symptoms:
565
+ - Slow API responses
566
+ - Many similar database queries in logs
567
+ - Performance degrades with list size
568
+
569
+ Why this breaks:
570
+ GraphQL resolvers run independently. Without batching, the author
571
+ resolver runs separately for each post. The database gets hammered
572
+ with repeated similar queries.
573
+
574
+ Recommended fix:
575
+
576
+ # USE DATALOADER
577
+
578
+ import DataLoader from 'dataloader';
579
+
580
+ // Create loader per request
581
+ const userLoader = new DataLoader(async (ids) => {
582
+ const users = await db.user.findMany({
583
+ where: { id: { in: ids } }
584
+ });
585
+ // IMPORTANT: Return in same order as input ids
586
+ const userMap = new Map(users.map(u => [u.id, u]));
587
+ return ids.map(id => userMap.get(id));
588
+ });
589
+
590
+ // Use in resolver
591
+ const resolvers = {
592
+ Post: {
593
+ author: (post, _, { loaders }) =>
594
+ loaders.userLoader.load(post.authorId)
595
+ }
596
+ };
597
+
598
+ # Key points:
599
+ # 1. Create new loaders per request (for caching scope)
600
+ # 2. Return results in same order as input IDs
601
+ # 3. Handle missing items (return null, not skip)
602
+
603
+ ### Deeply nested queries can DoS your server
604
+
605
+ Severity: CRITICAL
606
+
607
+ Situation: Your schema has circular relationships (user.posts.author.posts...).
608
+ A client sends a query 20 levels deep. Your server tries to resolve
609
+ it and either times out or crashes.
610
+
611
+ Symptoms:
612
+ - Server timeouts on certain queries
613
+ - Memory exhaustion
614
+ - Slow response for nested queries
615
+
616
+ Why this breaks:
617
+ GraphQL allows clients to request any valid query shape. Without
618
+ limits, a malicious or buggy client can craft queries that require
619
+ exponential work. Even legitimate queries can accidentally be too deep.
620
+
621
+ Recommended fix:
622
+
623
+ # LIMIT QUERY DEPTH AND COMPLEXITY
624
+
625
+ import depthLimit from 'graphql-depth-limit';
626
+ import { createComplexityLimitRule } from 'graphql-validation-complexity';
627
+
628
+ const server = new ApolloServer({
629
+ typeDefs,
630
+ resolvers,
631
+ validationRules: [
632
+ // Limit nesting depth
633
+ depthLimit(10),
48
634
 
49
- ### No DataLoader
635
+ // Limit query complexity
636
+ createComplexityLimitRule(1000, {
637
+ scalarCost: 1,
638
+ objectCost: 2,
639
+ listFactor: 10
640
+ })
641
+ ]
642
+ });
50
643
 
51
- ### No Query Depth Limiting
644
+ # Also consider:
645
+ # - Query timeout limits
646
+ # - Rate limiting per client
647
+ # - Persisted queries (only allow pre-registered queries)
52
648
 
53
- ### Authorization in Schema
649
+ ### Introspection enabled in production exposes your schema
54
650
 
55
- ## ⚠️ Sharp Edges
651
+ Severity: HIGH
56
652
 
57
- | Issue | Severity | Solution |
58
- |-------|----------|----------|
59
- | Each resolver makes separate database queries | critical | # USE DATALOADER |
60
- | Deeply nested queries can DoS your server | critical | # LIMIT QUERY DEPTH AND COMPLEXITY |
61
- | Introspection enabled in production exposes your schema | high | # DISABLE INTROSPECTION IN PRODUCTION |
62
- | Authorization only in schema directives, not resolvers | high | # AUTHORIZE IN RESOLVERS |
63
- | Authorization on queries but not on fields | high | # FIELD-LEVEL AUTHORIZATION |
64
- | Non-null field failure nullifies entire parent | medium | # DESIGN NULLABILITY INTENTIONALLY |
65
- | Expensive queries treated same as cheap ones | medium | # QUERY COST ANALYSIS |
66
- | Subscriptions not properly cleaned up | medium | # PROPER SUBSCRIPTION CLEANUP |
653
+ Situation: You deploy to production with introspection enabled. Anyone can
654
+ query your schema, discover all types, mutations, and field names.
655
+ Attackers know exactly what to target.
656
+
657
+ Symptoms:
658
+ - Schema visible via introspection query
659
+ - GraphQL Playground accessible in production
660
+ - Full type information exposed
661
+
662
+ Why this breaks:
663
+ Introspection is essential for development and tooling, but in
664
+ production it's a roadmap for attackers. They can find admin
665
+ mutations, internal fields, and deprecated but still working APIs.
666
+
667
+ Recommended fix:
668
+
669
+ # DISABLE INTROSPECTION IN PRODUCTION
670
+
671
+ const server = new ApolloServer({
672
+ typeDefs,
673
+ resolvers,
674
+ introspection: process.env.NODE_ENV !== 'production',
675
+ plugins: [
676
+ process.env.NODE_ENV === 'production'
677
+ ? ApolloServerPluginLandingPageDisabled()
678
+ : ApolloServerPluginLandingPageLocalDefault()
679
+ ]
680
+ });
681
+
682
+ # Better: Use persisted queries
683
+ # Only allow pre-registered queries in production
684
+ const server = new ApolloServer({
685
+ typeDefs,
686
+ resolvers,
687
+ persistedQueries: {
688
+ cache: new InMemoryLRUCache()
689
+ }
690
+ });
691
+
692
+ ### Authorization only in schema directives, not resolvers
693
+
694
+ Severity: HIGH
695
+
696
+ Situation: You rely entirely on @auth directives for authorization. Someone
697
+ finds a way around the directive, or complex business rules don't
698
+ fit in a simple directive. Authorization fails.
699
+
700
+ Symptoms:
701
+ - Unauthorized access to data
702
+ - Business rules not enforced
703
+ - Directive-only security bypassed
704
+
705
+ Why this breaks:
706
+ Directives are good for simple checks but can't handle complex
707
+ business logic. "User can edit their own posts, or any post in
708
+ groups they moderate" doesn't fit in a directive.
709
+
710
+ Recommended fix:
711
+
712
+ # AUTHORIZE IN RESOLVERS
713
+
714
+ // Simple check in resolver
715
+ Mutation: {
716
+ deletePost: async (_, { id }, { user, db }) => {
717
+ if (!user) {
718
+ throw new AuthenticationError('Must be logged in');
719
+ }
720
+
721
+ const post = await db.post.findUnique({ where: { id } });
722
+
723
+ if (!post) {
724
+ throw new NotFoundError('Post not found');
725
+ }
726
+
727
+ // Business logic authorization
728
+ const canDelete =
729
+ post.authorId === user.id ||
730
+ user.role === 'ADMIN' ||
731
+ await userModeratesGroup(user.id, post.groupId);
732
+
733
+ if (!canDelete) {
734
+ throw new ForbiddenError('Cannot delete this post');
735
+ }
736
+
737
+ return db.post.delete({ where: { id } });
738
+ }
739
+ }
740
+
741
+ // Helper for field-level authorization
742
+ User: {
743
+ email: (user, _, { currentUser }) => {
744
+ // Only show email to self or admin
745
+ if (currentUser?.id === user.id || currentUser?.role === 'ADMIN') {
746
+ return user.email;
747
+ }
748
+ return null;
749
+ }
750
+ }
751
+
752
+ ### Authorization on queries but not on fields
753
+
754
+ Severity: HIGH
755
+
756
+ Situation: You check if a user can access a resource, but not individual
757
+ fields. User A can see User B's public profile, and accidentally
758
+ also sees their private email and phone number.
759
+
760
+ Symptoms:
761
+ - Sensitive data exposed
762
+ - Privacy violations
763
+ - Field data visible to wrong users
764
+
765
+ Why this breaks:
766
+ Field resolvers run after the parent is returned. If the parent
767
+ query returns a user, all fields are resolved - including sensitive
768
+ ones. Each sensitive field needs its own auth check.
769
+
770
+ Recommended fix:
771
+
772
+ # FIELD-LEVEL AUTHORIZATION
773
+
774
+ const resolvers = {
775
+ User: {
776
+ // Public fields - no check needed
777
+ id: (user) => user.id,
778
+ name: (user) => user.name,
779
+
780
+ // Private fields - check access
781
+ email: (user, _, { currentUser }) => {
782
+ if (!currentUser) return null;
783
+ if (currentUser.id === user.id) return user.email;
784
+ if (currentUser.role === 'ADMIN') return user.email;
785
+ return null;
786
+ },
787
+
788
+ phoneNumber: (user, _, { currentUser }) => {
789
+ if (currentUser?.id !== user.id) return null;
790
+ return user.phoneNumber;
791
+ },
792
+
793
+ // Or throw instead of returning null
794
+ privateData: (user, _, { currentUser }) => {
795
+ if (currentUser?.id !== user.id) {
796
+ throw new ForbiddenError('Not authorized');
797
+ }
798
+ return user.privateData;
799
+ }
800
+ }
801
+ };
802
+
803
+ ### Non-null field failure nullifies entire parent
804
+
805
+ Severity: MEDIUM
806
+
807
+ Situation: You make fields non-null for convenience. A resolver throws or
808
+ returns null. The error propagates up, nullifying parent objects,
809
+ until the whole query response is null or errors out.
810
+
811
+ Symptoms:
812
+ - Queries return null unexpectedly
813
+ - One error affects unrelated fields
814
+ - Partial data can't be returned
815
+
816
+ Why this breaks:
817
+ GraphQL's null propagation means if a non-null field can't resolve,
818
+ its parent becomes null. If that parent is also non-null, it
819
+ propagates further. One failing field can break an entire response.
820
+
821
+ Recommended fix:
822
+
823
+ # DESIGN NULLABILITY INTENTIONALLY
824
+
825
+ # WRONG: Everything non-null
826
+ type User {
827
+ id: ID!
828
+ name: String!
829
+ email: String!
830
+ avatar: String! # What if no avatar?
831
+ lastLogin: DateTime! # What if never logged in?
832
+ }
833
+
834
+ # RIGHT: Nullable where appropriate
835
+ type User {
836
+ id: ID! # Always exists
837
+ name: String! # Required field
838
+ email: String! # Required field
839
+ avatar: String # Optional - may not exist
840
+ lastLogin: DateTime # Nullable - may be null
841
+ }
842
+
843
+ # For lists:
844
+ # [User!]! - Non-null list of non-null users (recommended)
845
+ # [User!] - Nullable list of non-null users
846
+ # [User]! - Non-null list of nullable users (rarely useful)
847
+ # [User] - Nullable list of nullable users (avoid)
848
+
849
+ # Rule of thumb:
850
+ # - Non-null if always present and failure should fail query
851
+ # - Nullable if optional or failure shouldn't break response
852
+
853
+ ### Expensive queries treated same as cheap ones
854
+
855
+ Severity: MEDIUM
856
+
857
+ Situation: Every query is processed the same. A simple user(id) query uses
858
+ the same resources as users(first: 1000) { posts { comments } }.
859
+ Expensive queries starve out cheap ones.
860
+
861
+ Symptoms:
862
+ - Expensive queries slow everything
863
+ - No way to prioritize queries
864
+ - Rate limiting is ineffective
865
+
866
+ Why this breaks:
867
+ Not all GraphQL operations are equal. Fetching 1000 users with
868
+ nested data is orders of magnitude more expensive than fetching
869
+ one user. Without cost analysis, you can't rate limit properly.
870
+
871
+ Recommended fix:
872
+
873
+ # QUERY COST ANALYSIS
874
+
875
+ import { createComplexityLimitRule } from 'graphql-validation-complexity';
876
+
877
+ // Define complexity per field
878
+ const complexityRules = createComplexityLimitRule(1000, {
879
+ scalarCost: 1,
880
+ objectCost: 10,
881
+ listFactor: 10,
882
+ // Custom field costs
883
+ fieldCost: {
884
+ 'Query.searchUsers': 100,
885
+ 'Query.analytics': 500,
886
+ 'User.posts': ({ args }) => args.limit || 10
887
+ }
888
+ });
889
+
890
+ // For rate limiting by cost
891
+ const costPlugin = {
892
+ requestDidStart() {
893
+ return {
894
+ didResolveOperation({ request, document }) {
895
+ const cost = calculateQueryCost(document);
896
+ if (cost > 1000) {
897
+ throw new Error(`Query too expensive: ${cost}`);
898
+ }
899
+ // Track cost for rate limiting
900
+ rateLimiter.consume(request.userId, cost);
901
+ }
902
+ };
903
+ }
904
+ };
905
+
906
+ ### Subscriptions not properly cleaned up
907
+
908
+ Severity: MEDIUM
909
+
910
+ Situation: Clients subscribe but don't unsubscribe cleanly. Network issues
911
+ leave orphaned subscriptions. Server memory grows as dead
912
+ subscriptions accumulate.
913
+
914
+ Symptoms:
915
+ - Memory usage grows over time
916
+ - Dead connections accumulate
917
+ - Server slows down
918
+
919
+ Why this breaks:
920
+ Each subscription holds server resources. Without proper cleanup
921
+ on disconnect, resources accumulate. Long-running servers
922
+ eventually run out of memory.
923
+
924
+ Recommended fix:
925
+
926
+ # PROPER SUBSCRIPTION CLEANUP
927
+
928
+ import { PubSub, withFilter } from 'graphql-subscriptions';
929
+ import { WebSocketServer } from 'ws';
930
+ import { useServer } from 'graphql-ws/lib/use/ws';
931
+
932
+ const pubsub = new PubSub();
933
+
934
+ // Track active subscriptions
935
+ const activeSubscriptions = new Map();
936
+
937
+ const wsServer = new WebSocketServer({
938
+ server: httpServer,
939
+ path: '/graphql'
940
+ });
941
+
942
+ useServer({
943
+ schema,
944
+ context: (ctx) => ({
945
+ pubsub,
946
+ userId: ctx.connectionParams?.userId
947
+ }),
948
+ onConnect: (ctx) => {
949
+ console.log('Client connected');
950
+ },
951
+ onDisconnect: (ctx) => {
952
+ // Clean up resources for this connection
953
+ const userId = ctx.connectionParams?.userId;
954
+ activeSubscriptions.delete(userId);
955
+ }
956
+ }, wsServer);
957
+
958
+ // Subscription resolver with cleanup
959
+ Subscription: {
960
+ messageReceived: {
961
+ subscribe: withFilter(
962
+ (_, { roomId }, { pubsub, userId }) => {
963
+ // Track subscription
964
+ activeSubscriptions.set(userId, roomId);
965
+ return pubsub.asyncIterator(`ROOM_${roomId}`);
966
+ },
967
+ (payload, { roomId }) => {
968
+ return payload.roomId === roomId;
969
+ }
970
+ )
971
+ }
972
+ }
973
+
974
+ ## Validation Checks
975
+
976
+ ### Introspection enabled in production
977
+
978
+ Severity: WARNING
979
+
980
+ Message: Introspection should be disabled in production
981
+
982
+ Fix action: Set introspection: process.env.NODE_ENV !== 'production'
983
+
984
+ ### Direct database query in resolver
985
+
986
+ Severity: WARNING
987
+
988
+ Message: Consider using DataLoader to batch and cache queries
989
+
990
+ Fix action: Create DataLoader and use .load() instead of direct query
991
+
992
+ ### No query depth limiting
993
+
994
+ Severity: WARNING
995
+
996
+ Message: Consider adding depth limiting to prevent DoS
997
+
998
+ Fix action: Add validationRules: [depthLimit(10)]
999
+
1000
+ ### Resolver without try-catch
1001
+
1002
+ Severity: INFO
1003
+
1004
+ Message: Consider wrapping resolver logic in try-catch
1005
+
1006
+ Fix action: Add error handling to provide better error messages
1007
+
1008
+ ### JSON or Any type in schema
1009
+
1010
+ Severity: INFO
1011
+
1012
+ Message: Avoid JSON/Any types - they bypass GraphQL's type safety
1013
+
1014
+ Fix action: Define proper input/output types
1015
+
1016
+ ### Mutation returns bare type instead of payload
1017
+
1018
+ Severity: INFO
1019
+
1020
+ Message: Consider using payload types for mutations (includes errors)
1021
+
1022
+ Fix action: Create CreateUserPayload type with user and errors fields
1023
+
1024
+ ### List field without pagination arguments
1025
+
1026
+ Severity: INFO
1027
+
1028
+ Message: List fields should have pagination (limit, first, after)
1029
+
1030
+ Fix action: Add arguments: field(limit: Int, offset: Int): [Type!]!
1031
+
1032
+ ### Query hook without error handling
1033
+
1034
+ Severity: INFO
1035
+
1036
+ Message: Handle query errors in UI
1037
+
1038
+ Fix action: Destructure and handle error: const { error } = useQuery(...)
1039
+
1040
+ ### Using refetch instead of cache update
1041
+
1042
+ Severity: INFO
1043
+
1044
+ Message: Consider cache update instead of refetch for better UX
1045
+
1046
+ Fix action: Use update function to modify cache directly
1047
+
1048
+ ## Collaboration
1049
+
1050
+ ### Delegation Triggers
1051
+
1052
+ - user needs database optimization -> postgres-wizard (Optimize queries for GraphQL resolvers)
1053
+ - user needs authentication system -> authentication-oauth (Auth for GraphQL context)
1054
+ - user needs caching layer -> caching-strategies (Response caching, DataLoader caching)
1055
+ - user needs real-time infrastructure -> backend (WebSocket setup for subscriptions)
67
1056
 
68
1057
  ## Related Skills
69
1058
 
70
1059
  Works well with: `backend`, `postgres-wizard`, `nextjs-app-router`, `react-patterns`
71
1060
 
72
1061
  ## When to Use
73
- This skill is applicable to execute the workflow or actions described in the overview.
1062
+
1063
+ - User mentions or implies: graphql
1064
+ - User mentions or implies: graphql schema
1065
+ - User mentions or implies: graphql resolver
1066
+ - User mentions or implies: apollo server
1067
+ - User mentions or implies: apollo client
1068
+ - User mentions or implies: graphql federation
1069
+ - User mentions or implies: dataloader
1070
+ - User mentions or implies: graphql codegen
1071
+ - User mentions or implies: graphql query
1072
+ - User mentions or implies: graphql mutation