言語を切り替える
テーマを切り替える

Supabase Storage 実践ガイド:ファイルアップロード、権限制御とCDN活用

深夜3時、コンソールのエラーメッセージを前に呆然としていた。ユーザーアバター機能のリリースから30分で、「全員のアバターが同じ人のものになってしまった」という報告が届いた。

調査してみると、問題は Storage の RLS Policy にあった——設定していなかったのだ。bucket は公開設定、アップロードパスにユーザー分離もなし。誰でも他人のファイルを上書きできる状態だった。つまり、権限設定のミスがほぼ本番事故になるところだった。

Supabase Storage。使うのは簡単だが、本当に使いこなすには——権限制御、CDN高速化、画像変換——ここには落とし穴が多い。この記事では、私が踏んだ穴と見つけた解決策をまとめて紹介する。

一、クイックスタート:標準ファイルアップロード

まず基本を——ファイルをアップロードする。

Bucketの作成

Supabaseコンソールを開き、左メニューから「Storage」を見つけ、「New bucket」をクリック。名前をつける——avatarsでアバター、postsで記事画像など。「Make this bucket public?」というオプションが出る——今は急いでチェックしないで。後の権限セクションで詳しく説明する。

私の習慣:プライベートbucketに機密ファイル、公開bucketに静的リソース。デフォルトはプライベートbucketを作成し、必要に応じて調整する。

アップロードコード

@supabase/supabase-jsをセットアップ済みなら、コードはシンプル:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
)

// ファイルをアップロード
async function uploadFile(file: File) {
  const filePath = `uploads/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')  // bucket名
    .upload(filePath, file, {
      cacheControl: '3600',  // 1時間キャッシュ
      upsert: false  // 存在時はエラー、上書きしない
    })

  if (error) {
    console.error('アップロード失敗:', error.message)
    return null
  }

  return data.path  // ファイルパスを返す
}

正直、このコードは10回以上書いた。filePathの設計が重要——後で時間戳プレフィックスを使う理由、ユーザー分離の方法を説明する。

ファイルサイズの制限

公式ドキュメント:標準アップロードは最大5GB。実体験では、6MB以下は標準アップロードで快適;6MB以上はTUSプロトコルのレジューム(再開)アップロードがおすすめ。

TUSとは?大容量ファイルのアップロードで中断・再開をサポート。ネットが切れても、再接続後続きから再開——最初からやり直さない。動画や大画像には、この機能が不可欠——90%進んだところで突然ネット切断、TUSなしでは全やり直し。

TUSアップロードの設定は追加必要。今は使わないなら、標準アップロードで大部分のケースは対応できる。

// TUSアップロード例、大容量ファイルに推奨
const { data, error } = await supabase.storage
  .from('videos')
  .upload('large-video.mp4', file, {
    duplex: 'half',  // ストリームアップロード有効
    // TUSが自動的にレジューム処理
  })

二、セキュリティ設定:RLS Policy詳解

深夜3時の穴に戻る——権限未設定、ファイルが自由に上書き可能。

SupabaseのStorageはデータベースと同じく、基盤はPostgreSQL。だから権限制御もRLS(Row Level Security)の仕組み。bucketはテーブル相当、各ファイルは1レコード。

Bucketの公開と非公開

bucket作成時の選択:「Public bucket」か「Private bucket」。

Public bucket:誰でも読み取り可能、認証不要。公開アバター、サイトロゴなどの静的リソースに適している。

Private bucket:認証が必要。ただし注意——認証は门槛、具体的に誰が読める・書けるはRLS Policyで決まる。

私の提案:ファイルが本当に完全公開ならPublic、それ以外はデフォルトPrivate bucket。権限をちゃんと設定してから公開する方が、公開してから対処するより安全。

RLS Policyの4種類

StorageのPolicyページで、4つの操作が見える:

  • SELECT:ファイル読み取り(ダウンロード、URL取得)
  • INSERT:新ファイルアップロード
  • UPDATE:既存ファイルの更新・上書き
  • DELETE:ファイル削除

各操作にPolicyを設定できる。最も一般的なパターン:

-- ユーザーは自分のファイルのみ操作可能
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);

このSQLは少し複雑、分解して説明:

  • auth.uid()は現在ログイン中のユーザーIDを取得
  • storage.foldername(name)はファイルパスの第1層ディレクトリ名を抽出
  • 例:パスがuser123/avatar.jpgなら、第1層ディレクトリはuser123

Policy全体の意図:ファイルパスの第1層ディレクトリがユーザーIDと一致する時のみ、そのユーザーがファイル操作可能。これがユーザー分離の基本方針。

ユーザー分離の実装

具体的には?アップロード時にユーザーIDをパス第1層に:

async function uploadAvatar(userId: string, file: File) {
  // パス設計:ユーザーID/ファイル名
  const filePath = `${userId}/avatar-${Date.now()}.jpg`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file)

  return data?.path
}

こうすると、各ユーザーのファイルは自分の「フォルダー」内に。RLS Policyで、ユーザーは自分IDで始まるパスのみ操作可能、他人のファイルには触れない。

署名付きURLの生成

Private bucketのファイルは、直接アクセスすると404エラー。署名付きURLを生成する必要:

// 一時アクセスリンク生成(1時間有効)
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl('user123/avatar.jpg', 3600)

console.log(data?.signedUrl)  // 署名付きURL

署名の有効期限は自由に設定。長すぎると安全ではない、短すぎるとUXが悪い。通常1〜4時間が適当。

完全公開したいがbucket設定を変更したくない場合、getPublicUrlを使う:

const { data } = supabase.storage
  .from('public-assets')
  .getPublicUrl('logo.png')

// このURLは認証不要、誰でもアクセス可能

Policy設定のよくある落とし穴

踏んだ穴をいくつか:

  1. INSERT Policy忘れ:ユーザーはログインできるが、アップロードできない。エラーは「new row violates row-level security policy」

  2. Policyが寛容すぎる:例えばUSING (true)を使うと、全ファイルが全員に操作可能。RLS未設定と同じ効果。

  3. パス設計が不合理:ユーザーIDがパス第1層にない場合、storage.foldernameの抽出が失敗。私は以前、パスをuploads/user123/file.jpgと書いてしまい、抽出結果がuploadsになり、Policy判断が狂った。

Policy設定時、まずコンソールのSQLエディタでテストし、ロジックが正しいことを確認してから本番環境に適用する。

三、パフォーマンス向上:Smart CDNと画像変換

ファイルをアップロードし、権限も設定した。次に考える:どう読み込みを速くする?

Smart CDNの原理

SupabaseのSmart CDNは普通のCDNではない。ファイルのアクセス頻度に応じて自動的にキャッシュ期間を決定——人気ファイルは長くキャッシュ、冷門ファイルは短くキャッシュ。

公式説明:キャッシュ無効化の世界中への同期は最大60秒。東京でファイルを更新しても、60秒内にニューヨークのユーザーも最新版を見られる。従来のCDNの数分〜数十分より、かなり速い。

ただしSmart CDNは有料機能、Pro Plan(月$25)が必要。Free Planの場合、CDN加速なし、Supabaseサーバーから直接読み込む。

画像変換パラメータ

この機能は便利——自分で画像のリサイズ、クロップを処理しなくていい、SupabaseがURLパラメータで直接処理。

基本パラメータ:

?width=300&height=200    // サイズ指定
?resize=contain          // 比率維持、クロップなし
?resize=cover            // サイズ埋め、余分をクロップ
?quality=80              // 画質(1-100)
?format=webp             // WebP形式、容量削減

組み合わせて使う:

const baseUrl = supabase.storage
  .from('avatars')
  .getPublicUrl('user123/avatar.jpg').data.publicUrl

// サムネイル生成
const thumbnailUrl = `${baseUrl}?width=100&height=100&resize=cover`

画像変換の制限:

  • サイズ範囲:1〜2500px
  • 元ファイルサイズ:25MB以下
  • 対応形式:jpeg、png、webp、gif、avif

制限超過でエラー。30MBの元画像をリサイズしようとして拒否されたことがある。

料金:各プロジェクトの無料枠

画像変換は変換回数で課金、ストレージ容量ではない。

各プロジェクト月100枚まで無料。超過後、1000枚で$5。

正直、個人プロジェクトや小規模用途なら、100枚は十分。私のブログプロジェクトでは、月数枚のアバターと記事画像程度。Instagram的な画像ソーシャルアプリを作らない限り、この費用は気にならない。

Next.js連携:Image Loader

Next.jsを使っている場合、SupabaseのImage Loaderを設定し、next/imageが自動処理:

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './supabase-image-loader.js',
  }
}

loaderファイルを書く:

// supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
  const params = new URLSearchParams()
  params.set('width', width.toString())
  params.set('quality', (quality || 75).toString())
  params.set('format', 'webp')

  return `${src}?${params.toString()}`
}

Next.jsで<Image src="..." width={300} />を使うと、自動的に変換パラメータが追加される。

Pro Planの必要性

前述のSmart CDN、画像変換はPro Planが必要。Free Planは基本的なアップロード・ダウンロードのみ。

アップグレードするか?プロジェクト要件で判断。数枚のアバターだけならFree Planで十分。大量の画像処理、パフォーマンス最適化が必要なら、Pro PlanのCDN加速と画像変換は手間を大幅に削減——CDN自建不要、画像処理サービス自作不要。

私の選択:プロジェクト開始時はFree Planでテスト、トラフィックが安定してからProにアップグレード。月$25は小さくない金額だ。

四、実践:ブログプロジェクトの完全設定

理論だけ話すより、実際のケースを見よう。私のブログプロジェクトのStorage設定——ゼロから運用可能まで。

シナリオ:ユーザーアバター + 記事画像

2つのbucketが必要:

  • avatars:ユーザーアバター、プライベートbucket、ユーザーは自分アバターのみ操作
  • post-images:記事画像、プライベートbucket、著者はアップロード可、誰でも読み取り可(署名URL使用)

Step 1:Bucket作成

コンソール操作:

  1. Storage > New bucket > 名前avatars入力、Privateチェック
  2. 同様にpost-imagesを作成

Step 2:RLS Policy設定

avatars bucketのPolicy:

-- 全員がアバター読み取り可能(SELECT許可)
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

-- ユーザーは自分アバターのみアップロード・更新
CREATE POLICY "Users manage own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

-- ユーザーは自分アバターのみ削除
CREATE POLICY "Users delete own avatar"
ON storage.objects FOR DELETE
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

post-images bucketのPolicy:

-- 著者が記事画像をアップロード(authorロール前提)
CREATE POLICY "Authors can upload post images"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'post-images'
  AND auth.jwt() ->> 'role' = 'author'
);

-- 全員が記事画像読み取り可能
CREATE POLICY "Public read post images"
ON storage.objects FOR SELECT
USING (bucket_id = 'post-images');

Step 3:フロントエンドアップロード

アバターアップロードコンポーネント:

async function handleAvatarUpload(file: File) {
  const user = await supabase.auth.getUser()
  if (!user.data.user) return alert('先にログインしてください')

  // パス:ユーザーID/avatar.jpg(固定名、毎回上書き)
  const filePath = `${user.data.user.id}/avatar.jpg`

  const { error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, { upsert: true })

  if (!error) {
    // 公開URL取得(SELECT Policyが全員読み取り許可)
    const url = supabase.storage.from('avatars').getPublicUrl(filePath)
    setUserAvatar(url.data.publicUrl)
  }
}

記事画像アップロード:

async function handlePostImageUpload(file: File) {
  const filePath = `posts/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('post-images')
    .upload(filePath, file)

  if (!error) {
    // 署名付きURL生成、24時間有効
    const { data: urlData } = await supabase.storage
      .from('post-images')
      .createSignedUrl(filePath, 86400)

    insertImageToEditor(urlData?.signedUrl)
  }
}

Step 4:テスト・検証

リリース前に以下を確認:

  1. 未ログインユーザーが記事画像を見られる?(SELECT Policyが許可)
  2. 一般ユーザーが記事画像をアップロード?(不可、authorロールのみ)
  3. ユーザーAがユーザーBのアバターを削除?(不可、パス分離)

各項目をテストし、Policy設定が正しいことを確認。深夜3時の教訓、二度と経験したくない。

まとめ

Supabase Storageの核心は3つ:アップロード、権限、加速。

アップロードは最も簡単、数行コードで完了。権限設定には手間が必要。RLS Policyは一回設定で終わりではない、ビジネスケースに合わせて慎重に設計。CDNと画像変換は锦上添花、Pro Plan限定だが、開発時間を大幅に節約できる。

私の経験:まず基本的なアップロードと権限を固め、安全事故を防ぐ。後に需要があればCDNを追加、画像処理需要があれば変換を追加。段階的に進め、一度に全部は求めない。

Supabase Storageを使っているなら、あなたの踏んだ穴をシェアして。深夜3時の教訓、私だけではないはず。

Supabase Storage 完全ワークフロー

Bucket作成から権限設定、CDN加速までの実践ガイド

⏱️ 目安時間: 30 分

  1. 1

    ステップ1: Bucketを作成

    SupabaseコンソールでプライベートBucketを作成:

    • Storage > New bucket
    • 名前入力(例:avatars)
    • Privateチェック、デフォルトはプライベート推奨
    • Create bucketをクリック
  2. 2

    ステップ2: RLS Policyを設定

    BucketにRow Level Securityを設定:

    • Storage > Bucket選択 > Policies
    • New Policyをクリック
    • 操作タイプ選択(SELECT/INSERT/UPDATE/DELETE)
    • Policyルール記述(ユーザー分離など)
    • テスト後本番環境に適用
  3. 3

    ステップ3: ファイルをアップロード

    SDKでファイルをアップロード:

    • パス設計(例:userId/filename)
    • storage.from().upload()呼び出し
    • cacheControlとupsertパラメータ設定
    • アップロードエラーとパス処理
  4. 4

    ステップ4: CDNと画像変換を設定(オプション)

    Pro Planで高度機能を利用:

    • Smart CDNが人気ファイルを自動キャッシュ
    • 画像変換URLパラメータ(width/height/format)
    • Next.js Image Loader連携
    • 無料枠監視(100枚/月)

FAQ

Public bucketとPrivate bucketの違いは?
Public bucketは誰でも読み取り可能、公開静的リソース(ロゴ、公開アバター)に適している。Private bucketは認証が必要、具体的権限はRLS Policyで制御——より安全。
RLS Policyでユーザー分離を実現する方法は?
基本方針はユーザーIDをパス第1層に配置:

• アップロードパス設計:userId/filename
• Policy:auth.uid()::text = (storage.foldername(name))[1]
• ユーザーは自分IDで始まるパスのみ操作可能
Private bucketのファイルを外部シェアする方法は?
createSignedUrl()で署名付き一時アクセスURLを生成、有効期限を設定可能。長すぎると安全ではない、短すぎるとUXが悪い——通常1〜4時間。
ファイルアップロードの制限は?
標準アップロードは最大5GB。6MB以下は標準アップロードで快適、6MB以上はTUSプロトコルのレジュームが推奨——ネット中断後も続きから再開可能。
画像変換のパラメータと制限は?
対応パラメータ:

• width/height:サイズ(範囲1〜2500px)
• resize:contain(比率維持)またはcover(クロップ埋め)
• quality:画質、1〜100
• format:webp/jpeg/png/gif/avif

制限:元ファイル25MB以下
Smart CDNと画像変換は有料?
その通りです、Pro Planが必要(月$25)。Free Planは基本アップロード・ダウンロードのみ利用可能。画像変換には無料枠あり:各プロジェクト月100枚、超過後は1000枚で$5。

6 min read · 公開日: 2026年4月9日 · 更新日: 2026年4月9日

コメント

GitHubアカウントでログインしてコメントできます

関連記事