Skip to main content

Manage Files

This guide covers listing, inspecting, checking existence, copying, moving, and deleting files in Postgres-Native Cloud Storage.

Initialize

import cloudbase from '@cloudbase/js-sdk';

const app = cloudbase.init({ env: '<env-id>' });
const bucket = app.storage.from('user-files');

All path parameters are object names inside the bucket. Do not pass cloud:// fileIDs.

List files

Use list() to list objects by prefix with cursor pagination:

const { data, error } = await bucket.list('user-123', {
limit: 20,
withDelimiter: true,
sortBy: {
column: 'created_at',
order: 'desc',
},
});

if (error) {
throw error;
}

console.log(data.folders);
console.log(data.objects);

if (data.hasNext) {
const nextPage = await bucket.list('user-123', {
cursor: data.nextCursor,
});
console.log(nextPage.data?.objects);
}

list() requires a SELECT policy on storage.objects. For private user folders, restrict the first path segment to the current user ID.

CREATE POLICY user_files_select ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'user-files'
AND (storage.foldername(name))[1] = auth.uid()
);

Get file information

const { data, error } = await bucket.info('user-123/avatar.png');

if (error) {
throw error;
}

console.log(data.bucketId);
console.log(data.contentType);
console.log(data.metadata);

You can also query metadata directly in SQL:

SELECT id, bucket_id, name, owner_id, metadata, user_metadata, created_at
FROM storage.objects
WHERE bucket_id = 'user-files'
AND name = 'user-123/avatar.png';

Check existence

const { data: exists, error } = await bucket.exists('user-123/avatar.png');

if (error) {
throw error;
}

console.log(exists);

exists() is also gated by SELECT. Objects the user cannot read should not reveal existence.

Copy files

const { data, error } = await bucket.copy(
'user-123/avatar.png',
'user-123/avatar-copy.png',
{
upsert: true,
copyMetadata: true,
}
);

if (error) {
throw error;
}

console.log(data.path);

For cross-bucket copy, the user needs read permission on the source object and write permission on the destination path.

await bucket.copy('user-123/avatar.png', 'archive/user-123/avatar.png', {
destinationBucket: 'archive-files',
copyMetadata: true,
});

Move files

const { error } = await bucket.move(
'user-123/avatar.png',
'user-123/archive/avatar.png'
);

if (error) {
throw error;
}

A move is conceptually "copy to destination → delete source". Cross-bucket moves require source SELECT / DELETE and destination INSERT / UPDATE permissions.

Delete files

const { data, error } = await bucket.remove([
'user-123/avatar.png',
'user-123/avatar-old.png',
]);

if (error) {
throw error;
}

console.log(data);

Deletion must go through the SDK or Storage API. Do not directly DELETE FROM storage.objects:

-- Not recommended; blocked by the protect_delete trigger
DELETE FROM storage.objects
WHERE bucket_id = 'user-files'
AND name = 'user-123/avatar.png';

Direct metadata deletion can orphan file bytes in the storage backend. See FAQ.

Batch operation recommendations

  • Use cursor pagination for listing; do not assume one request returns everything.
  • Check prefixes and policies carefully before batch deletion.
  • Run large cleanup jobs from trusted backend code with service_role and audit logs.
  • When deleting attachments referenced by business tables, prefer a compensating flow that keeps object and business data consistent.

RLS mapping

OperationRequired DMLNotes
list()SELECTList folders and objects
info() / exists()SELECTRead metadata or existence
copy()SELECT + INSERT / UPDATERead source, write destination
move()SELECT + INSERT / UPDATE + DELETECopy then delete source
remove()DELETEDelete objects

To distinguish "download" from "list directory", use storage.operation() helpers. See Permission management.

AI-friendly prompt

Bucket: team-files
Path template: <team_id>/<filename>
List: team members can list their team folder
Delete: only team admin can delete
Move: must stay under the same team_id folder
Batch size: process at most 100 objects per run