第4章 【最重要】自分のデータを他人に見せない — データ分離とRLS

📖 この章のゴール:「このデータは誰のもの?」を仕組みで守れるようになる。この教材の背骨にあたる、いちばん大事な章です。 ← 目次・はじめにへもどる


📱 Twitterではこう見える

あたりまえに使っているけれど、よく見るとTwitterはこんな約束を守っています。

  • 自分の下書き(非公開の投稿)は、自分にしか見えない
  • 他人のツイートを、自分が消すことはできない
  • 自分のアカウントとして投稿できるのは、自分だけ(他人になりすませない)

この「あたりまえ」は、作る人がちゃんと設定しないと成り立ちません。 サボると、他人の下書きが見えたり、他人の投稿を消せたりする——初心者がいちばんやりがちな事故です。


🤔 やらないとどうなる?(🟢 基礎)

第3章で、ツイートを保存する tweets テーブルを作りました。投稿はできます。でも、いまのままだと大問題です。

倉庫(データベース)に「全員のツイート」が一緒くたに入っている状態を想像してください。もし取り出すルールが 「全部ちょうだい」 しかなかったら——

あなたが「自分のツイート見せて」とお願いしたのに…
→ データベースは 全員ぶん を返してしまう
→ 他人の下書きも、まる見え 😱

消すときも同じです。「どの行でも消せる」状態なら、他人のツイートを消せてしまいます。

🏤 たとえ話:マンションの集合ポスト 全員の郵便が1つの大きな箱に鍵なしで入っていたら、誰でも他人の郵便を取り放題です。 これを直すには、①どの郵便が誰のものか分かるようにする(部屋番号を書く)と、②自分の部屋の分しか開かない鍵をかける——この2つが必要です。

Webアプリでも、やることは同じ2つです。

  1. データに「持ち主」を書いておくuser_id(=第2章で出てくる、あなたのID)
  2. 「自分のものしか触れない」ルールをかける … これが 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つ(insertupdatedelete)も形は同じで、操作の種類だけが違います。投稿(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点だけ必ずチェック:

  1. enable row level security がある?
  2. ポリシーに auth.uid() = user_id のような 持ち主チェックがある?

この2つが無ければ、そのアプリは他人にデータが見える穴あきです。


📝 ことばメモ

  • データ分離:データを「持ち主ごと」に分けて、他人に見せない・触らせないこと
  • 認可(authorization):「この人はこの操作をしていい?」を判断すること(※「認証=誰か」とは別。認証は第2章)
  • RLS(行レベルセキュリティ):DBが「行ごとに、触っていい人か」をチェックする鍵
  • auth.uid():いまログインしている人のID(DBが知っているので、ごまかせない)
  • ポリシー:「どの行に・誰が・何をしていいか」を決めたルール

➡️ 次の章へ

ここまでは「自分のものは自分だけ」という守りを作りました。次の第5章では、視点を変えて API(出し入れの窓口) そのものを設計します。「同じ窓口なのに、人によって返ってくる中身が違う」のはなぜか——その正体を見ていきます。

(「みんなに見せる公開ツイート」のルールは、第6章『公開タイムライン』でこのRLSに付け足します。)

← 目次・はじめにへもどる