File Upload in WeChat Mini Program with CloudBase Cloud Storage
In one sentence: After login, use
wx.chooseMediato select images or videos, push them to CloudBase Cloud Storage viaapp.uploadFile, save the returnedfileIDto the database, and useapp.getTempFileURLat render time to convert it into anhttpsURL that<image>can render directly — end-to-end.Estimated time: 30 minutes | Difficulty: Advanced
Applicable Scenarios
- Applicable: Mini Programs that have completed add-auth-wechat-miniprogram and need to implement avatar / product image / order proof / short video upload
- Applicable: Using
@cloudbase/js-sdk+@cloudbase/adapter-wx_mpwith a standalone CloudBase environment. This recipe is not for WeChat-native cloud development (wx.cloud) — that has its ownwx.cloud.uploadFilewith a different credential model - Not applicable: Large file multipart uploads (videos over 100MB) — compress first or upload in parts to COS and write back the fileID
- Not applicable: Scenarios requiring the frontend to directly obtain a permanent URL (permanent URLs only work for publicly readable files). Private-read files must exchange a Temporary URL on every access
Prerequisites
| Dependency | Version |
|---|---|
@cloudbase/js-sdk | 2.27.3 |
@cloudbase/adapter-wx_mp | 1.3.1 |
| WeChat DevTools | ≥ 1.06.x |
Also required:
- add-auth-wechat-miniprogram completed, with
auth.hasLoginState()returning true - Cloud Storage enabled in Console — there will be one empty bucket by default
chooseMediaadded torequiredPrivateInfosinapp.json(required); otherwise selecting media in a review-submitted version will fail
Step 1: Select media
wx.chooseMedia is the WeChat-recommended unified media selection API, replacing the older chooseImage / chooseVideo.
// pages/upload/upload.js
async function pickMedia() {
const res = await new Promise((resolve, reject) => {
wx.chooseMedia({
count: 9, // up to 9 files
mediaType: ['image'], // images only; change to ['video'] or ['mix'] for video
sourceType: ['album', 'camera'], // album + camera
sizeType: ['compressed'], // compressed to save bandwidth
success: resolve,
fail: reject,
});
});
return res.tempFiles;
}
Each returned tempFiles[i] looks like:
{
tempFilePath: 'http://tmp/...', // Mini Program local temporary path
size: 12345, // bytes
fileType: 'image',
width: 1080,
height: 1920,
}
Step 2: Upload to Cloud Storage
Call app.uploadFile once for each temporary file. The app here is the CloudBase app initialized in Step 4 of add-auth-wechat-miniprogram.
import app from '../../libs/cloudbase';
import { ensureLogin } from '../../libs/login';
async function uploadOne(tempFilePath, openid) {
const ext = tempFilePath.split('.').pop() || 'jpg';
// Use "openid path + timestamp + random string" for cloudPath to prevent collisions and enable permission isolation later
const cloudPath = `users/${openid}/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
const res = await app.uploadFile({
cloudPath,
filePath: tempFilePath, // pass tempFilePath directly in Mini Programs
});
return res.fileID; // 'cloud://your-env.bucket/users/openid/..../xxx.jpg'
}
export async function uploadAll(tempFiles) {
const user = await ensureLogin();
const openid = user.customUserId; // or user.uid
// Serial upload for easier error handling. Switch to Promise.all for parallel,
// but be aware of the Mini Program's concurrent request limit
const fileIDs = [];
for (const f of tempFiles) {
const id = await uploadOne(f.tempFilePath, openid);
fileIDs.push(id);
}
return fileIDs;
}
Common cloudPath naming pitfalls:
- Do not start with
/— CloudBase file naming rules prohibit a leading slash - Do not include
//— double slashes will be rejected - Chinese filenames can be stored, but they will be URL-encoded on access, making debugging less intuitive. Stick to English and numbers
- Use
openid/uidas the directory prefix so Security Rules can enforce "users can only access files under their own directory"
Step 3: Store the fileID in the database
After a successful upload, the fileID is typically written to the relevant business collection:
import { db } from '../../libs/cloudbase';
async function saveOrderImages(orderId, fileIDs) {
await db.collection('orders').doc(orderId).update({
images: fileIDs, // store as an array
updatedAt: db.serverDate(),
});
}
Why store fileID instead of URL:
- URLs can expire (Temporary URLs for private-read files have an expiration); fileID is permanent
- When switching between public-read / private-read permissions, the fileID stays the same — no code changes needed
- fileIDs are easier to update when migrating between environments (test / production)
Step 4: Convert to Temporary URL at render time
What you read back from the database is a set of fileIDs. Putting them directly in <image src> does not work — <image> only accepts https/http, not cloud://. Call getTempFileURL first to convert them.
import app from '../../libs/cloudbase';
export async function resolveImages(fileIDs) {
if (!fileIDs?.length) return [];
// Maximum 50 per call
const res = await app.getTempFileURL({
fileList: fileIDs,
});
// res.fileList[i] looks like:
// { fileID: 'cloud://...', tempFileURL: 'https://...', maxAge: 7200 }
// For public-read files, tempFileURL does not expire
return res.fileList.map((f) => f.tempFileURL);
}
Page side:
Page({
data: { imageUrls: [] },
async onLoad({ orderId }) {
const order = await db.collection('orders').doc(orderId).get();
const urls = await resolveImages(order.data[0].images);
this.setData({ imageUrls: urls });
},
});
WXML:
<image
wx:for="{{imageUrls}}"
wx:key="index"
src="{{item}}"
mode="aspectFill"
/>
Notes:
getTempFileURLaccepts a maximum of 50 fileIDs per call — batch for larger sets- For private-read files,
tempFileURLexpires. The expiration period can be configured in Console or Security Rules. Do not cache on the frontend for extended periods (a URL used after expiration will return 403). The simple approach is to re-fetch on every page load - URLs returned for public-read files generally do not expire and can be cached on the frontend
Step 5: Restrict permissions with Security Rules
Under the default permission mode, authenticated users can upload to their own directory, but others can also read / download (default is public-read). For sensitive content (proofs, conversation images, etc.), tighten the permissions.
In Console → Cloud Storage → Permission Settings → Custom Security Rules, paste something like:
{
"read": "auth != null && resource.openid == auth.openid",
"write": "auth != null && resource.openid == auth.openid"
}
This means: must be logged in, and the openid segment in the file path (users/{openid}/...) must match the current user's openid in order to read or write.
After switching to Custom Security Rules:
- The expiration on
getTempFileURLURLs for private files takes effect — links expire after the configured duration - Sharing the URL with others will not work (no identity attached)
- Make sure cloudPath always includes the
openidsegment. Avoid paths without a user identifier liketemp/{timestamp}.jpg— the rule cannot match them
For detailed rule syntax, see data-permission and secure-database-multi-tenant-rules.
Verification
- Compile in WeChat DevTools; once the login state is ready, navigate to the upload page
- Call
pickMedia→uploadAll; the Console should output a set of fileIDs starting withcloud:// - In Console → Cloud Storage → Browse, the uploaded files should be visible under
users/{openid}/ - In Console → Database → orders collection, the corresponding order's
imagesfield should be an array of fileIDs - Open the order detail page; images should render correctly. Console output for
tempFileURLshould all start withhttps://
Common Errors
| Error Code / Symptom | Cause | Fix |
|---|---|---|
STORAGE_REQUEST_FAIL / Upload timeout | Weak network / large file | Use sizeType: ['compressed'] so chooseMedia returns compressed files; increase timeout for videos |
INVALID_FILE_NAME | cloudPath starts with /, contains //, or is too long | Check naming — see Step 2 rules |
| Upload succeeds but not visible in Console | Wrong bucket selected | Each environment has one default bucket; confirm the Console's top-left environment ID matches the one the SDK is using |
<image src="cloud://..."> does not display | fileID used directly for rendering; Mini Program image tag doesn't accept it | Call getTempFileURL first to convert to https |
| Temporary URL 403 / expired | Private-read file URLs expire; using an expired URL fails | Re-fetch on page load; do not store URLs in the database — store only fileID |
| Other users can download my private files | No custom Security Rules set; default is public-read | See Step 5 — restrict read to resource.openid == auth.openid |
chooseMedia errors during App Store review | requiredPrivateInfos not declared in app.json | Add "requiredPrivateInfos": ["chooseMedia"] and resubmit |
For error code definitions, see error-code.
Related Documentation
- SDK File Management — Full API for
uploadFile / downloadFile / getTempFileURL / deleteFile - Cloud Storage Data Permissions — Public-read / private-read switching
- Cloud Storage Security Rules — Custom rule fields and syntax
- add-auth-wechat-miniprogram — Prerequisite: authentication integration
- add-database-wechat-miniprogram — Writing fileID to the database
Next Steps
- Multi-tenant file isolation rules: secure-database-multi-tenant-rules
- Run image moderation / background removal after upload: Console "Cloud Infinity" — not covered in this recipe
- Send a subscription message notification after a successful upload: add-subscribe-message-cloud-function