データベーススキーマ (Supabase)
Supabase PostgreSQLのスキーマ定義とテーブル構成
04. データベーススキーマ (Supabase)
1. 概要
このドキュメントは、Supabaseプロジェクトで使用されているPostgreSQLデータベースのスキーマについて説明します。
- 設計思想:
- オンラインWebサービス: Supabaseによって実装されるWebサービス(Publish・Explore等)において、プロジェクトの実体はコンテンツバケットとメタデータテーブルに保存されます。
- 所有権の保護: 既存の公開枠に対する更新操作はサービス層(
publishProject)で所有権(user_idの一致)が厳密に検証されます。これにより、他者の公開作品を誤って上書きするリスクを排除しています。詳細は01_architecture.mdの「データ永続化」セクションを参照してください。
クライアントサイドのIndexedDBスキーマ(Version 24)については、04_database_schema.md を参照してください。
型定義の自動生成について
重要: このドキュメントは手動で管理されていますが、TypeScript型定義は以下のコマンドで自動生成できます:
bunx supabase gen types typescript --linked > next-app/src/lib/types/supabase.tsマイグレーション適用後の必須作業:
- 上記コマンドで型定義を自動生成
- このドキュメントを更新して変更内容を反映
- 型定義とドキュメントの整合性を確認
- TypeScriptのビルドエラーがないことを確認
詳細は /doc/system/05_migration_guide.md の「7. 型定義の同期」セクションを参照してください。
2. テーブル定義
2.1. public.profiles
ユーザーの公開プロフィール情報を格納するテーブルです。auth.usersテーブルのレコードと1対1の関係にあります。
| カラム名 | 型 | 説明 |
|---|---|---|
id | uuid | 主キー。auth.users.idへの外部キー参照。ユーザー削除時にカスケード削除されます。 |
username | text | ユーザー名。OAuthプロバイダーから取得、またはメールアドレスのプレフィックスから生成されます。 |
avatar_url | text | アバター画像のURL。OAuthプロバイダーから取得されます。 |
is_pro | boolean | Proプランの有効フラグ。サブスクリプションの状態(active, trialing)に基づいてトリガーにより自動更新されます。このフラグは、is_pro_user() 関数を通じて参照されるキャッシュとして機能します。 |
updated_at | timestamp with time zone | レコードの最終更新日時。 |
2.2. public.published_projects
公開された作品のデータを格納するテーブルです。多言語メタデータに対応しています。
| カラム名 | 型 | 説明 |
|---|---|---|
id | uuid | 主キー。プロジェクトの一意なID。 |
user_id | uuid | 外部キー。auth.users.idおよびpublic.profiles.idへの参照。作者を示します。ユーザー削除時にカスケード削除されます。 |
title | text | **グローバル(英語)**のタイトル。既存のコンポーネントとの互換性を保つために使用されます。 |
headline | text | **グローバル(英語)**の見出しやキャッチコピー。 |
synopsis | text | **グローバル(英語)**のあらすじ。 |
tags | text[] | 英語と原典言語の両方のタグが混在する配列。 |
title_original | text | **原典言語(母語)**のタイトル。 |
headline_original | text | **原典言語(母語)**の見出しやキャッチコピー。 |
synopsis_original | text | **原典言語(母語)**のあらすじ。 |
original_language | text | 原典が何語だったかを記録(例: 'ja', 'en', 'es')。 |
cover_image_url | text | カバー画像の公開URL。storage.coversバケット内のオブジェクトを指します。 |
content_path | text | コンテンツJSONファイルのパス。storage.contentsバケット内のオブジェクト(例: {user_id}/{project_id}.json)を指します。 |
review_report | jsonb | AI査読によって生成された査読レポートのJSONオブジェクト。動的評価軸を含む詳細な評価情報が格納されます。 |
published_at | timestamp with time zone | 初めて公開された日時。 |
updated_at | timestamp with time zone | レコードの最終更新日時。 |
外部キー制約:
published_projects_user_id_fkey:auth.users(id)への参照(カスケード削除)published_projects_user_id_profiles_fkey:public.profiles(id)への参照(カスケード削除)
注記: user_idはauth.usersとprofilesの両方を参照しています。これにより、Supabaseのクエリで.select('*, profiles(username, avatar_url)')のように、作者情報を簡単に取得できます。
2.3. public.library_items
ユーザーがブックマーク(ライブラリに追加)した作品を管理するテーブルです。
| カラム名 | 型 | 説明 |
|---|---|---|
id | uuid | 主キー。レコードの一意なID。 |
user_id | uuid | 外部キー。auth.users.idへの参照。ライブラリの所有者。 |
project_id | uuid | 外部キー。public.published_projects.idへの参照。ブックマークされた作品。 |
display_order | integer | ライブラリ内での表示順序。ドラッグ&ドロップによる並べ替えに使用されます。 |
added_at | timestamp with time zone | ライブラリに追加された日時。 |
制約:
UNIQUE(user_id, project_id): 同じユーザーが同じ作品を重複して登録できないようにするユニーク制約。library_items_user_id_fkey:auth.users(id)への参照(カスケード削除)library_items_project_id_fkey:public.published_projects(id)への参照(カスケード削除)
3. ストレージ定義
3.1. covers バケット
作品のカバー画像を格納するための公開ストレージバケットです。
| プロパティ | 値 | 説明 |
|---|---|---|
id | covers | バケットの一意なID。 |
name | covers | バケット名。 |
public | true | バケットが公開されているかどうか。trueのため、URLを知っていれば誰でもアクセス可能です。 |
3.2. contents バケット
公開された作品の本文データ(JSONファイル)を格納するためのプライベートストレージバケットです。
| プロパティ | 値 | 説明 |
|---|---|---|
id | contents | バケットの一意なID。 |
name | contents | バケット名。 |
public | false | バケットが公開されているかどうか。falseのため、直接のURLアクセスはできません。読み取りには認証または署名付きURLが必要です。 |
3.3. user_backups バケット
E2E暗号化されたデータベースバックアップを格納するためのプライベートストレージバケットです。
| プロパティ | 値 | 説明 |
|---|---|---|
id | user_backups | バケットの一意なID。 |
name | user_backups | バケット名。 |
public | false | バケットが公開されているかどうか。falseのため、直接のURLアクセスはできません。 |
ファイル構造:
- パス:
{user_id}/backup.enc - 各ユーザーは自分のフォルダ内にのみアクセス可能
- ファイルはクライアント側でAES-256-GCM暗号化されており、サーバー側では復号不可能(Zero-Knowledge)
2.4. public.user_sync_state
クラウド同期のメタデータを管理するテーブルです。
| カラム名 | 型 | 説明 |
|---|---|---|
user_id | uuid | 主キー。auth.users.idへの外部キー参照。ユーザー削除時にカスケード削除されます。 |
last_synced_at | timestamp with time zone | 最後に同期が完了した日時。 |
device_name | text | 同期を実行したデバイスの識別情報(User Agent)。 |
backup_size | bigint | バックアップファイルのサイズ(バイト)。 |
current_backup_filename | text | 現在有効なバックアップファイル名(例: backup-1733241234567.enc)。冗長性のある同期ロジックで使用されます。 |
updated_at | timestamp with time zone | レコードの最終更新日時。自動更新トリガーにより管理されます。 |
| 外部キー制約: |
user_sync_state_user_id_fkey:auth.users(id)への参照(カスケード削除)
2.5. public.prompt_assets
ユーザーが作成したAIプロンプトアセットを格納するテーブルです。
| カラム名 | 型 | 説明 |
|---|---|---|
id | uuid | 主キー。プロンプトの一意なID。 |
user_id | uuid | 外部キー。auth.users.idへの参照。所有者を示します。 |
title | text | プロンプトのタイトル。 |
description | text | プロンプトの詳細説明。 |
content | text | プロンプトの本文(AIへの指示内容)。 |
tags | text[] | 検索・分類用のカテゴリタグ。 |
is_public | boolean | 他のユーザーに公開するかどうか(将来用)。 |
created_at | timestamp with time zone | 作成日時。 |
updated_at | timestamp with time zone | 最終更新日時。 |
外部キー制約:
prompt_assets_user_id_fkey:auth.users(id)への参照(カスケード削除)
2.6. public.customers
SupabaseユーザーとStripeの顧客IDを紐付けるテーブルです。
| カラム名 | 型 | 説明 |
|---|---|---|
id | uuid | 主キー。auth.users.idへの外部キー参照。 |
stripe_customer_id | text | Stripe側で発行された顧客ID(cus_...)。 |
外部キー制約:
customers_id_fkey:auth.users(id)への参照(カスケード削除)
2.7. public.subscriptions
Stripeのサブスクリプション詳細情報を格納するテーブルです。
| カラム名 | 型 | 説明 |
|---|---|---|
id | text | 主キー。StripeのサブスクリプションID(sub_...)。 |
user_id | uuid | 外部キー。auth.users.idへの参照。 |
status | text | サブスクリプションのステータス(active, canceled, past_due等)。 |
price_id | text | Stripeの価格ID(price_...)。 |
quantity | integer | 数量。通常は1。 |
cancel_at_period_end | boolean | 期間終了時にキャンセルされるかどうか。 |
created | timestamp with time zone | 作成日時。 |
current_period_start | timestamp with time zone | 現在の請求期間の開始日。 |
current_period_end | timestamp with time zone | 現在の請求期間の終了日。 |
ended_at | timestamp with time zone | 終了日時。 |
cancel_at | timestamp with time zone | キャンセル予定日時。 |
canceled_at | timestamp with time zone | キャンセルされた日時。 |
trial_start | timestamp with time zone | トライアル開始日。 |
trial_end | timestamp with time zone | トライアル終了日。 |
metadata | jsonb | 追加のメタデータ。 |
外部キー制約:
subscriptions_user_id_fkey:auth.users(id)への参照(カスケード削除)
4. Row Level Security (RLS) ポリシー
4.1. public.profiles
-
Public profiles are viewable by everyone.- 操作:
SELECT - 条件:
USING (true) - 説明: すべてのユーザー(非認証ユーザーを含む)が、すべてのプロフィール情報を閲覧することを許可します。
- 操作:
-
Users can update their own profile.- 操作:
UPDATE - 条件:
USING (auth.uid() = id) - 説明: 認証済みのユーザーが、自身の
idと一致するプロフィールレコードのみを更新することを許可します。
- 操作:
4.2. public.published_projects
-
Public projects are viewable by everyone.- 操作:
SELECT - 条件:
USING (true) - 説明: 公開された作品は誰でも閲覧可能です。
- 操作:
-
Users can insert their own projects.- 操作:
INSERT - 条件:
WITH CHECK (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のプロジェクトのみを公開できます。
- 操作:
-
Users can update their own projects.- 操作:
UPDATE - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のプロジェクトのみを更新できます。
- サービス層バリデーション: RLSに加え、
publishProjectサービス(service.ts)において、既存の公開スロットを更新する際、そのスロットのuser_idが現在の実行ユーザーと一致するかをDBから再取得してチェックします。これにより、クライアントサイドからの不正なID指定による上書きをサーバー側でも防御します。
- 操作:
-
Users can delete their own projects.- 操作:
DELETE - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のプロジェクトのみを削除できます。
- 操作:
4.3. public.library_items
-
Users can view their own library.- 操作:
SELECT - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のライブラリのみを閲覧できます。
- 操作:
-
Users can add items to their library.- 操作:
INSERT - 条件:
WITH CHECK (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のライブラリにのみアイテムを追加できます。
- 操作:
-
Users can update their library items.- 操作:
UPDATE - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のライブラリアイテム(並び順など)のみを更新できます。
- 操作:
-
Users can delete items from their library.- 操作:
DELETE - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身のライブラリからのみアイテムを削除できます。
- 操作:
4.4. storage.objects (coversバケット)
-
Cover images are publicly accessible.- 操作:
SELECT - 条件:
USING (bucket_id = 'covers') - 説明:
coversバケット内の画像は誰でも閲覧可能です。
- 操作:
-
Authenticated users can upload covers.- 操作:
INSERT - 条件:
WITH CHECK (bucket_id = 'covers' AND auth.role() = 'authenticated') - 説明: 認証済みのユーザーは
coversバケットに画像をアップロードできます。
- 操作:
-
Users can update their own covers.- 操作:
UPDATE - 条件:
USING (bucket_id = 'covers' AND auth.uid() = owner) - 説明: 画像の所有者のみがその画像を更新できます。
- 操作:
-
Users can delete their own covers.- 操作:
DELETE - 条件:
USING (bucket_id = 'covers' AND auth.uid() = owner) - 説明: 画像の所有者のみがその画像を削除できます。
- 操作:
4.5. storage.objects (contentsバケット)
-
Anyone can read published content.- 操作:
SELECT - 条件:
bucket_id = 'contents' AND EXISTS (SELECT 1 FROM published_projects WHERE content_path = storage.objects.name) - 説明:
published_projectsテーブルのcontent_pathに登録されている(=公開されている)ファイルであれば、誰でも(非認証ユーザー含む)読み取り可能です。これにより、Service Role Keyを使用せずに安全にコンテンツを提供できます。
- 操作:
-
User can manage their own content.- 操作:
ALL(SELECT,INSERT,UPDATE,DELETE) - 条件:
bucket_id = 'contents' AND auth.uid() = owner - 説明: 認証済みのユーザーは、自身が所有する(
ownerが自分のuser_idである)コンテンツファイルに対して、すべての操作(読み取り、作成、更新、削除)を実行できます。
- 操作:
4.6. public.user_sync_state
-
Users can view their own sync state.- 操作:
SELECT - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身の同期状態のみを閲覧できます。
- 操作:
-
Users can manage their own sync state.- 操作:
INSERT,UPDATE,DELETE - 条件:
WITH CHECK (auth.uid() = user_id)/USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは自身の同期状態のみを作成・更新・削除できます。
- 操作:
4.7. storage.objects (user_backupsバケット)
Users can manage their own backups.- 操作:
ALL(SELECT,INSERT,UPDATE,DELETE) - 条件:
bucket_id = 'user_backups' AND auth.uid() = (storage.foldername(name))[1]::uuid - 説明: 認証済みのユーザーは、自分のユーザーIDフォルダ内のバックアップファイルに対してのみ、すべての操作を実行できます。パスは
{user_id}/backup.encの形式を想定しています。
- 操作:
注記: 将来的にPro Planを実装する場合、is_pro_user() 関数を追加し、無料ユーザーのバックアップを制限することができます。
- 説明: コンテンツファイルの所有者(作者)は、自身のファイルに対して全ての操作を行えます。
4.8. public.prompt_assets
Users can manage their own prompt assets.- 操作:
ALL(SELECT,INSERT,UPDATE,DELETE) - 条件:
USING (auth.uid() = user_id) - 説明: 認証済みのユーザーは、自身が作成したプロンプトアセットのみを管理できます。
- 補足:
is_publicがtrueの場合に他人が閲覧できるポリシーは将来的に追加可能です。
- 操作:
5. 自動化ロジック (トリガー & 関数)
5.1. handle_new_user() 関数
- 目的:
auth.usersに新しいユーザーが登録された際に、自動的にpublic.profilesテーブルに対応するレコードを作成します。 - 実装:
plpgsql言語で記述された関数。 - トリガー:
on_auth_user_created(AFTER INSERT ON auth.users)
5.2. handle_project_update() 関数
- 目的:
published_projectsテーブルのレコードが更新された際に、updated_atカラムを自動的に現在のタイムスタンプに更新します。 - 実装:
plpgsql言語で記述された関数。 - トリガー:
on_project_update(BEFORE UPDATE ON published_projects)
5.3. get_tags_by_context(p_user_id, p_context) 関数
- 目的: 画面のコンテキスト(全体、ライブラリ、自分の作品)に応じて、タグの出現頻度を集計して返します。
- 引数:
p_user_id(uuid): ユーザーID(コンテキストがユーザー依存の場合に必須)p_context(text):'global','library','my_works'のいずれか
- 戻り値:
TABLE (tag text, count bigint) - 使用場所: Explore, Library, My Worksページのタグフィルター
5.4. is_pro_user() 関数
- 目的: 現在の認証ユーザーがProプラン会員であるかどうかを判定します。
- 戻り値:
boolean(Pro会員の場合はtrue) - 実装:
profiles.is_proカラムを参照して判定します。 - 使用場所:
- RLSポリシー内での権限チェック
- フロントエンドからのRPC呼び出し(
supabase.rpc('is_pro_user'))
設計上の注意点:
- この関数は、データソース(
profiles.is_proカラム)と、判定ロジック(RPC関数)を分離する設計パターンに従っています。 is_proは永続化されたデータキャッシュであり、is_pro_user()はそのデータを参照するインターフェースです。- 以前は
subscriptionsテーブルを直接参照していましたが、パフォーマンスと実装の簡素化のため、profiles.is_proを参照するように変更されました。
5.5. handle_pro_status_update() 関数
- 目的:
subscriptionsテーブルの変更時に、対応するユーザーのprofiles.is_proフラグを自動更新します。 - トリガー:
on_subscription_change(AFTER INSERT OR UPDATE OR DELETE ON public.subscriptions) - 更新ロジック:
subscriptions.statusが'active'または'trialing'の場合 →is_pro = true- それ以外の場合 →
is_pro = false
- 設計意図:
subscriptionsテーブルの変更をイベントとして捉え、profiles.is_proを正規化されたキャッシュとして維持します。これにより、Pro判定のためにsubscriptionsテーブルを直接参照する必要がなくなります。
関連ドキュメント:
01_architecture.md04_database_schema.md(クライアントサイドDB)15_stripe_integration_guide.md(Pro会員判定ロジック)