第七章:PostgreSQL 極小データベース設計 - 権限と注文のコアハブ
開発初期のシステムアーキテクチャ設計において、多くの新人エンジニア(そして時には経験のあるバックエンドエンジニアでさえ)がECサイトやコースデータベースを設計する際、「過剰設計 (Over-engineering)」の罠に陥りがちです。
彼らはリレーショナル図ソフトを開き、複雑に入り組んだアーキテクチャを描きます:
Usersテーブルを作成し、Coursesテーブルを作成し、何を購入したかを記録するためにOrdersテーブルを作成し、さらに明細を記録するためにOrder_Itemsテーブルが必要で、最後に金流を確認するためのTransactionsテーブルを追加します... 複雑な外部キー(Foreign Keys)とJOINクエリの山ができあがります。
しかし、「純粋な知識販売、仮想商品、在庫問題なし、物流配送なし」のSaaSプラットフォームにとって、このような巨大な従来型EC設計は開発が非常に苦痛で、将来のメンテナンスコストが極めて高く、99%のシナリオで完全に不要です。
Vibe Tutorのシステムアーキテクチャでは、私たちは「Minimal Viable Product (MVP、最小限の実行可能製品)」の精神を堅持しています。コアとなるビジネス権限ロジック全体を極限まで圧縮し、最終的にたった1つのテーブルだけで済ませました。
🧊 The One Table 哲学:vt_purchases
SupabaseのPostgreSQLデータベースでは、vt_purchases(購入記録)という名前のテーブルを作成しました。
このテーブルのフィールドは非常に簡潔ですが、サイト全体の「権限解除判断」と「バックエンド販売統計」を完璧にサポートしています。
フィールド設計の詳細分析
| フィールド名 | データ型 | 説明とビジネス価値 |
| :--- | :--- | :--- |
| id | uuid (Primary Key) | 各購入記録の一意の識別子、絶対に重複しないことを保証。 |
| created_at | timestamp with time zone | 購入完了時のタイムスタンプ、デフォルトはnow()。月次収益トレンドグラフの作成に使用。 |
| user_id | uuid (Foreign Key) | 購入者のID。このフィールドはSupabase組み込みの高セキュリティauth.users認証テーブルに安全に関連付けられます。 |
| item_id | text | (コアの魂) 購入した商品コード、例: "coding-101"。プログラムはこの文字列でどの権限を付与するかを決定します。 |
| item_type | text | 商品のタイプ。例: course(単一コース)、bundle(バンドル)。将来の物理商品販売拡張に備えて。 |
| price | integer | 実際の取引金額。これは重要で、一部の顧客が早期割引価格で購入している可能性があるため、財務調整のために実際の支払い金額を記録する必要があります。 |
💡 コアの魂:item_id 権限判断の技術
すべての「権限ロックロジック」をitem_idというプレーンテキスト文字列に圧縮しました。これにより、巨大な「コーステーブル」と「注文明細テーブル」の関連付けを作成する手間を省きました。
チェックアウト時にitem_idをどのように設定したか振り返りましょう:
- 99円入門コース購入:
item_id = "coding-101"と記録。このユーザーがこのコースのテキストコンテンツをアンロックしたことを意味します。 - 1999円上級地図コース購入:
item_id = "car-camping-map"と記録。同様にこのコースのテキストコンテンツをアンロック。 - 3999円ソースコード付きプラン購入:
item_id = "car-camping-map-source"と記録。フロントエンドコードでは、-sourceで終わるIDを持つ場合、テキストをアンロックし、プロジェクトのZIPダウンロードボタンを表示します。 - 9999円究極VIPパス購入:
item_id = "vip-all-access"と記録。プログラムがこのIDを見ると、古代の「免死金牌」を見たかのように、直接return trueし、サイト内のすべてのコースとダウンロードエリアに無条件でアクセス許可!
このフラットな設計により、データベースのクエリ速度は電光石火のように速く、コードも極めてクリーンです:
// 🚀 ユーザーが所有するものを一度に検出、複雑なテーブルをJOINする必要なし!
const { data: purchases } = await supabase
.from("vt_purchases")
.select("item_id")
.eq("user_id", user.id);
// 文字列配列に抽出:['coding-101', 'car-camping-map']
const purchasedItemIds = purchases.map(p => p.item_id);
// 特定のコースにアクセス可能か判断
const canAccess = purchasedItemIds.includes(currentCourseId) || purchasedItemIds.includes("vip-all-access");
🛡️ RLS (Row Level Security) 行レベルセキュリティ防御
SupabaseのようなBackend-as-a-Service (BaaS)を使用すると開発が非常に速くなりますが、初心者が犯しがちな致命的なセキュリティ問題が発生します: 「SupabaseのAPI Keyがフロントエンドで公開されている場合、プログラミングを知っているハッカーはブラウザのConsoleでAPIを叩いてデータベースに書き込み、自分に何百もの無料コースを追加できるのでは?」
この恐ろしい問題を解決するために、PostgreSQLには業界最強のRLS (Row Level Security)メカニズムが組み込まれています。
vt_purchasesという会社の収益に関わる重要なテーブルに対して、私たちは非常に厳格なRLSポリシーを設定しました:
-
Select (読み取り) ポリシー:
-- SQLポリシールール (auth.uid() = user_id)ビジネス保護の意味: 誰もが自分のログインUIDと等しい
user_idのデータのみを読み取ることができます。他人の購入記録を絶対に見ることはできず、競合他社が顧客リストと販売量をスクレイピングするのを防ぎ、厳格な個人情報保護法規制に準拠します。 -
Insert/Update/Delete (書き込みと���更) ポリシー: すべて閉鎖(禁止)! これは、フロントエンドのJavaScriptコードが、ハッカーがどのように改ざんしようとも、このテーブルに書き込みや変更を行う権限が「まったくない」ことを意味します。
❓ では通常の注文はどのように書き込むのか?
フロントエンドがデータを書き込めない場合、ユーザーが支払いを完了した後、どのように記録をデータベースに保存するのでしょうか? 答えはバックエンドサーバーの Service Role Key (無敵のスーパーキー) を通じてです。
サーバーサイド(Next.jsの/api Route Handlers内)では、最高のシステム権限を持つSupabase Clientを使用し、.envに保存されているSERVICE_ROLE_KEYを携帯しています。このキーはすべてのRLSポリシーを無視し、データベースに直接強力に書き込むことができます。
緑界科技(ECPay)のサーバーが私たちのバックグラウンドWebhook(支払い成功通知)を呼び出すとき、これは「サーバー間」の安全なチャネルで行われます。私たちのバックエンドAPIが検証に成功すると、このスーパーキーを使用してデータを安全にvt_purchasesテーブルに書き込みます。
この「フロントエンドは自分のデータのみを照会でき、バックエンドのみがコアデータを書き込める」という設計が、知識販売プラットフォームの堅牢な最後の防衛線を構成しています!
✅ 本章のまとめ
最高のアーキテクチャは、往々にして最もシンプルなアーキテクチャです。One Table哲学と強力なRLS防御を通じて、私たちは拡張性が高く、セキュリティが強固で���メンテナンスコストがゼロのデータベース層を構築しました。 大ボスに立ち向かう準備はできていますか?次の章では、台湾のECで最も困難で、最もお金を稼げる難関に挑戦します:緑界科技ECPay金流APIの接続と検証!
章のまとめ
- コアコンセプトと原理を理解
- 実装方法とテクニックを習得
- 一般的な問題と解決策に精通
- 実際のプロジェクトに適用可能
さらに読む
- 公式ドキュメントとAPIリファレンス
- GitHubのオープンソース例
- 技術書とオンラインコース
- コミュニティディスカッションと技術ブログ