0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

漏洩してもエラーは出ない。非公開データをアプリではなくDBで守る、SupabaseのRLSを選んだ理由

0
Last updated at Posted at 2026-10-06

はじめに

🤔「データベース、結局どれを選べばいいんだろう…」
😥「Cloudflare D1 も Turso も安くて速そう。Supabase じゃなくてもいいのでは…?」
🫠「比較記事を読むほど、決め手が分からなくなる…」

技術選定は、比べる軸が多いほど迷います。
価格・速さ・書きやすさ・エコシステム、どれも大事で、どれも決め手になりそうです。

個人開発している口コミサービス あじぴた では、
この迷いがほとんどありませんでした🍜
プロダクトの仕様が 1 つ決まった時点で、選べるデータベースが絞られていたからです。

この記事では、

  • 「低評価を公開しない」という仕様がどこから来たか
  • それがなぜ RLS(Row Level Security)を必須にしたか
  • その結果、D1 / Turso ではなく Supabase になった経緯

を、実際の判断の順番どおりに書きます📝

この記事は、個人開発サービス「あじぴた」の設計を書くシリーズの 1 本です。
単体で読めるように書いていますが、サービスの全体像はハブ記事にまとめています。

あじぴたは何を評価するサービスか

あじぴたは、味の好みが近い人の「美味しかった」を頼りに、自分好みの一皿を探すサービスです。
一般的なグルメサイトとは、評価の仕組みが 2 点違います。

一般的なグルメサイト あじぴた
評価の単位 店 料理
評価の形 星 1〜5 「美味しかった」の一択 + 理由タグ
低評価 公開される 公開されない

「店全体は普通だけど、この一品だけは抜群」という情報は、店単位の評価では平均に溶けてしまいます。
そこで評価の単位を料理にして、評価は「美味しかった」だけにしました。

「美味しかった」には、なぜ美味しかったかを表す理由タグを付けられます。
うま味・出汁感、サクサク、とろける のような、味覚に関する言葉だけを用意しています。
タグの傾向が、そのままその人の好みの輪郭になるからです👅

「合わなかった」は記録する。でも誰にも見せない

ここからが、この記事の本題です。

あじぴたには、低評価を投稿する機能がありません。
その代わり、「自分には合わなかった」を記録する機能があります。
この記録は、本人以外には絶対に表示されません🔒

あじぴたでは、この「記録するが見せない」という仕様をサイレントネガティブと呼んでいます。

なぜ見せないのか — 低評価は店を潰すことがある

出発点は、既存の口コミサービスへの違和感でした。

星 1 つのレビューは、書く側にとっては数分の作業です。
でも、口コミが 10 件しかない小さな店では、その 1 件で平均点が大きく動きます。
表示順が落ち、客足が遠のき、場合によっては閉店につながることもあります😢

それに、自分好みの一皿を探すのに、他人の低評価はそもそも必要ありません。
点数の低い店に自分にとっての最高の一皿があることは、普通にあります。

だから、あじぴたではネガティブな評価を表に出さないと決めました。
これは機能の取捨選択ではなく、このサービスが存在する理由です。

なぜ記録するのか — 一致だけでは味覚の近さが分からない

見せないなら、記録する必要もないのでは?と思うかもしれません。
記録するのは、味覚が近い人を見つける精度に直結するからです🔍

A さんと B さんは、同じ 5 品に「美味しかった」を付けている
  → 👀 一見、味覚が近そうに見える

しかし、別の 10 品では一方が「美味しかった」、もう一方が「合わなかった」
  → 🙅 実際には、全く似ていない

公開されている「美味しかった」だけを見ていると、この裏側のズレが見えません。
「合わなかった」を内部で持っておくことで、見かけだけ似ている人を正しく除外できます。

見せないから、正直に付けられる

非公開にしたことには、もう 1 つ効果がありました。ユーザーが正直になれることです✨

評価が公開されると、人は気を遣います。
「お店に悪いかな」と思えば、合わなかったものに「合わなかった」とは付けにくい。
誰にも見られないと分かっているから、正直に付けられます。

そして、その正直な記録がそのまま、味覚の近い人を探す精度になります。

店を守るための仕様が、結果として推薦の精度も上げている。
倫理と性能が同じ方向を向いたのは、自分でも気に入っている点です。

漏れたら終わるデータは、アプリのコードだけでは守れない

サイレントネガティブは、外に漏れた瞬間にサービスの前提が崩れるデータです。

「あの人が自分の店の料理に『合わなかった』を付けている」が見えてしまったら、
ユーザーは二度と正直に記録しなくなります。
正直な記録が無くなれば、推薦の精度も保てません⚠️

つまり、要件はこうなります。

非公開の保証が、サービスの信頼性そのもの。アプリのバグ 1 つで漏れてはいけない。

漏洩してもエラーは出ない

この種のデータが厄介なのは、漏れても何も起きないことです😇

アプリのコードで「本人の分だけ返す」と書いていた場合、
どこか 1 か所で条件を書き忘れると、他人の「合わなかった」がそのまま返ります。
画面は普通に表示され、エラーも出ません。誰も気づかないまま漏れ続けます。

API の数が増えるほど、この「書き忘れ」が起きる場所も増えます。
あじぴたの API は現在 49 本あります。1 本ずつ人の目で確認し続けるのは、現実的ではありません。

データベース自身に「見せない」を強制させる

そこで、見せる・見せないの判断をデータベース自身に持たせることにしました。
PostgreSQL の RLS(Row Level Security) です🛡️

RLS を使うと、テーブルに「この行は誰に見せてよいか」というルールを付けられます。
アプリがどんなクエリを投げても、ルールに合わない行は最初から結果に含まれません。

あじぴたの口コミテーブルに付けているルールは、要点だけ抜き出すとこうです。

-- positive(美味しかった)は誰でも読める
-- negative(合わなかった)は本人だけが読める
create policy reviews_select on reviews
  for select to authenticated, anon
  using (
    eval_type = 'positive'
    or user_id = (select auth.uid())
  );

「美味しかった」は誰でも読めて、「合わなかった」は本人だけが読める。
サービスの存在理由が、この数行の SQL にそのまま書かれている形です✍️

アプリ側で条件を書き忘れても、データベースが他人の「合わなかった」を返しません。
守る仕組みが、アプリのコードの正しさに依存しなくなります。

RLS をどうテストで固定しているかは、別の記事で詳しく書きました。
「漏れても静かに壊れる」性質があるので、テストの書き方にも工夫が要りました。
🔗 RLSが壊れてもテストは緑のまま。漏れても気づけない非公開データを、pgTAPで「落ちるテスト」にする

RLS が必須になると、データベースの候補が絞られる

ここで、データベース選びの話に戻ります。

開発を始めたとき、コスト面で魅力的な候補がいくつかありました。
Cloudflare D1 と Turso は、どちらも SQLite ベースで安く、速く、個人開発でも人気があります。

比べると、こうなりました。

候補 データベース RLS 判定
Supabase PostgreSQL 標準で使える ✅ 採用
Cloudflare D1 + 別の認証サービス SQLite 無し(自前で実装) ❌
Turso + 別の認証サービス SQLite 無し(自前で実装) ❌

決め手は 1 行目の RLS です。
SQLite には、RLS にあたる機能がありません。

D1 や Turso を選ぶと、「この行を誰に見せるか」はアプリのコードで書くことになります。
それは、前の節で書いた**「書き忘れ 1 つで全件漏洩」の状態そのもの**です🙅

サイレントネガティブを持つと決めた時点で、
RLS を持たないデータベースは選べなくなっていました。

ほかの理由も、同じ方向を向いていた

RLS ほど決定的ではありませんが、ほかにも Supabase を後押しする理由がありました。

1. 料理名のあいまい検索に、PostgreSQL の拡張機能を使いたかった

料理名を入力すると、同じ店に登録済みの似た名前を候補として出しています。
「油そば」と「油そば(並)」が別の料理として登録されると、同じ一皿を食べた人どうしが結びつかなくなるからです🍜

ここでは PostgreSQL の pg_trgm という拡張を使っています。
文字列を 3 文字ずつに区切って、どれくらい似ているかを数値で出せる機能です。
SQLite の全文検索(FTS5)には、この似ている度合いを出す機能が標準ではありません。

2. 認証を別契約すると、合計ではむしろ高くなる

D1 や Turso はデータベースだけのサービスです。
ログイン機能やファイル置き場、メール送信は別のサービスと契約することになります。

月間 1 万人が使う想定で、当時の料金を比べるとこうなりました💰

項目 Supabase D1 / Turso 構成
データベース $25 $0〜29
認証 込み Clerk $25 / Auth0 $35
ファイル置き場 込み R2 を別契約
メール送信 込み(Resend の無料枠と連携) SendGrid $15
サーバー側の処理 込み Workers $5
合計 $25 $50〜80

データベース単体の値段では D1 / Turso が安く見えても、
サービスとして必要なものを揃えると、Supabase のほうが安いという結果でした。

D1 / Turso が悪いわけではない

念のため書いておくと、D1 や Turso が劣っているという話ではありません🙏

向いているケース あじぴたは?
シンプルな CRUD アプリ 行ごとに見せる相手が違う
世界中から低遅延で読みたい 利用者はほぼ日本国内
Cloudflare Workers と一体で運用したい Vercel + Next.js
権限制御がシンプル 権限制御がサービスの核

これらに当てはまるサービスなら、D1 / Turso はとても良い選択肢だと思います。
あじぴたは、どれにも当てはまらなかったというだけです。

仕様から技術まで、一直線に降りてきた

ここまでの流れを並べると、こうなります。

既存の口コミサービスへの違和感(低評価が店を潰すことがある)
  ↓
評価は「料理」単位で、公開するのは「美味しかった」だけ
  ↓
「合わなかった」は記録するが、本人以外には見せない(サイレントネガティブ)
  ↓
アプリのバグ 1 つで漏れてはいけない → データベース自身が守る = RLS が必須
  ↓
SQLite 系には RLS が無い → PostgreSQL 系
  ↓
認証・ファイル置き場まで含めたコストも安い → Supabase

プロダクトの仕様が、インフラの選定まで一直線に降りてきた形です🎯

もし技術から入っていたら、「安くて速い」という理由で D1 や Turso を選んでいたかもしれません。
そして、サイレントネガティブを実装する段階で、権限制御をすべてアプリのコードで書くことになっていたはずです。

同じ考え方で決めたこと

この「仕様から降ろして決める」考え方は、ほかの判断にも効いています。

味覚の近さの計算方法も、「説明できること」で選んだ

推薦の分野には、評価データから人には読めない特徴を学習して、似ている人を探す手法があります。
大手の推薦エンジンで実績があり、ユーザーが増えても計算が破綻しにくい方法です。

あじぴたでは、これを採用しませんでした。

あじぴたの画面は「ぴたスコア◯% の〇〇さんが、美味しかったと言っている」という形で推薦を出します。
(ぴたスコアは、あじぴたでの味覚の近さの呼び名です)
この見せ方が成り立つには、なぜこの人と味覚が近いのかを、データで説明できる必要があります。
学習された特徴が近いから、では説明になりません🤔

計算方法の詳細はサービスの核なので書けませんが、精度より説明できることを優先したという方針だけ残しておきます。

焼肉や寿司でも、評価の単位は「料理」のままにした

焼肉・寿司・居酒屋のように、何品も頼んでシェアする業態では、
「1 回の口コミ = 1 品」という形が面倒になります。

案 判断
店単位の「ざっくり評価」を足す ❌ 何を食べたかが失われる
「焼肉コース」を 1 品として登録する ❌ 実際は複数の料理なので、意味がずれる
料理単位のまま、複数品をまとめて投稿できるようにする ✅ 採用

困っていたのは評価の単位ではなく、入力の手間でした。
「上カルビ」も「赤身の握り」も、実在する具体的な料理です。
そこで評価の単位はそのままに、同じ店の料理を一度にまとめて投稿できる画面を用意しました📝

意図的にやらないこと

仕様から降ろして決めると、やらないこともはっきりします。

やらないこと 理由
店の総合評価スコア(「この店は 4.2 点」) 大衆平均を主役にした時点で、既存サービスと同じになる
フォロー機能 見たいのは「味覚が近い人」であって「知り合い」ではない
低評価の公開 サービスの存在理由そのものが崩れる

特に最後の 1 行は、どれだけ要望が来ても変えないと決めています。
「低評価も見たい」という声は当然あり得ます。
でも、それを入れた瞬間に、あじぴたは既存の口コミサービスと同じものになります🙅

機能を足すかどうか迷ったときも、この表に立ち返れば答えが出ます。

まとめ

  • 「低評価を公開しない」は、店を守るための仕様であり、結果として推薦の精度も上げている
  • 非公開データは漏れてもエラーにならないので、アプリのコードではなくデータベースの RLS で守る🛡️
  • RLS が必須になった時点で、SQLite 系の D1 / Turso は候補から外れ、Supabase に決まった
  • 技術を比べる前に、「漏れたら終わるもの」をプロダクトの仕様から探すと、選定の軸がはっきりする🎯

データベース選びで迷ったときは、価格や速さを比べる前に、
**「このサービスで、絶対に起きてはいけないことは何か」**を考えてみると、決め手が見つかるかもしれません💡

シリーズの他の記事も、よろしければ📚

このシリーズでは、個人開発サービス「あじぴた」の設計をテーマごとに書いています。
サービスの全体像や、ほかの記事の一覧はハブ記事にまとめています👇

🔗 「低評価を公開しない」口コミサービスを個人開発した話 — 4,300コミット・6リポジトリの全体像

これまでに公開した記事です。

🔗 Google Places APIで月$1,440の請求が来る前に — 個人開発で従量課金を「呼ばない」8層の防壁
🔗 Next.js 16 で Web Vitals を測って直した実録!loading.tsx で LCP が 2 倍になった罠と関数リージョン
🔗 「全部見せるためのRLS」は書かない。Supabaseで管理画面だけRLSをバイパスした理由
🔗 ディレクトリ構成は「AIへの指示書」になる。Next.js App Routerで自分の設計論を答え合わせした話

次の記事では、RLS をどうテストで固定しているかを書きました🔐

🔗 RLSが壊れてもテストは緑のまま。漏れても気づけない非公開データを、pgTAPで「落ちるテスト」にする

設計の中身を通して読みたい方には、解剖ドキュメントも公開しています🔬
🔗 あじぴたの内側(ajipita-inside)

参考になれば幸いです🙏

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?