Claude-skill-registry coder-convex
Self-hosted Convex development in Coder workspaces with authentication, queries, mutations, React integration, and environment configuration
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/coder-convex" ~/.claude/skills/majiayu000-claude-skill-registry-coder-convex && rm -rf "$T"
skills/data/coder-convex/SKILL.mdCoder-Convex: Self-Hosted Convex Development in Coder Workspace
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
skill instead.coder-convex-setup
Environment Context
Coder Workspace Characteristics
- OS: Linux (Ubuntu/Debian-based), x86_64 architecture
- Runtime: Docker-in-Docker capability
- Networking: Internal cluster networking with port forwarding
- Package Manager: Node.js package manager (pnpm/npm/yarn)
Coder Convex Services
In a Coder workspace, Convex is exposed through multiple services:
| Slug | Display Name | Internal URL | Port | Hidden | Purpose |
|---|---|---|---|---|---|
| Convex Dashboard | | 6791 | No | Admin dashboard |
| Convex API | | 3210 | Yes | Main API endpoints |
| Convex Site | | 3211 | Yes | Site Proxy (Auth) |
Self-Hosted Convex in Coder
This workspace uses a self-hosted Convex deployment (not the convex.dev cloud service). Key differences:
- Deployment URL: Coder proxy URL (e.g.,
)https://convex-api--workspace--user.coder.hahomelabs.com - Authentication: Uses
with self-hosted configuration@convex-dev/auth - Dashboard: Available at
or via Coder proxylocalhost:6791 - Admin Key: Generated automatically by setup script
- Environment Variables: Managed via
file.env.convex.local
Required Scripts
The following operations should be available through your project's package manager:
Development:
- Run Convex dev server (runsdev:backend
for self-hosted)npx convex dev --local --once
- Deploy Convex functions (runsdeploy:functions
)npx convex deploy --yes
Docker (Self-Hosted Backend):
- Start self-hosted Convex via Docker Composeconvex:start
- Stop Docker servicesconvex:stop
- View Docker logsconvex:logs
- Check service statusconvex:status
Testing:
- Run end-to-end tests
- Run framework type checking
- Run TypeScript compiler checking
Project Structure
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 ├── router.ts # HTTP routes (required for auth endpoints) └── http.ts # HTTP exports with auth routes (required for Coder) ├── auth.ts # Auth utilities ├── 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/ ├── setup-convex.sh # Coder-specific setup script └── start-convex-backend.sh # Backend startup script .env.convex.local # Coder environment variables (auto-generated)
Convex Development Guidelines
Function Types
| Type | Runtime | Use Case | Import From |
|---|---|---|---|
| V8 | Read data, no side effects | |
| V8 | Write data, transactional | |
| Node.js | External API calls, long-running | |
| V8 | Private read functions | |
| V8 | Private write functions | |
| Node.js | Private Node.js operations | |
Function Syntax (Modern)
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; }, });
Schema Definition
// convex/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; // Your application tables const applicationTables = { tasks: defineTable({ title: v.string(), description: v.optional(v.string()), status: v.string(), priority: v.optional(v.number()), userId: v.id("users"), // Reference to auth users table }) .index("by_status", ["status"]) .index("by_priority", ["priority"]) .index("by_user", ["userId"]), }; export default defineSchema({ ...authTables, // Always include auth tables ...applicationTables, });
Key Schema Rules
- Always include
from...authTables
for Coder workspaces@convex-dev/auth/server - Never manually add
- it's automatic_creationTime - Never use
- it's built-in.index("by_creation_time", ["_creationTime"]) - Index names should be descriptive:
orby_fieldNameby_field1_and_field2 - All indexes include
automatically as the last field_creationTime - Indexes must be non-empty: define at least one field
Common Validators
v.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") );
Query Patterns
// 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 });
Mutation Patterns
// 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);
Calling Functions from Functions
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" }); }, });
Authentication in Coder Workspaces
Auth Configuration
Note: Modern
(v0.0.90+) uses the@convex-dev/authfunction directly. A separateconvexAuth()file is no longer required.auth.config.ts
Auth Setup (convex/auth.ts):
import { convexAuth, getAuthUserId } from "@convex-dev/auth/server"; import { Password } from "@convex-dev/auth/providers/Password"; import { Anonymous } from "@convex-dev/auth/providers/Anonymous"; import { query } from "./_generated/server"; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [Password, Anonymous], }); export const currentUser = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return null; return await ctx.db.get(userId); }, });
HTTP Router Setup (convex/http.ts):
import { auth } from "./auth"; import router from "./router"; const http = router; // CRITICAL: Add auth routes to the HTTP router auth.addHttpRoutes(http); export default http;
Critical: The
auth.addHttpRoutes(http) call is required for auth endpoints (/auth/*) to be accessible.
Using Auth in Functions
import { query } from "./_generated/server"; import { getAuthUserId } from "@convex-dev/auth/server"; export const getCurrentUser = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { return null; } return await ctx.db.get(userId); }, }); // Query that requires authentication export const getUserTasks = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Not authenticated"); } return await ctx.db .query("tasks") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); }, });
React Auth Integration
import { useQuery, useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; import { SignInButton, SignOutButton, useAuth } from "@convex-dev/auth/react"; export default function App() { const { isAuthenticated, user } = useAuth(); const tasks = useQuery(api.tasks.getUserTasks) || []; if (!isAuthenticated) { return ( <main> <h1>My App</h1> <SignInButton /> </main> ); } return ( <main> <h1>Welcome, {user?.name || 'User'}!</h1> <SignOutButton /> <ul> {tasks.map(task => ( <li key={task._id}>{task.title}</li> ))} </ul> </main> ); }
React Integration
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 = useAction(api.rag.generateQueryEmbedding); return ( <div> {tasks.map(task => ( <div key={task._id}>{task.title}</div> ))} <button onClick={() => createTask({ title: "New" })}> Add Task </button> </div> ); }
React Best Practices
-
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()); -
Always use ConvexProviderWithAuth for authentication:
import { ConvexReactClient } from "convex/react"; import { ConvexProviderWithAuth } from "@convex-dev/auth/react"; const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); ReactDOM.createRoot(document.getElementById("root")!).render( <ConvexProviderWithAuth client={convex}> <App /> </ConvexProviderWithAuth> );
Environment Variables
NOTE: For initial environment setup (creating
, generating admin keys, Docker configuration), use the.env.convex.localskill.coder-convex-setup
Available Environment Variables (Coder)
# Coder Workspace URLs (auto-generated by setup script) CONVEX_CLOUD_ORIGIN=<convex-api URL> # e.g., https://convex-api--...coder.hahomelabs.com CONVEX_SITE_ORIGIN=<convex-site URL> # e.g., https://convex-site--...coder.hahomelabs.com CONVEX_DEPLOYMENT_URL=<convex-api URL> # Same as CONVEX_CLOUD_ORIGIN # Frontend Configuration VITE_CONVEX_URL=<convex-api URL> # Same as CONVEX_CLOUD_ORIGIN # Admin Key CONVEX_SELF_HOSTED_ADMIN_KEY=<admin-key> # Auto-generated # JWT Configuration (for auth) JWT_ISSUER=<convex-site URL> # Same as CONVEX_SITE_ORIGIN (required for auth) # JWT_PRIVATE_KEY is loaded from jwt_private_key.pem via entrypoint script # Database (if using PostgreSQL) POSTGRES_URL=<postgres-connection-string> # e.g., postgresql://convex:convex@localhost:5432/convex # AI Services (if using) LITELLM_APP_API_KEY=<api-key> # For LiteLLM proxy LITELLM_BASE_URL=<proxy-url> # e.g., https://llm-gateway.hahomelabs.com OPENAI_API_KEY=<openai-key> # For embeddings/RAG # Feature Flags ENABLE_RAG=true/false # Enable RAG functionality
IMPORTANT: The Convex CLI reads
by default, NOT.env.local. If you need.env.convex.localto be available for the Convex CLI (e.g., forCONVEX_SITE_ORIGIN), add it tonpx convex devas well. The setup script should handle this automatically..env.local
Critical Variable Relationships
CONVEX_CLOUD_ORIGIN = CONVEX_DEPLOYMENT_URL = VITE_CONVEX_URL (all point to convex-api, port 3210) CONVEX_SITE_ORIGIN = JWT_ISSUER (both point to convex-site, port 3211)
Why this works:
- All Convex client communication goes through the API (port 3210)
- The
configuration uses deployment environment variables set viaconvexAuth()npx convex env set - The site proxy (port 3211) handles HTTP routes and auth endpoint discovery
- JWT tokens are validated against the
which must matchJWT_ISSUERCONVEX_SITE_ORIGIN
Accessing Environment Variables in Functions
export const checkEnv = query({ args: {}, handler: async (_ctx) => { return { convexCloudOrigin: process.env.CONVEX_CLOUD_ORIGIN, convexSiteOrigin: process.env.CONVEX_SITE_ORIGIN, jwtIssuer: process.env.JWT_ISSUER, apiKeyPresent: !!process.env.LITELLM_APP_API_KEY, }; }, });
Self-Hosted Convex Specifics
NOTE: For initial deployment workflow and Docker setup, use the
skill.coder-convex-setup
Docker Services Status
The self-hosted Convex runs via Docker Compose. Check status:
[package-manager] run convex:status # Check container status docker ps # List running containers [package-manager] run convex:logs # View backend logs
Common Runtime Issues
| Issue | Solution |
|---|---|
| Functions not updating | Run |
| Type errors after schema change | Run |
Module not found: | Run |
| Authentication not working | Check points to site proxy URL (port 3211) |
| Port 3211 not accessible | Verify Docker is running with site proxy enabled |
Development Workflow
Step 1: Define Schema
Edit convex/schema.ts:
import { authTables } from "@convex-dev/auth/server"; const applicationTables = { tasks: defineTable({ title: v.string(), status: v.string(), userId: v.id("users"), }).index("by_user", ["userId"]), }; export default defineSchema({ ...authTables, ...applicationTables, });
Step 2: Write Functions
Edit or create files in convex/:
// convex/tasks.ts import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db.query("tasks").collect(); }, }); export const getUserTasks = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return []; return await ctx.db .query("tasks") .withIndex("by_user", (q) => q.eq("userId", userId)) .collect(); }, }); export const create = mutation({ args: { title: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Not authenticated"); } await ctx.db.insert("tasks", { title: args.title, status: "pending", userId, }); }, });
Step 3: Deploy Functions
Deploy the Convex functions to your backend:
[package-manager] run deploy:functions
This regenerates convex/_generated/api.d.ts with type-safe references.
Step 4: Use in React
import { useQuery, useMutation } from "convex/react"; import { api } from "../../convex/_generated/api"; import { SignInButton, SignOutButton, useAuth } from "@convex-dev/auth/react"; export default function Tasks() { const { isAuthenticated } = useAuth(); const tasks = useQuery(api.tasks.getUserTasks) || []; const create = useMutation(api.tasks.create); if (!isAuthenticated) { return <SignInButton />; } return ( <div> <SignOutButton /> <ul> {tasks.map((t) => ( <div key={t._id}>{t.title}</div> ))} </ul> <button onClick={() => create({ title: "New" })}>Add</button> </div> ); }
Step 5: Quality Gates
Run appropriate quality gates based on the changes made. Consider what regressions are possible and what new functionality was added, then conduct relevant checks:
- Type checking (for TypeScript changes)
- Linting (for code style consistency)
- Build verification (to catch integration issues)
- Targeted tests (for new or modified functionality)
Run only the quality gates that are relevant to the changes made.
Testing Convex Functions
Unit Tests
// 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 });
Integration Tests (Playwright)
See tests/convex-chat-api.test.ts for examples.
Type Safety
Using Generated Types
import type { Doc, Id } from "./_generated/dataModel"; type Task = Doc<"tasks">; // Task document type type TaskId = Id<"tasks">; // Task ID type type UserId = Id<"users">; // User ID type (from auth tables) function processTask(taskId: TaskId) { // Type-safe! }
Function Reference Types
import type { FunctionReference } from "convex/server"; // Function references are fully typed const fn: FunctionReference<"query", "public", args, Doc<"tasks">> = api.tasks.get;
Best Practices
DO
- Use
functions for sensitive operationsinternal* - Always validate arguments with
validatorsv.*() - Use indexes for efficient queries
- Always include
in schema for Coder workspaces...authTables - Check authentication in mutations that modify user data
- Keep functions under 100 lines
- Use TypeScript strict mode
- Test in dev before deploying
DON'T
- Don't use
in queries - use indexes instead.filter() - Don't manually add
or_id
to schemas_creationTime - Don't use
- useundefined
insteadnull - Don't make files longer than 300 lines
- Don't call hooks conditionally in React
- Don't manually edit
files_generated/ - Don't forget to deploy functions after changes
Coder Workspace URL Patterns
Internal (Localhost)
| Service | URL |
|---|---|
| Convex API | |
| Site Proxy (Auth) | |
| Dashboard | |
External (Coder Proxy)
| Service | URL Pattern | Example |
|---|---|---|
| Convex API | | |
| Convex Site | | |
| Convex Dashboard | | |
Self-Hosted Convex vs Convex Cloud
| Feature | Coder Self-Hosted | Convex Cloud |
|---|---|---|
| Dashboard | Local at or Coder proxy URL | Web dashboard at convex.dev |
| Deployment URL | Coder proxy URL | |
| Environment Variables | file | Dashboard UI |
| Auth Configuration | Uses with providers, (site proxy, port 3211) | Auto-configured |
| Site Proxy Port | 3211 (auth/site proxy) | Not applicable |
| Initial Setup | Manual (use ) | Guided in dashboard |
| Pricing | Self-managed infrastructure | Usage-based pricing |
RAG (Retrieval Augmented Generation)
This project includes RAG capabilities for AI-powered document search.
Generating Embeddings
Run the embeddings generation script to process documents for RAG search.
Using RAG in Queries
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; }, });
Troubleshooting
NOTE: For setup-related issues (missing deployment URL, invalid admin key, Docker problems), use the
skill.coder-convex-setup
Common Runtime Errors
Type error: Property 'xxx' does not exist on type
Fix: Run
[package-manager] run dev:backend] to regenerate types after schema changes.
Error: Module not found: Can't resolve './_generated/api'
Fix: Run
[package-manager] run 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.
Authentication failing with "Invalid issuer"
Fix: Verify environment variables:
grep "CONVEX_SITE" .env.convex.local # CONVEX_SITE_ORIGIN should point to convex-site URL (port 3211) # JWT_ISSUER should match CONVEX_SITE_ORIGIN
Debug Queries
// 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), convexCloudOrigin: process.env.CONVEX_CLOUD_ORIGIN, convexSiteOrigin: process.env.CONVEX_SITE_ORIGIN, jwtIssuer: process.env.JWT_ISSUER, }; }, });
Quick Reference
| Operation | Purpose |
|---|---|
| Development mode with type sync |
| Update backend functions |
| Launch Docker services |
| Stop Docker services |
| View backend logs |
| Check service status |
| Type checking | Verify TypeScript correctness |
| Run tests | Execute test suite |
Summary
This workspace uses self-hosted Convex in Coder with:
- Docker-based deployment with Coder proxy URLs
for authentication@convex-dev/auth- Port 3211 for site proxy (auth)
- Port 3210 for API endpoints
- Dashboard at
localhost:6791 - Environment variables in
.env.convex.local
Remember: Always deploy Convex functions after changing Convex code, and run appropriate quality gates before committing.