🦸‍♂️

Supabaseは個人開発の味方 - 煩わしいセットアップは不要・RLSによる宣言的な権限管理

に公開

こんにちは近藤です
個人開発でサービスを構築する際、「認証どうしよう」「画像のストレージどこに置こう」「セキュリティ大丈夫かな」「テスト環境の構築が面倒」と悩むことはありませんか?

私は子供向け画像認識学習アプリ「KORENANI」を開発する中で、Supabaseを採用しました。

この記事で伝えること:

  • なぜSupabaseを選んだのか(メリット)
  • どんな制約があるのか(デメリット)
  • どんなプロジェクトに向いているのか(判断基準)

実体験に基づいて共有します。

TL;DR

Supabaseの強み:

  • 無料プランが充実(50,000 MAU、500MB DB、1GB Storage)
  • RLSで権限管理をSQLで宣言的に記述できる
  • supabase startだけでローカル環境が完結
  • Auth・DB・Storageが統合されている

向いているケース:
個人開発、初速を大事にしたいプロダクト、プロトタイピング、テスト重視の開発、ユーザーごとのファイル管理が必要なアプリなど(他にもあると思いますが、私の観点では)

この記事では、子供向けアプリ開発での実体験から、Supabaseの具体的なメリットと考慮点を解説します。


1. 無料プランの充実度

Supabaseの無料プランには以下が含まれます:

  • 500MB PostgreSQLデータベース
  • 1GBファイルストレージ
  • 5GB帯域幅
  • 50,000月間アクティブユーザー(MAU)
  • コミュニティサポート

KORENANIでの活用例

// 無料プランで動く構成
- PostgreSQL: 500MB(ユーザー・子供プロフィール・学習記録)
- Storage: 1GB(プロフィール画像・学習用画像)
- Auth: 50,000 MAU

個人開発の初期段階では十分な規模です。

料金体系

有料プランに移行する場合、Proプランは月額$25から。

Proプランの内訳($25/月)

  • 8GB データベース
  • 100GB ファイルストレージ
  • 250GB 帯域幅
  • 100,000 MAUs
  • $10分のコンピュートクレジット込み
  • 日次バックアップ、7日間ログ保持

使用量ベースの課金で、無料プランから段階的にアップグレードできます。

コスト比較(月間ユーザー1,000人、50GB使用)

Supabase Pro

  • 基本料金: $25
  • 追加ストレージ(100GBの無料枠内): $0
  • 合計: $25/月

AWS(S3 + RDS + Cognito)

  • S3(50GB): $1.15
  • RDS db.t3.micro(8GB): $15
  • Cognito(1,000 MAU): $0(無料枠内)
  • Lambda + API Gateway: ~$5
  • 合計: ~$21/月

2. StorageとAuthの統合 - 実装量の削減

認証とストレージを統合する際、アーキテクチャによって開発者の負担が変わります。

AWS S3 + Lambda + API Gateway での構成

AWSでは複数のマネージドサービスを組み合わせて構築するのが一般的です。私はGoogle Cloudの方が慣れているのですが、AWSの方が伝わりやすいかなと思うのでAWSのパターンで書いています。Google Cloudでも大きな差はないはずです。

// Lambda関数(API Gatewayオーソライザーで認証済み)
export const handler = async (event) => {
  const userId = event.requestContext.authorizer.claims.sub;
  const fileKey = `users/${userId}/${event.pathParameters.fileId}`;
  
  const url = await s3.getSignedUrlPromise('getObject', {
    Bucket: process.env.BUCKET,
    Key: fileKey,
    Expires: 3600
  });
  
  return { 
    statusCode: 200, 
    body: JSON.stringify({ url }) 
  };
};

必要な設定:

  • API Gateway(エンドポイント、オーソライザー)
  • Lambda(関数、IAMロール)
  • Cognito(ユーザープール、IDプール)
  • S3(バケット、バケットポリシー)
  • CloudFormation/Terraformでのインフラ管理
  • テスト環境のセットアップ(LocalStack等)

柔軟性は高いですが、初期セットアップの際に、AWS上でこれらを設定し、ローカルで検証するためにはemulatorなどを用意する必要があります。
まあ一度やってしまえばそれまでですが。ただ、supabaseのdashboard上で関連し合う情報を一括で見れるのは体験としてとても良いと思います。

Supabase Storage での構成

Supabaseは、Storage・Auth・DBを統合し、SQLで権限管理を記述できます。(ありがたい)

-- SQLで権限を定義
CREATE POLICY "Users can view own images"
ON storage.objects FOR SELECT
TO authenticated
USING (
    bucket_id = 'users'
    AND
    auth.uid()::text = (string_to_array(name, '/'))[1]
);
// クライアント側
const { data } = await supabase.storage
  .from('users')
  .download(`${userId}/file.jpg`);

動作フロー:

1. クライアントがリクエスト(JWT付き)
2. SupabaseがJWTを検証、auth.uid()抽出
3. RLSポリシー実行: auth.uid() vs パスのuid比較
4. 一致 → アクセス許可 / 不一致 → 403

内部構造:
調べた範囲ではSupabaseのマネージドホスティングは、AWS上で動作しています。デフォルトでS3をストレージバックエンドとして使用し、PostgreSQL・Auth・Storageを統一的なAPIで提供しています。セルフホスト時は、MinIOやCloudflare R2などのS3互換ストレージも選択できるそうです。


主要サービスとの比較

観点 Supabase AWS(複数サービス)
初期セットアップ ⭐⭐⭐⭐⭐ 非常に簡単 ⭐⭐⭐ 設定項目が多い
学習曲線 ⭐⭐⭐⭐ SQLとRLS ⭐⭐⭐ 複数サービスの理解
スケーラビリティ ⭐⭐⭐ 中規模まで ⭐⭐⭐⭐⭐ ペタバイト級
コスト(小規模) ⭐⭐⭐⭐⭐ 無料枠が大きい ⭐⭐⭐⭐ 従量課金
柔軟性 ⭐⭐⭐ ⭐⭐⭐⭐⭐
エコシステム ⭐⭐⭐ ⭐⭐⭐⭐⭐

個人開発では実装量を減らせる点とセットアップが簡単にできる点がメリットになります。


KORENANIでの実装例

子供向けサービスでは、他人の画像にアクセスされない仕組みが必要です。

私自身、子供を持つ親として、子供が撮った写真の漏洩リスクを可能な限り低減したいという思いがありました。KORENANIでは以下の二段階でアクセス制御を実装しています:

  1. アプリケーション層: API上でのアクセス制御
  2. データベース層: RLSによる権限チェック

この多層防御により、以下のようなケースでもRLSが防波堤として機能します:

  • アプリケーション層で権限チェックを忘れた場合
  • コード上で誤って他のユーザーのパスを指定した場合
  • ブラウザの開発者ツールなどでリクエストが改竄された場合

例えば、悪意のあるユーザーがAPIリクエストのパラメータを書き換えて他人の画像にアクセスしようとしても、認証トークンのuidとパスのuserIdが一致しないため、RLSによって403エラーが返されます。

データベース層(RLS):

-- 自分のパス配下のみ閲覧可能
CREATE POLICY "Users can view own images"
ON storage.objects FOR SELECT
TO authenticated
USING (
    bucket_id = 'users'
    AND
    auth.uid()::text = (string_to_array(name, '/'))[1]
);

-- アップロードも同様
CREATE POLICY "Users can upload to own path"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
    bucket_id = 'users'
    AND
    auth.uid()::text = (string_to_array(name, '/'))[1]
);

-- 削除も同様
CREATE POLICY "Users can delete own images"
ON storage.objects FOR DELETE
TO authenticated
USING (
    bucket_id = 'users'
    AND
    auth.uid()::text = (string_to_array(name, '/'))[1]
);

アプリケーション層(NestJS):

async uploadImage(path: string, buffer: Buffer, contentType: string) {
    const { data, error } = await this.supabase.storage
        .from('users')
        .upload(path, buffer, {
            contentType,
            upsert: false,
            duplex: 'half',
        });

    if (error) {
        return { ok: false, error: new Error(error.message || 'Upload failed') };
    }

    return { ok: true, value: { path: data.path } };
}

通常のSupabase Clientを使う限り、RLSは常に適用されます。

iOSクライアント

import Supabase

let client = SupabaseClient(
    supabaseURL: AppConfig.supabaseURL,
    supabaseKey: AppConfig.supabaseAnonKey,
    options: SupabaseClientOptions(
        auth: .init(
            redirectToURL: AppConfig.authRedirectURL,
            flowType: .pkce
        )
    )
)

func signIn(email: String, password: String) async throws -> String {
    let session = try await client.auth.signIn(email: email, password: password)
    return "\(session.user.id)"
}

let upload = try await client.storage
    .from("users")
    .upload(path: path, file: imageData)

3. RLSの役割

RLSは**認可(Authorization)**を担当します。

RLSができること

データアクセスの権限チェック

CREATE POLICY "own_data" ON users
FOR ALL USING (auth.uid() = id);

SQLで宣言的に記述

CREATE POLICY "team_access" ON documents
FOR ALL USING (
  auth.uid() IN (
    SELECT user_id FROM team_members 
    WHERE team_id = documents.team_id
  )
);

RLSができないこと

当然のことではあると思いながら、RLSはあくまで権限チェックに特化しており、その役割を強調したい。

ビジネスロジックのバグは防げない

// 関連画像を削除し忘れる
await supabase.from('children').delete().eq('id', childId);
// → 画像は残ったまま、孤立データが発生

レート制限やDoS対策にはならない

// 自分のデータに対する大量リクエスト
for (let i = 0; i < 10000; i++) {
  await supabase.storage.from('users').list(`${userId}/`);
}
// → 自分のデータなのでRLSは通過

入力バリデーションは別途必要

await supabase.from('users').insert({
  email: 'not-an-email',
  age: -5
});
// → RLSは権限のみチェック

セキュアなアプリケーションの構成

複数の防御層があると安心です:

1. 入力バリデーション層(Zodなど)
2. 認証層(Supabase Auth)
3. ビジネスロジック層(開発者が実装)
4. 認可層(Supabase RLS)
5. 監視・ログ層(開発者が実装)

RLSは「権限チェックの実装漏れ」や「クライアント側でのリクエスト改竄」といった問題からデータを保護しますが、すべてのセキュリティ問題を解決するわけではありません。各層で適切な対策が必要です。
ただし、セキュアな実装をする上でとても重要な役割を果たしてくれます。


4. 子供向けサービスでの実装例

KORENANIではすでに述べた通りデータ保護を重視しています。

パスベースのアクセス制御

/users/{userId}/children/{childId}/profile.jpg

パスの最初のセグメントが認証済みユーザーのuidと一致する場合のみアクセス可能にします。

auth.uid()::text = (string_to_array(name, '/'))[1]
  • 認証トークン必須
  • リクエストごとにチェック
  • パス情報だけでは不十分

実装ミスがあった場合

// 誤った実装:パスを直接受け取る
app.get('/download', async (req, res) => {
  const filePath = req.query.path;
  
  const { data, error } = await supabase.storage
    .from('users')
    .download(filePath);
  
  if (error) {
    // RLSが403を返す
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  return res.json({ data });
});

アプリケーション層で実装ミスがあっても、RLSがデータベースレベルで権限をチェックします。

ただし、これは「権限チェック漏れ」を防ぐものであり、すべてのセキュリティ問題を解決するわけではありません。

ロールベースのアクセス制御

-- 公開コンテンツ
CREATE POLICY "Public read access for korenani items"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'public');

-- 管理者のみ書き込み可能
CREATE POLICY "Admin write access for public korenani items"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
    bucket_id = 'public'
    AND
    EXISTS (
        SELECT 1 FROM public.user_role
        WHERE "userId"::uuid = auth.uid()
        AND "roleName" = 'PUBLIC_CONTENT_MANAGER'
    )
);

学習用の公開コンテンツと、ユーザーの個人データを分離できます。


5. テストのしやすさ

個人開発、特にAI駆動で開発を進める場合、Unitテストに加え、E2Eテストを書くことを意識しています(AI駆動じゃなくても大事)

AWS S3 + RDS + Cognito の場合

# docker-compose.ymlの準備
services:
  postgres:
    # PostgreSQLの設定
  localstack:
    # S3 emulatorの設定
  cognito-local:
    # Cognito emulatorの設定
docker-compose up -d

この他に必要な作業:

  • LocalStackのS3設定(バケット、ポリシー)
  • Cognito設定(ユーザープール、クライアント)
  • DBマイグレーション
  • 各サービス間の接続設定

各emulatorの仕様を理解し、設定ファイルを準備する必要があります。

Supabaseの場合

supabase start

これだけで以下が立ち上がります:

  • PostgreSQL(ポート: 54322)
  • Storage(S3互換)
  • Auth(GoTrue)
  • Supabase Studio(ポート: 54323)
  • Realtime
  • Edge Functions
supabase stop      # 停止
supabase db reset  # リセット

E2Eテストでの利点:

Auth・Storage・DBを横断するテスト(例:ログイン→画像アップロード→容量上限チェック)が必要な場合でも、全てが統合された環境なので追加のセットアップは不要です。AWSの場合は各emulatorを個別に設定し、サービス間の連携を構築する必要がありますが、Supabaseではその手間が省けます。

内部では様々なDockerコンテナが立ち上がっていますが、開発者が個別に意識する必要はありません。


6. Supabaseを使う際の考慮点

RLSのパフォーマンスへの影響

RLSはクエリ時にチェックされるため、パフォーマンスへの影響があります:

-- 実際に実行されるクエリ(内部的に)
SELECT * FROM storage.objects 
WHERE bucket_id = 'users' 
  AND name LIKE 'user-123/%'
  AND auth.uid()::text = (string_to_array(name, '/'))[1];  -- RLSチェック

Supabase公式ドキュメント「RLS Performance and Best Practices」によると、特にテーブル全体をスキャンするクエリ(select、limit、offset使用時)でパフォーマンスへの影響が大きくなります。

ただし、適切なインデックスを追加することで大幅に改善できます:

-- パスベースのインデックスを追加
CREATE INDEX idx_storage_objects_path 
ON storage.objects ((string_to_array(name, '/'))[1]);

公式のベンチマークテストでは、インデックス追加により100倍以上の性能改善が見られたケースもあります。

KORENANIでの体感:

実際のサービス運用では、パフォーマンス上の問題は感じていません。ただし、今後ユーザー数が増えた際の影響については、別途ベンチマークテストを実施して検証する予定です。

スケーラビリティ

  • 無料プラン: 500MB DB、1GB Storage
  • Proプラン: 8GB DB、100GB Storage
  • それ以上: カスタムプラン

Supabaseを使っているサービスには、LaunchDarkly(機能フラグサービス)やPeerlist(開発者向けSNS)などがあります。

学習曲線

PostgreSQLの知識があれば、すぐに習得できます。

-- シンプルなケース
CREATE POLICY "own_data" ON users
FOR ALL USING (auth.uid() = id);

-- 複雑なケース
CREATE POLICY "team_access" ON documents
FOR ALL USING (
  auth.uid() IN (
    SELECT user_id FROM team_members 
    WHERE team_id = documents.team_id
  )
);

Supabase Studioで直接テストできます。

ベンダーロックイン

Supabaseはオープンソースなので、理論的にはセルフホストできます。ただし:

  • セルフホストは運用負荷が高い
  • マネージドサービスの恩恵を受けられなくなる
  • 他のBaaSへの移行には時間がかかる

緩和策として:

  • オープンソースなので、最悪の場合はセルフホストに移行可能
  • PostgreSQLベースなので、データの移行は比較的容易
  • APIがRESTとPostgRESTベース

7. Studio UI

最初はWeb UIに違和感がありましたが、今では特に違和感なく使えています。pgAdminやTablePlusなどと比較しても、統合性の面で使いやすいです。

機能

  • ブラウザ完結
  • SQLエディタ(シンタックスハイライト、補完機能)
  • テーブル設計のビジュアル表示
  • RLSポリシーの管理
  • ログのリアルタイム表示
  • Storage/Authの統合管理
  • Realtime更新の確認

8. 開発フロー

ローカル開発

{
  "scripts": {
    "supabase:start": "cd local && supabase start",
    "supabase:stop": "cd local && supabase stop",
    "supabase:reset": "cd local && supabase db reset",
    "db:push": "cd local && supabase db push"
  }
}

Supabase CLIでローカル環境を再現できます:

  • PostgreSQL(ポート: 54322)
  • Studio(ポート: 54323)
  • Storage
  • Auth

本番環境へのデプロイ

supabase db diff -f add_storage_policies  # マイグレーション作成
supabase db push                          # 本番環境に適用

Gitでマイグレーションを管理できます。


9. どのサービスを選ぶべきか

プロジェクトの性質と開発リソースによって変わります。

Supabaseが向いているケース

  • 個人開発・小規模チーム
  • 高速なプロトタイピング
  • 宣言的な権限管理
  • テスト重視の開発
  • ユーザーごとのファイル管理
  • データ保護が重要だがクイックに済ませたい

AWSが向いているケース

  • 大規模サービス(ペタバイト級)
  • 柔軟性が必要
  • 既存のAWSインフラと統合
  • 専任のインフラチームがいる
  • 特殊な要件がある

まとめ

Supabaseのメリット

  1. 無料プランが充実(MAU 50,000まで)
  2. 認証・DB・Storageの統合により実装量が減る
  3. SQLで権限を宣言的に記述できる
  4. supabase startだけで全環境が立ち上がる
  5. Studio UIとCLIで開発効率が上がる
  6. PostgreSQLベース

トレードオフ

  1. Supabaseの機能に制約される
  2. ペタバイト級のサービスには向かない
  3. RLSチェックのオーバーヘッドがある(個人開発レベルでは許容範囲)
  4. SQLとRLSの概念を理解する必要がある

判断基準

以下に当てはまるなら、Supabaseを検討する価値があります:

  • 個人開発・小規模チームで開発速度を重視
  • セキュリティを確実に担保したいが手間はあまりかけたくない
  • テストを頻繁に書いて実行しながら進めていく開発スタイル
  • MVP・プロトタイプを素早く作りたい

過去の経験との比較

従来のアーキテクチャ:

  • サーバー側で認証付きURL発行を実装
  • DB側でアクセス権限チェックを実装
  • テスト環境のセットアップに時間がかかる
  • 各サービスのemulatorを個別に管理

Supabase:

  • RLSポリシーで宣言的にアクセス制御
  • supabase startだけで全環境が立ち上がる
  • ローカルと本番でコードが同じ

KORENANIプロジェクトでは、開発速度とセキュリティを重視したため、Supabaseを選択しました。結果として、短期間で安全なアプリを構築できました。

個人開発で「認証・Storage・DBどうしよう」と悩んでいるなら、まずSupabaseの無料プランで試してみることをお勧めします。


参考リンク

Supabase公式

比較記事


おまけ: プロダクト紹介「KORENANI」

この記事で紹介したSupabaseの実装は、子供向け画像認識学習アプリ「KORENANI」の開発経験に基づいています。

KORENANIとは

「これなに?」という子供の好奇心を育むことを目的とした、画像認識技術を活用した学習アプリです。世界を百科事典に変えることをコンセプトに、子供たちが身の回りのものをカメラで撮影すると、アプリがそれを認識し、学習コンテンツとして提供します。

技術スタック

サーバー側:

  • NestJS + Bun
  • Supabase(Auth、Storage、Database)
  • PostgreSQL with RLS

クライアント:

  • SwiftUI(iOS)

セキュリティへのこだわり

子供向けサービスということもあり、セキュリティには特に気を配りました。この記事で紹介したRLSによる権限管理は、「子供たちの画像が絶対に他人にアクセスされてはいけない」という要件から生まれたアーキテクチャです:

  • ユーザーごとに完全に隔離されたストレージ
  • パスベースの自動アクセス制御
  • 宣言的な権限管理
  • 学習用の公開コンテンツと個人データの明確な分離

執筆者について

子供向け画像認識学習アプリ「KORENANI」を開発中。サーバー側にNestJS + Bun、クライアントにFlutterとSwiftUIを採用し、SupabaseをAuth・Storage・DBとして活用しています。


補足:他の認証サービスとの統合

Supabaseは他の認証サービス(Auth0、Cognitoなど)との統合にも対応しています。公式ドキュメントによると、Auth0と統合した場合でもRLSは利用可能です。

KORENANIでは実際には試していませんが、Supabaseの他サービスとのインテグレーション体験は良好なため、既存の認証基盤がある場合でも選択肢になりえます。

せっかくなので、Zennfesのインフラ・セキュリティ・Auth0にも投稿する予定で、Auth0についてもサッと調べてみました。

他にもソーシャルログインのプロバイダーの登録の簡単さや、Emailの統合のしやすさなど色々良いなと思う点があるのですが、長くなってしまったので割愛しています。


注意事項

この記事は私の個人開発での経験から、Supabaseの使いやすいと感じた点に焦点を当てたものです。

Supabase自体は個人開発に特化したサービスではなく、多くの企業で本番環境で使用されています。同様に、AWSやその他のクラウドサービスも個人開発で広く使われています。「個人開発向き」「企業向き」という単純な分類ではなく、プロジェクトの要件、チーム構成、既存インフラなどによって最適な技術選択は異なります。

この記事の内容を一つの参考として、ご自身のプロジェクトに合った技術選択をしてください。

Discussion