T-CREATOR

【2025 年最新】Convex の全体像を 10 分で理解:リアルタイム DB× 関数基盤の要点まとめ

【2025 年最新】Convex の全体像を 10 分で理解:リアルタイム DB× 関数基盤の要点まとめ

現代の Web アプリケーション開発において、リアルタイム機能の実装は必須要件となりました。しかし、従来の開発手法では複雑なインフラ設定や同期処理が開発者の大きな負担となっています。

そんな中、注目を集めているのが Convex です。リアルタイムデータベースと関数基盤を統合した革新的なプラットフォームとして、開発体験を根本から変える可能性を秘めています。この記事では、Convex の全体像を技術的観点から詳しく解説し、なぜ多くの開発者が注目しているのかを明確にお伝えします。

Convex とは何か

リアルタイムデータベース+関数基盤の統合プラットフォーム

Convex は、リアルタイムデータベースとサーバーレス関数を一つのプラットフォームで提供する統合型の Backend as a Service(BaaS)です。

従来の BaaS サービスとは異なり、Convex はデータベースの変更を自動的にクライアントに同期する機能を標準で提供します。これにより、開発者は複雑な WebSocket や Server-Sent Events の実装を意識することなく、リアルタイムアプリケーションを構築できます。

typescript// Convexでのリアルタイムデータ取得例
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function MessageList() {
  // データベースの変更が自動的に反映される
  const messages = useQuery(api.messages.list);

  return (
    <div>
      {messages?.map((message) => (
        <div key={message._id}>{message.text}</div>
      ))}
    </div>
  );
}

この例では、useQueryフックを使用するだけで、メッセージデータの変更がリアルタイムで UI に反映されます。

従来の BaaS(Backend as a Service)との違い

Convex が他の BaaS サービスと大きく異なる点は、以下の 3 つの要素が緊密に統合されていることです。

機能FirebaseSupabaseConvex
リアルタイム同期限定的WebSocket 手動実装自動同期
関数統合Cloud Functions(別サービス)Edge Functions(別設定)完全統合
型安全性手動管理一部対応自動生成

特に Firebase と比較すると、Convex は Cloud Firestore のようなリアルタイム機能と Cloud Functions のようなサーバーレス処理を、単一のコードベースで管理できる点が画期的です。

typescript// Convexでの関数定義(convex/messages.ts)
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';

// クエリ関数(リアルタイム対応)
export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query('messages').collect();
  },
});

// ミューテーション関数(データ変更)
export const send = mutation({
  args: { text: v.string(), author: v.string() },
  handler: async (ctx, args) => {
    await ctx.db.insert('messages', {
      text: args.text,
      author: args.author,
      timestamp: Date.now(),
    });
  },
});

主要な特徴と強み

Convex の主要な特徴は、以下の 4 つの要素で構成されています。

mermaidflowchart TD
    client[クライアントアプリケーション]
    convex[Convex プラットフォーム]

    subgraph convex_features[Convex の主要機能]
        db[(リアルタイムDB)]
        functions[サーバーレス関数]
        sync[自動同期エンジン]
        auth[認証システム]
    end

    client -->|TypeScript型安全| convex
    db -.->|リアルタイム更新| sync
    functions -.->|データ操作| db
    auth -.->|認可制御| functions
    sync -->|自動反映| client

これらの機能により、開発者は以下の恩恵を受けることができます。

1. 完全な型安全性 Convex は関数の引数、戻り値、データベーススキーマから自動的に TypeScript 型を生成します。

2. リアルタイム同期の自動化 データベースの変更は、関連するすべてのクライアントに自動的に配信されます。

3. 単一コードベース管理 バックエンドロジック、データベーススキーマ、API エンドポイントを一箇所で管理できます。

Convex の背景

モダン Web アプリケーション開発の課題

現代の Web アプリケーション開発では、ユーザー体験の向上を目的としたリアルタイム機能の実装が不可欠となっています。

チャットアプリケーション、コラボレーションツール、ライブダッシュボードなど、多くのアプリケーションでリアルタイムなデータ更新が求められる一方で、従来の開発手法では以下のような課題が顕在化していました。

mermaidflowchart LR
    challenges[開発課題]

    subgraph backend[バックエンド開発の複雑性]
        api[REST API 設計]
        websocket[WebSocket実装]
        cache[キャッシュ管理]
        scale[スケーリング]
    end

    subgraph frontend[フロントエンド開発の負担]
        state[状態管理]
        sync_logic[同期ロジック]
        error_handling[エラーハンドリング]
        optimization[パフォーマンス最適化]
    end

    challenges --> backend
    challenges --> frontend

特に、データの一貫性を保ちながらリアルタイム更新を実現するには、WebSocket の管理、状態同期、エラーハンドリングなど、多岐にわたる技術的考慮が必要でした。

リアルタイム機能実装の複雑さ

従来のリアルタイム機能実装では、開発者が以下の要素を個別に管理する必要がありました。

データベース層

  • データの永続化
  • トランザクション管理
  • インデックス最適化

通信層

  • WebSocket サーバーの構築
  • 接続管理とハートビート
  • メッセージのブロードキャスト

クライアント層

  • 接続状態の監視
  • 再接続ロジック
  • ローカル状態との同期
typescript// 従来のWebSocket実装例(複雑な管理が必要)
class RealtimeManager {
  private ws: WebSocket;
  private reconnectAttempts = 0;
  private messageQueue: any[] = [];

  constructor(url: string) {
    this.connect(url);
  }

  private connect(url: string) {
    this.ws = new WebSocket(url);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.flushMessageQueue();
    };

    this.ws.onclose = () => {
      this.handleReconnect();
    };

    this.ws.onmessage = (event) => {
      this.handleMessage(JSON.parse(event.data));
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  }

  private handleReconnect() {
    if (this.reconnectAttempts < 5) {
      setTimeout(() => {
        this.reconnectAttempts++;
        this.connect(this.ws.url);
      }, Math.pow(2, this.reconnectAttempts) * 1000);
    }
  }

  // ... 更に多くの管理コードが必要
}

このような複雑性が、開発速度の低下とメンテナンスコストの増大を招いていました。

バックエンド開発の工数削減ニーズ

近年の Web 開発では、フロントエンド技術の急速な進歩に対して、バックエンド開発が工数のボトルネックとなるケースが増えています。

特に、スタートアップや小規模チームでは、限られたリソースで MVP(Minimum Viable Product)を迅速に開発することが重要ですが、従来のバックエンド開発では以下の工程に多くの時間を要していました。

開発工程従来の所要時間主な作業内容
インフラ設定1-2 週間サーバー設定、データベース構築、デプロイ環境
API 設計・実装2-3 週間エンドポイント設計、認証実装、バリデーション
リアルタイム機能2-4 週間WebSocket 実装、同期ロジック、エラーハンドリング
テスト・最適化1-2 週間ユニットテスト、統合テスト、パフォーマンス調整

これらの課題から、開発者は以下のような要求を持つようになりました。

  • 迅速なプロトタイピング能力
  • リアルタイム機能の標準化
  • インフラ管理の自動化
  • 開発者体験の向上

Convex が解決する課題

データの一貫性とリアルタイム同期

従来のリアルタイムアプリケーション開発では、データの一貫性を保ちながら複数のクライアント間で同期を取ることが最大の技術的チャレンジでした。

Convex は、この課題を独自のリアクティブアーキテクチャで解決しています。

mermaidsequenceDiagram
    participant C1 as クライアント1
    participant C2 as クライアント2
    participant DB as Convex DB
    participant Engine as 同期エンジン

    C1->>DB: データ更新要求
    DB->>DB: トランザクション処理
    DB->>Engine: 変更通知
    Engine->>C1: 更新完了応答
    Engine->>C2: 自動同期通知
    C2->>C2: UI自動更新

Convex では、データベースへの変更が即座に関連するクライアントに配信され、楽観的更新(Optimistic Updates)と悲観的整合性(Pessimistic Consistency)を組み合わせた仕組みを提供します。

typescript// Convexでのリアルタイム同期例
import { useMutation, useQuery } from 'convex/react';

function TodoApp() {
  // リアルタイムでデータを取得
  const todos = useQuery(api.todos.list);

  // 楽観的更新をサポートするミューテーション
  const addTodo = useMutation(api.todos.add);

  const handleAddTodo = async (text: string) => {
    // 楽観的にUIを更新し、サーバーで検証
    await addTodo({ text, completed: false });
    // 失敗時は自動的にロールバック
  };

  return (
    <div>
      {todos?.map((todo) => (
        <TodoItem key={todo._id} todo={todo} />
      ))}
    </div>
  );
}

この仕組みにより、開発者は複雑な同期ロジックを実装することなく、一貫性のあるリアルタイムアプリケーションを構築できます。

サーバーレス関数の管理複雑性

従来のサーバーレス開発では、関数の定義、デプロイ、監視、ログ管理などを個別に設定する必要がありました。

Convex は、関数の定義からデプロイまでを統一された開発体験で提供し、以下の管理複雑性を解決します。

従来の課題:

  • 複数のサービス間での設定管理
  • 環境変数の分散管理
  • デプロイプロセスの複雑性
  • 関数間の依存関係管理

Convex の解決策:

typescript// 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(),
    createdAt: v.number(),
  }).index('by_email', ['email']),

  messages: defineTable({
    text: v.string(),
    userId: v.id('users'),
    channelId: v.string(),
    timestamp: v.number(),
  }).index('by_channel', ['channelId']),
});
typescript// convex/users.ts - 関数定義
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';

export const create = mutation({
  args: { name: v.string(), email: v.string() },
  handler: async (ctx, args) => {
    // 自動的に型安全性が確保される
    const userId = await ctx.db.insert('users', {
      name: args.name,
      email: args.email,
      createdAt: Date.now(),
    });
    return userId;
  },
});

export const getByEmail = query({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    // インデックスを活用した効率的なクエリ
    return await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', args.email)
      )
      .first();
  },
});

これらの関数は、一つのコマンド(npx convex dev)でローカル開発から本番デプロイまで一元管理できます。

開発チームの生産性向上

Convex は、開発チームの生産性を以下の観点から向上させます。

1. 学習コストの削減 従来のバックエンド開発で必要だった複数の技術スタックを、Convex の統一された API で置き換えることができます。

2. 開発速度の向上 型安全性が自動的に保証されるため、実行時エラーの削減と開発者の認知負荷軽減を実現します。

typescript// 自動生成される型定義の例
// _generated/api.ts(自動生成)
export const api = {
  users: {
    create: publicMutation<
      { name: string; email: string },
      Id<'users'>
    >,
    getByEmail: publicQuery<
      { email: string },
      Doc<'users'> | null
    >,
  },
  // ... 他の関数も型安全
};

3. デバッグとモニタリングの統合 Convex ダッシュボードでは、関数の実行ログ、データベースクエリ、パフォーマンスメトリクスを統一されたインターフェースで監視できます。

従来の開発Convex 開発
複数のダッシュボード監視統一ダッシュボード
手動ログ設定自動ログ収集
分散メトリクス集約メトリクス
個別アラート設定統合アラート

Convex の解決策

リアルタイムデータベースの仕組み

Convex のリアルタイムデータベースは、Change Streams と呼ばれる技術を基盤として構築されています。

この仕組みにより、データベースレベルでの変更を即座に検知し、関連するクライアントに効率的に配信することが可能です。

mermaidflowchart TD
    write[データ書き込み要求]
    validation[バリデーション]
    transaction[トランザクション実行]
    change_log[変更ログ生成]
    subscription[サブスクリプション判定]
    client_update[クライアント更新]

    write --> validation
    validation --> transaction
    transaction --> change_log
    change_log --> subscription
    subscription --> client_update

    subgraph optimizations[最適化機能]
        batching[バッチ処理]
        dedup[重複除去]
        filtering[フィルタリング]
    end

    subscription --> optimizations
    optimizations --> client_update

変更検知メカニズム

Convex は、データベースの各変更操作(INSERT、UPDATE、DELETE)を追跡し、影響を受けるクエリを自動的に識別します。

typescript// データベース変更の自動追跡例
export const updateMessage = mutation({
  args: { id: v.id('messages'), text: v.string() },
  handler: async (ctx, args) => {
    // この変更により、以下のクエリが自動的に再実行される
    // - messages.list(全メッセージ取得)
    // - messages.getById(特定メッセージ取得)
    // - channels.getWithMessages(チャンネル詳細)

    await ctx.db.patch(args.id, {
      text: args.text,
      updatedAt: Date.now(),
    });
  },
});

効率的な配信システム

Convex は、不要な再計算を避けるため、依存関係グラフを構築し、最小限の更新のみを実行します。

サーバーレス関数の統合管理

Convex では、すべてのサーバーサイドロジックを統一された関数システムで管理できます。

これにより、従来のマイクロサービス的な複雑性を排除し、モノリシックな開発体験を提供します。

関数の種類と用途

typescript// 1. Query関数:データ取得(リアルタイム対応)
export const getUser = query({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

// 2. Mutation関数:データ変更
export const createPost = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    authorId: v.id('users'),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert('posts', {
      ...args,
      createdAt: Date.now(),
      published: false,
    });
  },
});

// 3. Action関数:外部API連携
export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  handler: async (ctx, args) => {
    // 外部メールサービスとの連携
    const response = await fetch(
      'https://api.mailservice.com/send',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.MAIL_API_KEY}`,
        },
        body: JSON.stringify(args),
      }
    );

    if (!response.ok) {
      throw new Error('Failed to send email');
    }

    return await response.json();
  },
});

関数間の連携

typescript// 関数間の依存関係を明確に表現
export const publishPost = mutation({
  args: { postId: v.id('posts') },
  handler: async (ctx, args) => {
    // 1. 投稿を公開状態に変更
    await ctx.db.patch(args.postId, {
      published: true,
      publishedAt: Date.now(),
    });

    // 2. 外部アクションを呼び出し(非同期)
    await ctx.scheduler.runAfter(
      0,
      api.posts.notifySubscribers,
      {
        postId: args.postId,
      }
    );
  },
});

型安全な API 生成機能

Convex の最大の特徴の一つは、関数定義から自動的に TypeScript 型を生成する機能です。

これにより、フロントエンドとバックエンド間の型安全性が完全に保証されます。

自動型生成の流れ

typescript// 1. バックエンド関数定義(convex/users.ts)
export const create = mutation({
  args: {
    name: v.string(),
    email: v.string(),
    age: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    // 型推論により、argsは以下の型になる
    // { name: string; email: string; age?: number }

    const userId = await ctx.db.insert('users', {
      name: args.name,
      email: args.email,
      age: args.age ?? null,
      createdAt: Date.now(),
    });

    return { success: true, userId };
  },
});
typescript// 2. 自動生成される型定義(_generated/api.ts)
export const api = {
  users: {
    create: publicMutation<
      { name: string; email: string; age?: number },
      { success: true; userId: Id<'users'> }
    >,
  },
};
typescript// 3. フロントエンドでの型安全な利用
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function UserForm() {
  const createUser = useMutation(api.users.create);

  const handleSubmit = async (formData: FormData) => {
    // TypeScriptが引数と戻り値の型を完全に理解
    const result = await createUser({
      name: formData.get("name") as string,
      email: formData.get("email") as string,
      // age: "invalid", // ← コンパイルエラー
    });

    // result.userId はId<"users">型として推論される
    console.log("Created user:", result.userId);
  };

  return (
    // フォームコンポーネント
  );
}

リアルタイム更新の自動化

Convex では、データベースの変更が自動的にクライアントに反映される仕組みが標準で提供されています。

開発者は、WebSocket の管理や同期ロジックを実装する必要がありません。

自動更新の動作原理

mermaidsequenceDiagram
    participant Client1 as クライアント A
    participant Client2 as クライアント B
    participant Convex as Convex サーバー
    participant DB as データベース

    Client1->>Convex: useQuery(api.messages.list)
    Convex->>DB: クエリ実行
    DB->>Convex: 結果返却
    Convex->>Client1: データ配信 + サブスクリプション登録

    Client2->>Convex: useMutation(api.messages.send)
    Convex->>DB: データ挿入
    DB->>Convex: 変更通知
    Convex->>Client1: 自動更新配信
    Convex->>Client2: ミューテーション完了

    Client1->>Client1: UI自動再レンダリング

実装例:チャットアプリケーション

typescript// メッセージ一覧コンポーネント
function MessageList({ channelId }: { channelId: string }) {
  // このクエリは自動的にリアルタイム更新される
  const messages = useQuery(api.messages.getByChannel, {
    channelId,
  });

  // ローディング状態の適切な処理
  if (messages === undefined) {
    return <LoadingSpinner />;
  }

  return (
    <div className='message-list'>
      {messages.map((message) => (
        <MessageItem key={message._id} message={message} />
      ))}
    </div>
  );
}

// メッセージ送信コンポーネント
function MessageInput({
  channelId,
}: {
  channelId: string;
}) {
  const sendMessage = useMutation(api.messages.send);
  const [text, setText] = useState('');

  const handleSend = async () => {
    if (!text.trim()) return;

    // メッセージ送信により、上記のMessageListが自動更新される
    await sendMessage({
      channelId,
      text: text.trim(),
      timestamp: Date.now(),
    });

    setText('');
  };

  return (
    <div className='message-input'>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyPress={(e) =>
          e.key === 'Enter' && handleSend()
        }
      />
      <button onClick={handleSend}>送信</button>
    </div>
  );
}

この実装では、メッセージの送信と受信が完全に分離されており、どちらのコンポーネントも単純なロジックで実装できています。

具体的な実装例

プロジェクトセットアップ

Convex プロジェクトの初期設定は、従来のバックエンド開発と比較して大幅に簡素化されています。

まず、新しい Next.js プロジェクトに Convex を統合する手順を見てみましょう。

bash# Next.jsプロジェクトの作成
npx create-next-app@latest my-convex-app --typescript --tailwind --app

# プロジェクトディレクトリに移動
cd my-convex-app

# Convexの初期化
npx convex dev

初期化コマンドを実行すると、以下のファイル構造が自動生成されます。

graphqlmy-convex-app/
├── convex/
│   ├── _generated/         # 自動生成される型定義
│   ├── schema.ts          # データベーススキーマ
│   └── README.md          # Convex固有の説明
├── src/
│   └── app/
└── package.json

環境設定ファイルの作成

typescript// 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(),
    avatar: v.optional(v.string()),
    createdAt: v.number(),
  }).index('by_email', ['email']),

  // メッセージテーブル
  messages: defineTable({
    text: v.string(),
    userId: v.id('users'),
    channelId: v.string(),
    timestamp: v.number(),
  })
    .index('by_channel', ['channelId'])
    .index('by_user', ['userId']),

  // チャンネルテーブル
  channels: defineTable({
    name: v.string(),
    description: v.optional(v.string()),
    isPrivate: v.boolean(),
    createdBy: v.id('users'),
    createdAt: v.number(),
  }),
});

Next.js アプリケーションの設定

typescript// src/app/layout.tsx - ConvexProviderの設定
'use client';

import {
  ConvexProvider,
  ConvexReactClient,
} from 'convex/react';

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='ja'>
      <body>
        <ConvexProvider client={convex}>
          {children}
        </ConvexProvider>
      </body>
    </html>
  );
}

データモデル定義

Convex では、TypeScript の型システムを活用したスキーマ定義により、実行時の型安全性とコンパイル時の検証を両立できます。

リレーショナルデータの設計

typescript// convex/schema.ts - 拡張されたスキーマ定義
export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    avatar: v.optional(v.string()),
    status: v.union(
      v.literal('online'),
      v.literal('offline'),
      v.literal('away')
    ),
    lastSeen: v.number(),
    createdAt: v.number(),
  })
    .index('by_email', ['email'])
    .index('by_status', ['status']),

  channels: defineTable({
    name: v.string(),
    description: v.optional(v.string()),
    type: v.union(
      v.literal('public'),
      v.literal('private'),
      v.literal('direct')
    ),
    createdBy: v.id('users'),
    members: v.array(v.id('users')), // メンバーのID配列
    createdAt: v.number(),
  })
    .index('by_type', ['type'])
    .index('by_creator', ['createdBy']),

  messages: defineTable({
    text: v.string(),
    userId: v.id('users'),
    channelId: v.id('channels'),
    replyTo: v.optional(v.id('messages')), // 返信機能
    attachments: v.optional(
      v.array(
        v.object({
          name: v.string(),
          url: v.string(),
          type: v.string(),
          size: v.number(),
        })
      )
    ),
    editedAt: v.optional(v.number()),
    timestamp: v.number(),
  })
    .index('by_channel', ['channelId'])
    .index('by_user', ['userId'])
    .index('by_reply', ['replyTo']),

  reactions: defineTable({
    messageId: v.id('messages'),
    userId: v.id('users'),
    emoji: v.string(),
    createdAt: v.number(),
  })
    .index('by_message', ['messageId'])
    .index('by_user_message', ['userId', 'messageId']),
});

リアルタイム機能の実装

定義したスキーマに基づいて、リアルタイムチャット機能を実装してみましょう。

バックエンド関数の実装

typescript// convex/messages.ts - メッセージ関連の関数
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';

// チャンネル内のメッセージを取得(リアルタイム対応)
export const getByChannel = query({
  args: { channelId: v.id('channels') },
  handler: async (ctx, args) => {
    const messages = await ctx.db
      .query('messages')
      .withIndex('by_channel', (q) =>
        q.eq('channelId', args.channelId)
      )
      .order('desc') // 新しいメッセージが先頭
      .take(50); // 最新50件のみ取得

    // メッセージにユーザー情報を結合
    const messagesWithUsers = await Promise.all(
      messages.map(async (message) => {
        const user = await ctx.db.get(message.userId);
        return {
          ...message,
          user: user
            ? { name: user.name, avatar: user.avatar }
            : null,
        };
      })
    );

    return messagesWithUsers;
  },
});

// メッセージの送信
export const send = mutation({
  args: {
    channelId: v.id('channels'),
    text: v.string(),
    replyTo: v.optional(v.id('messages')),
  },
  handler: async (ctx, args) => {
    // 認証済みユーザーの取得(後述の認証システムで実装)
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error('Unauthorized');
    }

    const user = await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', identity.email!)
      )
      .first();

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

    // チャンネルのメンバーシップ確認
    const channel = await ctx.db.get(args.channelId);
    if (!channel || !channel.members.includes(user._id)) {
      throw new Error('Access denied');
    }

    const messageId = await ctx.db.insert('messages', {
      text: args.text,
      userId: user._id,
      channelId: args.channelId,
      replyTo: args.replyTo,
      timestamp: Date.now(),
    });

    return messageId;
  },
});

// メッセージの編集
export const edit = mutation({
  args: {
    messageId: v.id('messages'),
    text: v.string(),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Unauthorized');

    const message = await ctx.db.get(args.messageId);
    if (!message) throw new Error('Message not found');

    const user = await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', identity.email!)
      )
      .first();

    // 自分のメッセージのみ編集可能
    if (!user || message.userId !== user._id) {
      throw new Error('Permission denied');
    }

    await ctx.db.patch(args.messageId, {
      text: args.text,
      editedAt: Date.now(),
    });
  },
});

フロントエンド実装

typescript// src/components/ChatRoom.tsx - チャットルームコンポーネント
'use client';

import { useQuery, useMutation } from 'convex/react';
import { api } from '../../convex/_generated/api';
import { Id } from '../../convex/_generated/dataModel';
import { useState, useRef, useEffect } from 'react';

interface ChatRoomProps {
  channelId: Id<'channels'>;
}

export function ChatRoom({ channelId }: ChatRoomProps) {
  // リアルタイムでメッセージを取得
  const messages = useQuery(api.messages.getByChannel, {
    channelId,
  });
  const sendMessage = useMutation(api.messages.send);

  const [newMessage, setNewMessage] = useState('');
  const [replyTo, setReplyTo] =
    useState<Id<'messages'> | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  // 新しいメッセージが追加されたら自動スクロール
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({
      behavior: 'smooth',
    });
  }, [messages]);

  const handleSendMessage = async () => {
    if (!newMessage.trim()) return;

    try {
      await sendMessage({
        channelId,
        text: newMessage.trim(),
        replyTo,
      });

      setNewMessage('');
      setReplyTo(null);
    } catch (error) {
      console.error('Failed to send message:', error);
    }
  };

  if (messages === undefined) {
    return (
      <div className='p-4'>メッセージを読み込み中...</div>
    );
  }

  return (
    <div className='flex flex-col h-full'>
      {/* メッセージリスト */}
      <div className='flex-1 overflow-y-auto p-4 space-y-4'>
        {messages.map((message) => (
          <MessageItem
            key={message._id}
            message={message}
            onReply={() => setReplyTo(message._id)}
          />
        ))}
        <div ref={messagesEndRef} />
      </div>

      {/* 返信表示 */}
      {replyTo && (
        <div className='px-4 py-2 bg-gray-100 border-l-4 border-blue-500'>
          <span className='text-sm text-gray-600'>
            返信中:{' '}
            {messages.find((m) => m._id === replyTo)?.text}
          </span>
          <button
            onClick={() => setReplyTo(null)}
            className='ml-2 text-red-500 text-sm'
          >
            キャンセル
          </button>
        </div>
      )}

      {/* メッセージ入力 */}
      <div className='p-4 border-t'>
        <div className='flex space-x-2'>
          <input
            type='text'
            value={newMessage}
            onChange={(e) => setNewMessage(e.target.value)}
            onKeyPress={(e) =>
              e.key === 'Enter' && handleSendMessage()
            }
            placeholder='メッセージを入力...'
            className='flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500'
          />
          <button
            onClick={handleSendMessage}
            disabled={!newMessage.trim()}
            className='px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50'
          >
            送信
          </button>
        </div>
      </div>
    </div>
  );
}

認証システムの構築

Convex では、複数の認証プロバイダーとの統合が標準で提供されています。

ここでは、Clerk 認証を使用した実装例を示します。

認証プロバイダーの設定

bash# Clerkライブラリのインストール
yarn add @clerk/nextjs
typescript// src/app/layout.tsx - Clerk認証の統合
import { ClerkProvider, useAuth } from '@clerk/nextjs';
import { ConvexProviderWithClerk } from 'convex/react-clerk';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='ja'>
      <body>
        <ClerkProvider
          publishableKey={
            process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!
          }
        >
          <ConvexProviderWithClerk
            client={convex}
            useAuth={useAuth}
          >
            {children}
          </ConvexProviderWithClerk>
        </ClerkProvider>
      </body>
    </html>
  );
}

認証機能を持つバックエンド関数

typescript// convex/users.ts - ユーザー管理関数
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';

// 現在のユーザー情報を取得
export const current = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;

    return await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', identity.email!)
      )
      .first();
  },
});

// ユーザーの作成または更新
export const upsert = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Unauthorized');

    const existingUser = await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', identity.email!)
      )
      .first();

    if (existingUser) {
      // 既存ユーザーの最終アクセス時刻を更新
      await ctx.db.patch(existingUser._id, {
        lastSeen: Date.now(),
        status: 'online',
      });
      return existingUser._id;
    } else {
      // 新規ユーザーの作成
      const userId = await ctx.db.insert('users', {
        name: identity.name || identity.email!,
        email: identity.email!,
        avatar: identity.pictureUrl,
        status: 'online',
        lastSeen: Date.now(),
        createdAt: Date.now(),
      });
      return userId;
    }
  },
});

// オンライン状態の更新
export const updateStatus = mutation({
  args: {
    status: v.union(
      v.literal('online'),
      v.literal('offline'),
      v.literal('away')
    ),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Unauthorized');

    const user = await ctx.db
      .query('users')
      .withIndex('by_email', (q) =>
        q.eq('email', identity.email!)
      )
      .first();

    if (user) {
      await ctx.db.patch(user._id, {
        status: args.status,
        lastSeen: Date.now(),
      });
    }
  },
});

認証状態を管理する React コンポーネント

typescript// src/components/AuthGuard.tsx - 認証ガード
import { useUser } from '@clerk/nextjs';
import { useMutation, useQuery } from 'convex/react';
import { api } from '../../convex/_generated/api';
import { useEffect } from 'react';

export function AuthGuard({
  children,
}: {
  children: React.ReactNode;
}) {
  const { isSignedIn, user } = useUser();
  const currentUser = useQuery(api.users.current);
  const upsertUser = useMutation(api.users.upsert);

  // ログイン時にユーザー情報を同期
  useEffect(() => {
    if (isSignedIn && currentUser === null) {
      upsertUser();
    }
  }, [isSignedIn, currentUser, upsertUser]);

  if (!isSignedIn) {
    return <LoginPage />;
  }

  if (currentUser === undefined) {
    return <div>ユーザー情報を読み込み中...</div>;
  }

  return <>{children}</>;
}

この実装により、認証状態の管理、ユーザー情報の同期、認証が必要な機能へのアクセス制御が自動化されます。

まとめ

Convex は、リアルタイムデータベースとサーバーレス関数を統合した革新的なプラットフォームとして、モダン Web アプリケーション開発の課題を包括的に解決しています。

従来の開発手法と比較して、Convex は以下の主要な価値を提供します。

技術的優位性

  • リアルタイム同期の自動化により、複雑な WebSocket 管理が不要
  • 完全な型安全性による開発時のエラー削減と保守性向上
  • 統合されたバックエンド環境による学習コストの削減

開発効率の向上

  • 単一コードベースでデータベース、API、リアルタイム機能を管理
  • 自動生成される型定義によるフロントエンド・バックエンド間の一貫性保証
  • 簡素化されたデプロイプロセスと統合監視機能

チーム生産性の向上

  • 従来のマイクロサービス的複雑性の排除
  • 標準化された開発フローによる新メンバーのオンボーディング効率化
  • 統一されたダッシュボードによる運用コストの削減

特に、リアルタイム機能を必要とするアプリケーション(チャット、コラボレーションツール、ライブダッシュボード等)の開発において、Convex は従来の開発期間を大幅に短縮する可能性を持っています。

ただし、Convex は比較的新しいプラットフォームであるため、エコシステムの成熟度や長期的なサポート体制については継続的な評価が必要です。また、特定のユースケースや既存システムとの統合要件によっては、従来のソリューションの方が適している場合もあります。

2025 年の現在、Convex は特にスタートアップや中小規模のプロジェクトにおいて、迅速な MVP 開発とリアルタイム機能の実装を実現する強力な選択肢として位置づけられています。

関連リンク