Bucket Management
A Bucket is the basic container for organizing files in Postgres-Native Cloud Storage. Each Bucket is a row in storage.buckets; object metadata is written to storage.objects and linked back through bucket_id.
If you are familiar with Supabase Storage, think of this as a File Bucket for general-purpose files: images, videos, documents, archives, user uploads, and static assets.
When to split buckets
Split buckets by access model, upload restrictions, and business lifecycle, not just by folder.
| Scenario | Recommended bucket | Typical path | Recommended access model |
|---|---|---|---|
| User avatars | avatars | <uid>/avatar.png | Owner writes; read visibility depends on business needs |
| Public static assets | public-assets | logos/cloudbase.png | Public read, server-side write |
| Private user files | private-files | <uid>/<filename> | Owner read/write |
| Team files | team-files | <team_id>/<filename> | Join business tables for team membership |
| Article / order attachments | article-assets | articles/<article_id>/<filename> | Follow the business object's permissions |
If two file groups have different permissions, size limits, MIME restrictions, or cleanup policies, they usually deserve separate buckets.
Create a bucket
You can create buckets through JS SDK, HTTP API, SQL, or the management SDK. The examples below use JS SDK and SQL.
JS SDK
import cloudbase from '@cloudbase/js-sdk';
const app = cloudbase.init({ env: '<env-id>' });
const { data, error } = await app.storage.createBucket('avatars', {
public: false,
fileSizeLimit: 5 * 1024 * 1024,
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
});
if (error) {
throw error;
}
console.log(data.id);
SQL
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
false,
5 * 1024 * 1024,
ARRAY['image/png', 'image/jpeg', 'image/webp']
);
For more options, see Quick experience: create a bucket and Create Bucket HTTP API.
Fields
| Field | Type | Description |
|---|---|---|
id | text | Unique bucket identifier, used as bucketId in object operations |
name | text | Bucket name, up to 100 characters |
public | boolean | Public flag, default false |
file_size_limit | bigint | Per-file size limit in bytes |
allowed_mime_types | text[] | Allowed MIME-type whitelist |
owner_id | text | Creator user ID, written by the server |
created_at | timestamptz | Creation time |
updated_at | timestamptz | Update time |
Public / Private access model
public is a metadata flag on the bucket. It does not automatically bypass RLS. Whether objects are actually publicly readable is still decided by the SELECT policy on storage.objects.
| Bucket shape | Recommended config | Access method | Use cases |
|---|---|---|---|
| Public assets | public = true + public-read RLS | getPublicUrl() / signed URL / download | Logos, public images, static assets |
| Private assets | public = false + owner RLS | download() / createSignedUrl() | Resumes, order attachments, private files |
| Team assets | public = false + RLS joining business tables | download() / createSignedUrl() | Team drives, project attachments |
| Official assets | public = true + public read, service_role write only | getPublicUrl() | Official icons, campaign banners |
To make buckets with public = true anonymously readable, add an explicit policy:
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
)
);
In production, public read does not mean public write. Control
SELECT,INSERT,UPDATE, andDELETEseparately.
Upload restrictions
Buckets can define two common upload restrictions:
| Restriction | Field | Description |
|---|---|---|
| Per-file size | file_size_limit | Uploads larger than the limit are rejected |
| File type | allowed_mime_types | Only MIME types in the whitelist are accepted |
Example: limit avatar uploads to common image formats within 5 MB:
UPDATE storage.buckets
SET
file_size_limit = 5 * 1024 * 1024,
allowed_mime_types = ARRAY['image/png', 'image/jpeg', 'image/webp']
WHERE id = 'avatars';
For role- or business-dependent limits, combine RLS with metadata. See FAQ: limiting file size / MIME type in RLS.
Update a bucket
const { data, error } = await app.storage.updateBucket('avatars', {
public: false,
fileSizeLimit: 10 * 1024 * 1024,
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
});
if (error) {
throw error;
}
console.log(data.updatedAt);
Or update it directly with SQL:
UPDATE storage.buckets
SET file_size_limit = 10 * 1024 * 1024
WHERE id = 'avatars';
Delete a bucket
Before deleting a bucket, make sure its objects have been cleaned up and no business table still references its paths.
const { error } = await app.storage.deleteBucket('avatars');
if (error) {
throw error;
}
Delete objects through SDK / Storage API. Do not directly
DELETE FROM storage.objects, otherwise file bytes may be orphaned. See FAQ.
AI-friendly design tips
To help AI assistants generate correct buckets, paths, and RLS policies, keep the following contract explicit:
| Item | Example |
|---|---|
| Bucket ID | avatars, private-files, team-files |
| Path template | <uid>/avatar.png, <team_id>/<filename> |
| Permission model | owner_id = auth.uid(), JOIN team_members |
| Upload limits | file_size_limit = 5242880, allowed_mime_types = ['image/png'] |
| Access method | Public assets use getPublicUrl(); private assets use createSignedUrl() |
Recommended contract format:
Bucket: avatars
Purpose: user avatars
Path: <uid>/avatar.png
Read: anyone can read
Write: only the owner can upload or overwrite their avatar
Limits: 5 MB; image/png, image/jpeg, image/webp