mcpField
C
convex-backend

convex-backend

github.com/bagelhole开发1.2k307 次安装23 周前更新
代码仓库

关于 convex-backend

convex-backend 是一项使用 Convex 构建类型安全后端的技能,适合开发实时响应式应用的开发者。内容涵盖查询、写入、操作、定时函数、身份验证与部署。

主要功能

  1. 01类型安全的架构与服务端函数
  2. 02响应式实时查询
  3. 03写入操作与外部 API 调用
  4. 04定时函数与 Cron 任务
  5. 05身份验证与文件存储
  6. 06React 客户端及部署指南

适用场景

  1. 01构建协作聊天和仪表盘
  2. 02跟踪 AI 状态与流式结果
  3. 03使用托管数据和函数快速原型开发

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bagelhole/devops-security-agent-skills --skill convex-backend

安装后在 Claude Code 中输入/convex-backend确认已加载。

详细介绍

Convex 后端

使用 Convex 构建类型安全的后端逻辑,并实现实时数据同步。

何时使用此技能

在以下情况下使用此技能:

  • 构建实时协作应用(聊天、仪表板、多人游戏)
  • 需要无需管理基础设施的后端
  • 希望使用支持自动缓存的类型安全服务端函数
  • 构建需要响应式数据的 AI 应用(Agent 状态、流式结果)
  • 使用托管数据库和函数快速构建原型

前提条件

  • Node.js 18+
  • npm 或 pnpm
  • Convex 账号(免费套餐:每月 1M 次函数调用)

快速开始

# Initialize Convex in an existing project
npm install convex
npx convex dev     # Start local development (syncs with cloud)

# In a new project
npm create convex@latest

架构定义

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    role: v.union(v.literal("admin"), v.literal("member")),
    avatarUrl: v.optional(v.string()),
    createdAt: v.number(),
  })
    .index("by_email", ["email"])
    .index("by_role", ["role"]),

  messages: defineTable({
    userId: v.id("users"),
    channelId: v.id("channels"),
    body: v.string(),
    attachments: v.optional(v.array(v.string())),
    createdAt: v.number(),
  })
    .index("by_channel", ["channelId", "createdAt"])
    .index("by_user", ["userId"]),

  channels: defineTable({
    name: v.string(),
    description: v.optional(v.string()),
    isPrivate: v.boolean(),
  }),
});

查询(实时读取)

// convex/messages.ts
import { query } from "./_generated/server";
import { v } from "convex/values";

export const listByChannel = query({
  args: {
    channelId: v.id("channels"),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .take(args.limit ?? 50);

    // Resolve user data for each message
    return Promise.all(
      messages.map(async (msg) => {
        const user = await ctx.db.get(msg.userId);
        return { ...msg, user: user ? { name: user.name, avatarUrl: user.avatarUrl } : null };
      })
    );
  },
});

修改(写入)

// convex/messages.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const send = mutation({
  args: {
    channelId: v.id("channels"),
    body: v.string(),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");

    // Find or create user
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", identity.email!))
      .unique();
    if (!user) throw new Error("User not found");

    return await ctx.db.insert("messages", {
      userId: user._id,
      channelId: args.channelId,
      body: args.body,
      createdAt: Date.now(),
    });
  },
});

操作(外部 API、AI)

// convex/ai.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { api } from "./_generated/api";

export const generateResponse = action({
  args: { prompt: v.string(), channelId: v.id("channels") },
  handler: async (ctx, args) => {
    // Call external AI API
    const response = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": process.env.ANTHROPIC_API_KEY!,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        messages: [{ role: "user", content: args.prompt }],
      }),
    });

    const data = await response.json();
    const aiMessage = data.content[0].text;

    // Save AI response as a message via mutation
    await ctx.runMutation(api.messages.send, {
      channelId: args.channelId,
      body: aiMessage,
    });

    return aiMessage;
  },
});

定时函数(Cron 作业)

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

// Run every hour
crons.interval("cleanup old messages", { hours: 1 }, internal.maintenance.cleanupOldMessages);

// Run daily at midnight UTC
crons.cron("daily report", "0 0 * * *", internal.reports.generateDailyReport);

export default crons;

身份验证集成

// convex/auth.config.ts
export default {
  providers: [
    {
      domain: process.env.AUTH_DOMAIN,
      applicationID: "convex",
    },
  ],
};
// React client setup
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ClerkProvider, useAuth } from "@clerk/clerk-react";

function App() {
  return (
    <ClerkProvider publishableKey={CLERK_KEY}>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        <MyApp />
      </ConvexProviderWithClerk>
    </ClerkProvider>
  );
}

React 客户端用法

// src/components/Chat.tsx
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

export function Chat({ channelId }: { channelId: string }) {
  // Real-time query — auto-updates when data changes
  const messages = useQuery(api.messages.listByChannel, { channelId });
  const sendMessage = useMutation(api.messages.send);

  const handleSend = async (body: string) => {
    await sendMessage({ channelId, body });
  };

  if (messages === undefined) return <div>Loading...</div>;

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg._id}>
          <strong>{msg.user?.name}</strong>: {msg.body}
        </div>
      ))}
    </div>
  );
}

部署

# Deploy to production
npx convex deploy

# Deploy with environment variables
npx convex deploy --env-file .env.production

# Set environment variables
npx convex env set ANTHROPIC_API_KEY sk-ant-...
npx convex env list

# View logs
npx convex logs
npx convex logs --follow

# Run a function manually
npx convex run messages:listByChannel '{"channelId": "abc123"}'

文件存储

// convex/files.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const generateUploadUrl = mutation(async (ctx) => {
  return await ctx.storage.generateUploadUrl();
});

export const getFileUrl = query({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    return await ctx.storage.getUrl(args.storageId);
  },
});

最佳实践

  • 在编写函数之前定义架构和验证规则
  • 尽可能确保修改操作具备幂等性
  • 在每个受限查询/修改操作中检查身份验证信息
  • 尽早为读取频繁的集合添加索引
  • 使用 internal 函数处理仅限服务端的逻辑(Cron 作业、Webhook)
  • 将密钥存储在 Convex 环境变量中,切勿写入代码
  • 在 React 客户端中使用乐观更新,立即反馈 UI 变化

故障排除

问题解决方案
函数超时操作的时限为 10 分钟;将任务拆分为更小的步骤
查询速度太慢添加与查询模式匹配的数据库索引
类型错误运行 npx convex dev 以重新生成类型
身份验证不起作用检查 auth.config.ts 和提供方域名
部署失败检查 npx convex logs,确认已设置环境变量

相关技能

评论

支持 Markdown