Skip to main content

File Upload in WeChat Mini Program with CloudBase Cloud Storage

In one sentence: After login, use wx.chooseMedia to select images or videos, push them to CloudBase Cloud Storage via app.uploadFile, save the returned fileID to the database, and use app.getTempFileURL at render time to convert it into an https URL 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_mp with a standalone CloudBase environment. This recipe is not for WeChat-native cloud development (wx.cloud) — that has its own wx.cloud.uploadFile with 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

DependencyVersion
@cloudbase/js-sdk2.27.3
@cloudbase/adapter-wx_mp1.3.1
WeChat DevTools1.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
  • chooseMedia added to requiredPrivateInfos in app.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 / uid as 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:

  • getTempFileURL accepts a maximum of 50 fileIDs per call — batch for larger sets
  • For private-read files, tempFileURL expires. 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 getTempFileURL URLs 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 openid segment. Avoid paths without a user identifier like temp/{timestamp}.jpg — the rule cannot match them

For detailed rule syntax, see data-permission and secure-database-multi-tenant-rules.

Verification

  1. Compile in WeChat DevTools; once the login state is ready, navigate to the upload page
  2. Call pickMediauploadAll; the Console should output a set of fileIDs starting with cloud://
  3. In Console → Cloud Storage → Browse, the uploaded files should be visible under users/{openid}/
  4. In Console → Database → orders collection, the corresponding order's images field should be an array of fileIDs
  5. Open the order detail page; images should render correctly. Console output for tempFileURL should all start with https://

Common Errors

Error Code / SymptomCauseFix
STORAGE_REQUEST_FAIL / Upload timeoutWeak network / large fileUse sizeType: ['compressed'] so chooseMedia returns compressed files; increase timeout for videos
INVALID_FILE_NAMEcloudPath starts with /, contains //, or is too longCheck naming — see Step 2 rules
Upload succeeds but not visible in ConsoleWrong bucket selectedEach 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 displayfileID used directly for rendering; Mini Program image tag doesn't accept itCall getTempFileURL first to convert to https
Temporary URL 403 / expiredPrivate-read file URLs expire; using an expired URL failsRe-fetch on page load; do not store URLs in the database — store only fileID
Other users can download my private filesNo custom Security Rules set; default is public-readSee Step 5 — restrict read to resource.openid == auth.openid
chooseMedia errors during App Store reviewrequiredPrivateInfos not declared in app.jsonAdd "requiredPrivateInfos": ["chooseMedia"] and resubmit

For error code definitions, see error-code.

Next Steps