一键导入
coder-convex
Self-hosted Convex development in Coder workspaces with authentication, queries, mutations, React integration, and environment configuration
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Self-hosted Convex development in Coder workspaces with authentication, queries, mutations, React integration, and environment configuration
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Initial Convex workspace setup in Coder workspaces with self-hosted Convex deployment, authentication configuration, Docker setup, and environment variable generation
Guides self-hosted Convex deployment, authentication setup, environment configuration, troubleshooting, and production deployment considerations.
Coder workspace environment for hahomelabs.com deployments. Includes networking, ports, convex config, nhost config
Manages git operations including commits, pull requests, merge requests, and branching. Use when creating commits, handling git conflicts, managing branches, or reviewing git history. Enforces clean commit messages without author attribution.
Refactors Claude Code skills to reduce token usage 80-95% using Progressive Disclosure Architecture (PDA). Splits monolithic skills into orchestrator + reference files, extracts scripts, creates reference/ directories. Use when optimizing skills, improving skill efficiency, refactoring large/bloated skills, reducing token costs, applying PDA, modularizing skills, breaking down skills, or converting encyclopedia-style skills to orchestrator pattern.
Coder CLI commands for workspace management, templates, and platform operations
| name | coder-convex |
| description | Self-hosted Convex development in Coder workspaces with authentication, queries, mutations, React integration, and environment configuration |
| updated | "2026-01-16T00:00:00.000Z" |
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.
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) |
This workspace uses a self-hosted Convex deployment (not the convex.dev cloud service). Key differences:
https://convex-api--workspace--user.coder.hahomelabs.com)@convex-dev/auth with self-hosted configurationlocalhost:6791 or via Coder proxy.env.convex.local fileThe 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 Composeconvex:stop - Stop Docker servicesconvex:logs - View Docker logsconvex:status - Check service statusTesting:
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)
| 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";
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,
});
...authTables from @convex-dev/auth/server for Coder workspaces_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" });
},
});
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.
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();
},
});
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>
);
}
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>
);
}
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>
);
NOTE: For initial environment setup (creating
.env.convex.local, generating admin keys, Docker configuration), use thecoder-convex-setupskill.
# 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.
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:
convexAuth() configuration uses deployment environment variables set via npx convex env setJWT_ISSUER which must match CONVEX_SITE_ORIGINexport 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,
};
},
});
NOTE: For initial deployment workflow and Docker setup, use the
coder-convex-setupskill.
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
| 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 |
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,
});
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,
});
},
});
Deploy the Convex functions to your backend:
[package-manager] run deploy:functions
This regenerates convex/_generated/api.d.ts with type-safe references.
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>
);
}
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
type UserId = Id<"users">; // User ID type (from auth tables)
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...authTables in schema for Coder workspaces.filter() in queries - use indexes instead_id or _creationTime to schemasundefined - use null instead_generated/ files| Service | URL |
|---|---|
| Convex API | http://localhost:3210 |
| Site Proxy (Auth) | http://localhost:3211 |
| Dashboard | http://localhost:6791 |
| 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 |
| 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 |
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 [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
// 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,
};
},
});
| 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 |
This workspace uses self-hosted Convex in Coder with:
@convex-dev/auth for authenticationlocalhost:6791.env.convex.localRemember: Always deploy Convex functions after changing Convex code, and run appropriate quality gates before committing.