agentsclimarketplace

Sc convex

Skill rahmanef63/si-coder-agent/skills/sc-convex

Convex self-hosted operations on Dokploy. Deploy compose template, rotate admin key, push schema, configure @convex-dev/auth (JWT_PRIVATE_KEY + JWKS), and probe the three backend subdomains (api-, site-, dash-).From its SKILL.md

Install
npx -y skills add rahmanef63/si-coder-agent --skill sc-convex

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

4 things to look at

  • skips confirmationTells the agent to proceed without asking first, 2 times: "If the hook is not installed, install it yourself instead of asking" and 1 more.
  • reads credentialsReads from 8 credential sources: `.env.local` and 7 more.
  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 7 commands, including `node ~/.claude/skills/sc-git/scripts/hook.js install --repo <name>` and 6 more.

SKILL.md

8.4 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it

/sc-convex — Convex on Dokploy

Use this skill when the user wants to deploy, debug, or maintain a self-hosted Convex backend running on Dokploy. The repo lives at https://github.com/rahmanef63/si-coder-agent.

NEVER ask the user to run Convex CLI by hand

For self-hosted Convex projects the Convex CLI (v1.27+) auto-detects the backend from .env.local (CONVEX_SELF_HOSTED_URL + CONVEX_SELF_HOSTED_ADMIN_KEY). The schema/function push happens automatically via the sc-git pre-push hook installed by /sc-all. Do not instruct the user to run npx convex deploy, pnpm convex:deploy, convex:push, or any Convex CLI command interactively. If the hook is not installed, install it yourself (node ~/.claude/skills/sc-git/scripts/hook.js install --repo <name>) instead of asking.

If the hook is genuinely unable to deploy (admin key invalid, backend unreachable), debug it with scripts/check-backend.js and fix root cause — do not punt the convex CLI call to the user.

Pre-requisites

  • DOKPLOY_API_URL, DOKPLOY_API_KEY — Dokploy admin
  • CONVEX_ADMIN_KEY (or generated on the fly from the running container)
  • Optional HOSTINGER_API_TOKEN for DNS A-record automation

If anything is missing, route the user to /sc-onboarding.

CORE MANDATES (LEARNED LESSONS)

  1. Self-Hosted Convex by default: never silently swap to Clerk. ALWAYS use @convex-dev/auth for new projects unless the user explicitly asks for Clerk.
  2. convex/_generated is checked in: never run npx convex codegen inside Dockerfile. Generate locally with npx convex dev --once and commit the folder.
  3. Admin Key Sync Rule: when generating or rotating the admin key, update BOTH the Dokploy Compose env (CONVEX_ADMIN_KEY) and the repo's local env file in a single pass. Never leave them out-of-sync.
  4. PEM via REST, not CLI: npx convex env set JWT_PRIVATE_KEY "-----BEGIN..." breaks because -- is parsed as a flag. Use scripts/set-auth-env.js (admin REST /api/update_environment_variables) instead.
  5. NEXT_PUBLIC_CONVEX_URL at build time: NEXT_PUBLIC_* is inlined at next build. If the Dockerfile uses a dummy URL, the deployed JS connects to the wrong server — see ## Dockerfile pattern below.
  6. No Scrypt/bcrypt password hashing: Dokploy proxy kills WebSocket at >60s. Use PBKDF2 (10k iter ≈ 50ms) via WebCrypto — see ## Password hashing below.
  7. Route auth:* actions via HTTP: WebSocket reconnect mid-flight aborts in-flight actions. Wrap the Convex client so auth:* goes via ConvexHttpClient — see ## Frontend pattern below.

Required env vars on the Convex backend

These live as Dokploy Compose env (NOT in .env.local):

VariableFormatPurpose
JWT_PRIVATE_KEYPEM PKCS8 RSASigns JWT via importPKCS8()
JWKSJSON {"keys":[{...}]}Served at /.well-known/jwks.json
CONVEX_SITE_ORIGINURLMaps to process.env.CONVEX_SITE_URL
CONVEX_CLOUD_ORIGINURLMaps to process.env.CONVEX_CLOUD_URL
INSTANCE_SECREThexPreserve across redeploys
INSTANCE_NAMEstringApp name

Scripts

All scripts live in ~/.claude/skills/sc-convex/scripts/ (symlink from this repo).

deploy-convex.js

Deploy or update a Convex compose service on Dokploy. Idempotent — finds existing compose by name or deploys the Convex template, then sets api-/site-/dash- domains + INSTANCE_SECRET + auth env.

node scripts/deploy-convex.js \
  --project <PROJECT_NAME> \
  --app <APP_NAME> \
  --domain <root-domain.tld> \
  [--with-auth-keys]   # also generate JWT_PRIVATE_KEY + JWKS

check-backend.js

Probe api-/site-/dash- subdomains + /version + /.well-known/jwks.json + admin-key validity. Prints a status table.

node scripts/check-backend.js --domain <root-domain.tld> [--admin-key "$CONVEX_ADMIN_KEY"] [--expect-auth]

--expect-auth makes a missing/non-200 JWKS (/.well-known/jwks.json) fatal; omit it and JWKS is advisory only (informational, non-fatal) — a backend deployed without @convex-dev/auth legitimately has no JWKS.

rotate-admin-key.js

Generate a fresh admin key from the running backend container, write to Dokploy Compose env, optionally update local .env.

node scripts/rotate-admin-key.js --compose-name <APP_NAME>-db [--env-file ./.env.local]

SC_DOCKER_SUDO1 force sudo for docker, 0 forbid, unset = auto-detect (used by dockerExec, lib/proc.js).

set-auth-env.js

Set JWT_PRIVATE_KEY and JWKS (and any extra vars) on a self-hosted backend via admin REST API. Avoids the CLI -- parsing bug.

node scripts/set-auth-env.js --domain <root-domain.tld> --admin-key "$CONVEX_ADMIN_KEY" [--generate]

--generate makes new RS256 keypair; omit to pull from JWT_PRIVATE_KEY / JWKS env.

Dockerfile pattern (NEXT_PUBLIC_CONVEX_URL)

# ❌ WRONG — dummy URL inlined into JS
ENV NEXT_PUBLIC_CONVEX_URL=https://dummy-for-build.convex.cloud

# ✅ CORRECT — ARG default + ENV; Dokploy build-arg overrides
ARG NEXT_PUBLIC_CONVEX_URL=https://api-<appname>.<your-domain>
ENV NEXT_PUBLIC_CONVEX_URL=$NEXT_PUBLIC_CONVEX_URL

Frontend pattern (ConvexClientProvider.tsx)

"use client";
import { ConvexReactClient } from "convex/react";
import { ConvexHttpClient } from "convex/browser";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
import { type ReactNode, useState } from "react";

export function ConvexClientProvider({ children }: { children: ReactNode }) {
  const [convex] = useState(() => {
    const client = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
    const http = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
    const orig = client.action.bind(client);
    (client as any).action = (ref: any, args?: any) => {
      const name = (ref as any)?._name ?? String(ref);
      if (typeof name === "string" && name.startsWith("auth:")) return http.action(ref as any, args);
      return orig(ref, args);
    };
    return client;
  });
  return <ConvexAuthProvider client={convex}>{children}</ConvexAuthProvider>;
}

Password hashing (PBKDF2 via WebCrypto)

// convex/auth.ts — inside Password({ ... })
crypto: {
  async hashSecret(password: string) {
    const salt = crypto.getRandomValues(new Uint8Array(16));
    const enc = new TextEncoder();
    const km = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
    const hb = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 10000, hash: "SHA-256" }, km, 256);
    const hex = (b: Uint8Array) => Array.from(b).map(x => x.toString(16).padStart(2, "0")).join("");
    return `pbkdf2_${hex(salt)}_${hex(new Uint8Array(hb))}`;
  },
  async verifySecret(password: string, hash: string) {
    if (hash.startsWith("pt_")) return hash === `pt_${password}`;
    const parts = hash.split("_");
    if (parts[0] !== "pbkdf2" || parts.length !== 3) return false;
    const salt = new Uint8Array(parts[1].match(/.{2}/g)!.map(b => parseInt(b, 16)));
    const enc = new TextEncoder();
    const km = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
    const hb = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 10000, hash: "SHA-256" }, km, 256);
    const hex = Array.from(new Uint8Array(hb)).map(b => b.toString(16).padStart(2, "0")).join("");
    return hex === parts[2];
  },
},

"Connection lost while action was in flight" — diagnosis

WebSocket reconnect during in-flight action. The server may have completed the work (user visible in dashboard!) but the client never got the response. Common causes for self-hosted Dokploy, in order of likelihood:

CauseSymptomFix
NEXT_PUBLIC_CONVEX_URL dummy at buildUsers in DB, browser errorsFix Dockerfile (see above)
Missing JWT_PRIVATE_KEY on backendAction crashes before creating userscripts/set-auth-env.js --generate
Missing JWKS on backend/.well-known/jwks.json returns 500scripts/set-auth-env.js --generate
Dokploy idle WS timeoutError only after sittingRoute auth:* via HTTP (see above)
Scrypt/bcrypt timeoutAction >60s, proxy kills WSUse PBKDF2 pattern (see above)

What ships with it: 4 files

22.2 KB alongside SKILL.md, 4 of them executable

scripts/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.