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設定のよくある落とし穴
踏んだ穴をいくつか:
-
INSERT Policy忘れ:ユーザーはログインできるが、アップロードできない。エラーは「new row violates row-level security policy」
-
Policyが寛容すぎる:例えば
USING (true)を使うと、全ファイルが全員に操作可能。RLS未設定と同じ効果。 -
パス設計が不合理:ユーザー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作成
コンソール操作:
- Storage > New bucket > 名前
avatars入力、Privateチェック - 同様に
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:テスト・検証
リリース前に以下を確認:
- 未ログインユーザーが記事画像を見られる?(SELECT Policyが許可)
- 一般ユーザーが記事画像をアップロード?(不可、authorロールのみ)
- ユーザーAがユーザーBのアバターを削除?(不可、パス分離)
各項目をテストし、Policy設定が正しいことを確認。深夜3時の教訓、二度と経験したくない。
まとめ
Supabase Storageの核心は3つ:アップロード、権限、加速。
アップロードは最も簡単、数行コードで完了。権限設定には手間が必要。RLS Policyは一回設定で終わりではない、ビジネスケースに合わせて慎重に設計。CDNと画像変換は锦上添花、Pro Plan限定だが、開発時間を大幅に節約できる。
私の経験:まず基本的なアップロードと権限を固め、安全事故を防ぐ。後に需要があればCDNを追加、画像処理需要があれば変換を追加。段階的に進め、一度に全部は求めない。
Supabase Storageを使っているなら、あなたの踏んだ穴をシェアして。深夜3時の教訓、私だけではないはず。
Supabase Storage 完全ワークフロー
Bucket作成から権限設定、CDN加速までの実践ガイド
⏱️ 目安時間: 30 分
- 1
ステップ1: Bucketを作成
SupabaseコンソールでプライベートBucketを作成:
• Storage > New bucket
• 名前入力(例:avatars)
• Privateチェック、デフォルトはプライベート推奨
• Create bucketをクリック - 2
ステップ2: RLS Policyを設定
BucketにRow Level Securityを設定:
• Storage > Bucket選択 > Policies
• New Policyをクリック
• 操作タイプ選択(SELECT/INSERT/UPDATE/DELETE)
• Policyルール記述(ユーザー分離など)
• テスト後本番環境に適用 - 3
ステップ3: ファイルをアップロード
SDKでファイルをアップロード:
• パス設計(例:userId/filename)
• storage.from().upload()呼び出し
• cacheControlとupsertパラメータ設定
• アップロードエラーとパス処理 - 4
ステップ4: CDNと画像変換を設定(オプション)
Pro Planで高度機能を利用:
• Smart CDNが人気ファイルを自動キャッシュ
• 画像変換URLパラメータ(width/height/format)
• Next.js Image Loader連携
• 無料枠監視(100枚/月)
FAQ
Public bucketとPrivate bucketの違いは?
RLS Policyでユーザー分離を実現する方法は?
• アップロードパス設計:userId/filename
• Policy:auth.uid()::text = (storage.foldername(name))[1]
• ユーザーは自分IDで始まるパスのみ操作可能
Private bucketのファイルを外部シェアする方法は?
ファイルアップロードの制限は?
画像変換のパラメータと制限は?
• width/height:サイズ(範囲1〜2500px)
• resize:contain(比率維持)またはcover(クロップ埋め)
• quality:画質、1〜100
• format:webp/jpeg/png/gif/avif
制限:元ファイル25MB以下
Smart CDNと画像変換は有料?
6 min read · 公開日: 2026年4月9日 · 更新日: 2026年4月9日
関連記事
n8n 実践ガイド:Webhook トリガーと IF/Switch 条件分岐の設計
n8n 実践ガイド:Webhook トリガーと IF/Switch 条件分岐の設計
Docker Compose マルチサービスオーケストレーション:ローカル開発環境を一発起動
Docker Compose マルチサービスオーケストレーション:ローカル開発環境を一発起動
GitHub Actions Matrix ビルド:マルチバージョン並列テストの実践

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