npx skills add ...
npx skills add tencentcloudbase/skills --skill cloud-storage-web
Complete guide for CloudBase cloud storage using Web SDK (@cloudbase/js-sdk) - upload, download, temporary URLs, file management, and best practices.
npx skills add tencentcloudbase/skills --skill cloud-storage-web
Sibling CloudBase skills ship beside this skill. Use local relative paths such as ../auth-tool-cloudbase/SKILL.md.
If a referenced sibling skill file is missing from this environment, ask the user to install the full CloudBase plugin (or the missing skill). Do not HTTP-fetch remote skill or protocol markdown into the agent context.
@cloudbase/js-sdk.uploadFile, getTempFileURL, deleteFile, or downloadFile in frontend code.../auth-web-cloudbase/SKILL.md../web-development/SKILL.md../cloudbase-platform/SKILL.mdmanageHosting(action="upload").STORAGE_NOT_EXIST; it means the target storage bucket/resource is not ready, not that the browser upload code should fabricate a URL.host:port before testing app.uploadFile().pgstore like the legacy NoSQL CloudBase storage. PG environments use a separate pgstore backend whose buckets are NOT auto-created from your old NoSQL bucket. If pgstore has no bucket, every upload returns STORAGE_BUCKET_NOT_FOUND and the SDK then issues PUT https://undefined/ (visible in DevTools as net::ERR_NAME_NOT_RESOLVED). Treat bucket existence as a hard prerequisite, just like Supabase: in Supabase Storage every upload must target an already-created bucket; CloudBase PG follows the same model.DescribeEnvs does NOT count as a usable pgstore bucket; create one explicitly before any browser upload. The legacy NoSQL bucket itself is still fine for legacy app.uploadFile() flows that already target it — PG and NoSQL storage coexist; this skill applies to BOTH.When the app runs on a local browser origin and must upload files from the frontend:
queryEnv with action="domains" to inspect the current security-domain whitelist.http://127.0.0.1:4173 -> whitelist entry 127.0.0.1:4173http://localhost:5173 -> whitelist entry localhost:5173envDomainManagement with action="create" and add that host entry before relying on app.uploadFile().host:port.queryEnv(action="domains") rather than blind-sleeping for a fixed long interval.app.uploadFile() flows.If app.uploadFile() returns STORAGE_NOT_EXIST, stop editing frontend code and fix the environment-side storage resource first. Re-check the environment storage list, create or select an available bucket if the task allows it, then retry the same SDK upload flow.
If the task uses browser-side file upload, treat this as a prerequisite rather than an optional cleanup.
Just like Supabase Storage, CloudBase Storage requires the target bucket to exist before any client-side upload. This is true for both legacy CloudBase NoSQL storage (STORAGE_NOT_EXIST) and the newer PG / pgstore backend (STORAGE_BUCKET_NOT_FOUND).
Mental model parity with Supabase:
| Step | Supabase | CloudBase |
|---|---|---|
| Create bucket | supabase.storage.createBucket('covers', { public: true }) (admin-side, with service role) | In PG mode, create a storage.buckets bucket through PG storage HTTP API / CLI / console / SQL on storage.buckets when appropriate. The browser SDK cannot create one. |
| Upload | supabase.storage.from('covers').upload('a.png', file) | PG 模式: app.storage.from('covers').upload('a.png', file) — from(bucketName) 指定 pgstore 存储桶。非 PG 模式: app.storage.from().upload('covers/a.png', file) — bucket 名作为路径第一段。 |
| Bucket missing error | Bucket not found | Browser sees STORAGE_BUCKET_NOT_FOUND (PG) or STORAGE_NOT_EXIST (NoSQL), then a follow-up PUT https://undefined/ because the SDK still tries to PUT a missing metadata.url. |
Required pre-upload steps in any task that needs browser uploads:
6d63-…-1409864723 shape returned by DescribeEnvs.Storages[]) is NOT a valid pgstore bucket — do not assume it works.covers), create one through the PG storage management surface BEFORE editing frontend upload code. Adding covers as a path prefix in code does not auto-create a bucket.app.storage.from('covers').upload('<file>', file) — bucket 名传入 from()app.storage.from().upload('covers/<file>', file) — bucket 名作为路径第一段net::ERR_NAME_NOT_RESOLVED going to https://undefined/ in DevTools, that is the SDK reacting to a missing metadata.url field — almost always because the bucket does not exist or the SDK request was rejected upstream. Inspect the failed POST .../v1/storages/get-objects-upload-info response in DevTools first; the code field (e.g. STORAGE_BUCKET_NOT_FOUND, STORAGE_CONTENT_LENGTH_REQUIRED, INVALID_PARAM) tells you exactly what to fix.Do not silently swallow upload failures. If uploadCoverImage() rejects, the parent createArticle() MUST also reject — never proceed to db.from(...).insert(...) with a fabricated URL or a placeholder, and never let the UI show a success toast.
app.storage.from('bucket'), NOT app.uploadFile()In PG / pgstore environments, use app.storage.from('covers').upload(key, file) for uploads and app.storage.from('covers').createSignedUrl(path, expiresIn) for getting access URLs.
Do NOT use the legacy NoSQL APIs in PG mode:
app.uploadFile() — 这是旧 NoSQL 的上传 APIapp.getTempFileURL() — 这是旧 NoSQL 的获取 URL 方式app.storage.from().upload('covers/file', file) — 没有传 bucket 名Use instead:
app.storage.from('covers').upload('file', file) — PG 模式上传app.storage.from('covers').createSignedUrl('file', 3600) — 获取签名 URL(返回 fullSignedURL 字段)Return shapes differ between modes (v3 SDK) — copy the right column:
| call | 传统模式 (from() 无参, cloud:// fileID) | PG 模式 (from('bucket'), bucket 内对象名) |
|---|---|---|
upload(path, file) | { data: { id, path, fullPath } };upsert 默认 true | { data: { id, ... } };upsert 默认 false |
createSignedUrl(path, expiresIn) | await → { data: { signedUrl } } | await → { data: { fullSignedURL } } |
getPublicUrl(path) | await → { data: { publicUrl } } | 同步调用(不 await) → { data: { publicUrl } } |
Source: webv3/storage.md · webv3-pg/storage.md(raw markdown)。
| Bucket 类型 | URL 策略 | 代码 |
|---|---|---|
公开桶(storage.buckets.public = true) | 直链,无需登录态,可直接进 <img src> | app.storage.from('covers').getPublicUrl('a.png') → { data: { publicUrl } } |
| 私有桶 | 签名 URL,带过期时间 | app.storage.from('covers').createSignedUrl('a.png', 3600) |
storage.objects 的 RLS SELECT 策略是否放行 anon —— 建桶 SQL 与策略模板见 postgresql-development-cloudbase/references/storage-pg.md "Public-read bucket template"。onerror 兜底(直链被策略拦下时降级到签名 URL),不要硬依赖单一取址流程。In PG / pgstore environments, storage access control is enforced through PostgreSQL Row Level Security (RLS) on storage.buckets / storage.objects — exactly like Supabase Storage. These tables are already granted to anon, authenticated, and service_role; RLS is the permission gate. Traditional storage permission labels (READONLY / PRIVATE / CUSTOM) and JSON storage safe rules do not apply. The default RLS policy is deny all, so even if the bucket exists, app.storage.from('covers').upload() from a browser will fail with STORAGE_PERMISSION_DENIED unless you configure policies.
Use managePgDatabase(action="execute", confirm=true) to run the following SQL after creating the bucket:
Key points:
storage.objects RLS is separate from CloudBase legacy NoSQL storage security rules (managePermissions / ModifyStorageSafeRule). In PG mode, always configure storage RLS via PG SQL, not the legacy security rule API.STORAGE_PERMISSION_DENIED when calling app.storage.from('covers').upload() in PG mode.IF NOT EXISTS in a DO $$ block when re-applying to avoid "policy already exists" errors on re-run.Use this skill for browser-side cloud storage operations through the CloudBase Web SDK.
Typical tasks:
Init reference: webv3/initialization.md
Initialization rules:
accessKey. Before writing client code, call queryAppAuth(action="getPublishableKey"); if empty, call manageAppAuth(action="ensurePublishableKey"); then write the key to .env.local as VITE_PUBLISHABLE_KEY (create the file if missing) and read it via import.meta.env.VITE_PUBLISHABLE_KEY. Never hardcode the key into source files. Only fall back to the console (https://tcb.cloud.tencent.com/dev?envId={env}#/env/apikey) if both MCP calls fail.app.uploadFile()app.getTempFileURL()app.deleteFile()app.downloadFile()cloudPath must include the filename./ to create folder structure.from(bucketName) argument is used as the bucket name (e.g. from('covers')), and upload(key, file) takes a key without bucket prefix. The bucket must already exist. Same model as Supabase Storage — never upload into a not-yet-created bucket.queryEnv(action="domains"), which is typically host:port instead of a full http://... URL.STORAGE_NOT_EXIST / STORAGE_BUCKET_NOT_FOUND, use CloudBase management/MCP storage tools to create or choose a bucket before retrying. Do not treat this as a successful optional upload.app.uploadFile() succeeds, do not fabricate a public-looking URL by concatenating envId, bucket domain, or cloudPath. Use the returned fileID with app.getTempFileURL() and store or display the SDK-resolved URL instead.Use temp URLs when the browser needs to preview or download private files without exposing a permanent public link.
Typical upload + preview flow:
Always inspect per-file results before assuming deletion succeeded.
Use this for browser-initiated downloads. For programmatic rendering or preview, prefer getTempFileURL().
To avoid CORS problems, add your frontend domain in CloudBase security domains. In MCP-enabled workflows, prefer checking and updating this through tools before coding browser uploads.
Use the actual browser origin when deciding what to add. If the page is running on a custom domain or a local dev port, add that exact host:port value instead of guessing from a hard-coded list.
Match the real browser origin to the whitelist entry format returned by queryEnv(action="domains"). For local Vite and preview servers, the port can vary between runs, so avoid assuming any fixed default port is sufficient.
Typical examples:
<your-local-host>:<actual-port><your-custom-domain>uploads/, avatars/, documents/.import cloudbase from "@cloudbase/js-sdk";
const app = cloudbase.init({
env: "your-env-id",
accessKey: import.meta.env.VITE_PUBLISHABLE_KEY, // publishable key — auto-provisioned, see below
});const result = await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile
});await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile,
onUploadProgress: ({ loaded, total }) => {
const percent = Math.round((loaded * 100) / total);
console.log(percent);
}
});const result = await app.getTempFileURL({
fileList: [
{
fileID: "cloud://env-id/uploads/avatar.jpg",
maxAge: 3600
}
]
});const uploadResult = await app.uploadFile({
cloudPath: "uploads/avatar.jpg",
filePath: selectedFile
});
const tempUrlResult = await app.getTempFileURL({
fileList: [{ fileID: uploadResult.fileID, maxAge: 3600 }]
});
const previewUrl = tempUrlResult.fileList?.[0]?.tempFileURL || tempUrlResult.fileList?.[0]?.download_url;
if (!previewUrl) {
throw new Error("Failed to resolve temporary file URL after upload");
}await app.deleteFile({
fileList: ["cloud://env-id/uploads/old-avatar.jpg"]
});await app.downloadFile({
fileID: "cloud://env-id/uploads/report.pdf"
});{ "tool": "queryEnv", "action": "domains" }{
"tool": "envDomainManagement",
"action": "create",
"domains": ["<actual-browser-host>:<actual-browser-port>"]
}try {
const result = await app.uploadFile({
cloudPath: "uploads/file.jpg",
filePath: selectedFile
});
console.log(result.fileID);
} catch (error) {
console.error("Storage operation failed:", error);
}