RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-13中級

Supabase Storage に画像を保存したのに URL が 403 になる ─ Rork 開発でよく引っかかる RLS とバケット設定の落とし穴

RorkアプリでSupabase Storageに画像をアップロードしても URL が 403 エラーになる問題を解決します。バケットの Public/Private 設定、RLS ポリシーの SELECT 許可、getPublicUrl と createSignedUrl の使い分けという3パターンを具体的なコードで解説します。

Supabase33StorageRLS3403エラートラブルシューティング77画像アップロード2

アップロードは成功しているのに、画像を表示しようとすると真っ白になります。ログを見ると 403 Forbidden。

個人でアプリ開発をしていると、こういう「動くはずなのに動かない」問題に何時間も消えることがあります。2014年からアプリ開発を続けてきた中で気づいたのは、認証や権限まわりのエラーは「コードが間違っている」より「設定の組み合わせが想定と違う」ことで起きる場合の方がずっと多い、ということです。

Rork の AI が生成するコードは Supabase Storage の API を正しく使いますが、Supabase のバケット設定や Row Level Security(RLS)の設定はダッシュボード側で行う必要があります。ここのギャップで引っかかる方が多いため、今回は 403 エラーの主な原因3パターンと、それぞれの解決方法を整理しました。

403 エラーが起きる3つの原因パターン

Supabase Storage で 403 が返ってくる場合、原因はほぼ次の3つに絞られます。

  • パターン1: バケットが非公開(Private)なのに getPublicUrl() で公開 URL を取得しようとしている
  • パターン2: RLS ポリシーが有効だが、SELECT(読み取り)を許可するポリシーを設定していない
  • パターン3: getPublicUrl()createSignedUrl() の使い分けを誤っている、またはアップロード直後に URL の反映が遅れている

順番に確認していきましょう。

パターン1: バケットの Public/Private 設定とコードが合っていない

Supabase Storage のバケットには「Public」と「Private」の2種類があります。Rork でデフォルトのコードを生成すると、多くの場合 getPublicUrl() を使うコードが出力されます。

// Rork が生成するよくあるコード
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/profile.jpg`);
 
// data.publicUrl を Image コンポーネントに渡す

このコードが 403 になる場合、まず Supabase ダッシュボードで avatars バケットが「Public」になっているかを確認してください。

ダッシュボードでの確認手順:

  1. Supabase ダッシュボード → Storage → Buckets を開く
  2. 対象バケットの「Public」スイッチがオンになっているかを確認する
  3. オフになっていれば、バケットを Public に変更するか、createSignedUrl() を使う実装に切り替える

Private バケットで署名付き URL を使う場合:

// Private バケットの場合は createSignedUrl を使う
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl(`${userId}/profile.jpg`, 3600); // 有効期限: 3600秒 = 1時間
 
if (error) {
  console.error('署名付きURL取得エラー:', error.message);
  return;
}
 
// data.signedUrl を Image に渡す(nullチェックを忘れずに)
if (data?.signedUrl) {
  setImageUrl(data.signedUrl);
}

署名付き URL は有効期限があるため、アプリのユースケースに応じて期限を調整してください。ユーザープロフィール画像のように頻繁に表示するものは期限を長め(24時間〜7日間)に設定するのが実用的です。

パターン2: RLS ポリシーに SELECT 許可が設定されていない

バケットが Public であっても、Storage オブジェクトに対する RLS ポリシーが有効になっていると、ポリシーで明示的に許可されていない操作はすべて拒否されます。

確認すべき箇所:

  1. Supabase ダッシュボード → Storage → Policies を開く
  2. storage.objects テーブルに対して SELECT 操作を許可するポリシーが存在するかを確認する

存在しない場合は、以下の SQL でポリシーを追加できます。

全ユーザーに公開バケットの読み取りを許可する例:

-- 公開バケットのファイルを全ユーザーが読み取れる
CREATE POLICY "Public avatars are viewable by everyone"
ON storage.objects
FOR SELECT
USING (bucket_id = 'avatars');

認証済みユーザーのみ自分のファイルを読み取れるようにする例:

-- 認証済みユーザーが自分のフォルダ内のファイルのみ読み取れる
CREATE POLICY "Users can view their own avatar"
ON storage.objects
FOR SELECT
TO authenticated
USING (
  bucket_id = 'avatars' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

storage.foldername(name) はファイルパスの最初のセグメントを返す Supabase 独自の関数です。ユーザー ID をフォルダ名として使うパターン(userId/filename.jpg)では、これで所有者チェックを実装できます。

INSERT ポリシーと SELECT ポリシーは別々に設定が必要です。「アップロードはできるが表示は 403」という状況は、INSERT ポリシーのみ設定して SELECT ポリシーを忘れている場合によく起きます。

パターン3: getPublicUrl と createSignedUrl の混同・アップロード後の反映遅れ

download()getPublicUrl() を混同しているケース:

// ❌ よくある間違い: download() でURL文字列を取得しようとしている
const { data, error } = await supabase.storage
  .from('avatars')
  .download(`${userId}/profile.jpg`);
// data は Blob — Image の source に直接使えない
 
// ✅ 正しい: URL文字列を取得するには getPublicUrl または createSignedUrl を使う
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/profile.jpg`);
// data.publicUrl は文字列 — そのまま Image の source に使える

download() はファイルをバイナリ(Blob)として取得するメソッドです。UI に画像を表示したい場合は、URL を取得する getPublicUrl() または createSignedUrl() を使ってください。

アップロード直後の反映遅れ:

アップロード完了直後に URL を取得して表示しようとすると、稀にストレージの反映が追いつかず 403 になる場合があります。アップロードの成功レスポンスを確認してから URL を取得するコードが望ましいです。

const uploadAndGetUrl = async (file: File, path: string) => {
  // まずアップロード
  const { error: uploadError } = await supabase.storage
    .from('avatars')
    .upload(path, file, { upsert: true });
 
  if (uploadError) {
    throw new Error(`アップロード失敗: ${uploadError.message}`);
  }
 
  // アップロード成功後に URL を取得
  const { data } = supabase.storage
    .from('avatars')
    .getPublicUrl(path);
 
  return data.publicUrl;
};

Rork AI が生成するコードで起きやすい追加の落とし穴

バケット名のスペルミス

AI が画面ごとにコードを生成する際、アップロード時と取得時でバケット名が微妙に違ってしまうことがあります。

// アップロード時
await supabase.storage.from('user-avatars').upload(path, file);
 
// 取得時(バケット名が違う ─ これが 403 の原因)
const { data } = supabase.storage
  .from('avatars') // ❌ 'user-avatars' であるべき
  .getPublicUrl(path);

バケット名を定数として管理すると、このミスを防げます。

// constants/storage.ts として定義しておく
export const STORAGE_BUCKETS = {
  AVATARS: 'user-avatars',
  POSTS: 'post-images',
} as const;
 
// 使用時
await supabase.storage.from(STORAGE_BUCKETS.AVATARS).upload(path, file);
const { data } = supabase.storage.from(STORAGE_BUCKETS.AVATARS).getPublicUrl(path);

パスの先頭スラッシュ

// ❌ 先頭にスラッシュが入っている
const path = `/${userId}/profile.jpg`;
 
// ✅ 先頭スラッシュなし
const path = `${userId}/profile.jpg`;

Supabase Storage のパスは先頭スラッシュなしで統一することを推奨します。バージョンによって挙動が異なる場合があるため、プロジェクト内で統一しておくとデバッグがシンプルになります。

画像アップロード実装後の動作確認チェックリスト

  • [ ] Supabase ダッシュボードでバケットの Public/Private 設定を確認した
  • [ ] コードで使っている getPublicUrl または createSignedUrl がバケット設定と合っている
  • [ ] Storage → Policies で SELECT 操作を許可するポリシーが存在する
  • [ ] アップロードと取得で同じバケット名・パスを使っている(タイポなし)
  • [ ] getPublicUrl() が返す URL をブラウザで直接開いて 403 が出ないことを確認した

この順番で確認すると、ほとんどのケースは原因を特定できます。

Rork での Supabase Storage の基本的なセットアップについては Rork + Supabase Storage 画像アップロード実装ガイド が参考になります。また、RLS の設計パターンについては Supabase Edge Functions と RLS の API 設計 で詳しく解説しています。

権限まわりのデバッグは「どこで何が拒否されているか」を一つずつ切り分けることが大切です。Supabase はダッシュボード上で SQL を直接実行してポリシーのテストもできます。焦らず一つずつ確認していくと、必ず原因にたどり着けます。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-04-18
Rork アプリで Supabase のデータが取得できない・表示されないときの原因別対処法
Rork × Supabase でデータが表示されない原因を体系的に診断する方法を解説。RLS設定ミス・APIキーの誤り・非同期処理の落とし穴など、実際によく起きるパターンをコード例付きで紹介します。
開発ツール2026-05-11
「CORSエラー」と表示されたとき、実は別の問題が隠れている — Rork + Supabase Edge Functions
RorkアプリでSupabase Edge FunctionsをAPIとして使うとCORSエラーが出る問題を解説。React Nativeの仕組み上、実は本当のCORSエラーは起きにくく、別の根本原因が隠れていることがほとんどです。
開発ツール2026-07-15
開発中は黄色い警告だけだったのに、復帰したら落ちた — ルートパラメータに関数を入れていた話
Rork が生成した画面遷移コードが、ルートパラメータに関数と Date を載せていました。開発中は警告で済むのに、アプリを復帰させると params.onFavorite is not a function で落ちます。原因と、ID だけを渡す形への直し方をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →