install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convex-integration" ~/.claude/skills/majiayu000-claude-skill-registry-convex-integration && rm -rf "$T"
manifest:
skills/data/convex-integration/SKILL.mdsource content
name: convex-integration description: Integrate Convex as the real-time backend (schema, queries, auth). Use when adding Convex persistence or wiring Convex APIs.
Convex Integration Skill
This skill provides guidance for integrating Convex as the real-time database backend for the RFP Discovery application.
Installation
npm install convex npx convex dev
Schema Design
Create
convex/schema.ts with the following tables:
RFP Table
import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ rfps: defineTable({ // Core fields externalId: v.string(), // ID from source platform source: v.string(), // "sam.gov", "rfpmart", "emma", etc. title: v.string(), summary: v.string(), url: v.string(), // Dates postedDate: v.optional(v.string()), deadline: v.optional(v.string()), questionDeadline: v.optional(v.string()), // Location/Category location: v.optional(v.string()), category: v.optional(v.string()), state: v.optional(v.string()), country: v.optional(v.string()), // Budget budget: v.optional(v.string()), // Eligibility eligibility: v.optional(v.string()), isUsaOnly: v.optional(v.boolean()), requiresOnshore: v.optional(v.boolean()), setAsideType: v.optional(v.string()), // Metadata fetchedAt: v.number(), rawData: v.optional(v.string()), }).index("by_external_id", ["externalId", "source"]) .index("by_source", ["source"]) .index("by_deadline", ["deadline"]), evaluations: defineTable({ rfpId: v.id("rfps"), userId: v.optional(v.string()), // Clerk user ID // Overall result isFit: v.boolean(), score: v.number(), maxScore: v.number(), // Per-criterion results technicalRelevance: v.object({ met: v.boolean(), details: v.optional(v.string()), }), scopeFit: v.object({ met: v.boolean(), details: v.optional(v.string()), }), categoryFocus: v.object({ met: v.boolean(), details: v.optional(v.string()), }), clientProfile: v.object({ met: v.boolean(), details: v.optional(v.string()), }), logistics: v.object({ met: v.boolean(), details: v.optional(v.string()), }), skillSetAlignment: v.object({ met: v.boolean(), details: v.optional(v.string()), }), // AI analysis aiProvider: v.optional(v.string()), aiAnalysis: v.optional(v.string()), // JSON string reasoning: v.optional(v.string()), // Timestamps evaluatedAt: v.number(), }).index("by_rfp", ["rfpId"]) .index("by_user", ["userId"]), pursuits: defineTable({ rfpId: v.id("rfps"), userId: v.string(), // Clerk user ID // Pipeline stage stage: v.union( v.literal("new"), v.literal("triage"), v.literal("bid"), v.literal("no_bid"), v.literal("capture"), v.literal("submitted"), v.literal("won"), v.literal("lost") ), // Decision tracking decision: v.optional(v.union( v.literal("pursue"), v.literal("partner_needed"), v.literal("reject") )), decisionReason: v.optional(v.string()), // Notes notes: v.optional(v.string()), // Timestamps createdAt: v.number(), updatedAt: v.number(), }).index("by_user", ["userId"]) .index("by_stage", ["stage"]), userSettings: defineTable({ userId: v.string(), // Clerk user ID // AI Settings selectedAiProvider: v.string(), aiProviderConfigs: v.optional(v.string()), // JSON corePromptTemplate: v.optional(v.string()), useAiForEvaluation: v.boolean(), // Criteria Config criteriaConfig: v.optional(v.string()), // JSON // Refresh Settings autoRefreshIntervalHours: v.number(), // UI Preferences theme: v.union(v.literal("light"), v.literal("dark")), }).index("by_user", ["userId"]), });
Query Patterns
Fetching RFPs with Evaluations
// convex/rfps.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const listWithEvaluations = query({ args: { source: v.optional(v.string()), limit: v.optional(v.number()) }, handler: async (ctx, args) => { let rfpsQuery = ctx.db.query("rfps"); if (args.source) { rfpsQuery = rfpsQuery.withIndex("by_source", (q) => q.eq("source", args.source) ); } const rfps = await rfpsQuery .order("desc") .take(args.limit ?? 50); // Fetch evaluations for each RFP const rfpsWithEvals = await Promise.all( rfps.map(async (rfp) => { const evaluation = await ctx.db .query("evaluations") .withIndex("by_rfp", (q) => q.eq("rfpId", rfp._id)) .first(); return { ...rfp, evaluation }; }) ); return rfpsWithEvals; }, });
Mutation Patterns
Saving an Evaluation
// convex/evaluations.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; export const saveEvaluation = mutation({ args: { rfpId: v.id("rfps"), isFit: v.boolean(), score: v.number(), maxScore: v.number(), criteriaResults: v.object({ technicalRelevance: v.object({ met: v.boolean(), details: v.optional(v.string()) }), scopeFit: v.object({ met: v.boolean(), details: v.optional(v.string()) }), categoryFocus: v.object({ met: v.boolean(), details: v.optional(v.string()) }), clientProfile: v.object({ met: v.boolean(), details: v.optional(v.string()) }), logistics: v.object({ met: v.boolean(), details: v.optional(v.string()) }), skillSetAlignment: v.object({ met: v.boolean(), details: v.optional(v.string()) }), }), aiProvider: v.optional(v.string()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); return await ctx.db.insert("evaluations", { rfpId: args.rfpId, userId: identity?.subject, isFit: args.isFit, score: args.score, maxScore: args.maxScore, ...args.criteriaResults, aiProvider: args.aiProvider, evaluatedAt: Date.now(), }); }, });
Integration with Clerk
When using Convex with Clerk, configure authentication in
convex/auth.config.js:
export default { providers: [ { domain: "https://your-clerk-domain.clerk.accounts.dev", applicationID: "convex", }, ], };
React Hooks Usage
import { useQuery, useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; function RfpList() { const rfps = useQuery(api.rfps.listWithEvaluations, { limit: 50 }); const saveEvaluation = useMutation(api.evaluations.saveEvaluation); // Component implementation }
Migration Strategy
-
Phase 1: Add Convex alongside existing localStorage
- Create Convex schema
- Add mutations to sync localStorage to Convex
- Keep localStorage as fallback
-
Phase 2: Migrate reads to Convex
- Replace localStorage reads with Convex queries
- Add real-time subscriptions
-
Phase 3: Remove localStorage
- Remove localStorage sync code
- Use Convex as single source of truth