Convex で「Permission denied」多発時の原因特定:認可/コンテキスト/引数を総点検
Convex で「Permission denied」エラーが頻発すると、開発が思うように進まず困ってしまいますよね。本記事では、そのエラーが発生する主な原因を「認可設定」「コンテキスト」「引数」の 3 つの観点から徹底的に解説します。初心者の方でも段階的に理解できるよう、図やコード例を交えながら、具体的な点検ポイントと解決策をご紹介しますので、ぜひ最後までお付き合いください。
背景
Convex は、リアルタイムデータベースと API を統合したバックエンドプラットフォームで、認証・認可の仕組みが組み込まれています。この仕組みにより、セキュアなアプリケーションを簡単に構築できる反面、設定や実装に誤りがあると「Permission denied」エラーが発生しやすくなります。
以下の図は、クライアント側からのリクエストが Convex のバックエンドでどのように処理されるかを示したものです。認証情報が正しく渡され、認可ルールが適切に設定されている場合のみ、データベースへのアクセスが許可されます。
mermaidflowchart LR
client["クライアント<br/>(React/Vue など)"]
auth["認証プロバイダ<br/>(Clerk/Auth0 など)"]
convex["Convex Backend<br/>(Query/Mutation)"]
authz["認可チェック<br/>(Rules)"]
db[("Database")]
client -->|ログイン| auth
auth -->|トークン発行| client
client -->|リクエスト + トークン| convex
convex -->|認証情報確認| authz
authz -->|OK| db
authz -->|NG| error["Permission denied"]
db -->|データ| convex
convex -->|レスポンス| client
この図から分かるように、認証トークンがクライアントから正しく送信され、バックエンド側で認可ルールが正しく評価される必要があります。どこか一箇所でも欠けていると、エラーが発生してしまうのです。
Convex の認証・認可の仕組み
Convex では、以下の要素が連携して動作します。
- 認証プロバイダ: Clerk、Auth0、Firebase Auth などの外部サービスと統合
- 認証情報の伝播: クライアントからのリクエストに含まれる JWT トークン
- コンテキスト(
ctx): サーバー側の関数で利用できる認証情報やデータベースアクセス - 認可ルール: 誰がどのデータにアクセスできるかを定義
これらの要素が正しく設定されていないと、エラーが発生します。
課題
「Permission denied」エラーが発生する主な原因は以下の 3 つに分類できます。
1. 認可ルールの不備
Convex では、データベースのテーブルごとに認可ルールを定義できますが、ルールが厳しすぎる、または曖昧な場合にエラーが発生します。
2. コンテキスト(ctx)の誤用
サーバー側の関数(Query/Mutation)では ctx オブジェクトを通じて認証情報やデータベースにアクセスします。しかし、ctx.auth が null の場合や、誤ったメソッドを使用している場合にエラーが発生します。
3. 引数の不一致
クライアントから送信される引数が、サーバー側の期待する型や値と一致しない場合、認可チェック以前にエラーが発生することがあります。
以下の図は、これら 3 つの原因がどのように関連しているかを示しています。
mermaidflowchart TD
req["クライアントリクエスト"]
argCheck["引数チェック"]
ctxCheck["コンテキスト確認<br/>(ctx.auth)"]
authzCheck["認可ルール評価"]
success["アクセス許可"]
error["Permission denied"]
req --> argCheck
argCheck -->|引数エラー| error
argCheck -->|OK| ctxCheck
ctxCheck -->|ctx.auth が null| error
ctxCheck -->|OK| authzCheck
authzCheck -->|ルール不一致| error
authzCheck -->|OK| success
この図から、エラーが発生するポイントは複数あり、それぞれを順番に点検する必要があることが分かりますね。
解決策
それでは、3 つの観点から具体的な解決策を見ていきましょう。各ステップを順番に確認することで、エラーの原因を特定できます。
解決策 1: 認可ルールの点検と修正
認可ルールは convex/schema.ts や個別のテーブル定義で設定されます。まず、どのようなルールが設定されているかを確認しましょう。
以下は、認可ルールの基本的な設定例です。
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(),
role: v.string(), // "admin" | "user"
}),
documents: defineTable({
title: v.string(),
content: v.string(),
ownerId: v.id('users'),
isPublic: v.boolean(),
}),
});
次に、Query や Mutation で認可チェックを実装します。
typescript// convex/documents.ts
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';
// ドキュメント取得(認可チェック付き)
export const getDocument = query({
args: { documentId: v.id('documents') },
handler: async (ctx, args) => {
// 認証情報を取得
const identity = await ctx.auth.getUserIdentity();
// 未認証の場合はエラー
if (!identity) {
throw new Error(
'Permission denied: Not authenticated'
);
}
// ドキュメントを取得
const document = await ctx.db.get(args.documentId);
if (!document) {
throw new Error('Document not found');
}
// 公開ドキュメントまたは所有者のみアクセス可能
if (
!document.isPublic &&
document.ownerId !== identity.subject
) {
throw new Error(
'Permission denied: Not authorized to view this document'
);
}
return document;
},
});
このコードでは、以下の点をチェックしています。
- ユーザーが認証されているか(
ctx.auth.getUserIdentity()) - ドキュメントが存在するか
- ユーザーがドキュメントの所有者か、または公開ドキュメントか
解決策 2: コンテキスト(ctx)の正しい使用
ctx オブジェクトは、Convex のサーバー側関数で利用できる重要な情報源です。特に ctx.auth を正しく扱うことが重要になります。
以下は、ctx.auth を使用した認証情報の取得例です。
typescript// convex/users.ts
import { query } from './_generated/server';
// 現在のユーザー情報を取得
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
// 認証情報を取得
const identity = await ctx.auth.getUserIdentity();
// 未認証の場合は null を返す
if (!identity) {
return null;
}
// データベースからユーザー情報を検索
const user = await ctx.db
.query('users')
.filter((q) => q.eq(q.field('email'), identity.email))
.first();
return user;
},
});
ポイントは以下の通りです。
ctx.auth.getUserIdentity()は非同期関数なのでawaitが必要- 認証されていない場合は
nullが返る identityオブジェクトにはsubject(ユーザー ID)、emailなどが含まれる
次に、認証が必須の Mutation の例を見てみましょう。
typescript// convex/documents.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
// ドキュメント作成(認証必須)
export const createDocument = mutation({
args: {
title: v.string(),
content: v.string(),
isPublic: v.boolean(),
},
handler: async (ctx, args) => {
// 認証情報を取得
const identity = await ctx.auth.getUserIdentity();
// 未認証の場合はエラーをスロー
if (!identity) {
throw new Error(
'Permission denied: Authentication required'
);
}
// ユーザー情報を取得
const user = await ctx.db
.query('users')
.filter((q) => q.eq(q.field('email'), identity.email))
.first();
if (!user) {
throw new Error('User not found in database');
}
// ドキュメントを作成
const documentId = await ctx.db.insert('documents', {
title: args.title,
content: args.content,
ownerId: user._id,
isPublic: args.isPublic,
});
return documentId;
},
});
このコードでは、以下の流れで処理を行っています。
- 認証情報の取得と確認
- データベースから対応するユーザー情報を検索
- ユーザー情報が存在する場合のみドキュメント作成
解決策 3: 引数の型チェックと検証
Convex では、引数の型を v オブジェクトを使って定義します。型が一致しない場合、Convex が自動的にエラーを返しますが、より詳細な検証を行うことも可能です。
以下は、引数の基本的な型定義例です。
typescript// convex/documents.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const updateDocument = mutation({
args: {
documentId: v.id('documents'),
title: v.optional(v.string()),
content: v.optional(v.string()),
isPublic: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
// 認証チェック
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Permission denied: Not authenticated'
);
}
// ドキュメント取得
const document = await ctx.db.get(args.documentId);
if (!document) {
throw new Error('Document not found');
}
// 所有者チェック
const user = await ctx.db
.query('users')
.filter((q) => q.eq(q.field('email'), identity.email))
.first();
if (!user || document.ownerId !== user._id) {
throw new Error('Permission denied: Not the owner');
}
// 更新データを準備
const updates: any = {};
if (args.title !== undefined)
updates.title = args.title;
if (args.content !== undefined)
updates.content = args.content;
if (args.isPublic !== undefined)
updates.isPublic = args.isPublic;
// ドキュメントを更新
await ctx.db.patch(args.documentId, updates);
return { success: true };
},
});
引数の型定義では、以下の点に注意してください。
v.id("テーブル名")でテーブルの ID 型を指定v.optional()で省略可能な引数を定義v.union()で複数の型を許可
さらに、カスタムバリデーションを追加することもできます。
typescript// convex/documents.ts(カスタムバリデーション付き)
export const createDocumentWithValidation = mutation({
args: {
title: v.string(),
content: v.string(),
isPublic: v.boolean(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Permission denied: Not authenticated'
);
}
// タイトルの長さチェック
if (args.title.length < 3 || args.title.length > 100) {
throw new Error(
'Title must be between 3 and 100 characters'
);
}
// コンテンツの長さチェック
if (args.content.length < 10) {
throw new Error(
'Content must be at least 10 characters'
);
}
// 以降、ドキュメント作成処理
const user = await ctx.db
.query('users')
.filter((q) => q.eq(q.field('email'), identity.email))
.first();
if (!user) {
throw new Error('User not found');
}
const documentId = await ctx.db.insert('documents', {
title: args.title,
content: args.content,
ownerId: user._id,
isPublic: args.isPublic,
});
return documentId;
},
});
このように、引数の型チェックとカスタムバリデーションを組み合わせることで、より堅牢なエラーハンドリングが実現できます。
具体例
ここからは、実際のアプリケーションでよくあるシナリオを元に、「Permission denied」エラーの発生から解決までの流れを見ていきましょう。
シナリオ: ブログアプリでの記事編集
以下の状況を想定します。
- ユーザーは自分の記事のみ編集可能
- 管理者は全ての記事を編集可能
- 未認証ユーザーは編集不可
このシナリオを実装する際の典型的なエラーと解決方法を紹介します。
ステップ 1: スキーマ定義
まず、記事とユーザーのスキーマを定義します。
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(),
role: v.union(v.literal('admin'), v.literal('user')),
}).index('by_email', ['email']),
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id('users'),
publishedAt: v.optional(v.number()),
}).index('by_author', ['authorId']),
});
このスキーマでは、ユーザーのロール(admin または user)と、記事の作成者を管理しています。
ステップ 2: 記事編集の Mutation
次に、記事を編集する Mutation を実装します。
typescript// convex/posts.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const updatePost = mutation({
args: {
postId: v.id('posts'),
title: v.optional(v.string()),
content: v.optional(v.string()),
},
handler: async (ctx, args) => {
// 1. 認証チェック
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Permission denied: Authentication required'
);
}
// 2. ユーザー情報取得
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 in database');
}
// 3. 記事取得
const post = await ctx.db.get(args.postId);
if (!post) {
throw new Error('Post not found');
}
// 4. 認可チェック: 作成者または管理者のみ編集可能
const isOwner = post.authorId === user._id;
const isAdmin = user.role === 'admin';
if (!isOwner && !isAdmin) {
throw new Error(
'Permission denied: Not authorized to edit this post'
);
}
// 5. 記事更新
const updates: any = {};
if (args.title !== undefined)
updates.title = args.title;
if (args.content !== undefined)
updates.content = args.content;
await ctx.db.patch(args.postId, updates);
return { success: true, postId: args.postId };
},
});
このコードでは、以下の 5 つのステップで認可チェックを行っています。
- ユーザーが認証されているか確認
- データベースからユーザー情報を取得
- 編集対象の記事を取得
- ユーザーが記事の作成者または管理者か確認
- 問題なければ記事を更新
ステップ 3: クライアント側の実装
クライアント側では、Convex の React Hooks を使用して Mutation を呼び出します。
typescript// app/components/EditPost.tsx
'use client';
import { useMutation } from 'convex/react';
import { api } from '@/convex/_generated/api';
import { useState } from 'react';
import type { Id } from '@/convex/_generated/dataModel';
interface EditPostProps {
postId: Id<'posts'>;
initialTitle: string;
initialContent: string;
}
export function EditPost({
postId,
initialTitle,
initialContent,
}: EditPostProps) {
const [title, setTitle] = useState(initialTitle);
const [content, setContent] = useState(initialContent);
const [error, setError] = useState<string | null>(null);
const updatePost = useMutation(api.posts.updatePost);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
await updatePost({
postId,
title,
content,
});
alert('記事を更新しました');
} catch (err) {
// エラーハンドリング
if (err instanceof Error) {
setError(err.message);
} else {
setError('予期しないエラーが発生しました');
}
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='title'>タイトル</label>
<input
id='title'
type='text'
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div>
<label htmlFor='content'>本文</label>
<textarea
id='content'
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type='submit'>更新</button>
</form>
);
}
クライアント側では、エラーが発生した場合にメッセージを表示し、ユーザーに適切なフィードバックを提供します。
よくあるエラーパターンと対処法
以下の表は、頻出するエラーパターンとその対処法をまとめたものです。
| # | エラーメッセージ | 原因 | 対処法 |
|---|---|---|---|
| 1 | Permission denied: Not authenticated | 認証情報が取得できない | クライアント側の認証設定を確認 |
| 2 | User not found in database | 認証済みだがユーザーレコードが未作成 | 初回ログイン時にユーザーレコードを作成 |
| 3 | Permission denied: Not authorized to edit this post | 作成者でも管理者でもない | 認可ロジックを見直し |
| 4 | Document not found | 存在しないドキュメント ID | クライアント側で ID の妥当性を確認 |
| 5 | Argument validation failed | 引数の型が不正 | 引数の型定義を確認 |
エラー解決の具体例: ユーザーレコードの自動作成
「User not found in database」エラーを防ぐため、初回ログイン時に自動的にユーザーレコードを作成する仕組みを実装できます。
typescript// convex/users.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
// ユーザーレコードを作成または取得
export const getOrCreateUser = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error('Not authenticated');
}
// 既存ユーザーを検索
const existingUser = await ctx.db
.query('users')
.withIndex('by_email', (q) =>
q.eq('email', identity.email!)
)
.first();
if (existingUser) {
return existingUser;
}
// 新規ユーザーを作成
const userId = await ctx.db.insert('users', {
name: identity.name ?? 'Unknown',
email: identity.email!,
role: 'user', // デフォルトは一般ユーザー
});
return await ctx.db.get(userId);
},
});
このコードでは、認証情報からユーザーを検索し、存在しない場合は新規作成します。これにより、「User not found」エラーを防ぐことができますね。
クライアント側では、アプリケーション起動時にこの Mutation を呼び出します。
typescript// app/components/AuthProvider.tsx
'use client';
import { useConvexAuth, useMutation } from 'convex/react';
import { api } from '@/convex/_generated/api';
import { useEffect } from 'react';
export function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
const { isAuthenticated } = useConvexAuth();
const getOrCreateUser = useMutation(
api.users.getOrCreateUser
);
useEffect(() => {
if (isAuthenticated) {
// 認証後にユーザーレコードを作成または取得
getOrCreateUser().catch((err) => {
console.error('Failed to create user:', err);
});
}
}, [isAuthenticated, getOrCreateUser]);
return <>{children}</>;
}
この実装により、ユーザーが認証されると自動的にユーザーレコードが作成され、以降の操作で「User not found」エラーが発生しなくなります。
デバッグのベストプラクティス
「Permission denied」エラーをデバッグする際は、以下の手順を踏むと効率的です。
1. ログ出力で認証情報を確認
サーバー側で console.log を使って認証情報を確認しましょう。
typescriptexport const debugAuth = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
console.log('Identity:', identity);
if (identity) {
console.log('User ID (subject):', identity.subject);
console.log('Email:', identity.email);
console.log('Name:', identity.name);
}
return identity;
},
});
このクエリを実行すると、Convex のダッシュボードまたはローカル開発環境のログで認証情報を確認できます。
2. エラーメッセージを詳細化
エラーメッセージには、具体的な情報を含めることが重要です。
typescriptexport const updatePostWithDetailedError = mutation({
args: {
postId: v.id('posts'),
title: v.optional(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Permission denied: User is not authenticated'
);
}
const user = await ctx.db
.query('users')
.withIndex('by_email', (q) =>
q.eq('email', identity.email!)
)
.first();
if (!user) {
throw new Error(
`Permission denied: User with email ${identity.email} not found in database`
);
}
const post = await ctx.db.get(args.postId);
if (!post) {
throw new Error(
`Post with ID ${args.postId} not found`
);
}
if (
post.authorId !== user._id &&
user.role !== 'admin'
) {
throw new Error(
`Permission denied: User ${user._id} is not authorized to edit post ${args.postId} (owner: ${post.authorId})`
);
}
// 更新処理
await ctx.db.patch(args.postId, { title: args.title });
return { success: true };
},
});
詳細なエラーメッセージにより、どの段階で問題が発生しているかが一目瞭然になります。
3. テストケースの作成
Convex では、サーバー側の関数をテストできます。以下は、認可ロジックのテストケース例です。
typescript// convex/posts.test.ts
import { test, expect } from 'vitest';
import { convexTest } from 'convex-test';
import schema from './schema';
import { api } from './_generated/api';
test('updatePost: 作成者は編集可能', async () => {
const t = convexTest(schema);
// ユーザーと記事を作成
const userId = await t.run(async (ctx) => {
return await ctx.db.insert('users', {
name: 'Test User',
email: 'test@example.com',
role: 'user',
});
});
const postId = await t.run(async (ctx) => {
return await ctx.db.insert('posts', {
title: 'Test Post',
content: 'Content',
authorId: userId,
});
});
// 認証情報を設定
t.withIdentity({
subject: 'test-subject',
email: 'test@example.com',
name: 'Test User',
});
// 記事更新を実行
const result = await t.mutation(api.posts.updatePost, {
postId,
title: 'Updated Title',
});
expect(result.success).toBe(true);
});
テストを作成することで、認可ロジックが意図通りに動作しているかを確認できます。
まとめ
Convex で「Permission denied」エラーが発生した際の原因特定と解決方法について、3 つの観点から詳しく解説しました。
認可ルールの設定では、誰がどのデータにアクセスできるかを明確に定義することが重要です。コンテキスト(ctx)を正しく使用し、特に ctx.auth.getUserIdentity() で認証情報を取得する際は、null チェックを忘れずに行いましょう。引数の型定義とバリデーションも、エラーを未然に防ぐために欠かせません。
エラーが発生したときは、ログ出力で認証情報を確認し、詳細なエラーメッセージを設定することで、問題箇所を素早く特定できます。テストケースを作成しておけば、リファクタリング時にも安心ですね。
これらのポイントを押さえることで、Convex アプリケーションのセキュリティと安定性が大きく向上します。今後の開発にぜひお役立てください。
関連リンク
articleConvex で「Permission denied」多発時の原因特定:認可/コンテキスト/引数を総点検
articleConvex でリアルタイムダッシュボード:KPI/閾値アラート/役割別ビューの実装例
articleConvex で Presence(在席)機能を実装:ユーザーステータスのリアルタイム同期
articleConvex で実践する CQRS/イベントソーシング:履歴・再生・集約の設計ガイド
articleConvex クエリ/ミューテーション/アクション チートシート【保存版シンタックス】
articleConvex 初期設定完全手順:CLI・環境変数・Secrets・権限までゼロから構築
articleCursor の自動テスト生成を検証:Vitest/Jest/Playwright のカバレッジ実測
articleDevin 運用ポリシー策定ガイド:利用権限・レビュー必須条件・ログ保存期間
articleCline × Claude/GPT/Gemini モデル比較:長文理解とコード品質の相性
articleClaude Code が編集差分を誤検出する時:競合・改行コード・改フォーマット問題の直し方
articleConvex で「Permission denied」多発時の原因特定:認可/コンテキスト/引数を総点検
articleBun コマンド チートシート:bun install/run/x/test/build 一括早見表
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来