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_roleand audit logs. - When deleting attachments referenced by business tables, prefer a compensating flow that keeps object and business data consistent.
RLS mapping
| Operation | Required DML | Notes |
|---|---|---|
list() | SELECT | List folders and objects |
info() / exists() | SELECT | Read metadata or existence |
copy() | SELECT + INSERT / UPDATE | Read source, write destination |
move() | SELECT + INSERT / UPDATE + DELETE | Copy then delete source |
remove() | DELETE | Delete 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