Supabase Storage は S3 互換のファイルストレージ。バケット単位で Public / Private を制御し、アクセス制御は RLS ポリシーと同じ仕組みで書ける。
upload → getPublicUrl or createSignedUrl の流れが基本storage.objects テーブルに対して設定するバケットはファイルの入れ物。Public / Private の選択がアクセス制御の起点になる。
| 種別 | アクセス | URL取得 | 用途例 |
|---|---|---|---|
| Public バケット | URL を知っていれば誰でも | getPublicUrl() | プロフィール画像・OGP画像・公開アセット |
| Private バケット | 署名付き URL が必要 | createSignedUrl() | 契約書・請求書・プライベートな画像 |
バケット作成はダッシュボードの Storage > New Bucket から、または SQL で行う。
-- SQL でバケットを作成
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true); -- public: true = Public バケット
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false); -- private バケット
ファイルサイズ上限・許可する MIME タイプもバケット単位で設定できる。
.upload() が基本。同じパスに上書きしたい場合は upsert: true を指定する。
// 基本アップロード
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${user.id}/avatar.png`, file)
// 上書き許可(同じパスに再アップロード)
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${user.id}/avatar.png`, file, {
upsert: true,
contentType: 'image/png', // 省略すると自動判別
})
avatars/
{user_id}/avatar.png ← ユーザーごとに分離
{user_id}/thumbnail.png
documents/
{user_id}/{filename} ← ユーザーが所有するファイル
shared/{filename} ← 共有ファイル
💡 ポイント: パスに
user_idを含めると、RLS ポリシーで(storage.foldername(name))[1] = auth.uid()::textという条件で「自分のフォルダ以下のみ操作可」を実現できる。
// 50MB 超のファイルには TUS プロトコルを使う
const { data, error } = await supabase.storage
.from('videos')
.uploadToSignedUrl(path, token, file, {
onUploadProgress: (progress) => {
console.log(`${progress.percent}% 完了`)
},
})
Public バケットは getPublicUrl()、Private バケットは createSignedUrl() を使う。混同しないこと。
// Public バケット → 期限なしの永続 URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${user.id}/avatar.png`)
console.log(data.publicUrl)
// → https://<project>.supabase.co/storage/v1/object/public/avatars/xxx/avatar.png
// Private バケット → 期限付き署名 URL
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(`${user.id}/contract.pdf`, 3600) // 3600秒 = 1時間有効
console.log(data?.signedUrl)
// 複数ファイルをまとめて署名 URL 取得
const { data } = await supabase.storage
.from('documents')
.createSignedUrls([
`${user.id}/file1.pdf`,
`${user.id}/file2.pdf`,
], 3600)
| メソッド | バケット | 有効期限 | 用途 |
|---|---|---|---|
getPublicUrl() | Public のみ | なし(永続) | 誰でも見える画像・アセット |
createSignedUrl() | Public / Private 両方 | 指定秒数 | 一時的な限定共有・ダウンロードリンク |
💡 ポイント:
getPublicUrl()は非同期ではない(await不要)。createSignedUrl()は非同期。セキュリティが重要なファイルには、Public バケットでもcreateSignedUrl()で期限を設けることを検討する。
その他の操作もメソッドチェーンで書ける。
// ダウンロード(Blob として取得)
const { data, error } = await supabase.storage
.from('documents')
.download(`${user.id}/contract.pdf`)
if (data) {
const url = URL.createObjectURL(data)
// <a href={url} download> などで使う
}
// ファイル削除(配列で複数指定可)
const { error } = await supabase.storage
.from('avatars')
.remove([`${user.id}/avatar.png`])
// フォルダ内のファイル一覧
const { data } = await supabase.storage
.from('avatars')
.list(user.id, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' },
})
// ファイルの移動 / コピー
await supabase.storage.from('avatars').move('old-path.png', 'new-path.png')
await supabase.storage.from('avatars').copy('source.png', 'destination.png')
Storage の RLS は storage.objects テーブルに対して設定する。DB の RLS と構文は同じだが対象テーブルが異なる。
-- storage.objects テーブルに対して RLS を設定する
-- ① 自分のフォルダ以下のファイルだけ読める
CREATE POLICY "自分のファイルを読める"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- ② 自分のフォルダ以下にだけアップロードできる
CREATE POLICY "自分のフォルダにアップロード可"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- ③ 自分のファイルだけ削除できる
CREATE POLICY "自分のファイルを削除できる"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
-- ④ Public バケットは全員読み取り可
CREATE POLICY "Public バケットは誰でも読める"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'public-assets');
Storage で使える便利関数:
| 関数 | 説明 | 例 |
|---|---|---|
storage.foldername(name) | パスをフォルダ名の配列に分解 | avatars/user123/photo.png → ['user123', 'photo.png'] |
storage.filename(name) | ファイル名部分を取得 | avatars/user123/photo.png → photo.png |
storage.extension(name) | 拡張子を取得 | photo.png → png |
💡 ポイント: ダッシュボードから「Authenticated users can upload files」などのテンプレートポリシーを適用するだけでも動くが、パスベースの制御を入れないと他人のフォルダに上書きできてしまう。本番では必ず
(storage.foldername(name))[1] = auth.uid()::textのようなパス制限を入れる。
Pro プラン以上で、URL パラメータによるリアルタイム画像変換が使える。
// リサイズ・フォーマット変換
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${user.id}/avatar.png`, {
transform: {
width: 200,
height: 200,
resize: 'cover', // cover / contain / fill
format: 'webp', // webp / avif / jpeg / png(originは元のまま)
quality: 80,
},
})
💡 ポイント: 画像変換は Pro プラン以上の機能。元ファイルを保持したまま変換後の画像を CDN キャッシュするため、サムネイル生成用に別ファイルを保存する必要がなくなる。
| ミス | 影響 | 対策 |
|---|---|---|
Private バケットに getPublicUrl() を使う | URL は生成されるが実際にアクセスするとエラー | Private は createSignedUrl() を使う |
| パスにユーザー識別子を含めない | RLS でユーザー単位の制御ができない | {user_id}/{filename} のようなパス設計にする |
| Storage RLS を設定しない | Public バケットなら誰でも全ファイルを操作できる | storage.objects に必ずポリシーを設定する |
upload() で upsert: false(デフォルト)のまま同パスに再アップロード | エラーになる | 上書きする場合は upsert: true を指定 |
| 署名 URL の有効期限を長く設定しすぎる | URLが漏洩した場合のリスクが高まる | 用途に応じた最短の有効期限を設定する |