Claude Code と Supabase で最初のアプリを作るとき、多くの未経験者が最初にぶつかるのが「テーブル設計」です。プログラミングの知識がまだ浅くても、AIに頼めば CREATE TABLE くらいはすぐ返ってきます。
でも、頼み方が雑だと「動くけど危ないスキーマ」が返ってきます。ローカルでは動いて、公開した瞬間に「他人のデータが全部見える」みたいな事故につながる。ここが未経験者の最初の落とし穴です。
この記事は「AIが書いた危険なコードの見抜き方」ではなく、その一歩手前 —— AIに「テーブル作って」と丸投げしたときに何が抜け落ちるのか、そして RLS(Row Level Security)まで含めて安全に出させる"頼み方" を、実際のSQLとプロンプトで見ていきます。
雑に頼むと返ってくるスキーマ
たとえば「TODOアプリのタスクを保存するテーブルを作って」とだけ頼むと、こういうものが返りがちです。
create table todos (
id serial primary key,
title text,
done boolean,
user_id text
);
一見それっぽいですが、未経験者向けに危ない点が3つ詰まっています。
- RLSが無い — Supabaseはテーブルを作っただけだと、匿名キー(anon key)で誰でも全行を読み書きできてしまう構造になり得ます。公開アプリで最悪。
-
型が緩い —
user_id textになっている。本来ユーザーIDはauth.usersのuuidと紐づけるべきで、textだと型のミスマッチやリレーション切れが起きます。done booleanにnot null default falseも無い。 -
リレーションが不明確 —
user_idが「誰の」IDなのか、外部キー制約が無いので保証されない。存在しないユーザーのタスクが混ざる余地が残ります。
ローカルの select * from todos では気づけません。公開して初めて「あれ、Aさんのタスクが全員に見えてる」となる。これがRLS抜けの典型です。
何を渡すと「安全なスキーマ」が返るか
AIは渡した情報の範囲でしか設計できません。テーブル設計を頼むときは、最低この3点を言葉で渡します。
- どんなデータを持つか(カラムと、それぞれの意味・必須かどうか)
- 誰が読み書きするか(本人だけ? ログインユーザー全員? 公開?)
- 他のテーブルとの関係(1対多か、多対多か。ユーザーと紐づくか)
「誰が読み書きするか」を渡すのが一番大事です。ここがRLSポリシーの中身に直結します。
こういう頼み方をします。
Supabaseで「タスク管理」のテーブルを設計してください。条件は次の通りです。
- 保存するデータ: タイトル(必須), 完了フラグ(初期false), 期限(任意), 作成日時
- 各タスクは「ログインしたユーザー本人」に紐づく
- 読み書きできるのは本人だけ。他人のタスクは見えないし、更新もできない
- ユーザーは Supabase Auth (auth.users) を使う
CREATE TABLE と、RLSポリシーの両方を出してください。
そして「なぜそのRLSポリシーになるのか」を1つずつ説明してください。
ポイントは最後の一文です。RLSも含めて出させ、「なぜこのポリシーか」を説明させる。説明を読めば、自分がコピペしているものが何をしているか分かるようになります。ここで「プログラミング」と「AIへの頼み方」が同時に身についていきます。
返ってくる正しいスキーマの読み方
上のように頼むと、こういうSQLが返ってきます。
-- テーブル定義
create table todos (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users (id) on delete cascade,
title text not null,
done boolean not null default false,
due_date date,
created_at timestamptz not null default now()
);
読み方の勘所はここです。
-
id uuid ... default gen_random_uuid()— 連番のserialではなくUUID。推測されにくく、公開アプリ向き。 -
user_id uuid not null references auth.users (id)— Supabase Authのユーザーと外部キーで紐づく。これで「誰のタスクか」がDBレベルで保証される。 -
on delete cascade— ユーザーが消えたら、そのタスクも自動で消える。孤児レコードが残らない。 -
not null/default—titleは必須、doneは初期false。型と制約がはっきりしている。
続いてRLS部分です。
-- RLSを有効化(これを忘れると全公開状態)
alter table todos enable row level security;
-- 本人だけ SELECT できる
create policy "select own todos"
on todos for select
using (auth.uid() = user_id);
-- 本人として INSERT できる(他人のuser_idで作れない)
create policy "insert own todos"
on todos for insert
with check (auth.uid() = user_id);
-- 本人だけ UPDATE できる
create policy "update own todos"
on todos for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- 本人だけ DELETE できる
create policy "delete own todos"
on todos for delete
using (auth.uid() = user_id);
ここが未経験者が一番理解しておくべきところです。
-
alter table ... enable row level security— これを実行して初めてポリシーが効きます。有効化しないままだと、ポリシーを書いても全部素通りしてしまうので、最初に必ず入れる。 -
auth.uid()— 「今リクエストしているログインユーザーのID」を返すSupabaseの関数。これとuser_idを突き合わせて「自分の行か?」を判定します。 -
usingとwith checkの違い —usingは「どの行を対象にできるか(読める・触れる行の絞り込み)」、with checkは「書き込もうとしている値が条件を満たすか」。INSERTにはusingが無くwith checkを、UPDATEには両方を付けるのが定石です。ここを取り違えると「他人のuser_idで作れてしまう」穴が開きます。
select own todos ポリシーがあることで、匿名キーで叩いても auth.uid() = user_id が成立する行しか返りません。「他人のタスクが全員に見える」事故がDB側で防がれている わけです。
「説明させる」がなぜ効くか
RLSは、SQLに慣れた人でも using と with check を混同したり、enable row level security を打ち忘れたりします。未経験者ならなおさら見抜けません。
だからこそAIに「なぜこのポリシーか」を説明させます。返ってきた説明を読んで、
- 「なぜ有効化が要るのか」
- 「なぜINSERTは
with checkなのか」
が自分の言葉で言えるなら、そのスキーマは自分で守れます。逆に説明が曖昧だったら、そのまま公開せずもう一段深掘りする。AIに出させて、AIに説明させて、自分で検証する — この往復が、コードを書く力とAIを使う力を同時に鍛えてくれます。
まずは自分のアプリで「誰が読み書きするか」を1行で言えるようにする。そこからRLSは自然と設計できます。
※この記事は未経験〜駆け出し向けです。中上級の方には物足りないので、社内研修・後輩育成・知り合いへの紹介にどうぞ。
未経験から Next.js + Supabase + Claude Code でWebアプリを作って公開するまでを全20セッションで体系化した教材を作っています。
- 無料で試す(未経験向け体験版)→ https://github.com/ayies128/next-ai-camp-trial
- 教材完全版+チャットサポート(月5,500円・面談なし)→ https://menta.work/plan/20251?ref=qiita
- AI×開発ニュースを毎日発信 → YouTube『AIエンジニア情報局』(別運営チャンネル)