RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-13Intermediate

Supabase Storage Returns 403 After Image Upload in Rork Apps ─ Fixing RLS Policies and Bucket Settings

Uploaded an image to Supabase Storage in your Rork app, but the URL returns 403 Forbidden? This guide covers the three most common causes: bucket Public/Private settings, missing RLS SELECT policies, and the getPublicUrl vs createSignedUrl confusion — with working code examples.

Supabase33Storage2RLS3403 errortroubleshooting65image upload

You successfully uploaded the image. The Supabase console shows the file is there. But when you try to display it in your app, you get a blank screen — and the logs show 403 Forbidden.

I've been building apps independently since 2014, with products accumulating over 50 million downloads. One pattern I've seen repeat itself constantly: permission errors almost always come from configuration mismatches, not broken code. The code is usually fine — it's the combination of settings on both the dashboard and code sides that don't align.

With Rork, the AI generates correct Supabase Storage API calls, but the bucket settings and Row Level Security (RLS) policies still need to be configured in the Supabase dashboard. This gap is where most 403 errors originate. Here are the three main patterns and how to fix each one.

The 3 Patterns Behind 403 Errors

When Supabase Storage returns 403, the cause almost always falls into one of these categories:

  • Pattern 1: The bucket is Private, but you're calling getPublicUrl() expecting a public URL
  • Pattern 2: RLS is enabled on storage.objects, but there's no policy granting SELECT access
  • Pattern 3: Confusion between getPublicUrl() and createSignedUrl(), or a timing issue right after upload

Let's walk through each one.

Pattern 1: Bucket Visibility Doesn't Match Your Code

Supabase Storage buckets are either Public or Private. When Rork generates code for image display, it typically uses getPublicUrl():

// Code Rork commonly generates
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/profile.jpg`);
 
// Use data.publicUrl as the Image source

If this returns 403, check whether the avatars bucket is actually set to Public in your dashboard.

How to check:

  1. Go to Supabase Dashboard → Storage → Buckets
  2. Look at the "Public" toggle for your bucket
  3. If it's off, either enable it or switch to createSignedUrl() in your code

Using signed URLs for Private buckets:

// For Private buckets, use createSignedUrl
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl(`${userId}/profile.jpg`, 3600); // Expires in 1 hour
 
if (error) {
  console.error('Failed to get signed URL:', error.message);
  return;
}
 
if (data?.signedUrl) {
  setImageUrl(data.signedUrl);
}

For images that appear frequently (like profile photos), set a longer expiry — 24 hours to 7 days is practical. For sensitive content, keep expiry short and regenerate on demand.

Pattern 2: Missing SELECT Policy in RLS

Even with a Public bucket, if RLS is enabled on storage.objects, any operation not explicitly covered by a policy will be denied. This includes reads.

Where to check:

  1. Supabase Dashboard → Storage → Policies
  2. Look for a policy on storage.objects that covers the SELECT operation

If none exists, add one:

Allow anyone to read files from a public bucket:

CREATE POLICY "Public avatars are viewable by everyone"
ON storage.objects
FOR SELECT
USING (bucket_id = 'avatars');

Allow authenticated users to read only their own files:

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) is a Supabase-specific function that returns the first path segment of the file name. When you upload files as userId/filename.jpg, this gives you a clean ownership check.

Important: INSERT and SELECT policies are configured separately. "I can upload but can't view" almost always means you added an INSERT policy and forgot the SELECT policy.

Pattern 3: Mixing Up download(), getPublicUrl(), and createSignedUrl()

The download() confusion:

// ❌ Wrong: trying to get a URL using download()
const { data, error } = await supabase.storage
  .from('avatars')
  .download(`${userId}/profile.jpg`);
// data is a Blob — not a URL string you can pass to Image
 
// ✅ Correct: use getPublicUrl or createSignedUrl for URL strings
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(`${userId}/profile.jpg`);
// data.publicUrl is a string — pass it directly to your Image component

download() returns binary data (a Blob). To display an image from Supabase Storage, use getPublicUrl() for public buckets or createSignedUrl() for private ones.

Timing issues right after upload:

In rare cases, trying to fetch the URL immediately after upload can cause a brief 403 before the file propagates. Always wait for the upload response before requesting the URL:

const uploadAndGetUrl = async (file: File, path: string): Promise<string> => {
  // Step 1: Upload the file
  const { error: uploadError } = await supabase.storage
    .from('avatars')
    .upload(path, file, { upsert: true });
 
  if (uploadError) {
    throw new Error(`Upload failed: ${uploadError.message}`);
  }
 
  // Step 2: Get the URL only after successful upload
  const { data } = supabase.storage
    .from('avatars')
    .getPublicUrl(path);
 
  return data.publicUrl;
};

Additional Pitfalls When Using Rork AI-Generated Code

Bucket Name Typos Across Generated Files

When Rork generates code across multiple screens, the bucket name can silently differ between the upload screen and the display screen:

// Upload screen
await supabase.storage.from('user-avatars').upload(path, file);
 
// Display screen (bucket name is wrong — root cause of 403)
const { data } = supabase.storage
  .from('avatars') // ❌ Should be 'user-avatars'
  .getPublicUrl(path);

Prevent this by centralizing bucket names as constants:

// constants/storage.ts
export const STORAGE_BUCKETS = {
  AVATARS: 'user-avatars',
  POSTS: 'post-images',
} as const;
 
// Consistent across all files
await supabase.storage.from(STORAGE_BUCKETS.AVATARS).upload(path, file);
const { data } = supabase.storage.from(STORAGE_BUCKETS.AVATARS).getPublicUrl(path);

Leading Slash in File Paths

// ❌ Leading slash causes issues in some versions
const path = `/${userId}/profile.jpg`;
 
// ✅ Always use paths without a leading slash
const path = `${userId}/profile.jpg`;

Supabase Storage paths should consistently omit the leading slash. Behavior can vary across versions, so standardizing this early saves debugging time later.

Checklist Before Moving On

  • [ ] Verified the bucket's Public/Private setting in the Supabase dashboard
  • [ ] Confirmed getPublicUrl or createSignedUrl usage matches the bucket visibility
  • [ ] Added a SELECT policy on storage.objects (or confirmed RLS is disabled)
  • [ ] Upload and display code use identical bucket names and file paths
  • [ ] Opened the URL directly in a browser and confirmed no 403

Going through these in order will surface the cause in most cases.

For the initial Supabase Storage setup in Rork, Rork + Supabase Storage Image Upload Guide walks through the fundamentals. For RLS design patterns more broadly, Supabase Edge Functions and RLS API Design goes deeper into the permission architecture.

Debugging permissions takes patience — it's a matter of isolating which layer is rejecting the request. Supabase lets you test RLS policies directly with SQL in the dashboard, which is useful for confirming your policy logic before revisiting the app code.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-11
That 'CORS Error' in Your Rork + Supabase Edge Function? It's Probably Something Else
Seeing CORS errors when calling Supabase Edge Functions from your Rork app? React Native apps don't enforce CORS — the real cause is usually something else entirely. Here's how to diagnose and fix it.
Dev Tools2026-04-18
Why Supabase Data Isn't Loading in Your Rork App — Diagnosis by Pattern
Systematically diagnose why Supabase data won't load in your Rork app. Covers the most common causes: RLS misconfiguration, wrong API keys, async handling mistakes, and table name typos — with code examples.
Dev Tools2026-07-08
When the Device Runs Out of Space, What Should Your App Protect?
Design Expo/React Native apps that assume writes can fail. Free-space budgets, LRU reclaim, data-protection tiers, and telemetry that surfaces silent failures — drawn from running six wallpaper apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →