coder-convex-setup
Coder-Convex-Setup: Initial Convex Workspace Setup in Coder
You are an expert at initial setup and configuration of self-hosted Convex in Coder workspaces. This skill is ONLY for the one-time setup of a new Convex workspace. For everyday Convex development, use the coder-convex skill instead.
When to Use This Skill
Use this skill when:
- Setting up Convex in a new Coder workspace for the first time
- Configuring a self-hosted Convex deployment
- Setting up Docker-based Convex backend
- Configuring environment variables for Convex
- Generating admin keys and deployment URLs
DO NOT use this skill for:
- Everyday Convex development (use
coder-convexinstead) - Writing queries, mutations, or actions (use
coder-convexinstead) - Schema modifications (use
coder-convexinstead) - React integration issues (use
coder-convexinstead)
Prerequisites
Before setting up Convex in a Coder workspace, ensure:
-
Node.js and a package manager are installed:
node --version # Should be v18+ # Check for package manager: pnpm, yarn, npm, or bun pnpm --version # Or: yarn --version, npm --version, bun --version -
Docker is available:
docker --version docker compose version -
Project has package.json with Convex dependency:
{ "dependencies": { "convex": "^1.31.3" } }
Coder Workspace Services Overview
In a Coder workspace, Convex is exposed through multiple services. Understanding these is critical:
| 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) |
Step 1: Install Convex Dependencies
# Install Convex package
[package-manager] add convex
# Install auth dependencies (required for Coder workspaces)
[package-manager] add @convex-dev/auth
# Install dev dependencies if not present
[package-manager] add -D @types/node typescript
Step 2: Create Convex Directory Structure
Create the following directory structure:
mkdir -p convex/lib
The structure should look like:
convex/
├── lib/ # Internal utilities (optional)
├── schema.ts # Database schema (required)
├── auth.ts # Auth setup (required for Coder)
├── router.ts # HTTP routes (required for auth endpoints)
└── http.ts # HTTP exports with auth routes (required for Coder)
Step 3: Create Initial Schema
Create 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 = {
// Add your tables here
tasks: defineTable({
title: v.string(),
status: v.string(),
}).index("by_status", ["status"]),
};
export default defineSchema({
...authTables,
...applicationTables,
});
Key Schema Rules:
- Always include
...authTablesfrom@convex-dev/auth/serverfor Coder workspaces - Never manually add
_idor_creationTime- they're automatic - Index names should be descriptive:
by_fieldName - All indexes automatically include
_creationTimeas the last field - Don't use
.index("by_creation_time", ["_creationTime"])- it's built-in
Step 4: Create Auth Configuration
Note: Modern
@convex-dev/auth(v0.0.90+) uses theconvexAuth()function directly. A separateauth.config.tsfile is no longer required.
Create 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";
// Configure auth with providers
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password, Anonymous],
});
// Query to get the current user
export const currentUser = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) {
return null;
}
return await ctx.db.get(userId);
},
});
Create convex/router.ts:
import { httpRouter } from "convex/server";
const http = httpRouter();
export default http;
Create 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. Without this, authentication will not work.
Step 5: Create Coder Setup Script
Create scripts/setup-convex.sh:
#!/bin/bash
# Detect Coder workspace environment
# Check for both CODER and CODER_WORKSPACE_NAME to confirm we're in a Coder workspace
if [ -n "$CODER" ] && [ -n "$CODER_WORKSPACE_NAME" ]; then
# Running in Coder workspace
# Extract protocol and domain from CODER_URL (e.g., https://coder.hahomelabs.com)
CODER_PROTOCOL="${CODER_URL%%://*}"
CODER_DOMAIN="${CODER_URL#*//}"
WORKSPACE_NAME="${CODER_WORKSPACE_NAME}"
USERNAME="${CODER_WORKSPACE_OWNER_NAME:-$USER}"
# Generate Coder-specific URLs
# Format: <protocol>://<service>--<workspace>--<owner>.<coder-domain>
CONVEX_API_URL="${CODER_PROTOCOL}://convex-api--${WORKSPACE_NAME}--${USERNAME}.${CODER_DOMAIN}"
CONVEX_SITE_URL="${CODER_PROTOCOL}://convex-site--${WORKSPACE_NAME}--${USERNAME}.${CODER_DOMAIN}"
CONVEX_DASHBOARD_URL="${CODER_PROTOCOL}://convex--${WORKSPACE_NAME}--${USERNAME}.${CODER_DOMAIN}"
else
# Local development
CONVEX_API_URL="http://localhost:3210"
CONVEX_SITE_URL="http://localhost:3211"
CONVEX_DASHBOARD_URL="http://localhost:6791"
fi
# Determine PostgreSQL URL from environment
# Priority order: DATABASE_URL → POSTGRES_URI → POSTGRES_URL
# Note: We strip the database name from the URL since Convex appends INSTANCE_NAME automatically
# E.g., "postgres://...:5432/app" becomes "postgres://...:5432"
_RAW_POSTGRES_URL="${DATABASE_URL:-${POSTGRES_URI:-${POSTGRES_URL:-}}}"
if [ -n "$_RAW_POSTGRES_URL" ]; then
# Remove trailing database name (e.g., /app) if present
_STRIPPED_URL="${_RAW_POSTGRES_URL%/[^/]*}"
# For Coder PostgreSQL with self-signed certificates, use sslmode=disable
# Note: Rust postgres crate may not accept sslmode parameter in URL, depends on version
if [[ "$_STRIPPED_URL" == *"?"* ]]; then
# URL already has query parameters
POSTGRES_URL="${_STRIPPED_URL}&sslmode=disable"
else
# Add query parameters - use disable for self-signed certs
POSTGRES_URL="${_STRIPPED_URL}?sslmode=disable"
fi
else
# Fallback to default for local development
POSTGRES_URL="postgresql://convex:convex@localhost:5432/convex?sslmode=disable"
fi
# Verify PostgreSQL URL is configured
if [ -z "$POSTGRES_URL" ]; then
echo "❌ POSTGRES_URL is not set"
echo " Please set DATABASE_URL or POSTGRES_URL in your environment"
echo ""
echo " In Coder workspaces, these variables are automatically provided."
echo " For local development, ensure PostgreSQL is running and set the variable."
exit 1
fi
# Admin key will be generated by the container on first start
# The container's generate_admin_key.sh script is the proper way to generate keys
# We'll retrieve it after the container starts
CONVEX_ADMIN_KEY="${CONVEX_ADMIN_KEY:-}"
# Generate JWT private key for auth (PKCS#8 format)
if [ ! -f jwt_private_key.pem ]; then
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out jwt_private_key.pem 2>/dev/null
fi
# Create .env.convex.local (only if missing or incomplete)
ENV_FILE=".env.convex.local"
ENV_FILE_MISSING=0
if [ ! -f "$ENV_FILE" ]; then
echo "📝 Creating $ENV_FILE..."
ENV_FILE_MISSING=1
else
# Check if required variables are present
if ! grep -q "^POSTGRES_URL=" "$ENV_FILE" 2>/dev/null; then
echo "📝 $ENV_FILE exists but missing POSTGRES_URL, updating..."
ENV_FILE_MISSING=1
fi
if ! grep -q "^CONVEX_CLOUD_ORIGIN=" "$ENV_FILE" 2>/dev/null && [ -n "$CONVEX_API_URL" ]; then
echo "📝 $ENV_FILE exists but missing Convex URLs, updating..."
ENV_FILE_MISSING=1
fi
fi
if [ $ENV_FILE_MISSING -eq 1 ]; then
# Create or update the env file
cat > "$ENV_FILE" << ENVEOF
# Self-hosted Convex configuration
# Auto-generated by setup-convex.sh
# PostgreSQL connection URL
POSTGRES_URL=$POSTGRES_URL
# Convex Cloud Origin - External URL for Convex API access
CONVEX_CLOUD_ORIGIN=$CONVEX_CLOUD_ORIGIN
# Convex Site Origin - HTTP actions endpoint (for auth)
CONVEX_SITE_ORIGIN=$CONVEX_SITE_ORIGIN
# Convex Site URL - Used by @convex-dev/auth for provider domain
CONVEX_SITE_URL=$CONVEX_API_URL
# Convex Deployment URL
CONVEX_DEPLOYMENT_URL=$CONVEX_DEPLOYMENT_URL
# Frontend Configuration
VITE_CONVEX_URL=$CONVEX_CLOUD_ORIGIN
# Admin Key will be retrieved from container after it starts
# CONVEX_ADMIN_KEY and CONVEX_SELF_HOSTED_ADMIN_KEY are set by the container
# JWT Configuration (for auth)
# JWT_ISSUER should match CONVEX_SITE_ORIGIN for proper auth validation
JWT_ISSUER=$CONVEX_SITE_ORIGIN
ENVEOF
echo "✅ Created $ENV_FILE"
fi
# Source environment variables from the (now existing) file
echo "📦 Loading environment variables from $ENV_FILE"
set -a
source "$ENV_FILE"
set +a
echo "Convex environment configured!"
echo "API URL: ${CONVEX_API_URL}"
echo "Site URL: ${CONVEX_SITE_URL}"
echo "Dashboard URL: ${CONVEX_DASHBOARD_URL}"
echo "Admin Key: ${CONVEX_ADMIN_KEY:0:20}..."
Make it executable and run:
chmod +x scripts/setup-convex.sh
./scripts/setup-convex.sh
Step 6: Create Custom Entrypoint Script
Create convex-backend-entrypoint.sh:
#!/bin/bash
# Wrapper script to start Convex backend with JWT_PRIVATE_KEY from file
# Based on the original run_backend.sh but with JWT_PRIVATE_KEY loading
set -e
export DATA_DIR=${DATA_DIR:-/convex/data}
export TMPDIR=${TMPDIR:-"$DATA_DIR/tmp"}
export STORAGE_DIR=${STORAGE_DIR:-"$DATA_DIR/storage"}
export SQLITE_DB=${SQLITE_DB:-"$DATA_DIR/db.sqlite3"}
# Database driver flags
POSTGRES_DB_FLAGS=(--db postgres-v5)
MYSQL_DB_FLAGS=(--db mysql-v5)
mkdir -p "$TMPDIR" "$STORAGE_DIR"
# NOTE: INSTANCE_NAME and INSTANCE_SECRET are set via Docker environment variables
# in docker-compose.convex.yml. They are NOT sourced from a credentials script.
# IMPORTANT: Set JWT_PRIVATE_KEY BEFORE sourcing anything else
# This environment variable MUST be set before the Convex backend starts
# for it to be available in the isolate workers
if [ -f /jwt_private_key.pem ]; then
echo "Loading JWT_PRIVATE_KEY from /jwt_private_key.pem..."
DECODED_KEY=$(cat /jwt_private_key.pem)
echo "JWT_PRIVATE_KEY loaded (length: ${#DECODED_KEY})"
export JWT_PRIVATE_KEY="$DECODED_KEY"
echo "JWT_PRIVATE_KEY exported successfully"
echo "Verifying: ${#JWT_PRIVATE_KEY} characters"
elif [ -n "$JWT_PRIVATE_KEY_BASE64" ]; then
echo "Loading JWT_PRIVATE_KEY from JWT_PRIVATE_KEY_BASE64..."
DECODED_KEY=$(echo "$JWT_PRIVATE_KEY_BASE64" | base64 -d)
echo "JWT_PRIVATE_KEY loaded (length: ${#DECODED_KEY})"
export JWT_PRIVATE_KEY="$DECODED_KEY"
echo "JWT_PRIVATE_KEY exported successfully"
echo "Verifying: ${#JWT_PRIVATE_KEY} characters"
fi
# Make JWT_PRIVATE_KEY available to child processes via env file
if [ -n "$JWT_PRIVATE_KEY" ]; then
# Export to a file that will be sourced by child processes
# This is necessary because Convex isolate workers don't inherit all environment variables
echo "export JWT_PRIVATE_KEY=\"$JWT_PRIVATE_KEY\"" > /convex/jwt_env.sh
echo "JWT environment written to /convex/jwt_env.sh"
# Source it ourselves for good measure
. /convex/jwt_env.sh
fi
# Determine database configuration
if [ -n "$POSTGRES_URL" ]; then
DB_SPEC="$POSTGRES_URL"
DB_FLAGS=("${POSTGRES_DB_FLAGS[@]}")
elif [ -n "$MYSQL_URL" ]; then
DB_SPEC="$MYSQL_URL"
DB_FLAGS=("${MYSQL_DB_FLAGS[@]}")
elif [ -n "$DATABASE_URL" ]; then
echo "Warning: DATABASE_URL is deprecated."
DB_SPEC="$DATABASE_URL"
DB_FLAGS=("${POSTGRES_DB_FLAGS[@]}")
else
DB_SPEC="$SQLITE_DB"
DB_FLAGS=()
fi
# Use local storage (S3 not configured)
STORAGE_FLAGS=(--local-storage "$STORAGE_DIR")
# Run the Convex backend with JWT_PRIVATE_KEY explicitly set in the environment
# Using env to ensure the variable is passed to the child process
exec env JWT_PRIVATE_KEY="$JWT_PRIVATE_KEY" "$@" ./convex-local-backend \
--instance-name "$INSTANCE_NAME" \
--instance-secret "$INSTANCE_SECRET" \
--port 3210 \
--site-proxy-port 3211 \
--convex-origin "$CONVEX_CLOUD_ORIGIN" \
--convex-site "$CONVEX_SITE_ORIGIN" \
--beacon-tag "self-hosted-docker" \
${DISABLE_BEACON:+--disable-beacon} \
${REDACT_LOGS_TO_CLIENT:+--redact-logs-to-client} \
${DO_NOT_REQUIRE_SSL:+--do-not-require-ssl} \
"${DB_FLAGS[@]}" \
"${STORAGE_FLAGS[@]}" \
"$DB_SPEC"
Make it executable:
chmod +x convex-backend-entrypoint.sh
Step 7: Create Docker Compose Configuration
Create docker-compose.convex.yml:
services:
convex-backend:
image: ghcr.io/get-convex/convex-backend:latest
container_name: convex-backend-local
env_file:
- .env.convex.local
stop_grace_period: 10s
stop_signal: SIGINT
ports:
- "3210:3210" # Convex API port
- "3211:3211" # Convex site proxy port (for auth)
volumes:
- convex-data:/convex/data
- ./convex-backend-entrypoint.sh:/convex-backend-entrypoint.sh:ro
- ./jwt_private_key.pem:/jwt_private_key.pem:ro
entrypoint: ["/bin/bash", "/convex-backend-entrypoint.sh"]
environment:
# Convex Cloud Origin - External URL for Convex API access
# For local development, defaults to http://localhost:3210
# In Coder workspaces, set via .env.convex.local
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://localhost:3210}
# Convex Site Origin - HTTP actions endpoint (for auth)
# For local development, defaults to http://localhost:3211
# In Coder workspaces, set via .env.convex.local
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://localhost:3211}
# PostgreSQL Database URL (required)
- POSTGRES_URL=${POSTGRES_URL}
# Instance name for identification (matches PostgreSQL database name)
- INSTANCE_NAME=app
# Admin key for authentication (generated on first start)
- CONVEX_ADMIN_KEY=${CONVEX_ADMIN_KEY:-}
# Logging
- RUST_LOG=info,convex=debug
# Lower document retention for development
- DOCUMENT_RETENTION_DELAY=172800
# Disable SSL requirement for local development
- DO_NOT_REQUIRE_SSL=true
# Auth configuration for @convex-dev/auth
# Note: JWT_PRIVATE_KEY is set by convex-backend-entrypoint.sh from mounted file
- JWT_ISSUER=${JWT_ISSUER:-http://localhost:3211}
# Instance secret (auto-generated by backend if not set)
- INSTANCE_SECRET=${INSTANCE_SECRET:-}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3210/version"]
interval: 5s
start_period: 10s
timeout: 5s
retries: 3
convex-dashboard:
image: ghcr.io/get-convex/convex-dashboard:latest
container_name: convex-dashboard-local
env_file:
- .env.convex.local
stop_grace_period: 10s
stop_signal: SIGINT
ports:
- "6791:6791" # Dashboard port
environment:
# Deployment URL for dashboard
- NEXT_PUBLIC_DEPLOYMENT_URL=${CONVEX_DEPLOYMENT_URL:-http://localhost:3210}
depends_on:
convex-backend:
condition: service_healthy
volumes:
convex-data:
driver: local
Critical Configuration Explained:
- Custom Entrypoint: Loads
JWT_PRIVATE_KEYfrom mounted file before starting backend - Volume Mount:
jwt_private_key.pemis mounted at/jwt_private_key.pem:ro - Ports: 3210 (API), 3211 (site proxy for auth), 6791 (dashboard)
CONVEX_CLOUD_ORIGIN: External URL for the API (for internal Convex communication)CONVEX_SITE_ORIGIN: External URL for the site proxy (for auth provider discovery, set vianpx convex env set)JWT_ISSUER: Points to site proxy URL- Healthcheck: Ensures backend is ready before dashboard starts
Step 8: Create Startup Script
Create start-convex-backend.sh:
#!/bin/bash
# Load environment
if [ -f .env.convex.local ]; then
set -a
source .env.convex.local
set +a
fi
# Start Docker services
docker compose -f docker-compose.convex.yml up -d
echo "Waiting for Convex backend to be healthy..."
until curl -s http://localhost:3210/version > /dev/null 2>&1; do
echo "Waiting for Convex API..."
sleep 2
done
echo "Convex backend is running!"
echo "Dashboard: ${CONVEX_DASHBOARD_URL:-http://localhost:6791}"
CRITICAL DEPLOYMENT ORDER: The startup sequence must follow this order:
- Start Docker services (backend becomes healthy)
- Initialize deployment environment variables (
npx convex env set) - These MUST be set before deployment!- Then deploy functions (
npx convex deploy --yes)Why this order: Auth-related environment variables (like
CONVEX_SITE_ORIGIN,JWT_ISSUER,JWKS) must be set before deploying functions. If you deploy first, the deployment may fail or auth may not work properly.
Step 9: Add NPM Scripts
Add these scripts to your package.json:
{
"scripts": {
"dev": "npm-run-all --parallel dev:frontend convex:start",
"dev:frontend": "vite",
"dev:backend": "convex dev --local --once",
"convex:start": "./scripts/setup-convex.sh",
"convex:stop": "docker compose -f docker-compose.convex.yml down",
"convex:logs": "docker compose -f docker-compose.convex.yml logs -f",
"convex:status": "docker compose -f docker-compose.convex.yml ps",
"deploy:functions": "npx convex deploy --yes"
}
}
Script explanations:
dev- Starts both frontend and Convex backend in paralleldev:frontend- Runs the frontend development server (Vite, Next.js, etc.)dev:backend- Runs Convex in development mode against local backend, then exitsconvex:start- Sets up environment and starts Docker servicesconvex:stop- Stops Docker servicesconvex:logs- Shows Convex backend logsconvex:status- Shows status of Docker containersdeploy:functions- Deploys Convex functions to the self-hosted backend
Step 10: Initialize Convex Deployment
# Setup environment and start backend
[package-manager] run convex:start
# Initialize Convex (creates schema, generates types)
[package-manager] run dev:backend
This will:
- Generate Coder-specific environment variables
- Start Docker services with correct configuration
- Create the database schema
- Generate type definitions in
convex/_generated/
Step 11: Initialize Deployment Environment Variables
IMPORTANT: Run this step BEFORE deploying functions. Auth environment variables must be set first.
Create scripts/init-convex-env.sh:
#!/bin/bash
# Initialize Convex deployment environment variables
# Reads from .env.convex.deployment and sets variables via npx convex env set
set -e
DEPLOYMENT_ENV_FILE=".env.convex.deployment"
CONTAINER_ENV_FILE=".env.convex.local"
echo "🔐 Initializing Convex deployment environment variables..."
# Create deployment env file if it doesn't exist
if [ ! -f "$DEPLOYMENT_ENV_FILE" ]; then
echo "📝 Creating $DEPLOYMENT_ENV_FILE..."
cat > "$DEPLOYMENT_ENV_FILE" << 'EOF'
# Convex Deployment Environment Variables
# These variables are set via npx convex env set and appear in the dashboard
# This file should be gitignored (contains secrets)
# === AUTO-GENERATED VARIABLES (do not edit manually) ===
# These are managed by scripts/init-convex-env.sh
# Multi-line values are stored as base64 for safe env file storage
JWT_PRIVATE_KEY_BASE64=""
JWT_ISSUER=""
JWKS=""
# === USER VARIABLES (add your own below) ===
# Add your environment variables here, one per line
# Example:
# OPENAI_API_KEY=sk-...
# STRIPE_SECRET_KEY=sk_live_...
# ANTHROPIC_API_KEY=sk-ant-...
EOF
fi
# Source container env file to get CONVEX_SITE_ORIGIN
set -a
source "$CONTAINER_ENV_FILE"
set +a
# Check if JWT key file exists and has content
JWT_KEY_FILE="jwt_private_key.pem"
if [ -f "$JWT_KEY_FILE" ] && [ -s "$JWT_KEY_FILE" ]; then
# Read existing key from file
JWT_PRIVATE_KEY=$(cat "$JWT_KEY_FILE")
echo "📂 Using existing JWT key from $JWT_KEY_FILE"
else
# Generate a new key
echo "🔑 Generating new JWT private key..."
JWT_PRIVATE_KEY=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null | openssl pkcs8 -topk8 -nocrypt -outform PEM 2>/dev/null)
if [ -z "$JWT_PRIVATE_KEY" ]; then
echo "❌ Failed to generate JWT private key"
exit 1
fi
echo "✅ Generated new JWT private key"
# Write the key to the host file for persistence
echo "$JWT_PRIVATE_KEY" > "$JWT_KEY_FILE"
echo "📝 Saved key to $JWT_KEY_FILE"
echo ""
echo "⚠️ Note: The convex-backend container will use this key on next restart."
echo " To restart: docker compose -f docker-compose.convex.yml restart convex-backend"
fi
# Generate JWKS from private key
echo "🔑 Generating JWKS from private key..."
JWKS=$(node -e "
const crypto = require('crypto');
const privateKey = \`$JWT_PRIVATE_KEY\`;
const publicKey = crypto.createPublicKey(privateKey);
const jwk = publicKey.export({ format: 'jwk' });
// JWKS format requires {\"keys\": [...]} wrapper
const jwks = { keys: [{ use: 'sig', ...jwk }] };
console.log(JSON.stringify(jwks));
")
# Update auto-generated variables in the deployment env file
echo "📝 Updating auto-generated variables in $DEPLOYMENT_ENV_FILE..."
TEMP_FILE=$(mktemp)
# Encode the multi-line JWT private key as base64 for safe env file storage
JWT_PRIVATE_KEY_BASE64=$(echo "$JWT_PRIVATE_KEY" | base64 -w 0)
# Process the file and update auto-generated variables
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^JWT_PRIVATE_KEY_BASE64= ]]; then
echo "JWT_PRIVATE_KEY_BASE64=\"$JWT_PRIVATE_KEY_BASE64\""
elif [[ "$line" =~ ^JWT_ISSUER= ]]; then
echo "JWT_ISSUER=\"$CONVEX_SITE_ORIGIN\""
elif [[ "$line" =~ ^JWKS= ]]; then
echo "JWKS=\"$JWKS\""
else
echo "$line"
fi
done < "$DEPLOYMENT_ENV_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$DEPLOYMENT_ENV_FILE"
# Now set all variables via npx convex env set
echo "📤 Setting deployment environment variables..."
# Set JWT_PRIVATE_KEY (multi-line value, use stdin)
echo " Setting JWT_PRIVATE_KEY..."
if ! echo "$JWT_PRIVATE_KEY" | npx convex env set JWT_PRIVATE_KEY; then
echo "❌ Failed to set JWT_PRIVATE_KEY"
exit 1
fi
# Set CONVEX_SITE_ORIGIN (required for auth provider discovery)
echo " Setting CONVEX_SITE_ORIGIN..."
if ! npx convex env set CONVEX_SITE_ORIGIN "$CONVEX_SITE_ORIGIN"; then
echo "❌ Failed to set CONVEX_SITE_ORIGIN"
exit 1
fi
# Set JWT_ISSUER
echo " Setting JWT_ISSUER..."
if ! npx convex env set JWT_ISSUER "$CONVEX_SITE_ORIGIN"; then
echo "❌ Failed to set JWT_ISSUER"
exit 1
fi
# Set JWKS (multi-line value, use stdin)
echo " Setting JWKS..."
if ! echo "$JWKS" | npx convex env set JWKS; then
echo "❌ Failed to set JWKS"
exit 1
fi
# Now set user variables from the deployment env file
# Parse only the user section (after the USER VARIABLES comment)
USER_SECTION=false
while IFS= read -r line || [ -n "$line" ]; do
# Start processing after USER VARIABLES comment
if [[ "$line" == *"USER VARIABLES"* ]]; then
USER_SECTION=true
continue
fi
# Only process user variables
[ "$USER_SECTION" = false ] && continue
# Skip comments and empty lines
[[ "$line" == \#* ]] && continue
[ -z "$line" ] && continue
# Extract variable name and value
VAR_NAME="${line%%=*}"
VAR_VALUE="${line#*=}"
# Skip empty values
[ -z "$VAR_VALUE" ] && continue
echo " Setting $VAR_NAME..."
npx convex env set "$VAR_NAME" "$VAR_VALUE"
done < "$DEPLOYMENT_ENV_FILE"
echo "✅ Convex deployment environment variables initialized"
echo " Verify in dashboard: Environment Variables section"
Make it executable:
chmod +x scripts/init-convex-env.sh
Run the script to initialize deployment environment variables:
bash scripts/init-convex-env.sh
What this script does:
- Creates
.env.convex.deploymentfile for tracking deployment variables - Generates or reads existing JWT private key from
jwt_private_key.pem - Generates JWKS from the private key using Node.js crypto API
- Sets
JWT_PRIVATE_KEY,CONVEX_SITE_ORIGIN,JWT_ISSUER, andJWKSvianpx convex env set - Sets any user-defined variables from the deployment env file
Note: The
.env.convex.deploymentfile usesJWT_PRIVATE_KEY_BASE64for safe storage of the multi-line key as a single-line value. The script decodes it before setting in Convex.
Step 12: Deploy Functions
Now that environment variables are initialized, deploy your Convex functions:
[package-manager] run deploy:functions
This deploys your Convex functions to the self-hosted backend.
Why this order matters: Auth-related environment variables (
CONVEX_SITE_ORIGIN,JWT_ISSUER,JWKS) must be set before deploying functions. If you deploy first, the deployment may fail or authentication may not work properly.
Step 13: Create Frontend Integration
Create or update src/main.tsx:
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithAuth } from "convex/react";
import React from "react";
import ReactDOM from "react-dom/client";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ConvexProviderWithAuth client={convex}>
<App />
</ConvexProviderWithAuth>
</React.StrictMode>
);
Create src/App.tsx:
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { SignInButton, SignOutButton, useAuth } from "@convex-dev/auth/react";
export default function App() {
const { isAuthenticated } = useAuth();
const tasks = useQuery(api.tasks.list) || [];
return (
<main>
<h1>Convex in Coder</h1>
{isAuthenticated ? (
<>
<p>Welcome!</p>
<SignOutButton />
<ul>
{tasks.map(task => (
<li key={task._id}>{task.title}</li>
))}
</ul>
</>
) : (
<SignInButton />
)}
</main>
);
}
Verification Checklist
After setup, verify:
-
.env.convex.localexists with correct Coder URLs -
convex/_generated/directory exists with type definitions -
convex/schema.tsincludes...authTables -
convex/auth.tsusesconvexAuth()with providers -
convex/http.tscallsauth.addHttpRoutes(http) - Docker services are running:
docker ps - Can access API:
curl http://localhost:3210/version - Can access site proxy:
curl http://localhost:3211/ - Can run
[package-manager] run dev:backendwithout errors - Can run
[package-manager] run deploy:functionssuccessfully - Frontend can import from
convex/_generated/api
Troubleshooting Setup Issues
Issue: Authentication fails
Solution: Verify your 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
Issue: CONVEX_SITE_ORIGIN not set in deployment
Solution: Run ./scripts/setup-convex.sh to regenerate environment.
Issue: Port 3211 not accessible
Solution: Verify Docker is running the site proxy:
docker ps | grep 3211
curl http://localhost:3211/
Issue: Docker container not starting
Solution:
# Check container logs
[package-manager] run convex:logs
# Check if ports are already in use
lsof -i :3210
lsof -i :3211
lsof -i :6791
# Recreate container
[package-manager] run convex:stop
[package-manager] run convex:start
Issue: Type definitions not generating
Solution:
# Clear Convex cache
rm -rf convex/_generated
# Re-run dev backend
[package-manager] run dev:backend
# Or explicitly deploy
[package-manager] run deploy:functions
Issue: Cannot connect to Convex deployment
Solution:
# Verify Docker services are running
docker ps
# Check deployment URL is correct
grep CONVEX .env.convex.local
# Test connection
curl $CONVEX_CLOUD_ORIGIN/version
curl $CONVEX_SITE_ORIGIN/
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 |
Environment Variables Reference
Required for Coder Convex
# 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
# 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
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
CONVEX_SITE_ORIGINis used for auth provider discovery (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
Docker Commands Reference
# Start services
[package-manager] run convex:start # Setup and start all services
# Stop services
[package-manager] run convex:stop # Stop all services
# View logs
[package-manager] run convex:logs # View backend logs
# Check status
[package-manager] run convex:status # Check container status
# Restart services
docker compose -f docker-compose.convex.yml restart
# Execute command in container
docker exec -it <container-name> sh
Post-Setup: Next Steps
After completing the setup:
- Switch to
coder-convexskill for everyday development - Define your schema in
convex/schema.ts(inapplicationTables) - Write queries and mutations in
convex/*.tsfiles - Integrate with React using
convex/reacthooks - Deploy functions with
[package-manager] run deploy:functions]
Common Setup Patterns
Pattern 1: Minimal Setup with Auth
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
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,
});
Pattern 2: With AI/RAG
Requires:
OPENAI_API_KEYin environmentENABLE_RAG=true- Embeddings generation script
Quick Setup Command Sequence
For a complete fresh setup:
# 1. Install dependencies
[package-manager] add convex @convex-dev/auth
[package-manager] add -D @types/node typescript
# 2. Create directories
mkdir -p convex lib scripts
# 3. Create schema with auth
cat > convex/schema.ts << 'EOF'
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
const applicationTables = {
tasks: defineTable({
title: v.string(),
status: v.string(),
}).index("by_status", ["status"]),
};
export default defineSchema({
...authTables,
...applicationTables,
});
EOF
# 4. Create auth file
cat > convex/auth.ts << 'EOF'
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);
},
});
EOF
# 5. Create HTTP router files
cat > convex/router.ts << 'EOF'
import { httpRouter } from "convex/server";
const http = httpRouter();
export default http;
EOF
cat > convex/http.ts << 'EOF'
import { auth } from "./auth";
import router from "./router";
const http = router;
auth.addHttpRoutes(http);
export default http;
EOF
# 6. Create setup script (copy from Step 5 above)
# ...
# 7. Create docker-compose file (copy from Step 7 above)
# ...
# 8. Run setup
[package-manager] run convex:start
# 9. Initialize env vars and deploy
bash scripts/init-convex-env.sh
[package-manager] run deploy:functions
Summary
This skill covers the one-time setup of self-hosted Convex in Coder workspaces:
- Install dependencies (including
@convex-dev/auth) - Create directory structure
- Define schema with auth tables
- Configure auth (
convexAuth()inauth.ts,router.ts,http.ts) - Create Coder-specific setup script
- Configure Docker with proper flags
- Generate environment variables
- Initialize deployment environment variables
- Deploy functions
- Verify setup
For everyday Convex development (queries, mutations, React integration, etc.), use the coder-convex skill instead.
Working Example Reference
For a complete, working implementation of self-hosted Convex in a Coder workspace, you can reference:
This project demonstrates:
- Self-hosted Convex deployment with Docker Compose
- Complete authentication setup using
@convex-dev/auth - Coder workspace environment configuration
- PostgreSQL database integration
- React frontend with Convex integration
start.shandstop.shscripts that fully sequence the initialization (env files, admin key generation, deployment)
Use this reference to:
- See how all the pieces connect in a real project
- Verify your setup against a working implementation
- Copy configuration patterns (docker-compose, environment setup, scripts)
- Reference the
start.shscript for the complete initialization sequence
Note: This is a demonstration project. Follow the setup steps in this skill for your own project rather than cloning the repo directly.
Key Differences from Standard Convex
| Aspect | Standard Convex | Coder Convex |
|---|---|---|
| Deployment URL | *.convex.cloud |
Custom Coder proxy URL |
| Environment Variables | CONVEX_DEPLOYMENT |
CONVEX_CLOUD_ORIGIN, CONVEX_SITE_ORIGIN |
| Auth Configuration | Uses Convex Cloud | Uses convexAuth() with providers, CONVEX_SITE_ORIGIN (site proxy, port 3211) |
| Site Proxy Port | Not applicable | 3211 |
| Dashboard | Web dashboard at convex.dev | Local at localhost:6791 |
| Setup Script | Guided in dashboard | Custom setup-convex.sh script |