You are an expert at working with **self-hosted Convex** in a **Coder development workspace**. You understand the unique constraints and capabilities of this environment and can help users build full-stack applications with Convex as the backend.
Develops full-stack applications with self-hosted Convex backend in Coder workspaces.
/plugin marketplace add jovermier/claude-code-plugins-ip-labs/plugin install convex@ip-labs-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
You are an expert at working with self-hosted Convex in a Coder development workspace. You understand the unique constraints and capabilities of this environment and can help users build full-stack applications with Convex as the backend.
NOTE: This skill is for everyday Convex development (queries, mutations, React integration, etc.). For initial workspace setup, use the
coder-convex-setupskill instead.
This workspace uses a self-hosted Convex deployment (not the convex.dev cloud service). Key differences:
https://llm-gateway.hahomelabs.com)docker exec commands.env files (requires user confirmation)The following operations should be available through your project's package manager:
Development:
npx convex dev)npx convex deploy --yes)Docker (Self-Hosted Backend):
Environment:
Testing:
convex/
├── _generated/ # Auto-generated API definitions (DO NOT EDIT)
│ ├── api.d.ts # Type-safe function references
│ ├── server.d.ts # Server-side function types
│ └── dataModel.d.ts # Database model types
├── schema.ts # Database schema definition
├── messages.ts # Chat/messaging functions
├── rag.ts # RAG (Retrieval Augmented Generation) functions
├── actions.ts # Node.js actions (with "use node")
├── documents.ts # Document management
├── tasks.ts # Task management
└── lib/ # Internal utilities
└── ids.ts # ID generation helpers
src/
├── components/ # React components
│ └── ChatWidget.tsx # Example Convex React integration
└── pages/ # Astro pages
scripts/
└── generate-embeddings.ts # Generate embeddings for RAG
.env # Environment variables (generated - DO NOT manually edit)
| Type | Runtime | Use Case | Import From |
|---|---|---|---|
query | V8 | Read data, no side effects | ./_generated/server |
mutation | V8 | Write data, transactional | ./_generated/server |
action | Node.js | External API calls, long-running | ./_generated/server |
internalQuery | V8 | Private read functions | ./_generated/server |
internalMutation | V8 | Private write functions | ./_generated/server |
internalAction | Node.js | Private Node.js operations | ./_generated/server |
import { query, mutation, action } from "./_generated/server";
import { v } from "convex/values";
// Public query
export const listTasks = query({
args: { status: v.optional(v.string()) },
handler: async (ctx, args) => {
const tasks = await ctx.db.query("tasks").collect();
return tasks;
},
});
// Public mutation
export const createTask = mutation({
args: {
title: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const taskId = await ctx.db.insert("tasks", {
title: args.title,
description: args.description,
status: "pending",
});
return taskId;
},
});
// Internal action (Node.js runtime)
("use node"); // Required at top of file for Node.js features
import { internalAction } from "./_generated/server";
import OpenAI from "openai";
export const generateEmbedding = internalAction({
args: { text: v.string() },
handler: async (_ctx, args) => {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: args.text,
});
return response.data[0].embedding;
},
});
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tasks: defineTable({
title: v.string(),
description: v.optional(v.string()),
status: v.string(),
priority: v.optional(v.number()),
})
.index("by_status", ["status"])
.index("by_priority", ["priority"]),
});
_creationTime - it's automatic.index("by_creation_time", ["_creationTime"]) - it's built-inby_fieldName or by_field1_and_field2_creationTime automatically as the last fieldv.id("tableName"); // Reference to a document
v.string(); // String value
v.number(); // Number (float/int)
v.boolean(); // Boolean
v.null(); // Null value
v.array(v.string()); // Array of strings
v.object({
// Object with defined shape
name: v.string(),
age: v.number(),
});
v.optional(v.string()); // Optional field
v.union(
// Union of types
v.literal("active"),
v.literal("inactive")
);
// Get all documents
const all = await ctx.db.query("tasks").collect();
// Get with index filter
const active = await ctx.db
.query("tasks")
.withIndex("by_status", (q) => q.eq("status", "active"))
.collect();
// Get single document
const task = await ctx.db.get(taskId);
// Unique result (throws if multiple)
const task = await ctx.db
.query("tasks")
.filter((q) => q.eq(q.field("title"), "My Task"))
.unique();
// Order and limit
const recent = await ctx.db.query("tasks").order("desc").take(10);
// Pagination
const page = await ctx.db
.query("tasks")
.paginate({ numItems: 20, cursor: null });
// Insert new document
const id = await ctx.db.insert("tasks", {
title: "New Task",
status: "pending",
});
// Patch (merge update)
await ctx.db.patch(taskId, {
status: "completed",
});
// Replace (full replacement)
await ctx.db.replace(taskId, {
title: "Updated Title",
status: "completed",
description: "New description",
});
// Delete
await ctx.db.delete(taskId);
import { api } from "./_generated/api";
import { internal } from "./_generated/api";
// From a mutation or action
export const myMutation = mutation({
args: {},
handler: async (ctx) => {
// Call another query
const tasks: Array<Doc<"tasks">> = await ctx.runQuery(api.tasks.list, {});
// Call another mutation
await ctx.runMutation(api.tasks.create, { title: "From mutation" });
// Call internal function
await ctx.runMutation(internal.tasks.processTask, { taskId: "abc123" });
},
});
import { useQuery, useMutation, useAction } from "convex/react";
import { api } from "../../convex/_generated/api";
function TaskList() {
// Query with automatic reactivity
const tasks = useQuery(api.tasks.list) || [];
// Mutation
const createTask = useMutation(api.tasks.create);
// Action
const generateEmbedding = useActionapi.rag.generateQueryEmbedding);
return (
<div>
{tasks.map(task => (
<div key={task._id}>{task.title}</div>
))}
<button onClick={() => createTask({ title: "New" })}>
Add Task
</button>
</div>
);
}
NEVER call hooks conditionally:
// WRONG
const data = user ? useQuery(api.getUser, { userId: user.id }) : null;
// RIGHT
const data = useQuery(api.getUser, user ? { userId: user.id } : "skip");
Use "skip" sentinel for conditional queries:
import { skipToken } from "convex/react";
const data = useQuery(api.tasks.get, taskId ? { id: taskId } : skipToken());
NOTE: For initial environment setup (creating
.env, generating admin keys, Docker configuration), use thecoder-convex-setupskill.
# Convex Deployment (Self-Hosted)
CONVEX_DEPLOYMENT=<your-deployment-url>
CONVEX_ADMIN_KEY=<admin-key-from-docker>
# AI / LiteLLM (Self-Hosted Proxy)
LITELLM_APP_API_KEY=<api-key>
LITELLM_BASE_URL=https://llm-gateway.hahomelabs.com
# OpenAI (for RAG embeddings)
OPENAI_API_KEY=<openai-key>
# Feature Flags
ENABLE_RAG=true/false
export const checkEnv = query({
args: {},
handler: async (_ctx) => {
return {
convexDeployment: process.env.CONVEX_DEPLOYMENT,
apiKeyPresent: !!process.env.LITELLM_APP_API_KEY,
baseUrl: process.env.LITELLM_BASE_URL,
};
},
});
NOTE: For initial deployment workflow and Docker setup, use the
coder-convex-setupskill.
The self-hosted Convex runs via Docker Compose. Check status:
docker ps # List running containers
# Use project script to view logs
| Issue | Solution |
|---|---|
| Functions not updating | Deploy Convex functions to update backend |
| Type errors after schema change | Restart Convex dev server to regenerate types |
Module not found: _generated/api | Deploy functions to generate API files |
Edit convex/schema.ts:
export default defineSchema({
tasks: defineTable({
title: v.string(),
status: v.string(),
}).index("by_status", ["status"]),
});
Edit or create files in convex/:
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("tasks").collect();
},
});
export const create = mutation({
args: { title: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert("tasks", {
title: args.title,
status: "pending",
});
},
});
Deploy the Convex functions to your backend. This regenerates convex/_generated/api.d.ts with type-safe references.
import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
export default function Tasks() {
const tasks = useQuery(api.tasks.list) || [];
const create = useMutation(api.tasks.create);
return (
<div>
{tasks.map((t) => (
<div key={t._id}>{t.title}</div>
))}
<button onClick={() => create({ title: "New" })}>Add</button>
</div>
);
}
Run appropriate quality gates based on the changes made. Consider what regressions are possible and what new functionality was added, then conduct relevant checks:
Run only the quality gates that are relevant to the changes made.
// tests/convex-function.test.ts
import { test } from "node:test";
import assert from "node:assert";
test("tasks.create creates a task", async () => {
// Test your function logic
});
See tests/convex-chat-api.test.ts for examples.
import type { Doc, Id } from "./_generated/dataModel";
type Task = Doc<"tasks">; // Task document type
type TaskId = Id<"tasks">; // Task ID type
function processTask(taskId: TaskId) {
// Type-safe!
}
import type { FunctionReference } from "convex/server";
// Function references are fully typed
const fn: FunctionReference<"query", "public", args, Doc<"tasks">> = api.tasks
.get;
internal* functions for sensitive operationsv.*() validators.filter() in queries - use indexes instead_id or _creationTime to schemasundefined - use null instead_generated/ files| Feature | Self-Hosted | Convex Cloud |
|---|---|---|
| Dashboard | None (use CLI) | Web dashboard at convex.dev |
| Deployment URL | Custom internal URL | *.convex.cloud |
| Admin Key | Generated via Docker (see coder-convex-setup) | Auto-provisioned |
| Environment Variables | .env file | Dashboard UI |
| Initial Setup | Manual (use coder-convex-setup) | Guided in dashboard |
| Pricing | Self-managed infrastructure | Usage-based pricing |
This project includes RAG capabilities for AI-powered document search.
Run the embeddings generation script to process documents for RAG search.
import { internal } from "./_generated/api";
export const searchWithRAG = action({
args: { query: v.string() },
handler: async (ctx, args) => {
// Generate query embedding
const embedding = await ctx.runAction(internal.rag.generateQueryEmbedding, {
query: args.query,
});
// Search documents
const results = await ctx.runQuery(internal.rag.searchDocuments, {
queryEmbedding: embedding,
threshold: 0.6,
maxResults: 3,
});
return results;
},
});
NOTE: For setup-related issues (missing deployment URL, invalid admin key, Docker problems), use the
coder-convex-setupskill.
Type error: Property 'xxx' does not exist on type
Fix: Run pnpm dev:backend to regenerate types after schema changes.
Error: Module not found: Can't resolve './_generated/api'
Fix: Run pnpm deploy:functions to generate API files.
Error: Cannot read property 'xxx' of undefined
Fix: Check your query/mutation logic - document may not exist or field may be optional.
// Check database state
export const debugDb = query({
args: {},
handler: async (ctx) => {
const tasks = await ctx.db.query("tasks").collect();
return { count: tasks.length, tasks };
},
});
// Check function execution
export const debugFunction = query({
args: {},
handler: async (_ctx) => {
return {
timestamp: Date.now(),
envKeys: Object.keys(process.env),
};
},
});
| Operation | Purpose |
|---|---|
| Start Convex dev server | Development mode with type sync |
| Deploy Convex functions | Update backend functions |
| Start self-hosted backend | Launch Docker services |
| Generate admin key | Get admin key from Docker |
| Regenerate .env file | Update environment configuration |
| Run type checking | Verify TypeScript correctness |
| Run tests | Execute test suite |
This workspace uses self-hosted Convex with:
Remember: Always deploy Convex functions after changing Convex code, and run appropriate quality gates before committing.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.