Why your Base44 image upload is not working
Base44 image uploads in a live app go through the UploadFile integration, which accepts PNG, JPG, JPEG, WEBP and SVG images up to 50MB each and videos up to 100MB. When an upload does nothing, the cause is one of five: the file is over the limit or in an unlisted format such as HEIC; the code never awaits the upload or never reads the file_url it returns; the record update that saves the URL is denied by the table's access rules; a private file's signed URL has expired; or the workspace has run out of integration credits. The fixes are: validate size and type client-side and convert HEIC to JPEG, await the upload and read file_url, save the URL to the record in the same handler, test the table's rules with Test Permissions in preview, store private files as a file_uri and sign them on demand, and check credit usage. The request in the Activity Monitor or the browser's Network tab, plus a log of the upload result, shows which one is biting in a couple of minutes.
You wire up an image upload in your Base44 app. The agent scaffolds an upload component, a file picker, an upload handler. In testing, the file picker opens, you select an image, the button shows a loading spinner, and then... nothing. No image. No error. The form returns to its default state as if you never tried. Or worse: the upload says it succeeded, but the image never appears anywhere the next time you load the page.
This bug has five distinct root causes that all manifest as the same blank UI. The fix depends on which root cause is biting, and you cannot guess — you have to look at the request and at what the upload returned.
The five causes:
- File over the limit or in an unlisted format. Live-app image uploads take PNG, JPG, JPEG, WEBP and SVG up to 50MB. HEIC, the iPhone default, is not on the list, and agent-generated handlers rarely surface the failure.
- The upload is not awaited, or its
file_urlis never read or saved.UploadFilereturns{ file_url }. Code that does not await the call, reads a field that does not exist, or never writes the URL to the record leaves an empty image. - The record update is denied. Saving the URL is an ordinary entity update, and the table's access rules decide who may make it.
- A private file's signed URL expired. Files uploaded with
UploadPrivateFileare shown through signed URLs that expire, by default after 300 seconds. - The workspace is out of integration credits. Each upload uses about 1 integration credit, and actions that use credits pause when the workspace reaches its limit.
Each of these needs a different fix. The diagnostic phase — reproduce with the request visible, log the upload result, check the record — takes a couple of minutes and tells you exactly which fix to apply.
What causes Base44 image upload failures
Base44 uploads are a Core integration, not a storage bucket you configure. base44.integrations.Core.UploadFile({ file }) uploads to public storage and returns { file_url }, and anyone with that URL can open the file. UploadPrivateFile stores a file privately and returns a file_uri, which CreateFileSignedUrl turns into a temporary signed_url. Base44's media guide sets the per-file limits and says there is no hard storage limit per app (read 17 Oct 2026). The agent generates upload code that handles the happy path — file picker, upload call, optimistic UI update — but typically does not handle the failure modes that show up in production.
File limits and formats. For files people upload through your live app, the limits are 50MB each for images (PNG, JPG, JPEG, WEBP, SVG), documents (PDF, TXT, HTML), data files (CSV, XLS, XLSX, JSON) and audio (MP3, WAV), and 100MB per video. HEIC, HEIF, TIFF and GIF are not on the live-app image list. iPhone photos default to HEIC, and the browser File API reports the type correctly, so validate file.type before you upload. Files you add to the editor's file library while building have separate limits: images up to 40MB (SVG 5MB), resized automatically above 1024x1024 pixels.
A dropped file_url. The SDK returns one field, file_url. Base44's own SDK guide for AI coding agents lists uploadFile(file) and storage.upload(file) as hallucinated calls and integrations.Core.UploadFile({file}) as the correct one, which tells you how often generated code gets this wrong. Code that calls a made-up helper, forgets to await the real one, or reads result.url instead of result.file_url treats the upload as done and renders an empty image tag.
Missing database write. The agent often scaffolds the upload and forgets the save. The file lands in storage, the URL comes back, the handler returns successfully, but the record's image_url field is never updated. The next render reads the record, finds no URL, and shows the placeholder.
Denied record update. The upload is two steps: put the file in storage, then write its URL to your entity record. The second step is an ordinary entity update, so the table's access rules apply. Base44 sets up data permissions automatically as you build, and its docs warn that "If permissions are not set up correctly, the wrong people could view or change data in your app." A rule such as Creator Only on Update lets a person change only records they created, so an upload that saves to someone else's record fails at the save, not at the upload.
Expired signed URLs. If the app stores private files, the record should hold the file_uri, and the page should call CreateFileSignedUrl when it shows the image. A signed URL expires after expires_in seconds, 300 by default. Save the signed URL itself and the image works for a few minutes, then breaks.
Integration credits. Base44's credits page puts UploadFile at about 1 integration credit per upload. When a workspace reaches its credit limit, actions that use credits pause until the next reset, so every upload in the app can start failing at once.
Each of these has a distinct signature:
| Symptom | What to look at | Root cause |
|---|---|---|
| Error before any request fires | file.size and file.type in the console | File over the limit or format not listed |
| Upload request fails | The request's status code and response body | Request error, or integration credits used up |
| Upload succeeds, no image | The logged upload result | file_url not awaited or not read |
| URL returned, record field empty | The record after a reload | Missing or denied entity update |
| Works for you, fails for users | Test Permissions in preview | Update rule denies that user |
| Image loads, then breaks later | Whether the record holds a signed_url | Expired signed URL |
How to confirm Base44 image upload is broken (reproduction)
- Open your app's preview in the editor and open the Activity Monitor, which shows every request your app makes in preview, with status codes, timing, and request and response details. On the published app, open the browser DevTools Network tab and filter to "Fetch/XHR" instead.
- Attempt the upload that is failing, with the image file you would normally use.
- If no upload request fires at all, the handler stopped before the call: check
file.size,file.type, and the console for an exception. - If the request fires, note its status code and read the response body.
- Log the result of the upload call. If it has a
file_url, open that URL in a new tab. If the image loads, the upload worked. - Reload the page and look at the record that should hold the URL. If its field is empty, the entity update is missing or was denied; look in the console for a failed update call.
- If it works for you and fails for a real user, open the table's Permissions page and click Test Permissions in preview to act as that user, or pick Act as a user from the editor's More actions menu. Changes you make while acting as someone are saved to that person's data, so test with sample people where you can.
How to fix Base44 image upload not working
Fix 1: Add client-side size and type validation
Before the upload call fires, validate. Convert failures the user never sees into actionable UI errors.
// Live-app image limits from Base44's media guide.
// A decimal 50,000,000 bytes stays under the 50MB cap however it is counted.
const MAX_IMAGE_BYTES = 50_000_000;
const IMAGE_TYPES = new Set([
"image/png",
"image/jpeg", // JPG and JPEG
"image/webp",
"image/svg+xml",
]);
function validateImageFile(file: File): string | null {
if (file.size > MAX_IMAGE_BYTES) {
return `File is ${(file.size / 1_000_000).toFixed(1)}MB. The limit is 50MB.`;
}
if (!IMAGE_TYPES.has(file.type)) {
return `${file.type || "This file type"} is not supported. Use PNG, JPG, WEBP or SVG.`;
}
return null;
}
Base44 lists the cap as 50MB without saying how it counts a megabyte, so validating at 50,000,000 bytes keeps you safely under it either way. For videos, use the 100MB per-video limit instead.
Fix 2: Convert and compress before upload
For HEIC support, add heic2any and convert before anything else. Then route every image through browser-image-compression. Phone photos are well under the 50MB cap, so compression here is about upload time and page weight, not the limit.
import heic2any from "heic2any";
import imageCompression from "browser-image-compression";
async function normalizeFile(file: File): Promise<File> {
if (file.type === "image/heic" || file.type === "image/heif") {
const blob = await heic2any({ blob: file, toType: "image/jpeg", quality: 0.85 });
const converted = Array.isArray(blob) ? blob[0] : blob;
return new File([converted], file.name.replace(/\.hei[cf]$/i, ".jpg"), {
type: "image/jpeg",
});
}
return file;
}
async function compressForUpload(file: File): Promise<File> {
if (file.type === "image/svg+xml") return file; // vector files need no compression
return imageCompression(file, {
maxSizeMB: 2,
maxWidthOrHeight: 1920,
useWebWorker: true,
fileType: "image/jpeg",
});
}
This pipeline handles camera photos, HEIC iPhone exports, and oversize PNGs before the validator from Fix 1 ever sees them.
Fix 3: Await the upload and read file_url
Never assume an upload succeeded just because the promise resolved. Inspect what came back.
import { base44 } from "@/api/base44Client";
async function uploadImage(rawFile: File): Promise<string> {
const file = await compressForUpload(await normalizeFile(rawFile));
const error = validateImageFile(file);
if (error) throw new Error(error);
const result = await base44.integrations.Core.UploadFile({ file });
console.log("upload result:", result); // log the full object while debugging
if (!result?.file_url) {
throw new Error("Upload finished but returned no file_url.");
}
return result.file_url;
}
The log line is non-negotiable during debugging. If the result has no file_url, or the code was reading some other field, you need to know that — not assume the absence of a thrown exception means success.
Fix 4: Save the URL to the record in the same handler
The file is in storage. You have a URL. Now write it to the record that should reference it.
async function uploadProductImage(productId: string, rawFile: File) {
const fileUrl = await uploadImage(rawFile);
await base44.entities.Product.update(productId, { image_url: fileUrl });
return fileUrl;
}
// For the signed-in user's own profile, updateMe writes custom User fields.
async function uploadAvatar(rawFile: File) {
const fileUrl = await uploadImage(rawFile);
await base44.auth.updateMe({ avatar_url: fileUrl });
return fileUrl;
}
updateMe can change any custom field defined in your User entity schema, so avatar_url has to exist there first. The two writes (storage + entity) should be in the same handler. If they get split across components or hooks, you will eventually ship a flow where one runs and the other does not, and you will spend an afternoon debugging why some users have orphan files and an empty image_url on the record.
Fix 5: Check the table's access rules
If the save works for you and fails for your users, the upload code is not the problem. In the dashboard, click the table under Data, then Permissions in the top right, and read who may update records and on what condition. Click Test Permissions in preview to open your app's preview acting as different people, then adjust the Update rule until each person can save exactly what they should. Base44 sets these rules automatically as you build, so re-check them after any AI edit to the table.
Fix 6: Store private files as a file_uri and sign them on demand
For files a stranger should not be able to open, use private storage, and never save the temporary URL.
// Upload: keep the file_uri on the record, not a URL
const { file_uri } = await base44.integrations.Core.UploadPrivateFile({ file });
await base44.entities.Contract.update(contractId, { file_uri });
// Display: create a fresh signed URL each time the file is shown
const { signed_url } = await base44.integrations.Core.CreateFileSignedUrl({
file_uri: contract.file_uri,
expires_in: 3600, // one hour; the default is 300 seconds
});
Assume anyone who can read the record holding a file_uri can request a signed URL for it, and protect that record with a Read rule.
Fix 7: Check integration credits when every upload fails at once
If uploads that worked yesterday all fail today, check the workspace's credit usage before touching code. Each UploadFile call uses about 1 integration credit, and when you reach your credit limit, actions that use credits pause until your next reset. Base44's documented route is to upgrade or adjust your plan from your billing dashboard, or to wait for the reset.
Fix 8: Add progress UI and an error boundary
Silent failure is the worst UX. Every upload should show a progress indicator and resolve into either a success state with the image or a failure state with a specific message.
function UploadButton() {
const [status, setStatus] = useState<"idle" | "uploading" | "error" | "done">("idle");
const [error, setError] = useState<string | null>(null);
async function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setStatus("uploading");
setError(null);
try {
await uploadAvatar(file);
setStatus("done");
} catch (err) {
setError(err instanceof Error ? err.message : "Upload failed");
setStatus("error");
}
}
return (
<div>
<input type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml,image/heic" onChange={onChange} disabled={status === "uploading"} />
{status === "uploading" && <p>Uploading...</p>}
{status === "error" && <p className="text-red-600">{error}</p>}
{status === "done" && <p>Upload complete.</p>}
</div>
);
}
For large files, such as videos near the 100MB cap, keep the uploading state visible for the whole call. Users tolerate slow uploads; they do not tolerate uploads that look frozen.
How long does it take to fix Base44 image upload not working?
Timeline depends on which of the five root causes is biting and whether you fix them all defensively or just the one in front of you.
- Single-cause fix (size or format,
file_url, entity write, access rule, or signed URL): 15-30 minutes. Reproduce with the request visible, identify the failing step, apply the matching fix from the section above. - Defensive rewrite (all fixes applied): 1-2 hours. Add the validation, conversion, compression, result check, entity update, private-file handling, and progress UI as a single hardened upload helper. Worth doing once; reuse across every upload in the app.
- Storage migration (Base44 → Cloudinary or similar): 4-8 hours for a small app, 1-3 days for an app with thousands of existing files. Only needed when you want format conversion, responsive resizing, or CDN delivery features that Base44's media docs do not describe.
In all cases, deploy the defensive rewrite once and apply it to every upload surface. The root causes recur across every upload in your app — fixing them in one place and importing the helper everywhere prevents the bug from coming back in a different component.
DIY vs hire decision
DIY this if: You can reproduce the failure with the request visible, read a response body, and edit a TypeScript handler. All the fixes above are 5-30 lines of code each. The diagnosis is the hard part, and the diagnostic procedure in this guide gets you there in a couple of minutes.
Hire help if: Uploads were working and broke after a Base44 platform update, access rules were configured by someone who is no longer on the team, or you have a backlog of orphan files that need to be reconciled with database records. These three scenarios involve cross-cutting changes to access rules, upload code, and historical data that benefit from a structured engagement rather than a one-off patch.
Need this fixed in the next 24 hours?
Our fix-sprint engagement covers the full upload diagnostic and hardening: Network-tab reproduction, identification of the failing root cause, defensive rewrite of the upload pipeline, RLS policy audit, and a backfill script for orphan storage records if needed.
Start a fix-sprint for image upload issues
Related problems
- Data loss when returning to the app — the sibling state-loss bug that often hides behind "uploads not working" reports.
- Data binding undefined field — the entity-write half of the upload pipeline, when the URL field is misnamed or missing from the schema.
- Rate limit 429 production throttle — a burst of uploads from one user can trip Base44's per-person rate limit, which looks like a silent failure.