convex-backend について
convex-backend は、Convex で型安全なバックエンドを構築するためのスキルです。リアクティブなアプリを開発する人向けに、クエリ、ミューテーション、アクション、認証、デプロイを扱います。
主な機能
- 01型安全なスキーマとサーバー関数
- 02リアルタイムのリアクティブクエリ
- 03ミューテーションと外部 API アクション
- 04スケジュール関数と Cron ジョブ
- 05認証とファイルストレージ
- 06React クライアントとデプロイのガイド
活用シーン
- 01共同作業用チャットやダッシュボードの構築
- 02AI の状態やストリーミング結果の追跡
- 03マネージドデータと関数による迅速な試作
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill convex-backendインストール後、Claude Code で/convex-backendを入力して読み込まれたか確認します。
詳細
Convex バックエンド
Convex を使用して、型安全なバックエンドロジックとリアルタイムのデータ同期を構築します。
このスキルを使う場面
次のような場合にこのスキルを使用します:
- リアルタイムの共同作業アプリ(チャット、ダッシュボード、マルチプレイヤー)を構築する
- インフラ管理が不要なバックエンドが必要
- 自動キャッシュ機能を備えた型安全なサーバー関数が必要
- リアクティブなデータ(エージェントのステータス、ストリーミング結果)を扱う AI アプリを構築する
- マネージドデータベースと関数を使ってすばやくプロトタイプを作成する
前提条件
- Node.js 18+
- npm または pnpm
- Convex アカウント(無料プラン:月あたり 100 万回の関数呼び出し)
クイックスタート
# 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);
},
});
ベストプラクティス
- 関数を記述する前にスキーマと検証を定義します
- 可能な限りミューテーションを冪等に保ちます
- 権限が必要なすべてのクエリ/ミューテーションで認証 ID を確認します
- 読み取り頻度の高いコレクションには早めにインデックスを追加します
- サーバー専用ロジック(Cron、Webhook)には
internal関数を使用します - シークレットは Convex の環境変数に保存し、コードには記述しません
- React クライアントで楽観的更新を使用して、UI に即座に反映します
トラブルシューティング
| 問題 | 解決方法 |
|---|---|
| 関数がタイムアウトする | アクションの制限時間は 10 分です。より小さなステップに分割します |
| クエリが遅すぎる | クエリのパターンに合ったデータベースインデックスを追加します |
| 型エラーが発生する | npx convex dev を実行して型を再生成します |
| 認証が機能しない | auth.config.ts とプロバイダーのドメインを確認します |
| デプロイに失敗する | npx convex logs を確認し、環境変数が設定されていることを確かめます |
関連スキル
- firebase-app-platform — 代替のマネージドバックエンド
- vercel-deployments — フロントエンドのホスティング
- agent-observability — AI 駆動のバックエンドフローを計測します
コメント
Markdown 対応