Skip to main content

FAQ

How does it differ from classic-mode cloud storage?

DimensionClassic cloud storagePostgres-Native cloud storage
Bucket conceptTightly bound to the environment, mainly a path prefix; cannot be configured per bucketA row in storage.buckets; per-bucket public / file_size_limit / allowed_mime_types; usable in RLS via bucket_id
Permission descriptionBuilt-in modes (READONLY / PRIVATE / ADMINWRITE / ADMINONLY / CUSTOM) + JSON security rulesRLS policies (SQL)
GranularityBucket / path prefix / simple rolesAny SQL expression, with cross-table JOINs
Upload pathClient SDK gets a temporary signature and uploads directly to COSClient → Storage API → COS; metadata and object coordinated in one transaction
owner_id / metadataBackfilled by the application — risk of forgery and inconsistencyStorage API injects JWT sub on write — cannot be forged; size / mimetype etc. are read directly via SQL
Metadata accessConsole / SDKDirect SQL on storage.objects, joinable with business tables
Linkage with business dataSynced manuallySame instance, same schema; JOIN directly
How it is selectedPick a permission mode when creating a bucketDetermined automatically by the environment type
Triggers / custom functionsNot supportedSupported (PL/pgSQL, PL/V8, ...)
PricingSame as classic modeSame 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.

See Postgres-Native overview.

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:

  1. Are you signed in? anon and authenticated are different policy sets — anonymous calls cannot see authenticated-only objects.

  2. Inspect your JWT identity: in the SQL console, run SELECT auth.role(), auth.uid(); (works under the PostgREST path).

  3. Look at the real data with service_role (god view):

    -- In the SQL console (defaults to service_role)
    SELECT id, bucket_id, name, owner_id
    FROM storage.objects
    WHERE bucket_id = 'xxx' AND name LIKE 'yyy%';
  4. 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';
  5. 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 storage schema 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 review public semantics, 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.objects together 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?