入門!Supabase Storageの使い方|画像・ファイルのアップロードを簡単実装

2025.09.29

目次

目次

Supabase Storageは、画像やPDF、動画を簡単に保存・配信できるクラウドストレージです。Next.jsなどのアプリと相性が良く、公開・非公開の切り替えや最適化も柔軟。

この記事では、基本操作から安全な運用方法までをわかりやすく解説します。

Supabase Storageとは?手軽にファイル管理ができるクラウドストレージ

最初にSupabase Storageの全体像をつかんでから、実装に入ると迷いが少なくなります。ここでは「何ができるか」をシンプルに押さえます。

  • アプリ専用のクラウドストレージとして、画像・PDF・動画等を保存できる

  • そのまま配信(CDN)でき、表示もスムーズ

  • データベース(Postgres)と統合され、アクセス制御を設計しやすい

ポイントは「フロントエンドからも扱いやすいAPI」と「DBと一体のセキュリティ設計」。この2つが運用の楽さに直結します。

Supabase Storageの主な特徴3つ

1. 大容量ファイルの保存と配信が可能

導入のハードルが低く、重めの画像や動画も運用可能です。

  • 写真投稿・ECのサムネイル/詳細画像

  • セミナー動画/eラーニング教材

  • 資料配布用PDF

補足:配信はCDN経由になるため、世界中のユーザーに対しても安定した速度で提供できます。数十MBを超える動画などは、標準のuploadよりもTUS(レジューム対応アップロード)を検討すると安心です。

2. データベースと連携したアクセス制御

ユーザーIDや商品IDなど、アプリのデータ構造と揃えて制御できます。

  • ログイン済みの本人だけ閲覧

  • チーム/組織単位での共有

  • 購入済みユーザーのみアクセス 等

補足:この仕組みはRow-Level Security(RLS)で柔軟に表現します。後述の「セキュリティ」章で詳しく扱います。

3. ファイルの変換・最適化を自動で実行

画像のリサイズやWebP変換などをURLパラメータで呼び出すだけで適用できます。

サイト速度(LCP等)や転送量の削減に効果的です。

Supabase Storageを使い始めるための初期設定

ステップ1:Supabaseで新規プロジェクトを作成する

  • Supabaseにログイン → New Project

  • プロジェクト名・パスワード・リージョンを設定

  • 数十秒でDB+Storageが起動

最初のうちは無料枠で十分に試せます。課金の前にプロトタイプで操作感を確認しましょう。

ステップ2:ファイルを保存するバケットを用意する

  • Storage → Buckets → New Bucket

  • 公開(Public) or 非公開(Private)を選択

    • 公開:URLが分かれば誰でも読める

    • 非公開:原則読めない(署名URLなどで限定公開)

使い分けの目安

  • サイトで誰でも見られる画像:Public

  • ログイン後だけ閲覧したいファイル:Private

ステップ3:アプリケーションにSupabaseクライアントを導入する

npm i @supabase/supabase-js
// src/lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseKey);

補足NEXT_PUBLIC_ が付くキーは公開前提。読み取り専用のAnon Keyに限って使い、Service Role Keyは絶対にクライアントへ出さない(サーバー専用)。

【基本操作】Supabase Storageのファイル操作方法

導入したら、まずは基本の4操作(アップロード/一覧/ダウンロード/削除)を試しましょう。流れが掴めると、UIの設計もイメージしやすくなります。

ファイルをアップロードする方法

  • 使いどころ:フォームから画像を登録、管理画面でPDFを追加 など

const { data, error } = await supabase.storage
  .from('public-images')
  .upload('example.png', file); // 第2引数は File/Blob

if (error) console.error(error);

補足upload(path, file, { upsert })upsertfalse にしておくと、誤上書きを防げます。大容量ファイル(数十MB以上)はuploadではなくTUS APIでの分割アップロードが推奨です。

アップロードしたファイルの一覧を取得する方法

  • 使いどころ:管理画面のファイルブラウザ、ギャラリー表示

const { data, error } = await supabase.storage
  .from('public-images')
  .list('', { limit: 100, sortBy: { column: 'name', order: 'desc' } });

if (!error) data?.forEach((f) => console.log(f.name));

補足:公式ドキュメントではsortBy.columnnameを指定するのが標準です。作成日時での並べ替えをしたい場合は、ファイル情報を別テーブルに保存して管理すると確実です。大量データはページング(limit/offset)で扱うとUIが軽くなります。

特定のファイルをダウンロードする方法

  • 使いどころ:PDFを開く、画像を一時URL化してプレビュー

const { data, error } = await supabase.storage
  .from('public-images')
  .download('example.png');

if (data) {
  const url = URL.createObjectURL(data);
  // <img src={url} /> 等でプレビュー
}

補足:Publicバケットの場合は getPublicUrl の方が手軽です。

不要になったファイルを削除する方法

  • 使いどころ:差し替えや退会時の削除、週次の整理

const { error } = await supabase.storage
  .from('public-images')
  .remove(['example.png']);

補足:運用では「誰がいつアップしたか」をDBに残しておくと、棚卸しや一括削除が楽になります(後述)。

セキュリティを強化するポリシー(RLS)の設定手順

導入直後に必ず見直したいのがアクセス制御です。ここを曖昧にすると、意図しないアップロードや閲覧が発生しやすくなります。

  • 前提整理(導入)

    アクセス制御は「誰が」「どのバケットの」「何を(read/write)」できるかを切り分けて考えます。

  • チェックポイント(箇条書き)

    • 公開バケットでも「書き込みは誰でもOK」にはならない(別途ポリシーが必要)

    • 認証済みユーザーだけにinsert許可

    • 非公開は署名URLサーバー経由で配布

    • ユーザーIDでの絞り込み(自分のファイルのみ)を用意

  • 補足(解説)

    バケットの「公開/非公開」は読み取り(select)の初期方針に過ぎません。書き込み(insert/update/delete)は別管理です。スパム対策や誤操作防止のため、書き込みポリシーの明示は必須です。

非公開バケットで署名付きURLを使う(限定公開)

  • 導入

    会員限定・決済後限定のコンテンツ配布に有効です。有効期限付きURLを都度発行します。

  • ポイント

    • URLには期限(短め)を設定

    • 都度発行するため、直リンク共有への抑止になる

    • 失効後はアクセスできない

  • サンプル

const { data, error } = await supabase.storage
  .from('user-files') // Privateバケット
  .createSignedUrl('path/to/file.png', 60); // 60秒

if (data?.signedUrl) {
  // <img src={data.signedUrl} />
}

サーバー経由アップロード(Route Handler/Server Actions)

  • 導入

    クライアント直書き込みは簡単ですが、最終バリデーションやPrivate運用を考えると、サーバーで受けてからStorageへが堅実です。

  • 箇条書き

    • Service Role Keyはサーバー専用(公開NG)

    • MIME/サイズ検証をサーバー側で最終確認

    • ユーザーIDの紐づけとメタ情報のDB保存がしやすい

  • 最小サンプル(POST /api/upload)

// app/api/upload/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; // server only
const supabase = createClient(url, serviceKey);

export async function POST(req: Request) {
  const form = await req.formData();
  const file = form.get('file') as File | null;
  if (!file) return NextResponse.json({ error: 'file required' }, { status: 400 });

  if (!file.type.startsWith('image/')) {
    return NextResponse.json({ error: 'invalid type' }, { status: 400 });
  }
  if (file.size > 5 * 1024 * 1024) {
    return NextResponse.json({ error: 'file too large' }, { status: 400 });
  }

  const arrayBuffer = await file.arrayBuffer();
  const key = `uploads/${crypto.randomUUID()}-${file.name}`;

  const { error } = await supabase.storage
    .from('user-files')
    .upload(key, Buffer.from(arrayBuffer), {
      contentType: file.type,
      upsert: false,
    });

  if (error) return NextResponse.json({ error: error.message }, { status: 500 });

  const { data } = await supabase.storage
    .from('user-files')
    .createSignedUrl(key, 300); // 5分

  return NextResponse.json({ url: data?.signedUrl, path: key });
}

ファイル名衝突と上書き対策

  • 導入

    Date.now()は簡単ですが完全ではありません。UUID+ユーザーディレクトリが基本。

  • 箇条書き

    • crypto.randomUUID() などでユニーク化

    • upsert: false で誤上書き防止

    • /userId/yyyy/mm/uuid.ext のような階層化で整理

  • 補足

    ディレクトリ設計は、棚卸し・課金・削除まで見据えると効きます。

MIME/拡張子・サイズの検証(フロント + サーバー)

  • 導入

    二段構えで安全性を担保。偽装MIMEや巨大ファイルを早期ブロック。

  • 箇条書き

    • フロント:accept="image/*" と早期アラート

    • サーバー:file.typefile.size を最終検証

    • 必要に応じてマルウェアスキャン等の外部連携

Next.js Image最適化とリモート許可設定

  • 導入

    next/imageを使うなら、Supabaseのストレージドメインを許可します。

  • サンプル

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: '**.supabase.co', pathname: '/storage/v1/object/**' },
    ],
  },
};
  • 補足

    sizesfill を正しく指定し、不要な巨大画像配信を避けます。

変換・キャッシュ(表示速度のチューニング)

  • 導入

    画像の幅・品質・フォーマットを最適化し、CDNキャッシュで高速化。

  • 箇条書き

    • ?width=…&quality=…&format=webp などの変換パラメータ

    • cache-control を付与して再配信コストを削減

  • 補足

    サムネイル/詳細など用途ごとにサイズを決め打ちすると安定します。

メタデータ保存(誰の・どのファイルか)と削除運用

  • 導入

    filesテーブルを持ち、user_id / bucket / path / size / mime / created_at 等を記録。

  • 箇条書き

    • 一覧表示・権限チェック・削除時に活用

    • 「退会ユーザーの資産を一括削除」などの保守が楽に

  • 最小DDL

create table public.files (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null,
  bucket text not null,
  path text not null,
  size int8,
  mime text,
  created_at timestamptz default now()
);

Next.jsでの具体的な実装例(フロントに画像を表示するまで)

ここは“まず動かす”体験を重視した最短ルートです。そのうえで、上の安全対策を段階的に足していきましょう。

ファイルをアップ→URL取得→その場で表示(公開バケット)

  • 導入

    getPublicUrlで即時に表示できます。まずはここから。

  • コード

'use client';
import { useState } from 'react';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const BUCKET = process.env.NEXT_PUBLIC_SUPABASE_BUCKET!;

export default function UploadImage() {
  const [url, setUrl] = useState<string | null>(null);
  const [msg, setMsg] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);

  async function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    if (!file.type.startsWith('image/')) return setMsg('画像のみアップ可能です。');
    if (file.size > 5 * 1024 * 1024) return setMsg('5MB超はアップできません。');

    setBusy(true); setMsg(null);
    try {
      const key = `public/${crypto.randomUUID()}-${file.name}`;
      const { error } = await supabase.storage
        .from(BUCKET)
        .upload(key, file, { upsert: false, contentType: file.type, cacheControl: '3600' });
      if (error) throw error;

      const { data } = supabase.storage.from(BUCKET).getPublicUrl(key);
      setUrl(data.publicUrl);
      setMsg('アップロード成功');
    } catch (err: any) {
      setMsg(err?.message ?? 'アップロードに失敗しました');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div>
      <input type="file" accept="image/*" disabled={busy} onChange={onChange} />
      {msg && <p style={{ marginTop: 8 }}>{msg}</p>}
      {url && <img src={url} alt="" style={{ maxWidth: '100%', marginTop: 12 }} />}
    </div>
  );
}
  • 補足

    本番では上の「セキュリティ」セクションの対策を必ず追加してください。

補足:非公開バケットを使いたい場合(署名URL)

  • 導入

    会員限定や有料コンテンツなどでは、Privateバケット+署名URLが基本。

  • 箇条書き

    • createSignedUrl(path, seconds)期限付きURLを発行

    • クライアント直発行でも可だが、サーバーでの発行・検証がより安全

    • ダウンロード履歴や制限が必要ならサーバー経由を検討

  • サンプル(最小)

const { data } = await supabase.storage
  .from('user-files')
  .createSignedUrl('userId/2025-09/xxx.png', 120);

まとめ

Supabase Storageは、シンプルに始められて拡張もしやすいファイル管理の仕組みです。この記事で紹介した流れを意識すれば、運用の失敗を防ぎやすくなります。

最初から完璧を目指すより、「まず動かす → 少しずつ安全策を追加 → 実運用に耐えられる設計に整える」というステップを踏むと、より理解が進むと思います。

Contact

制作のご依頼やサービスに関するお問い合わせ、
まだ案件化していないご相談など、
お気軽にお問い合わせください。

お問い合わせはこちら