agentsclimarketplace

Convex skill

Skill PolarCoding85/convex-agent-skillz/.claude/skills/convex-skill

Expert guidance for Convex backend development including queries, mutations, actions, schemas, authentication, scheduling, file storage, search, and Next.js integration. Use when working with Convex functions, database operations, convex/ directory code, or Next.js App Router with Convex. Triggers: convex functions, ctx.db, useQuery, useMutation, usePreloadedQuery, preloadQuery, fetchQuery, convex schema, convex auth, convex cron, convex actions, convex scheduling, ctx.storage, generateUploadUrl, file upload, storage.store, storage.getUrl, Id<"_storage">, ConvexProvider, ConvexClientProvider, db.system, _scheduled_functions, _storage system table, OCC, optimistic concurrency, transaction atomicity, schema evolution, searchIndex, vectorIndex, withSearchIndex, ctx.vectorSearch, full-text search, vector search, embeddings, RAG, semantic search, typeahead search, ConvexError, httpAction, httpRouter, CORS, webhook.From its SKILL.md

Install
npx -y skills add PolarCoding85/convex-agent-skillz --skill convex-skill

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.5 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it

Convex Backend Development

Core Architecture

Convex is a reactive database where queries are TypeScript functions. The sync engine (queries + mutations + database) is the heart of Convex — center your app around it.

Function Types

TypeDB AccessDeterministicCached/ReactiveUse For
queryRead onlyYesYesAll reads, subscriptions
mutationRead/WriteYesNoAll writes (transactions)
actionVia ctx.run*NoNoExternal APIs, LLMs, email
httpActionVia ctx.run*NoNoWebhooks, custom HTTP

Key rule: Queries and mutations cannot make network requests. Actions cannot directly access the database.

Project Structure (Best Practice)

convex/
├── _generated/         # Auto-generated types (commit this)
├── schema.ts           # Database schema
├── model/              # Helper functions (most logic lives here)
│   ├── users.ts
│   └── messages.ts
├── users.ts            # Thin wrappers exposing public API
├── messages.ts
├── crons.ts            # Cron job definitions
└── http.ts             # HTTP action routes

Essential Patterns

1. Function Structure

// convex/messages.ts
import { query, mutation, internalMutation } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';

// PUBLIC query with validators (always validate public functions)
export const list = query({
  args: { channelId: v.id('channels') },
  handler: async (ctx, { channelId }) => {
    return await ctx.db
      .query('messages')
      .withIndex('by_channel', (q) => q.eq('channelId', channelId))
      .order('desc')
      .take(50);
  }
});

// PUBLIC mutation with validators and auth check
export const send = mutation({
  args: { channelId: v.id('channels'), body: v.string() },
  handler: async (ctx, { channelId, body }) => {
    const user = await ctx.auth.getUserIdentity();
    if (!user) throw new Error('Unauthorized');

    await ctx.db.insert('messages', {
      channelId,
      body,
      authorId: user.subject
    });
  }
});

// INTERNAL mutation (for scheduling, crons, actions)
export const deleteOld = internalMutation({
  args: { before: v.number() },
  handler: async (ctx, { before }) => {
    const old = await ctx.db
      .query('messages')
      .withIndex('by_createdAt', (q) => q.lt('_creationTime', before))
      .take(100);
    for (const msg of old) {
      await ctx.db.delete(msg._id);
    }
  }
});

2. Helper Functions Pattern

Most logic should live in helper functions, NOT in query/mutation handlers:

// convex/model/users.ts
import { QueryCtx, MutationCtx } from '../_generated/server';
import { Doc } from '../_generated/dataModel';

export async function getCurrentUser(
  ctx: QueryCtx
): Promise<Doc<'users'> | null> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) return null;

  return await ctx.db
    .query('users')
    .withIndex('by_tokenIdentifier', (q) =>
      q.eq('tokenIdentifier', identity.tokenIdentifier)
    )
    .unique();
}

export async function requireUser(ctx: QueryCtx): Promise<Doc<'users'>> {
  const user = await getCurrentUser(ctx);
  if (!user) throw new Error('Unauthorized');
  return user;
}

3. Actions with Scheduling

// convex/ai.ts
import { action, internalMutation } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';

export const summarize = action({
  args: { documentId: v.id('documents') },
  handler: async (ctx, { documentId }) => {
    // Read data via internal query
    const doc = await ctx.runQuery(internal.documents.get, { documentId });

    // Call external API
    const response = await fetch('https://api.openai.com/v1/...', {...});
    const summary = await response.json();

    // Write result via internal mutation
    await ctx.runMutation(internal.documents.setSummary, {
      documentId,
      summary: summary.text
    });
  }
});

// Trigger action from mutation (not directly from client)
export const requestSummary = mutation({
  args: { documentId: v.id('documents') },
  handler: async (ctx, { documentId }) => {
    const user = await ctx.auth.getUserIdentity();
    if (!user) throw new Error('Unauthorized');

    await ctx.db.patch(documentId, { status: 'processing' });

    // Schedule action (runs after mutation commits)
    await ctx.scheduler.runAfter(0, internal.ai.summarizeInternal, {
      documentId
    });
  }
});

4. Application Errors

import { ConvexError } from 'convex/values';

export const assignRole = mutation({
  args: { roleId: v.id('roles'), userId: v.id('users') },
  handler: async (ctx, { roleId, userId }) => {
    const existing = await ctx.db
      .query('assignments')
      .withIndex('by_role', (q) => q.eq('roleId', roleId))
      .first();

    if (existing) {
      throw new ConvexError({
        code: 'ROLE_TAKEN',
        message: 'Role is already assigned'
      });
    }

    await ctx.db.insert('assignments', { roleId, userId });
  }
});

Critical Rules

DO ✅

  • Use internal. functions for all ctx.run*, ctx.scheduler, and crons
  • Always validate args for public functions with v.* validators
  • Always check auth in public functions: ctx.auth.getUserIdentity()
  • Use indexes with .withIndex() instead of .filter()
  • Await all promises (enable no-floating-promises ESLint rule)
  • Keep actions small — put logic in queries/mutations
  • Batch database operations in single mutations
  • Use ConvexError for user-facing errors

DON'T ❌

  • Don't use api. functions for scheduling (use internal.)
  • Don't use .filter() on queries — use indexes or TypeScript filter
  • Don't use .collect() on unbounded queries (use .take() or pagination)
  • Don't use Date.now() in queries (breaks caching)
  • Don't call actions directly from client (trigger via mutation + scheduler)
  • Don't make sequential ctx.runQuery/runMutation calls in actions (batch them)
  • Don't use ctx.runAction unless switching runtimes (use helper functions)

Reference Guides

For detailed patterns, see:

Auth Provider Skills (add to project as needed):

  • convex-auth — Universal auth patterns, storing users, debugging
  • convex-clerk — Clerk setup, webhooks, JWT configuration
  • convex-workos — WorkOS AuthKit setup, auto-provisioning

What ships with it: 12 files

140.0 KB alongside SKILL.md

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.