mcpField
C
convex-quickstart

convex-quickstart

github.com/get-convex開発117219 回インストール2 日前に更新
リポジトリ

convex-quickstart について

React、Next.js、Expo プロジェクトで Convex バックエンドを導入する開発者向けのガイドスキルです。初期化からスキーマ、認証、CRUD、開発と本番の手順まで扱います。

主な機能

  1. 01Convex プロジェクトの初期化
  2. 02スキーマと認証設定の案内
  3. 03CRUD クエリとミューテーションの例
  4. 04React hooks による Convex 連携
  5. 05開発用と本番用コマンドの区別

活用シーン

  1. 01新しい Convex アプリの開始
  2. 02既存 React アプリへの Convex 追加
  3. 03リアルタイムデータ機能の試作
  4. 04実際の構成を通じた Convex の学習

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/get-convex/convex-agent-plugins --skill convex-quickstart

インストール後、Claude Code で/convex-quickstartを入力して読み込まれたか確認します。

詳細

Convex クイックスタート

本番環境に対応した Convex バックエンドを数分でセットアップできます。このスキルでは、Convex の初期化、スキーマの作成、認証のセットアップ、最初の CRUD 操作の構築を順を追って説明します。

使用する場面

  • Convex を使った新しいプロジェクトを始めるとき
  • 既存の React/Next.js アプリに Convex を追加するとき
  • リアルタイムデータを使った新機能をプロトタイプするとき
  • 別のバックエンドから Convex に移行するとき
  • Convex を初めて誰かに教えるとき

前提条件の確認

始める前に、次の項目を確認してください。

node --version  # v18 or higher
npm --version   # v8 or higher

クイックスタートの流れ

ステップ 1:インストールと初期化
# Install Convex
npm install convex

# Initialize (creates convex/ directory)
npx convex dev

このコマンドでは、次の処理を行います。

  • convex/ ディレクトリを作成します
  • 認証をセットアップします
  • 開発サーバーを起動します
  • TypeScript 型を生成します
ステップ 2:スキーマを作成する

convex/schema.ts を作成します。

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

export default defineSchema({
  users: defineTable({
    tokenIdentifier: v.string(),
    name: v.string(),
    email: v.string(),
  }).index("by_token", ["tokenIdentifier"]),

  // Add your tables here
  // Example: Tasks table
  tasks: defineTable({
    userId: v.id("users"),
    title: v.string(),
    completed: v.boolean(),
    createdAt: v.number(),
  })
    .index("by_user", ["userId"])
    .index("by_user_and_completed", ["userId", "completed"]),
});
ステップ 3:認証をセットアップする

セットアップを最小限に抑えながら、認証機能一式を提供する WorkOS AuthKit を使用します。

npm install @workos-inc/authkit-react
React/Vite アプリの場合:
// src/main.tsx
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

// Configure Convex to use WorkOS auth
convex.setAuth(useAuth);

function App() {
  return (
    <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
      <ConvexProvider client={convex}>
        <YourApp />
      </ConvexProvider>
    </AuthKitProvider>
  );
}
Next.js アプリの場合:
npm install @workos-inc/authkit-nextjs
// app/layout.tsx
import { AuthKitProvider } from "@workos-inc/authkit-nextjs";
import { ConvexClientProvider } from "./ConvexClientProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AuthKitProvider>
          <ConvexClientProvider>
            {children}
          </ConvexClientProvider>
        </AuthKitProvider>
      </body>
    </html>
  );
}
// app/ConvexClientProvider.tsx
"use client";

import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";
import { useAuth } from "@workos-inc/authkit-nextjs";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
  const { getToken } = useAuth();

  convex.setAuth(async () => {
    return await getToken();
  });

  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
環境変数:
# .env.local
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id

# For Next.js:
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
NEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_API_KEY=your_workos_api_key
WORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string

認証プロバイダーの代替案: 別のプロバイダー(Clerk、Auth0、カスタム JWT)を使う場合は、Convex の認証ドキュメントを参照してください。

ステップ 4:認証ヘルパーを作成する

convex/lib/auth.ts を作成します。

import { QueryCtx, MutationCtx } from "../_generated/server";
import { Doc } from "../_generated/dataModel";

export async function getCurrentUser(
  ctx: QueryCtx | MutationCtx
): Promise<Doc<"users">> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new Error("Not authenticated");
  }

  const user = await ctx.db
    .query("users")
    .withIndex("by_token", q =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();

  if (!user) {
    throw new Error("User not found");
  }

  return user;
}

convex/users.ts を作成します。

import { mutation } from "./_generated/server";

export const store = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");

    const existing = await ctx.db
      .query("users")
      .withIndex("by_token", q =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (existing) return existing._id;

    return await ctx.db.insert("users", {
      tokenIdentifier: identity.tokenIdentifier,
      name: identity.name ?? "Anonymous",
      email: identity.email ?? "",
    });
  },
});
ステップ 5:最初の CRUD 操作を作成する

convex/tasks.ts を作成します。

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";

// List all tasks for current user
export const list = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUser(ctx);
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", q => q.eq("userId", user._id))
      .order("desc")
      .collect();
  },
});

// Get a single task
export const get = query({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    const task = await ctx.db.get(args.taskId);

    if (!task) throw new Error("Task not found");
    if (task.userId !== user._id) throw new Error("Unauthorized");

    return task;
  },
});

// Create a task
export const create = mutation({
  args: { title: v.string() },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    return await ctx.db.insert("tasks", {
      userId: user._id,
      title: args.title,
      completed: false,
      createdAt: Date.now(),
    });
  },
});

// Update a task
export const update = mutation({
  args: {
    taskId: v.id("tasks"),
    title: v.optional(v.string()),
    completed: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    const task = await ctx.db.get(args.taskId);

    if (!task) throw new Error("Task not found");
    if (task.userId !== user._id) throw new Error("Unauthorized");

    const updates: any = {};
    if (args.title !== undefined) updates.title = args.title;
    if (args.completed !== undefined) updates.completed = args.completed;

    await ctx.db.patch(args.taskId, updates);
  },
});

// Delete a task
export const remove = mutation({
  args: { taskId: v.id("tasks") },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    const task = await ctx.db.get(args.taskId);

    if (!task) throw new Error("Task not found");
    if (task.userId !== user._id) throw new Error("Unauthorized");

    await ctx.db.delete(args.taskId);
  },
});
ステップ 6:React アプリで使用する
// app/tasks/page.tsx
"use client";

import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";

export default function TasksPage() {
  const tasks = useQuery(api.tasks.list);
  const create = useMutation(api.tasks.create);
  const update = useMutation(api.tasks.update);
  const remove = useMutation(api.tasks.remove);

  if (!tasks) return <div>Loading...</div>;

  return (
    <div>
      <h1>Tasks</h1>

      {/* Create task */}
      <form onSubmit={(e) => {
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        create({ title: formData.get("title") as string });
        (e.target as HTMLFormElement).reset();
      }}>
        <input name="title" placeholder="New task" required />
        <button type="submit">Add</button>
      </form>

      {/* Task list */}
      {tasks.map(task => (
        <div key={task._id}>
          <input
            type="checkbox"
            checked={task.completed}
            onChange={(e) => update({
              taskId: task._id,
              completed: e.target.checked
            })}
          />
          <span>{task.title}</span>
          <button onClick={() => remove({ taskId: task._id })}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}
ステップ 7:開発環境と本番環境

開発環境の場合(こちらを使ってください!):

# Start development server (NOT production!)
npx convex dev

# This runs locally and auto-reloads on changes
# Use this for all development work

本番環境へのデプロイの場合:

# ONLY use this when deploying to production!
npx convex deploy

# WARNING: This deploys to your production environment
# Don't use this during development

重要: 開発中は必ず npx convex dev を使用してください。npx convex deploy は、本番環境にリリースする準備が整ったときにのみ使用してください。

よく使われるパターン

ページネーション付きクエリ
export const listPaginated = query({
  args: {
    cursor: v.optional(v.string()),
    limit: v.number(),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    const results = await ctx.db
      .query("tasks")
      .withIndex("by_user", q => q.eq("userId", user._id))
      .order("desc")
      .paginate({ cursor: args.cursor, limit: args.limit });

    return results;
  },
});
ステータスによる絞り込み
export const listByStatus = query({
  args: {
    completed: v.boolean(),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);

    return await ctx.db
      .query("tasks")
      .withIndex("by_user_and_completed", q =>
        q.eq("userId", user._id).eq("completed", args.completed)
      )
      .collect();
  },
});
スケジュール済みジョブ
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.daily(
  "cleanup-old-tasks",
  { hourUTC: 0, minuteUTC: 0 },
  internal.tasks.cleanupOld
);

export default crons;

// convex/tasks.ts
export const cleanupOld = internalMutation({
  handler: async (ctx) => {
    const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
    const oldTasks = await ctx.db
      .query("tasks")
      .filter(q =>
        q.and(
          q.eq(q.field("completed"), true),
          q.lt(q.field("createdAt"), thirtyDaysAgo)
        )
      )
      .collect();

    for (const task of oldTasks) {
      await ctx.db.delete(task._id);
    }
  },
});

プロジェクトテンプレート

React + Vite + Convex
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install convex @workos-inc/authkit-react
npx convex dev
Next.js + Convex
npx create-next-app@latest my-app
cd my-app
npm install convex @workos-inc/authkit-nextjs
npx convex dev
Expo(React Native)+ Convex
npx create-expo-app my-app
cd my-app
npm install convex
npx convex dev

チェックリスト

  • npm install convex を実行済み
  • npx convex dev を実行中(こちらを使用してください。deploy は使用しないでください!)
  • 適切なインデックスを含むスキーマを作成済み
  • 認証プロバイダーを設定済み(WorkOS/カスタム)
  • getCurrentUser ヘルパーを実装済み
  • ユーザー保存用の mutation を作成済み
  • 認証チェック付きの CRUD 操作を実装済み
  • フックを使ってフロントエンドと連携済み
  • クエリと mutation をローカルでテスト済み
  • 本番環境への準備が整ったら、npx convex deploy を使用する

忘れないでください: 開発作業ではすべて npx convex dev を使用してください。npx convex deploy は、本番環境にデプロイするときにのみ使用してください!

次のステップ

クイックスタートの後に行うこと:

  1. スキーマにテーブルを追加する
  2. テーブル間のリレーションを実装する
  3. ファイルストレージを追加する(Convex はファイルのアップロードをサポートしています)
  4. AI 機能向けにベクトル検索をセットアップする
  5. スケジュールされたタスク用に cron ジョブを追加する
  6. 本番環境の環境変数を設定する

トラブルシューティング

「Not authenticated」エラー
  • 認証プロバイダーが設定されていることを確認します
  • 初回サインイン時に storeUser mutation を呼び出します
  • getCurrentUser が正しくインポートされていることを確認します
型が更新されない
  • npx convex dev を実行します(型が再生成されます)
  • エディターで TypeScript サーバーを再起動します
クエリが遅い
  • スキーマにインデックスを追加します
  • .filter() の代わりに .withIndex() を使用します
  • ダッシュボードでクエリのパターンを確認します

詳細情報

コメント