
マルチテナントSaaSでのRLS導入に際し、方針策定からDevinを活用した並列実装までを行った実践例
Text by 池田 仁俊
Published
本記事は、マルチテナント SaaS における Row-Level Security(RLS)ポリシーの設計方針を定め、それを全テーブルに適用するための技術的な検討と実践内容をまとめたものです。
当初、PoC フェーズとして機能を開発していた段階では問題にならなかったものの、組織機能の実装が進むにつれて、他組織のデータを誤って参照できてしまう事象への対処が必要になりました。
そこで、アクセス制御の仕組みとして Supabase の RLS 機能を活用し、組織単位でデータを保護するポリシーを全テーブルに適用する方針を取りました。
ただし、テーブルは 15〜20 個ほど存在しており、それぞれに対して RLS を適用するには、アプリケーションの構造的な見直しと、RLS ポリシーの一貫性ある設計が求められました。
加えて、テーブル数が多いにもかかわらず、リリーススケジュールには限りがあり、チームには高いベロシティが求められていました。
AI の活用はチーム内でも一般的ではありましたが、その状況の中で、私は AI エージェントである Devin を活用し、実装作業を並列・効率的に進める手段を選択しました。
本記事では以下を中心に記載していきます
- RLS 導入の方針・アプリケーション側への移行の影響を最小限にとどめる工夫
- Devin を活用するうえで準備したこと、実行方法、得られた知見
- 実装にあたって発生した調整・失敗の内容
一連の作業によって、AI を活用することで効率化された一方、人間が設計上の粒度やルールを先に整備することの重要性を確認できました。
技術的な実装やツール選定にとどまらず、実装プロセスをいかに再現可能なものにするか、何か参考になる情報があれば幸いです。
1. AIエージェントへ任せるために、RLS の方針を決定する
繰り返しになりますが、組織(Organization)単位でのアクセス制御を導入するにあたり、PostgreSQL の RLS(Row-Level Security)を用いて、各テーブルに制御ポリシーを適用することにしました。
Supabase では auth.uid()
を通じてログインユーザーの ID が取得できます。さらに、私たちのプロダクトでは、ユーザーと所属組織の関係は organization_members
テーブルで管理されています。
この構造を用いると、各テーブルが organization_id
を持っていれば、認可ポリシーの実装は比較的明確になります。
方針を ADR にまとめて明文化
RLS の設計方針は ADR(Architecture Decision Record)として文書化しました。
- 全テーブルに
organization_id
を追加し、RLS はこれを基準に設計する - ユーザーは、自身が所属する組織のデータにのみアクセスできる
- クライアントとジョブで異なるアクセスロール(
anon
とservice_role
)を区別して制御する - 外部サービス連携など、一般的でない操作については個別に設計する
実装者だけではなく、開発エージェントもこの設計方針を参照できることで、生成するコードの整合性や精度の向上が期待できます。
私たちのプロダクトは OSS で開発中のものとなるため、ADR の実物もご覧いただけます。1
アプリケーションへの影響を抑える実装構造・トリガーと関数による自動補完
RLS を適用するうえで、organization_id
を全テーブルに追加する方針は明確になりましたが、そのまま進めるとアプリケーション側の実装にも大きな変更が必要になることがわかっていました。
たとえば、レコードの INSERT 時にアプリケーション側から organization_id
を明示的に渡す必要が出てきます。しかしこれは、以下のような実装上の懸念を伴います。
- 複数の既存フォームやロジックに影響が波及する
- メンバーのタスクとコンフリクトしやすくなる
- 型定義上
organization_id
が必須となり、アプリケーションのあちこちに影響が出る
そこで「organization_id
はアプリケーションから渡さない。DB 側で自動的に補完する構造にする」という方針を取りました。
Supabase では PostgreSQL をベースにしているため、テーブルに対してDB トリガーとDB Functionsを設定することで、INSERT や UPDATE のタイミングで organization_id
を自動的に設定できます。
この構造を使うことで、アプリケーション側では organization_id
を意識せず、従来通りの操作ができます。結果として、アプリケーションのコードにはほとんど変更を加えずに済みました。
具体的な実装例は2章で紹介します。
2. Devin と共にプロンプトの作成を行い、モデルケースでそのプロンプトの精度を検証する
RLS を全テーブルに適用するにあたり、最初に 1 つのテーブルに対してある程度時間をかけて実装しました。この工程により、Devin に正確な指示を出すために必要な実装的な検討事項を洗い出し、並列で動くセッションのアウトプットの質を均質にすることができました。
Devin を採用した理由
私たちのチームでは日常的に AI ツールを活用していますが、今回は Devin を使う判断をしました。
Devin は、以下のような特性があり、今回のような並列・大量の作業には相性が良いと感じていました。
- 指示に対して PR 単位で成果物をまとめてくれる
- エージェントを複数セッションとして同時に立ち上げられる
- 推論能力・推進力が高く、プロンプトのテンプレートを読んで、個々のケースで自己解決しやり切る力がある
Devin とプロンプトの叩き台を作る
Devin で並列実行することを想定したプロンプトを仮で用意し、モデルケースでも Devin を使用して実装を進めていきます。このプロンプト自体も Devin と壁打ちしながら作成します。
そうすることで、Devin にこれまで論じてきた実装方針の決定を隈なく網羅したプロンプトを与えた上で、どうしても実装漏れになってしまう箇所などを浚うことができます。実装漏れになる箇所は、プロンプトを補強するか、Devin での対応を諦めて人の手で対応することになります。
モデルケースとして選んだのは、組織ID(organization_id
)を直接持たず、別のテーブルを介して間接的に参照している構造のテーブルです。
組織(Organization)のテーブルとの関係が間接的であるという意味で、RLS の設計上の典型的なケースであり、多くのテーブルに共通する課題を持っていました。
アプリケーションの影響を極小にする実装戦略の検証
1 章で述べたように、アプリケーション側に影響を出さずに organization_id
を設定するため、PostgreSQL のトリガーと関数を用いた補完構造を組みました。モデルケースの実装では、この構造が実際に正しく動作するかを重点的に検証しました。
CREATE OR REPLACE FUNCTION public.set_knowledge_suggestions_organization_id()
RETURNS trigger AS $$
BEGIN
NEW.organization_id := (
SELECT organization_id
FROM public.projects
WHERE id = NEW.project_id
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER set_knowledge_suggestions_organization_id_trigger
BEFORE INSERT OR UPDATE ON public.knowledge_suggestions
FOR EACH ROW
EXECUTE FUNCTION public.set_knowledge_suggestions_organization_id();
この構造により、project_id
さえ渡せば organization_id
は自動補完され、アプリケーション側の変更は不要であることを確認できました。
Supabase の型自動生成との整合性の課題と対応
PostgreSQL の トリガーと Functions を使用するにあたって、出てきた課題が Supabase のスキーマ定義から自動生成される TypeScript の 型でした。アプリケーション側では値を明示的に指定しない設計にもかかわらず、Supabase の自動生成型では NOT NULL 制約に基づいて必須項目とみなされてしまうため、型エラーが発生してしまいます。
このため、型のオーバーライド構造を導入し、Supabase の自動生成型に対して organization_id?: string | null
とする再定義を行うことで、型エラーを回避しました。2 これは明示的な入力を不要にするという設計方針と整合するものであり、機能開発の手戻りを生まない重要な設計要素になりました。
その他プロンプトから見えていなかった実装上の考慮事項
Devin へのプロンプトは、初期の段階でおおまかな構造やテンプレートを生成できていました。しかし、このモデルケースの実装を通じて、チームで議論する中で、実際には Devin に指示する前に明文化されていなかった細かな検討点が多く見つかりました。
たとえば
- アプリケーションの中で使用されていない操作(SELECT/INSERT/UPDATE/DELETE)は、RLS で許可しない方が安全である
organization_id
を DB 側で補完する場合、Supabase の自動生成型と整合性を取る必要があるUPDATE ... FROM
形式ではなくサブクエリ形式を採用することで、関数やトリガーと一貫性を保てる
これらは、実際の PR 作成時の手作業によって明らかになったポイントであり、プロンプト作成時に Devin を単に使うだけでは見えてこなかった部分です。
モデルケース作成のまとめ
この 1 テーブルへの RLS 適用を通じて、Devin に依頼する際に必要な実装上の考慮点が整理できました。事前に実装方針を PullRequest くらい具体的な粒度で明確化できたことで、後続の作業をスムーズに進める土台となりました。
モデルケースの PullRequest を作成することで、マニュアルをより具体的にでき、Devin に出すプロンプトの精度をあげ、複数テーブルへの適用をより均質な出力で並列に行えるようになりました。
この進め方は、コード規約を作成して、メンバーと一つの一貫したアーキテクチャのシステムを作るテックリード的な仕草のようでした。
3. Devin による並列作業の実行
モデルケースを通して実装の方針が固まった後は、それを他の十数テーブルにも展開していくフェーズに移りました。 Devin を活用することで、複数の作業を並列に進める体制を整えることができました。
テーブルごとに並列セッションを作成し、Devinへタスクを依頼
RLS のポリシー適用には、各テーブルの親子関係を把握する必要があります。モデルケースの実装が完了した後は、Liam ERD 3を使ってすべての対象テーブルの依存関係を明確にしました。
ERD の図上で親子関係を視覚的に把握できるため、どのテーブルが他のテーブルに依存しているかが一目で分かります。このツールによって、RLS を適用すべき順序や、PR の依存関係を正しく構成する助けになりました。4

Devin を動かすための準備
依存関係が整理できたら、Devin へマニュアルプロンプトとモデルケースの PullRequest を渡し、タスクを依頼していきました。最大で 4〜5 セッション程度を並列に立ち上げて PR を生成する形を取りました。
マニュアルプロンプトには、以下のような情報が含まれています。
- 方針を明文化した ADR
- 作業の手順
- 使用する DB 関数や RLS ポリシーパターンのコード例
- 各テーブルの実装順と依存関係の一覧
最大で 4〜5 セッションになったのは、テーブルの依存関係が原因です。たとえば、ある「親テーブル」に対して複数の「子テーブル」がぶら下がっている場合、子は親に依存しているため、まず親テーブルの修正 PR がドラフトでも出ていなければ作業を進められません。そして、Devin が子のテーブルの作業を進めるには、親 PR のブランチからチェックアウトする必要があるため、孫テーブルに至ってはさらにその子の PR 作成完了を待つ必要があります。
また、Devin には PR の内容を最小限に留め、不要なリファクタや追加のコミットを控えるよう指示しました。私たちのケースでは、自律的な補完が増えるほど、最終的なレビューやマージ処理の負荷が増してしまったためです。
Devin による PRの完成度
実際に作成された PR は、以下の要素が満たされており、ある程度均質化されたものでした。
- organization_id カラムの追加と外部キー制約の設定
- トリガーと Function を用いた organization_id の自動補完
- アクセスパターン(UI/ジョブ)に応じた RLS ポリシーの定義
- migration ファイルと schema.sql の更新
- Supabase 型の生成とオーバーライド対応
PR はすべてドラフト状態で作成し、CI にも反応しないようにしていたため、安全に手元での動作確認とレビューが行えました。
短期間で 15~20 件ほどの PR 作成・mergeすることができました。
今回の作業は OSS リポジトリ上で進めており、実際に提出した PR も誰でも閲覧することができます。
以下にいくつか代表的な PR を紹介します。具体的なトリガーの記述やポリシーの定義、Devin とのやりとりの痕跡も残っているため、関心のある方は参照してみてください。
knowledge_suggestions
テーブルへの RLS 適用review_feedbacks
テーブルの対応例(DB トリガーの動作確認つき)overall_reviews
テーブル(複数の親参照)
4. Devin との協働の中で行った人間による補完
Devin を用いた並列作業によって、organization_id カラムの追加と RLS 適用の作業は想定通り進みましたが、いくつかの場面で調整や人手での補完が必要となるケースがありました。この章では、PR の進行中に発生した課題と、品質を担保するために工夫した点についてまとめます。
人間が行う必要があったこと
1. schema.sql のコンフリクトと型生成の不整合
migration ファイルはテーブルごとに分かれていたためコンフリクトは発生しませんでしたが、supabase cliで生成している型は全体の最新状態を反映する必要があるため、並列で作成された PR 同士で コンフリクトしやすい状況が生まれました。
Devin上で解決できなかったため、人の手で再生成・コミットを行う必要があるケースも一部で発生しました。
2. アクセス経路やポリシーの過不足
RLS ポリシーの設定は、Devin による .from('テーブル名')
検索に基づいて実施されていましたが、一部の操作が意図しない形で許可されている PR もありました。
特に、アプリケーションからは使用されていない操作(INSERT や DELETE など)が明示的に許可されているケースがあり、使用されていない操作はすべて禁止とする方針で明示的に修正を行いました。また、UI とジョブのアクセス経路判定に誤りがあった PR についても、人の目でコードベースを確認して調整しました。
3. 人間の残りタスクはレビュー、 merge のみ
私たちのチームでは、DevinのPRは、「Devinへの依頼者」と「レビュワー」の両者のApproveが必要な運用ルールにしています。
タスクの性質上、mainブランチへDBのスキーマの変更が発生するたびに自動生成のschema.sql
がコンフリクトしてしまうため、一旦schema.sql
のコンフリクトは無視して、大量のPRのレビュワーにレビューしていただきました。
特に以下の観点を中心にレビューを依頼しました。
- SELECT / INSERT / UPDATE / DELETE の各操作に対して、過剰・不足のないポリシーが定義されているか
- 操作が UI 経由か、ジョブ経由か、想定されるアクセス経路に応じたロール設定がされているか
- 特殊なポリシーや例外が必要なテーブル(外部連携など)で、制限が妥当かどうか
organization_id
の自動補完トリガーについては、最初のいくつかの PR のみ手元で実際に動作確認を行い、その後は実装者が PR 内にログ出力やサンプルデータ操作結果を記載する形式に移行しました。PR ではその証拠をもとにレビューを行うことで、チーム全体の確認負荷を抑えました。
レビュワーによりレビューが完了すれば、schema.sql
の差分によるコンフリクトは発生するものの、マージ後に rebase して手元で整合性を取るだけで済みます。レビュー完了後のマージと修正は自分のペースでこなせる作業負荷の低いタスクでした。
このやり方により、他のタスクと並行しながらマージ対応を進めることができ、個人のベロシティにも影響を与えることはありませんでした。
人間による補完を通して実感した、有効なプロンプト設計の工夫
Devin を効果的に活用するために行ったプロンプト設計の工夫としては、以下の点が特に有効でした。
- 操作対象のテーブル名を明示する
- チェックアウトすべきブランチ名を指定する
- 参考にすべき PR やサンプルコードを渡す
- ADR やコードの該当ディレクトリパスを明記する
- プロンプトに「Draft で止める」「CI は無視」といった実装ルールを明示する
並列作業で品質を担保するには、具体的な構文レベルの指示や、実装スタイルまで明文化されたテンプレートをベースに進めるのが有効でした。
5. おわりに
本記事では、マルチテナント SaaS における RLS(Row-Level Security)の導入方針を整理し、Supabase 上の複数テーブルに組織単位のセキュリティ制御を適用していった実践例を紹介しました。
特に要点は以下の通りです。
organization_id
を各テーブルに持たせることで、ポリシーの一貫性とセキュリティを担保できる- トリガーと関数を活用すれば、アプリケーション側の影響を抑えて構造的に対応できる
- 明確な構造とテンプレートを持つことで、Devin のような AI エージェントを活用した並列作業が機能する
Devin を活用することで、一人で十数本のマイグレーションと RLS ポリシーの導入を、短期間ののスプリント内で完了できました。
うまくいった背景には、事前に例外ケースや構造的な共通化を丁寧に洗い出した上で、AI に渡すプロンプトをテンプレート化しておいた点が大きく影響しています。
最終的に、AI を活用するには「作業の構造化」と「例外の見極め」が鍵であり、それはテックリードとしての視点とよく似たものであると感じました。AI は強力な実行力を持ちますが、全体を整理し、動かしやすい構造に落とし込むのは現状まだ人間の役割になりそうです。
RLS のような横断的かつ変更範囲の大きいタスクも、構造とプロセス設計次第で、現実的なコストでやりきることができるという実感が得られた取り組みでした。
本記事が、同様の課題を抱える開発チームや、Supabase・Devin を検討中の方々の参考になれば幸いです。
Liam ERD について
今回の RLS 対応において、Liam ERD はテーブル間の親子関係や参照経路の整理に役立ちました。
特に Devin のようなエージェントに作業を委ねる場合でも、開発者自身が構造を視覚的に把握できることは、プロンプトのテンプレート化や例外の洗い出し、タスク依頼の順番の決定を容易にします。
Liam ERD は Supabase や PostgreSQL をはじめ、さまざまなデータベースに対応しており、
マルチテナント設計やセキュリティポリシーの検討時にも有効なERD可視化ツールです。
我々のチームでは日常的に使っており、OSS/チーム開発における意思決定の質を高める助けになっています。
あなたのプロジェクトでも、設計の見通しをよくするツールとして、ぜひ活用してみてください。
Footnotes
Text by池田 仁俊
ROUTE06, Inc. でLiamの開発に携わっているソフトウェアエンジニアです。フロントエンドを中心にWebアプリケーションの開発を行いながら、要件整理や機能設計にも広く関与しています。
Last edited on