Supabase Storage 403: Fix 'New Row Violates Row-Level Security Policy' Properly
If a Supabase Storage upload fails with __INLINE_CODE_0__, do not immediately disable Row Level Security or replace the client key with a service-role key.
The error means PostgreSQL rejected an operation against the Storage metadata table under the permissions and RLS context of the current request. The confusing part is that the policy which appears to be missing is not always the obvious one.
Supabase's August 2026 troubleshooting guidance documents an important case: an upload can fail even when the __INLINE_CODE_0__ policy is correct because the Storage API inserts the object metadata and then needs to return that metadata. If the new row is not visible through an appropriate __INLINE_CODE_1__ policy, the operation can fail with the same RLS error.
The fastest diagnosis is therefore:
Identify operation
↓
Verify current auth role/session
↓
Check table grants
↓
Check RLS policy for the operation
↓
Check any required SELECT visibility
↓
Check bucket/path/owner conditions
↓
Retest as the real userThis guide explains that sequence and the common cases that make Supabase RLS errors look more mysterious than they are.
What does "new row violates row-level security policy" actually mean?
Supabase Storage uses PostgreSQL metadata tables under the __INLINE_CODE_0__ schema, including __INLINE_CODE_1__.
When Row Level Security applies, PostgreSQL evaluates the request against:
- the role and privileges used by the request
- the RLS policies attached to the table
- the policy expression for the particular operation
- the values of the row being read or written
A failing write can therefore come from several different conditions:
No table privilege
No matching policy
Wrong role
Expired/missing auth session
Wrong bucket condition
Wrong folder condition
Wrong owner/user ID
Missing SELECT visibility
UPDATE/upsert missing required permissionsThey can all feel like "the upload is broken", but the fixes are different.
Supabase's current Row Level Security documentation also makes an important distinction between grants and policies:
- grants decide whether a role may perform an operation on the table at all
- RLS policies decide which rows that operation may affect
A __INLINE_CODE_0__ permission failure can occur before an RLS policy even gets a chance to allow the row.
So do not debug policies in isolation.
Why can an upload fail when the INSERT policy looks correct?
This is the most useful recent Supabase troubleshooting detail.
Supabase's current Storage 403 troubleshooting guide explains that the Storage API performs an insert and returns the created object metadata.
If the request can insert the row but cannot read the row that has just been created, the request can fail.
Conceptually:
insert into storage.objects (...)
values (...)
returning *;That means this policy can look sufficient:
create policy "Users can upload own files"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);but the request can still fail if the resulting object is not visible to the same user.
A matching read policy may be required:
create policy "Users can read own files"
on storage.objects
for select
to authenticated
using (
bucket_id = 'avatars'
and owner_id = (select auth.uid()::text)
);The important lesson is:
An RLS error reported during INSERT does not prove that the INSERT policy is the only policy involved in the request path.
That is why adding increasingly broad __INLINE_CODE_0__ policies can fail to solve the real problem.
But Supabase says a normal upload only needs INSERT. Which is correct?
Supabase's Storage access-control documentation describes the baseline permissions this way:
- a new upload requires __INLINE_CODE_0__
- an upsert requires __INLINE_CODE_0__, __INLINE_CODE_1__, and __INLINE_CODE_2__
That is still the correct starting model.
However, Supabase's newer troubleshooting note documents a specific failure mode where the Storage API's metadata-return path can require the inserted row to be readable.
The practical interpretation is:
- build the correct operation-specific policies first
- if a normal upload still fails with the documented 403 despite a valid __INLINE_CODE_0__ policy, inspect whether the inserted metadata is visible under __INLINE_CODE_1__
- do not respond by making the entire bucket public or disabling RLS
This is precisely why debugging from the actual request path is safer than memorising a single permissions table.
Step 1: confirm which Storage operation you are actually using
"Upload" can describe several different API operations.
New upload
A new object at a path that does not already exist generally needs an __INLINE_CODE_0__ path.
Example:
await supabase.storage
.from('avatars')
.upload(
'${user.id}/avatar.png',
file,
fileOptions: const FileOptions(
upsert: false,
),
);Upsert
If __INLINE_CODE_0__ is used, Supabase may need to determine whether an existing object is present and update it.
Supabase documents additional __INLINE_CODE_0__ and __INLINE_CODE_1__ requirements for upserts.
So this:
fileOptions: const FileOptions(
upsert: true,
)can require more policy coverage than:
fileOptions: const FileOptions(
upsert: false,
)Replace/update
Replacing an existing object requires an update path, not merely insert permission.
Move/copy/delete
These operations have their own RLS requirements.
Before editing SQL, write down exactly which SDK method and options are used.
A surprising number of "my INSERT policy is correct" bugs are really:
"The client is doing an upsert."
Step 2: verify that the request is actually authenticated
Supabase maps unauthenticated and authenticated requests to different Postgres roles.
A policy such as:
to authenticated
with check (
(select auth.uid()) = user_id
)will never allow a request that has no authenticated user session.
Supabase's RLS documentation states that __INLINE_CODE_0__ returns __INLINE_CODE_1__ when no authenticated user is present.
That can happen when:
- the user has not signed in
- the session expired
- the app created the Storage client before session restoration
- server-side rendering is using a different client/session than expected
- the request lacks the user's access token
- authentication state has not finished initialising
In Flutter, check the actual session before the upload:
final session = supabase.auth.currentSession;
final user = supabase.auth.currentUser;
debugPrint('Session exists: ${session != null}');
debugPrint('User ID: ${user?.id}');Do not log access tokens.
The diagnostic question is simply:
Does the request have the identity the RLS policy expects?If __INLINE_CODE_0__ is null, a perfectly written user-owned policy will deny the row.
Use __INLINE_CODE_0__ rather than making authentication implicit
Supabase recommends naming the role that a policy applies to.
Prefer:
create policy "Authenticated users upload own avatar"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);over a broad policy with no explicit target role.
This makes the intent clearer and prevents the user-ID expression from even being considered for __INLINE_CODE_0__ requests.
Step 3: verify the path your policy thinks it is checking
Storage policies often restrict folders.
For example:
(storage.foldername(name))[1] = (select auth.uid()::text)assumes an object path shaped like:
USER_UUID/avatar.pngIf the actual client sends:
avatars/USER_UUID/avatar.pngthen the first folder is:
avatarsnot the user ID.
The policy rejects the upload even though authentication is correct.
Map the path visually:
bucket: avatars
object name:
7f3...-uuid/avatar.png
foldername(name):
[ "7f3...-uuid" ]versus:
bucket: avatars
object name:
users/7f3...-uuid/avatar.png
foldername(name):
[ "users", "7f3...-uuid" ]The policy index changes.
A path-policy mismatch is often easier to spot by printing the intended object path before upload than by rewriting SQL repeatedly.
Step 4: understand __INLINE_CODE_0__ vs __INLINE_CODE_1__
PostgreSQL applies these clauses to different states.
The PostgreSQL __INLINE_CODE_0__ documentation defines the broad rule:
- __INLINE_CODE_0__ controls which existing rows are visible/targetable
- __INLINE_CODE_0__ controls whether a newly created or resulting row is acceptable
That maps naturally to common operations.
| Operation | Typical policy clause |
|---|---|
| SELECT | __INLINE_CODE_0__ |
| INSERT | __INLINE_CODE_0__ |
| UPDATE | __INLINE_CODE_0__ + __INLINE_CODE_1__ |
| DELETE | __INLINE_CODE_0__ |
For INSERT:
create policy "Create own profile"
on public.profiles
for insert
to authenticated
with check (
(select auth.uid()) = user_id
);For SELECT:
create policy "Read own profile"
on public.profiles
for select
to authenticated
using (
(select auth.uid()) = user_id
);For UPDATE, the existing row must be targetable and the resulting row must remain allowed:
create policy "Update own profile"
on public.profiles
for update
to authenticated
using (
(select auth.uid()) = user_id
)
with check (
(select auth.uid()) = user_id
);Supabase's current RLS guide also states that an __INLINE_CODE_0__ needs a corresponding __INLINE_CODE_1__ policy to work as expected.
Step 5: separate Storage RLS from normal application-table RLS
The Storage system uses __INLINE_CODE_0__ for metadata.
Your own table may use:
public.profiles
public.documents
public.attachmentsThose are different RLS domains.
A common application flow is:
Upload file to Storage
↓
Insert document row into public.documentsTwo policy evaluations occur.
If the app reports only the final caught error, developers can investigate the wrong table.
Log the stage, not secrets, and temporarily wrap the two operations separately during diagnosis.
The phrase "row-level security policy" does not tell you which table failed unless you inspect the full error and request stage.
Step 6: check ownership correctly
Supabase Storage automatically records object ownership from the authenticated user's JWT.
Current Supabase Storage documentation says the relevant column is:
owner_idThe older __INLINE_CODE_0__ field is deprecated.
An owner-based SELECT policy can look like:
create policy "Users can read own objects"
on storage.objects
for select
to authenticated
using (
owner_id = (select auth.uid()::text)
);But object ownership alone does not grant access.
Ownership is data that you can use in an RLS policy; it is not itself an authorization rule.
Step 7: check the bucket condition separately from the folder condition
Suppose this fails:
with check (
bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);Reason through each predicate.
- Does the client use __INLINE_CODE_0__?
- Does the real object path begin with the user's UUID?
- Is the request actually authenticated?
- Does the authenticated user's ID equal the folder ID?
RLS policies are boolean expressions. Debug them as boolean expressions.
Step 8: understand why __INLINE_CODE_0__ matters for UPDATE too
This is not unique to Storage.
Supabase's general RLS documentation says an update operation requires a corresponding SELECT policy.
A matching read policy ensures the row is visible to the updater before the UPDATE rule can act as expected.
This is why policy design works better when you model operations together:
SELECT
INSERT
UPDATE
DELETErather than creating one policy at a time in response to errors.
Step 9: do not "fix" frontend RLS by exposing a service-role or secret key
This is the most dangerous shortcut.
Supabase's current data-security guidance says publishable/anon-style keys are intended for frontend use when RLS is configured properly.
Service-role and secret keys bypass RLS and must remain on trusted servers.
Never solve:
Storage upload denied by RLSwith:
put service-role key in Flutter / React / browserThat does not fix the policy. It removes the security boundary.
Why can a server-side service-role client still appear to hit RLS?
Supabase published current troubleshooting guidance for this too.
A client whose effective __INLINE_CODE_0__ header carries a service-role credential bypasses RLS.
If a supposed service-role client receives RLS behavior, Supabase recommends checking whether the authorization context was replaced by a user session or publishable key.
This is especially relevant with SSR.
Use separate clients for separate trust boundaries:
User/SSR client
→ request user session
→ RLS applies
Admin server client
→ secret/service-role credential
→ trusted server onlyDo not let a single mixed-purpose client blur those boundaries.
Step 10: distinguish a public bucket from unrestricted writes
A public Storage bucket changes public file retrieval. It does not mean arbitrary clients should be able to upload.
A common design is:
public read
+
authenticated write
+
owner-only update/deleteRead visibility and write authority are separate decisions.
Do not create a world-writable __INLINE_CODE_0__ upload policy merely because downloads are public.
A diagnosis matrix
| Symptom | Most likely next check |
|---|---|
| New Storage upload gets 403 | INSERT policy, auth session, bucket/path conditions; then SELECT visibility if INSERT looks correct |
| Upload works with __INLINE_CODE_0__ but fails with __INLINE_CODE_1__ | SELECT and UPDATE policies |
| User can upload but cannot display private file | SELECT policy |
| User can read object but cannot replace it | UPDATE policy plus required SELECT visibility |
| __INLINE_CODE_0__-based policy always rejects | Session missing/expired or wrong client |
| Policy uses folder __INLINE_CODE_0__, path begins with __INLINE_CODE_1__ | Folder index/path mismatch |
| Service-role server client receives RLS error | Effective Authorization may be user-scoped or the wrong key is used |
| Upload succeeds but application-row insert fails | RLS on your application table, not Storage |
| Public bucket downloads work but uploads fail | Expected: public read does not imply public write |
| Update returns no affected rows | Check SELECT visibility and __INLINE_CODE_0__ condition |
| Missing grant returns 42501 | Check table grants before rewriting RLS |
A secure avatar policy example
Suppose the desired behavior is:
Bucket: avatars
Authenticated user:
- uploads only to <their-user-id>/...
- reads their own object through authenticated APIs
- updates only their own object
- deletes only their own objectA starting policy set could look like:
create policy "Avatar insert own folder"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);create policy "Avatar select own objects"
on storage.objects
for select
to authenticated
using (
bucket_id = 'avatars'
and owner_id = (select auth.uid()::text)
);create policy "Avatar update own objects"
on storage.objects
for update
to authenticated
using (
bucket_id = 'avatars'
and owner_id = (select auth.uid()::text)
)
with check (
bucket_id = 'avatars'
and owner_id = (select auth.uid()::text)
);create policy "Avatar delete own objects"
on storage.objects
for delete
to authenticated
using (
bucket_id = 'avatars'
and owner_id = (select auth.uid()::text)
);Treat this as a pattern, not paste-ready authorization for every product.
Your application may require public reads, organisation membership, shared folders, admin access, branch-level access, document approval state, or different upload/read permissions.
Multi-tenant Storage should validate membership, not only folder strings
For a multi-tenant SaaS, a path such as:
organisation-id/user-id/document.pdfis useful for organisation.
But a tenant ID in a path is not proof that the current user belongs to that tenant.
The policy should validate membership using your membership table, for example with an __INLINE_CODE_0__ check tied to __INLINE_CODE_1__.
The key principle is:
Object paths organise data; membership rules authorise access.
Do not rely on an untrusted client to choose a correct tenant folder and then treat the folder itself as proof of access.
Do not modify __INLINE_CODE_0__ metadata directly
Supabase's Storage schema documentation says the Storage metadata tables should be treated as read-only from application SQL.
Uploads, moves, copies and deletes should go through the Storage API because the binary object lives in the underlying object-storage provider while PostgreSQL stores metadata.
Directly manipulating __INLINE_CODE_0__ can leave metadata and stored files inconsistent.
Use RLS to govern the metadata access path, but use the Storage API to perform file operations.
How to test RLS without weakening it
Supabase's RLS documentation recommends writing policy tests.
This matters because denials do not all fail the same way:
- a rejected __INLINE_CODE_0__ write can raise __INLINE_CODE_1__
- a __INLINE_CODE_0__ policy can simply filter out a row so an update affects zero rows
- an overly permissive SELECT can silently return data it should not
A useful policy test matrix is:
owner → allowed
different authenticated user → denied
anonymous user → allowed/denied according to product rule
trusted server path → tested separatelySupabase supports pgTAP-based database tests through its local development tooling.
Security should be reproducible, not dependent on "I clicked through it once and it worked."
RLS performance also matters once the policy is correct
Authorization logic runs inside database queries.
Supabase's current RLS guide recommends indexing columns used in policy filters and commonly wraps stable auth helper calls such as __INLINE_CODE_0__ in a __INLINE_CODE_1__ expression so PostgreSQL can optimise them per statement where appropriate.
If your policies frequently filter on:
user_idan index may be appropriate:
create index documents_user_id_idx
on public.documents (user_id);A correct policy that forces sequential scans over a large table can become a production performance problem.
A practical debugging checklist
When you see:
403 Forbidden
new row violates row-level security policywork through this order.
Request
- Which SDK method is being called?
- Is it upload, upsert, update, move, copy or delete?
- Which bucket?
- What exact object path?
- Is the app authenticated at that moment?
Identity
- Does __INLINE_CODE_0__ exist?
- Does the request carry the expected JWT?
- Does __INLINE_CODE_0__ correspond to the user ID expected by the policy?
- Is an SSR/server client accidentally using another authorization context?
Database security
- Does the role have the required grant?
- Is RLS enabled?
- Is there a policy for the actual operation?
- Does INSERT use __INLINE_CODE_0__ appropriately?
- Does SELECT use __INLINE_CODE_0__?
- Does UPDATE have row visibility and resulting-row checks?
- If a Storage INSERT still fails, can the created metadata row be returned under SELECT?
Storage-specific conditions
- Does the bucket ID match exactly?
- Do folder indexes match the real object path?
- Are you using __INLINE_CODE_0__ rather than deprecated ownership assumptions?
- Do upserts have SELECT and UPDATE coverage?
- Have public reads been confused with public writes?
Security hygiene
- No service-role/secret key exposed to a client?
- No temporary __INLINE_CODE_0__ policy left in production?
- No direct SQL manipulation of Storage metadata?
- Negative policy tests exist, not only success tests?
FAQs
Why does Supabase Storage say "new row violates row-level security policy"?
PostgreSQL rejected the Storage metadata operation under the request's current privileges and RLS policies. Common causes include a missing or failing INSERT policy, wrong authentication state, incorrect bucket/path conditions, and, in Supabase's documented Storage 403 case, missing SELECT visibility for the newly created metadata row.
Why do I need a SELECT policy when I am uploading a file?
A new upload is primarily an INSERT operation. However, Supabase's August 2026 Storage troubleshooting guidance documents that the API can insert metadata and then return the created row; if RLS prevents that row from being selected, the request can fail. Upserts also explicitly require SELECT and UPDATE permissions in addition to INSERT.
What is the difference between __INLINE_CODE_0__ and __INLINE_CODE_1__ in Supabase RLS?
__INLINE_CODE_0__ determines which existing rows the user may see or target. __INLINE_CODE_1__ validates rows being created or the resulting row after an update. INSERT policies typically use __INLINE_CODE_2__, SELECT/DELETE policies use __INLINE_CODE_3__, and UPDATE commonly uses both.
Why does __INLINE_CODE_0__ return null?
Supabase documents that __INLINE_CODE_0__ returns __INLINE_CODE_1__ when the request has no authenticated user, such as when no access token is provided or the session has expired. A user-owned policy therefore denies the request until a valid session is present.
Can I fix RLS errors with the Supabase service-role key?
A trusted backend can use a secret/service-role credential when it genuinely needs administrative access, but that is not a frontend RLS fix. Supabase secret/service-role credentials bypass RLS and must never be exposed in Flutter, browser or other untrusted client code.
Why does my service-role client still get RLS errors?
Supabase says RLS enforcement follows the effective __INLINE_CODE_0__ header. In SSR or mixed-client designs, a user session can replace the expected service-role authorization context. Use a separate trusted server client for administrative operations and keep user-scoped clients separate.
Conclusion
The Supabase Storage __INLINE_CODE_0__ error is easiest to solve when you stop treating RLS as one magic gate.
The real request passes through several layers:
Client operation
↓
Authentication / JWT
↓
Postgres role + grants
↓
Operation-specific RLS
↓
Bucket / folder / ownership conditions
↓
Any metadata read-back
↓
Storage operation completesThat sequence explains why an INSERT policy can appear correct while an upload still fails, why an upsert needs more access than a new upload, and why a missing session can make a reasonable __INLINE_CODE_0__ policy reject every row.
Fix the narrow failing layer.
Do not disable RLS, expose a service-role key, or make an entire Storage bucket writable just to get one upload working.
A good RLS policy should do two things at the same time:
allow the exact workflow your product needs and reject the same operation from every identity that should not have it.
If you are building a Supabase-backed web or mobile product and need help designing secure multi-tenant data, Storage policies or backend workflows, Softotic's custom software development service can handle the architecture, while mobile app development and web application development cover the client integration.