0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Supabase RLS入門:顧客ごとに「見えるデータ」を分ける方法と、プロジェクトを分ける方法の比較

0
Last updated at Posted at 2026-10-22

対象: 「A社のデータがB社に見えたらどうしよう」が頭をよぎっている、業務アプリをつくり始めた方
所要時間: 40分前後 / 使うもの: Supabaseの無料プラン、ブラウザだけ
続きの記事(このしくみを、実際のWebアプリにしてみた): https://qiita.com/tomozilla/items/7ef7e48699cb38868b67
ビジネス編(やさしい解説・note): https://note.com/tomozilla/n/n5d457af8981d

取引先ごとの請求書を管理するアプリをつくる、と想像してください。A社の人がログインしたらA社の請求書だけ、B社の人ならB社の分だけ。当たり前のようで、AIに作ってもらった画面を眺めていると「これ、本当にそうなってる?」と急に不安になります。私はなりました。

この記事では、次の2つの分け方を実際に両方さわって比べます。

  • A. 1つのプロジェクトに全社のデータを入れて、RLS(行レベルセキュリティ)で見える行を分ける
  • B. お客さまごとにプロジェクトを分ける

メインはAのRLSを手を動かして確かめるところです。Bは「どんな感じか」を、実際にプロジェクトを作って見てみました。

0. 先に伝えておきたいこと(正直ベース)

  • 試したのは、この記事のために作った無料プランのデモ環境です。ダミーのユーザー(alice@example.com と bob@example.com)を2人作りました。
  • 「ログインした人になりきる」確認は、SQL Editorで、ログイン済みユーザーの状態を再現して行いました。実際のアプリ(ブラウザ)からログインして確かめたわけではありません。仕組みは同じはずですが、「アプリ経由では未確認」と書いておきます。
  • Bのプロジェクト分離は、プロジェクトを1つ追加で作って見え方を確認しただけです。データの移行や、運用の手間は測っていません。比較表の後半は、私の想像・整理を含みます(そう書いた箇所には印をつけます)。

1. 題材:取引先ごとの請求書

3つのテーブルを使います。

テーブル 役割
tenants 取引先(A社=Acme、B社=Globex)
memberships 「どのユーザーが、どの取引先の人か」の対応表
invoices 請求書(どの取引先のものか、tenant_id を持つ)

ポイントは memberships です。「ログインしている人」と「取引先」をつなぐ表で、あとで「この人はどの会社の人?」を調べるのに使います。

ダミーユーザーを作る

Authentication → Users → Add user → Create new user で、メールアドレスとパスワードを入れます(Auto Confirm User にチェックを入れると、確認メールなしで使えます)。2人作りました。

作成した2人のダミーユーザー

💡 パスワードは使い捨てのランダムな文字列にしました。この記事には載せません。

テーブルを作る

SQL Editorで実行します。

create table public.tenants (
  id uuid primary key default gen_random_uuid(),
  name text not null
);
create table public.memberships (
  tenant_id uuid not null references public.tenants(id),
  user_id uuid not null references auth.users(id),
  primary key (tenant_id, user_id)
);
create table public.invoices (
  id bigint primary key generated always as identity,
  tenant_id uuid not null references public.tenants(id),
  title text not null,
  amount_jpy int not null
);

実行しようとすると、こんな画面が出ました。

Potential issue detected: RLSなしでテーブルを作ろうとしています

Potential issue detected
This query creates tables without enabling Row Level Security. Clients using anon or authenticated keys may be able to access these tables.

「RLSを有効にしないままテーブルを作ろうとしていますよ」という親切な警告です。選択肢は Run without RLS と Run and enable RLS の2つ。

普通は Run and enable RLS を選ぶのがよさそうですが、今回は**「RLSがないと何が起きるか」を見たかった**ので、あえて Run without RLS を選びました。

2. RLSなしだと、どうなる?

ダミーデータを入れます。Alice は Acme、Bob は Globex の所属にします。

insert into public.tenants (name) values ('Acme Inc.'), ('Globex Corp.');

insert into public.memberships (tenant_id, user_id)
select t.id, u.id from public.tenants t
join auth.users u on (t.name = 'Acme Inc.' and u.email = 'alice@example.com')
                  or (t.name = 'Globex Corp.' and u.email = 'bob@example.com');

insert into public.invoices (tenant_id, title, amount_jpy)
select t.id, v.title, v.amount from public.tenants t
join (values
  ('Acme Inc.', 'Website renewal', 120000),
  ('Acme Inc.', 'Support plan', 30000),
  ('Globex Corp.', 'Data migration', 450000),
  ('Globex Corp.', 'Training workshop', 80000)
) as v(tname, title, amount) on v.tname = t.name;

次に、ログインしていない人(anon) になりきって、請求書を見てみます。

begin;
set local role anon;
select t.name as tenant, i.title, i.amount_jpy
from public.invoices i join public.tenants t on t.id = i.tenant_id
order by i.id;

begin; と set local role を使うと、この1回の実行の間だけ別の役割になれます。実行が終わるともとのpostgresに戻ります(あとで select current_user で確かめました)。

結果:

RLSなしだと、ログインしていない人にも全社の請求書が見える

4件、ぜんぶ見えました。 ログインしていない人にも、A社とB社の請求書が。

これは、プロジェクトを作るときの設定が初期のままだったからです(私の場合、「新しいテーブルを自動でAPIに公開する」にチェックが入っていました)。RLSをオンにしていないテーブルは、公開されたテーブルとして、誰からでも読めてしまう。ここが一番「ヒヤッ」としたところでした。

3. RLSをオンにして、ルール(ポリシー)を書く

alter table public.tenants enable row level security;
alter table public.memberships enable row level security;
alter table public.invoices enable row level security;

オンにしただけでは「誰にも何も見せない」状態になります(ルールがないので)。次に、見せていい条件を書きます。

-- 自分の所属(memberships)の行だけ見える
create policy "members see their own memberships" on public.memberships
  for select to authenticated
  using (user_id = (select auth.uid()));

-- 自分が所属する取引先だけ見える
create policy "members see their own tenants" on public.tenants
  for select to authenticated
  using (id in (select tenant_id from public.memberships
                where user_id = (select auth.uid())));

-- 自分が所属する取引先の請求書だけ見える
create policy "members see their tenant's invoices" on public.invoices
  for select to authenticated
  using (tenant_id in (select tenant_id from public.memberships
                       where user_id = (select auth.uid())));

-- 自分が所属する取引先の請求書だけ追加できる
create policy "members add invoices to their tenant" on public.invoices
  for insert to authenticated
  with check (tenant_id in (select tenant_id from public.memberships
                            where user_id = (select auth.uid())));

読み方のコツです。

  • to authenticated:ログイン済みの人に対するルール(ログインしていない人にはルール自体がないので、何も見えません)
  • auth.uid():今ログインしている人のID
  • using (...):見ていい行の条件(select)
  • with check (...):書いていい行の条件(insert)
  • (select auth.uid()) と括弧で包んでいるのは、速度のための書き方と言われているものです(私は計測していません)

ダッシュボードの Authentication → Policies でも、できあがったルールを確認できます。

invoicesに付いたポリシー

4. 3人になりきって確かめる

ログインしていない人

begin;
set local role anon;
select count(*) as visible_invoices from public.invoices;

RLSをオンにした後、ログインしていない人は0件

0。さっきは4件見えていたものが、見えなくなりました。

Alice(Acmeの人)になりきる

ログイン済みユーザーを再現するには、「誰としてログインしているか」を表す情報(JWTのclaims)をセットして、役割を authenticated にします。

begin;
select set_config('request.jwt.claims',
  json_build_object('sub', (select id from auth.users where email = 'alice@example.com'),
                    'role', 'authenticated')::text, true);
set local role authenticated;
select t.name as tenant, i.title, i.amount_jpy
from public.invoices i join public.tenants t on t.id = i.tenant_id
order by i.id;

💡 auth.users を読むのは、authenticated に切り替える前にしています。切り替えたあとは、auth.users は読めないので。

Aliceには、Acmeの2件だけが見える

Acmeの請求書2件だけ。

Bob(Globexの人)になりきる

上のSQLの alice@example.com を bob@example.com に変えるだけです。

Bobには、Globexの2件だけが見える

Globexの2件だけ。同じ質問(SQL)なのに、人によって答えが違う。これが、RLSがやってくれることです。

5. 書き込みはどうなる?(ここで私は勘違いしました)

AliceがGlobexの請求書を勝手に追加できてしまわないか、確かめます。最初に、こう書きました。

-- (最初に書いたもの)
insert into public.invoices (tenant_id, title, amount_jpy)
select id, 'Sneaky invoice', 1 from public.tenants where name = 'Globex Corp.';

結果は…エラーが出ませんでした。「あれ、通っちゃった?」と一瞬あわてました。

でも請求書の件数は4件のままでした。よく考えると、Aliceには Globexの tenants の行がそもそも見えません(さっきのポリシーのおかげです)。だから select ... where name = 'Globex Corp.' が0行を返し、0件のinsertが成功していただけでした。

つまずきメモ:「エラーが出ない」=「安全が確かめられた」ではありませんでした。RLSは見えない行を、エラーにせず“なかったこと”にすることがあります。

そこで、GlobexのIDを先に取っておいて、そのIDを直接指定して試しました。

begin;
select set_config('app.globex_id',
  (select id::text from public.tenants where name = 'Globex Corp.'), true);
select set_config('request.jwt.claims',
  json_build_object('sub', (select id from auth.users where email = 'alice@example.com'),
                    'role', 'authenticated')::text, true);
set local role authenticated;
insert into public.invoices (tenant_id, title, amount_jpy)
values (current_setting('app.globex_id')::uuid, 'Sneaky invoice', 1);

AliceがGlobexの請求書を追加しようとすると、拒否される

ERROR: 42501: new row violates row-level security policy for table "invoices"

今度ははっきり拒否されました。「書く側」も守られています。

6. もうひとつの方法:お客さまごとにプロジェクトを分ける

ここからがBです。Aは「1つの家に、部屋ごとの鍵」。Bは「お客さまごとに、別の家」。実際に、Globex用のプロジェクトを作ってみました。

プロジェクトの一覧がこうなりました。

プロジェクトが2つ並んだ

作ってみて気づいたことです。

  • 1つ作るのに、名前・データベースのパスワード・リージョンを選ぶ。1つ目は ap-southeast-2、2つ目は ap-south-1 と、気づかないうちにリージョンがバラバラになりました(作成画面の「おすすめ」を選んだ結果で、私が意図したものではありません)。
  • テーブルやRLSの設定、Edge Functionなどは、プロジェクトごとに別々。お客さまが増えるたびに、同じ作業を繰り返すことになります。
  • 無料プランの画面には、プロジェクト数の上限があるようです(私は2つまで作成。3つ目は試していません)。

2つの方法の比較

観点 A. 1プロジェクト + RLS B. お客さまごとにプロジェクト
分離の強さ ポリシーの書き方しだい(書き間違えると漏れる) データベース自体が別。とても強い
お客さまが増えたとき 行を足すだけ プロジェクトを足す(作業・管理が増える)※
テーブルの変更(マイグレーション) 1回 プロジェクトの数だけ ※
費用 まとめられる ※ プロジェクトごとにかかる ※
向いていそうな場面 小〜中規模のお客さま多数、機能は共通 数が少ない大口顧客、契約上「完全分離」が必須
私が実際に確かめたこと ✅ ポリシーで見える行が変わる ✅ プロジェクトを作ると完全に別になる(データ移行・運用負荷は未測定)

※は、私の想像・整理で、実際には測っていません。料金は変わるので、最新の情報を確認してください。

気をつけたいこと(ドキュメントで見かけたもの・未検証)

次のことは、私は今回試していません。

  • サーバー用の強い鍵(service_role)はRLSを無視する、と聞きます。ブラウザなど外には出さない。
  • ユーザー自身が書き換えられる情報(user_metadata)を、権限の判断に使ってはいけない、と言われています。
  • ビューは、RLSをすり抜ける作りになることがある、と聞きます。使うときは設定の確認が必要そうです。

まとめ

  • RLSがないと、ログインしていない人にも全部見える状態になりえた(私のデモでは実際に4件ぜんぶ見えた)
  • RLSをオンにして、ポリシーを書くと、同じSQLでも人によって見える行が変わる
  • 「エラーが出ない」は「安全」とは限らない。書く側(insert)も、拒否されるかを確かめる
  • プロジェクト分離は強力だけれど、お客さまの数だけ作業が増える

次回は、AIエージェントにDBを触らせるときの「権限の絞り方」に進みます。


この記事のスクリーンショットは、記事用に作った無料プランのデモ環境で撮影したものです。メールアドレスはダミー(example.com)です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?