coder-convex

SKILL.md

Coder-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 coder-convex-setup skill instead.

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 Convex Dashboard localhost:6791 6791 No Admin dashboard
convex-api Convex API localhost:3210 3210 Yes Main API endpoints
convex-site Convex Site localhost:3211 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:

  1. Deployment URL: Coder proxy URL (e.g., https://convex-api--workspace--user.coder.hahomelabs.com)
  2. Authentication: Uses @convex-dev/auth with self-hosted configuration
  3. Dashboard: Available at localhost:6791 or via Coder proxy
  4. Admin Key: Generated automatically by setup script
  5. Environment Variables: Managed via .env.convex.local file

Required Scripts

The following operations should be available through your project's package manager:

Development:

  • dev:backend - Run Convex dev server (runs npx convex dev --local --once for self-hosted)
  • deploy:functions - Deploy Convex functions (runs npx convex deploy --yes)

Docker (Self-Hosted Backend):

  • convex:start - Start self-hosted Convex via Docker Compose
  • convex:stop - Stop Docker services
  • convex:logs - View Docker logs
  • convex:status - Check service 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
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

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

  1. Always include ...authTables from @convex-dev/auth/server for Coder workspaces
  2. Never manually add _creationTime - it's automatic
  3. Never use .index("by_creation_time", ["_creationTime"]) - it's built-in
  4. Index names should be descriptive: by_fieldName or by_field1_and_field2
  5. All indexes include _creationTime automatically as the last field
  6. 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 @convex-dev/auth (v0.0.90+) uses the convexAuth() function directly. A separate auth.config.ts file is no longer required.

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

  1. 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");
    
  2. Use "skip" sentinel for conditional queries:

    import { skipToken } from "convex/react";
    const data = useQuery(api.tasks.get, taskId ? { id: taskId } : skipToken());
    
  3. 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 .env.convex.local, generating admin keys, Docker configuration), use the coder-convex-setup skill.

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 .env.local by default, NOT .env.convex.local. If you need CONVEX_SITE_ORIGIN to be available for the Convex CLI (e.g., for npx convex dev), add it to .env.local as well. The setup script should handle this automatically.

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 convexAuth() configuration uses deployment environment variables set via npx convex env set
  • The site proxy (port 3211) handles HTTP routes and auth endpoint discovery
  • JWT tokens are validated against the JWT_ISSUER which must match CONVEX_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 coder-convex-setup skill.

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 [package-manager] run deploy:functions]
Type errors after schema change Run [package-manager] run dev:backend]
Module not found: _generated/api Run [package-manager] run deploy:functions]
Authentication not working Check CONVEX_SITE_ORIGIN 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 internal* functions for sensitive operations
  • Always validate arguments with v.*() validators
  • Use indexes for efficient queries
  • Always include ...authTables in schema for Coder workspaces
  • 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 .filter() in queries - use indexes instead
  • Don't manually add _id or _creationTime to schemas
  • Don't use undefined - use null instead
  • Don't make files longer than 300 lines
  • Don't call hooks conditionally in React
  • Don't manually edit _generated/ files
  • Don't forget to deploy functions after changes

Coder Workspace URL Patterns

Internal (Localhost)

Service URL
Convex API http://localhost:3210
Site Proxy (Auth) http://localhost:3211
Dashboard http://localhost:6791

External (Coder Proxy)

Service URL Pattern Example
Convex API https://convex-api--<workspace>--<user>.<domain> https://convex-api--myproject--johndoe.coder.hahomelabs.com
Convex Site https://convex-site--<workspace>--<user>.<domain> https://convex-site--myproject--johndoe.coder.hahomelabs.com
Convex Dashboard https://convex--<workspace>--<user>.<domain> https://convex--myproject--johndoe.coder.hahomelabs.com

Self-Hosted Convex vs Convex Cloud

Feature Coder Self-Hosted Convex Cloud
Dashboard Local at localhost:6791 or Coder proxy URL Web dashboard at convex.dev
Deployment URL Coder proxy URL *.convex.cloud
Environment Variables .env.convex.local file Dashboard UI
Auth Configuration Uses convexAuth() with providers, CONVEX_SITE_ORIGIN (site proxy, port 3211) Auto-configured
Site Proxy Port 3211 (auth/site proxy) Not applicable
Initial Setup Manual (use coder-convex-setup) 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 coder-convex-setup skill.

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
dev:backend Development mode with type sync
deploy:functions Update backend functions
convex:start Launch Docker services
convex:stop Stop Docker services
convex:logs View backend logs
convex:status 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
  • @convex-dev/auth for authentication
  • 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.

Weekly Installs
2
First Seen
Feb 15, 2026
Installed on
opencode2
claude-code2
replit2
github-copilot2
codex2
droid2