第4章 【最重要】自分のデータを他人に見せない — データ分離とRLS
📖 この章のゴール:「このデータは誰のもの?」を仕組みで守れるようになる。この教材の背骨にあたる、いちばん大事な章です。 ← 目次・はじめにへもどる
📱 Twitterではこう見える
あたりまえに使っているけれど、よく見るとTwitterはこんな約束を守っています。
- 自分の下書き(非公開の投稿)は、自分にしか見えない
- 他人のツイートを、自分が消すことはできない
- 自分のアカウントとして投稿できるのは、自分だけ(他人になりすませない)
この「あたりまえ」は、作る人がちゃんと設定しないと成り立ちません。 サボると、他人の下書きが見えたり、他人の投稿を消せたりする——初心者がいちばんやりがちな事故です。
🤔 やらないとどうなる?(🟢 基礎)
第3章で、ツイートを保存する tweets テーブルを作りました。投稿はできます。でも、いまのままだと大問題です。
倉庫(データベース)に「全員のツイート」が一緒くたに入っている状態を想像してください。もし取り出すルールが 「全部ちょうだい」 しかなかったら——
あなたが「自分のツイート見せて」とお願いしたのに…
→ データベースは 全員ぶん を返してしまう
→ 他人の下書きも、まる見え 😱
消すときも同じです。「どの行でも消せる」状態なら、他人のツイートを消せてしまいます。
🏤 たとえ話:マンションの集合ポスト 全員の郵便が1つの大きな箱に鍵なしで入っていたら、誰でも他人の郵便を取り放題です。 これを直すには、①どの郵便が誰のものか分かるようにする(部屋番号を書く)と、②自分の部屋の分しか開かない鍵をかける——この2つが必要です。
Webアプリでも、やることは同じ2つです。
- データに「持ち主」を書いておく …
user_id(=第2章で出てくる、あなたのID) - 「自分のものしか触れない」ルールをかける … これが RLS です
🛠 Supabaseでこう作る(🟢 基礎)
Supabaseの中身は PostgreSQL(ポストグレス) というデータベースです。そこには RLS(Row Level Security = 行レベルセキュリティ) という、まさに「自分の行しか触らせない鍵」の機能があります。
📝 SQLって? これから出てくるのは SQL(エスキューエル) という、データベースへの命令を書く言葉です。いまは一語一句の暗記は不要。「こういう短い命令で鍵をかけられる」 という雰囲気だけ掴めばOKです。
① テーブルに「持ち主」の列を足す
第3章では、まだ持ち主のない簡単な tweets を作りました。ここでは 持ち主(user_id)まで入った”完成版” にします(すでに作ってある場合は、新規作成のかわりに user_id 列を足すだけでもOKです)。
create table tweets (
id bigint generated always as identity primary key, -- 1件ごとの通し番号
user_id uuid not null references auth.users (id) default auth.uid(), -- ★持ち主のID
body text not null, -- ツイート本文
created_at timestamptz not null default now() -- 投稿した時刻
);
1行ずつ読むと、こうです。
create table tweets (… 「tweets(ツイート)という名前の表を作る」という宣言。id ... primary key… 1件ごとの通し番号。表の中で行を区別する目印(「主キー」=重複しない番号)。user_id uuid ... references auth.users (id)… ★この章の主役。このツイートの持ち主を表す列。中身はauth.users表のid(ログインしている人のID)を指す。型はuuid(後述)。default auth.uid()… 投稿のとき、いまログインしている人のIDを自動で入れる。だから「書いた人=持ち主」になる。body text not null… ツイート本文。not nullは「空はダメ(必ず何か入れる)」という意味。created_at ... default now()… 投稿した時刻を自動で記録(あとで新しい順に並べるのに使う)。
🪪
user_idって何者?(このIDの正体)
user_idは「この投稿は、どのユーザーのものか」を指す番号です。中身はauth.users表のid——つまり 第2章でログインしたときに、あなたに割り当てられるID そのものです。 このIDは1, 2, 3…のような連番ではなく、uuid(例:a1b2c3d4-89ab-…のような長いランダムな文字列)になっています。なぜ連番にしないのか、そしてユーザーまわりに出てくる「いろいろなID」(Googleが渡すID/auth.usersのID/表示名@ハンドル…)の整理は、次の第2章でしっかり扱います。 ここでは「持ち主を指す、ごまかせないID」とだけ押さえてください。
② テーブルに鍵をかける(これを忘れると筒抜け!)
alter table tweets enable row level security;
この1行で「行ごとに、触っていい人かどうかチェックする」モードが入ります。ここを忘れるのが最大の事故です。
③ ルール(ポリシー)を書く
「自分のツイートだけ、読む・作る・直す・消す ができる」というルールを4つ書きます。
-- 読む:自分のツイートだけ
create policy "自分のツイートを読む" on tweets
for select using ( auth.uid() = user_id );
-- 作る:自分として投稿する(他人のIDでは作れない)
create policy "自分として投稿する" on tweets
for insert with check ( auth.uid() = user_id );
-- 直す:自分のツイートだけ
create policy "自分のツイートを直す" on tweets
for update using ( auth.uid() = user_id );
-- 消す:自分のツイートだけ
create policy "自分のツイートを消す" on tweets
for delete using ( auth.uid() = user_id );
1つめのポリシーを1行ずつ読むと:
create policy "自分のツイートを読む"… 「自分のツイートを読む」という名前のルールを作る(名前は自由)。on tweets… 対象はtweets表。for select… 「読む」操作のときのルール、という意味(select= 読む)。using ( auth.uid() = user_id )… 通す条件。auth.uid()(いまリクエストしている人のID)と、その行のuser_id(持ち主)が 一致する行だけ を読める。
残り3つ(insert/update/delete)も形は同じで、操作の種類だけが違います。投稿(insert)だけ、using ではなく with check を使います。
| 言葉 | 意味 |
|---|---|
auth.uid() |
いまリクエストしている人のID(DBが知っているのでごまかせない) |
user_id |
その行(ツイート)の持ち主のID |
auth.uid() = user_id |
「リクエストした人 = 持ち主」のときだけ通す |
using (...) |
すでにある行の、どれに触っていいかの条件(読む・直す・消す) |
with check (...) |
新しく作る行が満たすべき条件(作る)。insert で「自分以外のIDで作らせない」ために使う |
④ すると、こうなる
ブラウザ側のコードは、こうお願いするだけです。
// 「ツイート一覧ちょうだい」とお願いする(Supabaseクライアント)
const { data } = await supabase
.from('tweets')
.select('*')
.order('created_at', { ascending: false }); // 新しい順
supabase.from('tweets')… 「tweets表に用がある」と伝える。.select('*')… 「全部の列をちょうだい」(*は「全部」の意味)。.order('created_at', { ascending: false })… 「created_at(投稿時刻)で 新しい順に並べて」(ascending: false= 降順=新しいものが上)。
注目してください。このコードには 「自分のだけ」とはどこにも書いていません。 それでも、RLSのおかげで 自分のツイートしか返ってきません。
✅ これがDB側で守る強さ:たとえ画面側のコードで絞り込みを書き忘れても、データベースが最後の砦として他人の分を弾いてくれます。
🔧 補足:RLSのない普通のDB(PostgreSQL素/MySQL)では?
RLSは便利ですが、すべてのデータベースにある機能ではありません。 とくに MySQL にはRLSがありません(素のPostgreSQLでも、明示的にONにしないと使いません)。
その場合は、アプリ側のコードで毎回 自分で絞り込むことになります。
-- アプリが毎回こう書く(「ログイン中の人のID」を必ず渡す)
select * from tweets where user_id = 'いまの人のID' order by created_at desc;
| Supabase の RLS | 普通のDB(素のPostgreSQL / MySQL) | |
|---|---|---|
| 誰が絞り込む? | データベースが自動で | アプリが毎回 手で |
| 書き忘れたら? | それでも守られる | そこから全部 漏れる 😱 |
| 向いている人 | 初心者・少人数 | 仕組みを自分で握りたい上級者 |
⚠️ 「手で毎回
where user_id = ...を付ける」方式は、1か所でも付け忘れると、そこが穴になります。 だから「DB側で強制できる(RLS)」方が安全なのです。くわしくは付録Cで扱います。
⚠️ ハマりどころ
- RLSをONにし忘れる … いちばん危険。テーブルを作っただけで安心しない。必ず
enable row level securityを確認。 - ポリシーを「全部許可」にしてしまう …
using ( true )のように書くと、誰でも全行OKになり分離が消えます。 - 画面(フロント)側だけで隠して満足する … 「自分のじゃないからボタンを非表示」だけでは、APIを直接たたかれたら漏れます。止めるのはサーバー/DB側(RLS)。画面側の制御は「見た目を整えるおまけ」と考える。
user_idをブラウザから受け取って信じる … なりすませます。持ち主の判定は、DBが知っているauth.uid()を使う。
🤖 AIに頼むなら(Vibe codingのコツ)
AIはテーブルは作ってくれますが、RLSを忘れがちです。だから明示的に頼みます。
🗣 プロンプト例: 「
tweetsテーブルを作って。RLSを有効化して、auth.uid() = user_idで自分の行だけ select / insert / update / delete できるポリシーも書いて。 なぜそのポリシーが必要かもコメントで説明して」
出てきたコードは、この2点だけ必ずチェック:
enable row level securityがある?- ポリシーに
auth.uid() = user_idのような 持ち主チェックがある?
この2つが無ければ、そのアプリは他人にデータが見える穴あきです。
📝 ことばメモ
- データ分離:データを「持ち主ごと」に分けて、他人に見せない・触らせないこと
- 認可(authorization):「この人はこの操作をしていい?」を判断すること(※「認証=誰か」とは別。認証は第2章)
- RLS(行レベルセキュリティ):DBが「行ごとに、触っていい人か」をチェックする鍵
auth.uid():いまログインしている人のID(DBが知っているので、ごまかせない)- ポリシー:「どの行に・誰が・何をしていいか」を決めたルール
➡️ 次の章へ
ここまでは「自分のものは自分だけ」という守りを作りました。次の第5章では、視点を変えて API(出し入れの窓口) そのものを設計します。「同じ窓口なのに、人によって返ってくる中身が違う」のはなぜか——その正体を見ていきます。
(「みんなに見せる公開ツイート」のルールは、第6章『公開タイムライン』でこのRLSに付け足します。)