Claude CodeでSupabase連携を実装する完全ガイド: Auth、RLS、Storage、Edge Functionsまで
Claude CodeでSupabaseを安全に実装する手順。Auth、RLS、Storage、Edge Functions、テスト、レビュー観点まで解説。
Supabase連携は、Claude Codeに「ログイン機能を作って」と丸投げすると危険です。画面はすぐ動いても、Row Level Security、環境変数、マイグレーション、Storageの権限、Edge Functionsの認証確認が抜けると、本番でデータ漏えいにつながります。
Supabaseは、Postgresデータベース、Auth、Storage、Edge Functionsをまとめて使えるBaaSです。BaaSは「Backend as a Service」の略で、認証やファイル保存などのバックエンド機能をクラウド側から借りる仕組みです。Firebaseに近い手軽さがありますが、中核がPostgresなのでSQL、制約、RLSを正しく設計できる点が強みです。
この記事ではNext.js App RouterとTypeScriptを前提に、Claude Codeへ渡す要件ファイル、スキーマとRLSのレビュー、実装コード、マイグレーションとテストコマンド、公開前チェックまでを一通り整理します。認証だけを深掘りしたい場合はClaude Code認証実装ガイド、DB設計の考え方はデータベース設計ガイド、変更管理はDBマイグレーション自動化も合わせて確認してください。
公式ドキュメントで先に確認する範囲
Supabaseは更新が速いので、Claude Codeに任せる前に公式ドキュメントを基準にします。特に見るべきページは、Supabase Docs、Auth、Row Level Security、Edge Functions、Storageです。
2026年時点では、Next.jsのSSR認証では@supabase/ssrを使い、ブラウザに出すキーは新しいNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYを使う方針が推奨されています。古いanonキーはまだ残っているプロジェクトもありますが、新規記事や新規実装ではpublishable keyを前提にするほうが読者にとって安全です。
全体アーキテクチャ
今回作るのは「ログイン済みユーザーが自分のメモを作成し、必要なら添付ファイルをStorageへアップロードし、Edge Functionで通知処理を起動する」小さな実装です。学習用ですが、SaaSの管理画面、会員向けナレッジ、社内ポータルにそのまま応用できます。
flowchart LR
User["Browser"] --> Next["Next.js App Router"]
Next --> SSR["@supabase/ssr client"]
SSR --> Auth["Supabase Auth"]
SSR --> DB["Postgres tables"]
SSR --> Storage["Storage bucket"]
Next --> Fn["Edge Function"]
Fn --> DB
DB --> RLS["RLS policies"]
Storage --> StorageRLS["storage.objects policies"]
設計のポイントは、アプリ側のif文だけで権限を守らないことです。Claude CodeにUIやRoute Handlerを書かせても、最終的な境界はPostgresのRLSとStorage policyに置きます。これにより、クライアント側のコードが改ざんされても「自分の行しか読めない」「自分のフォルダにしかアップロードできない」という制約が残ります。
Claude Codeへ渡す要件ファイル
まず要件をリポジトリ内のMarkdownに落とします。Claude Codeは会話だけでも動きますが、認証やDB権限のような失敗コストが高い作業では、要件ファイルを読む形にしたほうがレビューしやすくなります。
# docs/supabase-notes-requirements.md
## Goal
Build a Supabase-backed project notes feature in Next.js App Router.
## Stack
- Next.js App Router
- TypeScript
- @supabase/supabase-js
- @supabase/ssr
- Supabase Auth, Postgres, Storage, Edge Functions
## Data model
- project_notes table
- Each note belongs to auth.users.id through owner_id
- Public notes are readable by anyone
- Private notes are readable only by the owner
- Owners can insert, update, and delete only their own notes
## Security rules
- Never expose a secret key or service role key in browser code
- Use NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY for browser and SSR clients
- Enable RLS on every public table
- Use explicit TO anon or TO authenticated in every policy
- Storage uploads must be restricted to a user-owned folder
## Claude Code workflow
1. Create SQL migration first.
2. Review RLS policies before writing UI.
3. Generate TypeScript database types.
4. Implement Supabase clients.
5. Implement server actions and upload helper.
6. Add test or manual verification commands.
7. Return a review checklist with file paths.
このファイルを作ったうえで、Claude Codeには「この要件だけを読んで、まずSQL migration案を出して。実装はまだ書かない」と依頼します。Masaが過去に失敗したのは、画面実装とRLSを同時に出させて、ポリシーの欠落がUIの成功に隠れたケースです。Supabaseはデモが速いぶん、最初に権限を止めて見る癖が重要です。
セットアップと環境変数
依存関係は最小限から始めます。Supabase CLIはローカルDB、マイグレーション、型生成、Edge Functionsのローカル実行に使います。
npm install @supabase/supabase-js @supabase/ssr zod
npm install --save-dev supabase vitest
npx supabase init
npx supabase start
.env.localには実値を置きますが、Claude Codeのプロンプトには貼りません。Claude Codeへ共有するのは変数名と用途だけです。
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxxxxxxxxxxxxxx
管理作業用のsecret keyや旧service role keyが必要な場合も、ブラウザで読み込まれるNEXT_PUBLIC_には絶対に置きません。サーバー専用の管理処理は別のRoute Handler、ジョブ、Edge Functionに閉じ込め、RLSを迂回する必要がある理由をレビューに残します。
SQL migrationとRLSを先に作る
次のSQLは、メモ機能の最小スキーマです。visibilityで公開範囲を持たせ、owner_idをauth.usersへ結びます。公開メモは誰でも読めますが、作成、更新、削除はログインユーザー本人だけです。
-- supabase/migrations/202606010001_create_project_notes.sql
create table if not exists public.project_notes (
id uuid primary key default gen_random_uuid(),
owner_id uuid not null references auth.users(id) on delete cascade,
title text not null check (char_length(title) between 1 and 120),
body text not null default '',
visibility text not null default 'private'
check (visibility in ('private', 'public')),
attachment_path text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists project_notes_owner_created_idx
on public.project_notes (owner_id, created_at desc);
create or replace function public.set_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists set_project_notes_updated_at on public.project_notes;
create trigger set_project_notes_updated_at
before update on public.project_notes
for each row
execute function public.set_updated_at();
alter table public.project_notes enable row level security;
create policy "Anyone can read public notes"
on public.project_notes
for select
to anon, authenticated
using (
visibility = 'public'
or (select auth.uid()) = owner_id
);
create policy "Owners can insert notes"
on public.project_notes
for insert
to authenticated
with check ((select auth.uid()) = owner_id);
create policy "Owners can update notes"
on public.project_notes
for update
to authenticated
using ((select auth.uid()) = owner_id)
with check ((select auth.uid()) = owner_id);
create policy "Owners can delete notes"
on public.project_notes
for delete
to authenticated
using ((select auth.uid()) = owner_id);
RLSではauth.uid()をそのまま魔法のように信じないことが大切です。未ログインではauth.uid()はnullになるため、公開用とログイン用の条件を分けます。また、公式のRLSガイドが推奨するようにto authenticatedやto anonを明示すると、不要なロールでポリシー評価が走りにくくなります。
レビューでは、各policyを「誰が」「どの操作で」「既存行に対して」「変更後の行に対して」許可されるのかに分解して読みます。特にupdateはusingだけでなくwith checkも確認します。usingは更新前の行を対象にし、with checkは更新後の行が条件を満たすかを見ます。ここを曖昧にすると、本人の行を更新する途中でowner_idや公開範囲を不正に変えられる設計になります。
Storageも同じmigrationでバケットとポリシーを作れます。ファイル名の先頭フォルダをユーザーIDにし、RLSで縛ります。
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values (
'note-attachments',
'note-attachments',
false,
5242880,
array['image/png', 'image/jpeg', 'application/pdf']
)
on conflict (id) do update
set public = excluded.public,
file_size_limit = excluded.file_size_limit,
allowed_mime_types = excluded.allowed_mime_types;
create policy "Users can read own note attachments"
on storage.objects
for select
to authenticated
using (
bucket_id = 'note-attachments'
and (select auth.uid())::text = (storage.foldername(name))[1]
);
create policy "Users can upload own note attachments"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'note-attachments'
and (select auth.uid())::text = (storage.foldername(name))[1]
);
create policy "Users can update own note attachments"
on storage.objects
for update
to authenticated
using (
bucket_id = 'note-attachments'
and (select auth.uid())::text = (storage.foldername(name))[1]
)
with check (
bucket_id = 'note-attachments'
and (select auth.uid())::text = (storage.foldername(name))[1]
);
create policy "Users can delete own note attachments"
on storage.objects
for delete
to authenticated
using (
bucket_id = 'note-attachments'
and (select auth.uid())::text = (storage.foldername(name))[1]
);
ここまで書いたら、まだ画面を作らずにローカルへ反映します。
npx supabase db reset
npx supabase gen types typescript --local > src/lib/database.types.ts
npm run typecheck
db resetはローカル開発DBを作り直すコマンドなので、本番プロジェクトへ向けて実行しないでください。本番反映はCIでnpx supabase db pushやリンク済みプロジェクトへのマイグレーション適用手順を決めてから行います。
Supabase clientを分ける
Next.js App Routerでは、ブラウザ用とサーバー用のclientを分けます。これを混ぜると、Cookieが同期されない、Server Componentでセッションが読めない、secret keyを誤って露出する、という失敗が起きます。
// src/lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/lib/database.types";
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
);
}
// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/lib/database.types";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
} catch {
// Server Components cannot set cookies directly.
}
},
},
},
);
}
Claude Codeへレビューさせるときは「createBrowserClientがサーバーファイルで使われていないか」「NEXT_PUBLIC_にsecret相当の値がないか」「cookies()をawaitしているか」を見てもらいます。Next.jsのバージョン差で壊れやすい場所なので、記事読者にもチェックポイントとして残す価値があります。
AuthとCRUDの実装
ログインはServer Actionで扱います。実プロダクトではバリデーションやエラーメッセージの整形を追加しますが、基本の流れは次の形です。
// app/login/actions.ts
"use server";
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
export async function signIn(formData: FormData) {
const email = String(formData.get("email") ?? "");
const password = String(formData.get("password") ?? "");
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return { error: error.message };
}
redirect("/dashboard");
}
export async function signOut() {
const supabase = await createClient();
await supabase.auth.signOut();
redirect("/login");
}
メモのCRUDはサーバー側に置きます。owner_idはフォームから受け取らず、現在のログインユーザーから決めます。ここをClaude Codeがフォーム入力にしてきたら修正対象です。
// src/features/notes/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";
type CreateNoteInput = {
title: string;
body: string;
visibility?: "private" | "public";
attachmentPath?: string | null;
};
export async function listMyNotes() {
const supabase = await createClient();
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
throw new Error("Authentication required");
}
const { data, error } = await supabase
.from("project_notes")
.select("id,title,body,visibility,attachment_path,created_at,updated_at")
.order("created_at", { ascending: false });
if (error) throw error;
return data;
}
export async function createNote(input: CreateNoteInput) {
const supabase = await createClient();
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
throw new Error("Authentication required");
}
const { data, error } = await supabase
.from("project_notes")
.insert({
owner_id: user.id,
title: input.title,
body: input.body,
visibility: input.visibility ?? "private",
attachment_path: input.attachmentPath ?? null,
})
.select("id,title,visibility")
.single();
if (error) throw error;
revalidatePath("/dashboard");
return data;
}
このコードはRLSに依存しています。サーバー側でgetUser()を呼んでいますが、それはUXと入力整形のためです。最終的にはproject_notesのRLSが、他人のowner_idでinsertされることを拒否します。
Storage uploadの実装
Storageへのアップロードはブラウザから直接行えます。大事なのは、アップロード先のパスをuserId/fileNameに固定し、SQL側のStorage policyと一致させることです。
// src/features/notes/upload-note-attachment.ts
"use client";
import { createClient } from "@/lib/supabase/client";
export async function uploadNoteAttachment(file: File, userId: string) {
const supabase = createClient();
const ext = file.name.split(".").pop()?.toLowerCase() ?? "bin";
const path = `${userId}/${crypto.randomUUID()}.${ext}`;
const { error } = await supabase.storage
.from("note-attachments")
.upload(path, file, {
cacheControl: "3600",
upsert: false,
contentType: file.type,
});
if (error) throw error;
return path;
}
公開URLが必要なサービスならgetPublicUrl()を使う設計もありますが、今回のバケットはprivateです。ダウンロード時は署名付きURLやサーバー経由の配信を検討します。会員制メディアや社内ファイルでは、最初からpublic bucketにしないほうが後戻りを減らせます。
Edge Functionsで通知処理を分離する
Edge Functionsは、Deno互換の実行環境で動くTypeScript関数です。Webhook受信、通知、軽いAI処理、外部API連携のように、Next.js本体から少し離したい処理に向いています。長時間のバッチや重いDB接続を持つ処理は別のワーカーを検討します。
npx supabase functions new notify-note-created
// supabase/functions/notify-note-created/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";
Deno.serve(async (req) => {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const authorization = req.headers.get("Authorization");
if (!authorization) {
return Response.json({ error: "Missing authorization" }, { status: 401 });
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{
global: {
headers: { Authorization: authorization },
},
},
);
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
return Response.json({ error: "Authentication required" }, { status: 401 });
}
const { noteId } = (await req.json()) as { noteId?: string };
if (!noteId) {
return Response.json({ error: "noteId is required" }, { status: 400 });
}
const { data: note, error } = await supabase
.from("project_notes")
.select("id,title,owner_id")
.eq("id", noteId)
.single();
if (error) {
return Response.json({ error: error.message }, { status: 404 });
}
return Response.json({
ok: true,
userId: user.id,
note,
});
});
ローカル実行とデプロイはCLIで行います。
npx supabase functions serve notify-note-created --env-file .env.local
npx supabase functions deploy notify-note-created
Edge Functionでも、Authorizationヘッダーを受け取り、supabase.auth.getUser()で本人確認し、通常のRLSを通してDBを読む構成にします。secret keyで何でも読める関数にすると、便利な代わりにレビュー難易度が上がります。
実装後の確認コマンド
Claude Codeに実装させた後は、最低限この順番で確認します。
npx supabase db reset
npx supabase gen types typescript --local > src/lib/database.types.ts
npm run typecheck
npm test
npx supabase functions serve notify-note-created --env-file .env.local
プロジェクトにSQL lintやE2Eがあるなら、ここに追加します。広いビルドを毎回走らせる必要はありませんが、RLS変更、認証変更、Storage policy変更を含むPRでは、型生成と最小テストを省かないほうが結果的に速いです。
3つの実用ユースケース
| ユースケース | Supabaseで使う機能 | Claude Codeに任せる作業 | 人間が見るべき点 |
|---|---|---|---|
| SaaSのチームメモ | Auth、Postgres、RLS | テーブル、Server Action、一覧UI | チーム境界とowner_idの混同 |
| 会員向け教材配布 | Auth、Storage、署名付きURL | アップロードUI、配信API、監査ログ | public bucketにしていないか |
| イベント予約管理 | Postgres、Edge Functions | 予約テーブル、通知関数、キャンセル処理 | 二重予約、再送、通知失敗時の復旧 |
どのケースでも、先に「誰がどの行を読めるか」をSQLで表現し、そのあとUIを作ります。Claude CodeはUIとAPIを一気に作るのが得意ですが、収益につながるサービスほど、境界を先に固定したほうが手戻りが少なくなります。
具体的な落とし穴
1つ目は、RLSを有効にしただけで満足することです。RLSは有効化すると、ポリシーがない限りanonやauthenticatedから読めなくなります。逆に、ポリシーを雑にusing (true)にすると、公開してはいけない行まで見えます。公開用と本人用を分けてテストしてください。
2つ目は、service role keyをClaude Codeの作業範囲に混ぜることです。管理者キーはRLSを迂回できます。ローカルの.env.localを読ませたり、ログに環境変数を出させたりしないでください。
3つ目は、StorageのファイルパスをUI任せにすることです。ユーザーが任意のパスを送れると、ポリシー設計と実際の配置がずれます。userId/random-fileのように決まった規則を作り、SQL policyとコードの両方で同じ前提にします。
4つ目は、型生成を忘れることです。SupabaseはDBからTypeScript型を生成できます。migration後にdatabase.types.tsを更新しないと、Claude Codeが古い型に合わせたコードを書き続けます。
5つ目は、Edge Functionで認証済みユーザーを確認しないことです。HTTPで呼べる関数は、Route Handlerと同じく入口です。Authorizationヘッダー、JWT検証、RLSを通す読み取りをセットで確認します。
Claude Codeレビュー用チェックリスト
最後に、Claude Codeへ次のレビューを依頼します。実装を作らせた同じ会話でレビューさせるより、差分が落ち着いたあとに「批判的に見る」依頼を分けるほうが効果的です。
Review only the Supabase integration.
Check these points:
- public tables have RLS enabled
- every policy has an explicit TO role
- auth.uid() is used only in RLS-safe expressions
- browser code uses only NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
- no secret key or service role key appears in client code, logs, tests, or docs
- owner_id is derived from the authenticated user, not from form input
- Storage paths match storage.objects policies
- migration and generated database types are in sync
- Edge Functions validate Authorization and use RLS-aware clients
Return findings with file paths and line numbers.
人間側では、ログイン前の公開メモ一覧、ログイン後の自分のprivateメモ、他人のメモIDを指定した更新失敗、別ユーザーのStorageパスへのアップロード失敗、Edge Functionの未認証リクエスト失敗を確認します。AdSenseやPV目的の記事でも、このレベルの検証観点があると単なる概要記事から実務記事に変わります。
ClaudeCodeLabの研修・相談で扱えること
SupabaseとClaude Codeの組み合わせは、プロトタイプの速度を上げる一方で、RLS、認証、Storage policy、マイグレーション運用の理解が浅いと本番事故の入口になります。ClaudeCodeLabでは、既存リポジトリに合わせたCLAUDE.md、Supabase実装レビュー、チーム向けClaude Code研修、DB権限設計の相談を扱えます。自社アプリにAuthや会員向けファイル配信を入れるなら、Claude Code導入相談・研修で現在の構成と不安な箇所を整理してから進めるのが安全です。
この記事で紹介した内容を実際に試した結果、いちばん効果があったのは「UIより先にRLS migrationをClaude Codeへレビューさせる」順番でした。Masaの検証では、最初に画面を作った場合はowner_idをフォーム由来にする差分が混ざりやすく、あとから直すのに時間がかかりました。逆に、要件ファイル、SQL、型生成、Server Actionの順に分けると、Claude Codeの出力も人間のレビューも小さくなり、公開前チェックで見るべき点が明確になりました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。