FAQ
How does it differ from classic-mode cloud storage?
| Dimension | Classic cloud storage | Postgres-Native cloud storage |
|---|---|---|
| Bucket concept | Tightly bound to the environment, mainly a path prefix; cannot be configured per bucket | A row in storage.buckets; per-bucket public / file_size_limit / allowed_mime_types; usable in RLS via bucket_id |
| Permission description | Built-in modes (READONLY / PRIVATE / ADMINWRITE / ADMINONLY / CUSTOM) + JSON security rules | RLS policies (SQL) |
| Granularity | Bucket / path prefix / simple roles | Any SQL expression, with cross-table JOINs |
| Upload path | Client SDK gets a temporary signature and uploads directly to COS | Client → Storage API → COS; metadata and object coordinated in one transaction |
owner_id / metadata | Backfilled by the application — risk of forgery and inconsistency | Storage API injects JWT sub on write — cannot be forged; size / mimetype etc. are read directly via SQL |
| Metadata access | Console / SDK | Direct SQL on storage.objects, joinable with business tables |
| Linkage with business data | Synced manually | Same instance, same schema; JOIN directly |
| How it is selected | Pick a permission mode when creating a bucket | Determined automatically by the environment type |
| Triggers / custom functions | Not supported | Supported (PL/pgSQL, PL/V8, ...) |
| Pricing | Same as classic mode | Same as classic mode |
SDK methods (
uploadFile/deleteFile/getTempFileURL, ...) stay identical to classic mode — application code does not need to be aware of the underlying transport change.
What does the public flag on storage.buckets actually mean?
public boolean DEFAULT false is just a metadata flag. It does not let PostgreSQL bypass RLS automatically. Whether the bucket is actually publicly readable depends on the SELECT policy you write on storage.objects.
If you want public = true buckets to be anonymously readable:
CREATE POLICY objects_public_read ON storage.objects
FOR SELECT TO anon, authenticated
USING (
EXISTS (
SELECT 1 FROM storage.buckets b
WHERE b.id = storage.objects.bucket_id AND b.public
)
);
Why does DELETE FROM storage.objects raise an error directly?
storage.objects and storage.buckets have a protect_delete trigger:
ERROR: Direct deletion from storage tables is not allowed.
Use the Storage API instead.
HINT: This prevents accidental data loss from orphaned objects.
SQLSTATE: 42501
Why — a direct DELETE only removes the metadata row in the database; the actual file in the storage backend becomes an orphan. Therefore deletion must go through the SDK / Storage API, which transactionally deletes the object first and then the row.
Correct usage:
// Client
await app.deleteFile({
fileList: [`cloud://<env-id>.<bucket>/<path>`],
});
Escape hatch (for one-off ops only):
SET LOCAL storage.allow_delete_query = 'true';
DELETE FROM storage.objects WHERE bucket_id = 'xxx' AND name = 'yyy';
-- Note: this only deletes the metadata; the file in the storage backend must be cleaned up separately
Why don't I need to GRANT on storage.objects?
Initialization scripts have already run:
GRANT ALL ON storage.objects TO anon, authenticated, service_role;
GRANT ALL ON storage.buckets TO anon, authenticated, service_role;
Therefore the only gate is RLS — different from the "GRANT + RLS" two-layer model used by business tables. Calling GRANT again on storage.objects is harmless but unnecessary.
How is owner_id filled in on upload?
The Storage API injects owner_id based on the sub of the current JWT when writing into storage.objects. Inside the WITH CHECK of an RLS policy you simply write:
WITH CHECK (owner_id = auth.uid())
so the database performs an additional check, preventing forgery.
RLS rejected a request — how do I debug it?
Walk through the checklist:
-
Are you signed in?
anonandauthenticatedare different policy sets — anonymous calls cannot seeauthenticated-only objects. -
Inspect your JWT identity: in the SQL console, run
SELECT auth.role(), auth.uid();(works under the PostgREST path). -
Look at the real data with
service_role(god view):-- In the SQL console (defaults to service_role)SELECT id, bucket_id, name, owner_idFROM storage.objectsWHERE bucket_id = 'xxx' AND name LIKE 'yyy%'; -
Reproduce the client role:
SET LOCAL role = 'authenticated';SET LOCAL request.jwt.claims = '{"sub":"<target uid>","role":"authenticated"}';SELECT * FROM storage.objects WHERE bucket_id = 'xxx'; -
For INSERT failures, use the pre-check function:
SELECT storage.can_insert_object('xxx', '<uid>/test.png', '{"size":1}'::jsonb);-- Throws PT200 → RLS passed; the function rolls back intentionally-- Throws permission error → RLS rejected
Can I limit file size / MIME type from inside RLS?
You can, but the built-in file_size_limit and allowed_mime_types on storage.buckets are usually enough. If you need rules more complex than "one limit per bucket" — e.g. "admins can upload up to 50 MB while regular users top out at 5 MB" — branch on metadata:
CREATE POLICY size_limit_for_user ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'mixed'
AND owner_id = auth.uid()
AND COALESCE((metadata->>'size')::bigint, 0) <= 5 * 1024 * 1024
);
Can I migrate from classic mode / Supabase Storage?
- From classic-mode cloud storage: because the permission models differ, you must rewrite rules as RLS policies; file bytes can be copied via the SDK or batch-migrated in the console.
- From Supabase Storage: the
storageschema and table layout closely match Supabase (same lineage). SQL policies and helper functions (storage.foldername,storage.filename, ...) can be reused largely as-is, but you should reviewpublicsemantics, SDK initialization, response fields, and signed URL behavior.
See Migrate from Supabase Storage for the detailed steps.
How do I do file versioning?
storage.objects has a version column, but automatic version management is not enabled by default. Options:
- Application side: convention paths like
<path>/v<n>/<filename>. - Use
storage.objectstogether with a business table to record version metadata. - For complex needs, combine with the underlying object-storage capability (COS versioning).
How do I use CDN, content moderation, and CI image processing?
CDN, extensions, content moderation, and CI features (image processing, document conversion, smart search) work the same as in classic mode and are shared:
More questions?
- See Permission management and RLS policy patterns.
- Or check Classic cloud storage — FAQ for shared common questions.