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?​