
coder-convex
by jovermier
SKILL.md
name: coder-convex description: Self-hosted Convex development in Coder workspaces with authentication, queries, mutations, React integration, and environment configuration updated: 2026-01-16
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-setupskill 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:
- Deployment URL: Coder proxy URL (e.g.,
https://convex-api--workspace--user.coder.hahomelabs.com) - Authentication: Uses
@convex-dev/authwith self-hosted configuration - Dashboard: Available at
localhost:6791or via Coder proxy - Admin Key: Generated automatically by setup script
- Environment Variables: Managed via
.env.convex.localfile
Required Scripts
The following operations should be available through your project's package manager:
Development:
dev:backend- Run Convex dev server (runsnpx convex dev --local --oncefor self-hosted)deploy:functions- Deploy Convex functions (runsnpx convex deploy --yes)
Docker (Self-Hosted Backend):
convex:start- Start self-hosted Convex via Docker Composeconvex:stop- Stop Docker servicesconvex:logs- View Docker logsconvex: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
- Always include
...authTablesfrom@convex-dev/auth/serverfor Coder workspaces - Never manually add
_creationTime- it's automatic - Never use
.index("by_creation_time", ["_creationTime"])- it's built-in - Index names should be descriptive:
by_fieldNameorby_field1_and_field2 - All indexes include
_creationTimeautomatically as the last field - 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 theconvexAuth()function directly. A separateauth.config.tsfile 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
-
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
.env.convex.local, generating admin keys, Docker configuration), use thecoder-convex-setupskill.
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.localby default, NOT.env.convex.local. If you needCONVEX_SITE_ORIGINto be available for the Convex CLI (e.g., fornpx convex dev), add it to.env.localas 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 vianpx convex env set - The site proxy (port 3211) handles HTTP routes and auth endpoint discovery
- JWT tokens are validated against the
JWT_ISSUERwhich must matchCONVEX_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-setupskill.
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
...authTablesin 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
_idor_creationTimeto schemas - Don't use
undefined- usenullinstead - 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-setupskill.
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/authfor 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.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon