Home / lwc / file-upload-patterns
file-upload-patterns
Upload files in LWC: lightning-file-upload, manual multipart, large-file chunked upload, and ContentDocument associations. NOT for ContentDocument query patterns — use data/salesforce-files-architecture. Trigger keywords: file upload, lightning-file-upload, uploadfinished, FileReader, base64, ContentVersion, ContentDocumentLink, ShareType, Visibility, VersionData, FirstPublishLocationId, Apex heap, guest user upload.
Open the skill folder on GitHub
LWC File Upload Patterns
Getting bytes from a browser into Salesforce Files, and attaching the result to the right record. The whole domain turns on one question — does a record id exist at the moment of upload — and on one limit, the Apex heap, which decides whether the bytes may pass through Apex at all.
Adoption Signals
Any file intake UI. Choose the tier by whether a record id exists, by file size, and by who must see the result.
| Signal | Tier |
|---|---|
| Record already saved, no pre-processing needed | lightning-file-upload |
| No record id yet, or the bytes must be inspected or transformed first | custom input → FileReader → Apex |
| File larger than a couple of megabytes | lightning-file-upload — the Apex path cannot carry it |
| Guest or Experience Cloud audience | Neither, until the access design is settled first |
The guide routes the first two itself: lightning-file-upload is "a file selector for uploading a file that's associated with a record ID. To upload a file without a record ID, use lightning-input with type="file" instead." (lwc_guide base-components-all L4603).
Before Starting
- Does the record the file attaches to exist at the moment the user picks the file?
- What is the largest file the business actually sends, not the largest anyone imagines?
- Who is allowed to see the stored file — internal only, or an Experience Cloud audience?
- What surface is the component on: a Lightning record page, an LWR site, a standalone app?
- Is anything expected to inspect, scan or transform the bytes before they are stored?
Questions to Ask Before Configuring
Ask these before writing markup. Each answer closes a branch that is expensive to reverse once the component ships, and each one traces to a gotcha in references/gotchas.md.
| Ask | Why it matters | What a good answer adds |
|---|---|---|
| "Does the parent record exist when the user picks the file?" | It is the whole tier decision. With a record id the platform does the association; without one you own it | The tier, and for the Apex path the moment at which the link is created (gotcha 1) |
| "What is the biggest file this has to accept in the next two years?" | Apex heap is 6 MB synchronous (apexdev L19577) and base64 adds about 37 % (object_reference L79833); past a couple of megabytes the Apex path is not available at any chunk size | A hard cap written into the Apex controller, not only the JavaScript (gotchas 5, 6) |
| "Who must be able to open the file once it is stored — internal staff only, or a customer?" | ShareType and Visibility decide it, and AllUsers on a record surfaced externally is how internal documents leak | Explicit values at every ContentDocumentLink insert site (gotchas 3, 4) |
| "Will the file ever need to move to a different parent record?" | FirstPublishLocationId is set only on first publish and cannot be changed later | Whether the one-statement form is safe or explicit links are required (gotcha 2) |
| "Which surface is this component on — Lightning Experience, an LWR site, a standalone app?" | Confirmation UX differs: lightning/platformShowToastEvent is not supported in LWR sites or standalone apps (lwc_guide base-components-patterns L4773) | The confirmation mechanism, chosen rather than defaulted (gotcha 9) |
| "Which file types must be refused, and what happens if a refused file gets through?" | accept is a picker filter; only a server-side check on decoded bytes rejects anything | The magic-number allowlist and the Apex rejection path (anti-pattern 5) |
| "Which permission set grants this, and does it include Apex class access?" | Class access is a separate grant: an @AuraEnabled method is reachable only when the profile or a permission set allows the class (lwc_guide apex-security L7436) | A permission set shipped in the same deployment as the component (gotcha 10) |
What a proper configuration adds over just wiring an uploader: the file is visible on the record because the link was a deliberate insert rather than a hoped-for side effect, its audience is a chosen value rather than an inherited one, the size ceiling is enforced where a browser cannot bypass it, and the Jest and Apex tests fail when any of those three regress.
Recommended Workflow
- Answer the record-id question first. If the record exists, build Bundle A from
references/code-examples.md—lightning-file-uploadwithrecord-id, anuploadfinishedhandler, and no Apex at all. Stop here unless something below forces the Apex path. - If the Apex path is forced, size it before writing it. Take the largest real file, add
~37 % for base64 (
object_reference L79833), and check it against the 6 MB synchronous heap (apexdev L19577). If it does not clear that with margin, the requirement is wrong, not the implementation — go back to step 1 or to a direct API upload. - Build Bundle B as a pair.
references/code-examples.md§ B: client size guard →FileReader→@AuraEnabled(nevercacheable=true) →ContentVersion→ContentDocumentLinkwith explicitShareTypeandVisibility→ return theContentDocumentId. Validate the decoded bytes, not the extension. - Write both tests before deploying. The Jest suite dispatches a synthetic
uploadfinishedevent and mocksFileReaderand the Apex import; the Apex test uses a four-byteBloband asserts theContentDocumentLinkrow exists with the intendedShareType. Both are inreferences/code-examples.md§§ A.5, B.6, B.7. - Run the checker over the source tree:
python3 scripts/check_file_upload_patterns.py --manifest-dir force-app/main/default. Use--strictin CI to fail on warnings. It catches an orphanContentVersion, a cacheable upload method, a missingShareType, an unguarded base64 payload and alightning-file-uploadwith neither arecord-idnor a documented alternative. - Deploy Apex before the components, and the permission set with class access in the
same release.
@salesforce/*imports are validated against org metadata (lwc_guide get-started-oss L126), so the component will not deploy against a class that is not there yet. - Verify against
ContentDocumentLink, not against the file. Run the filtered query inreferences/code-examples.md§ Verification. An unfilteredContentDocumentLinkquery needs View All Data from API 59.0 on (object_reference L77161-L77162), so filter byLinkedEntityIdand confirmShareType,Visibilityand the raw byte count.
Key Considerations
| Fact | Source | Consequence |
|---|---|---|
| Apex total heap: 6 MB synchronous, 12 MB asynchronous | apexdev L19577 | An @AuraEnabled upload runs in the synchronous bucket |
| Base64 conversion "increases the document size by approximately 37%" | object_reference L79833 | The 4/3 arithmetic understates it; budget from the documented figure |
| Maximum file size uploadable via the SOAP API is 50 MB, measured after base64 conversion | object_reference L79771-L79774 | The API ceiling and the component ceiling are different numbers |
ContentDocumentLink.ShareType is Required; C cannot be created from an Apex trigger | object_reference L77269, L77276-L77277 | Set it at every insert; move trigger-based collaborator links elsewhere |
Visibility values are AllUsers, InternalUsers, SharedUsers; external users can only set AllUsers | object_reference L77302-L77317 | Write it explicitly; default to InternalUsers in code |
FirstPublishLocationId shares the file in a single transaction, but is set only on first publish | object_reference L79403-L79409 | Use explicit links wherever the parent may change |
ContentDocument has no create() call | object_reference L76648-L76649 | You cannot manufacture a document to repair an orphan version |
| A cacheable Apex method "must only get data, it can't mutate (change) data" | lwc_guide apex-result-caching L7254 | An upload method is never cacheable=true |
lightning/platformShowToastEvent is unsupported in LWR sites and standalone apps | lwc_guide base-components-patterns L4773 | Choose the confirmation mechanism from the target |
Virus scanning is not part of any of this. UNVERIFIED (2026-09-05): the claim that Salesforce performs no automatic malware scan on uploaded Files is not stated in the grounding corpus (Object Reference, Apex Developer Guide, LWC Developer Guide, App Limits cheat sheet); treat scanning as a separate design and see security/file-upload-virus-scanning.
Worked Examples (see references/examples.md)
- Simple record attachment — attach a signed PDF to a Case with no Apex
- Large document through Apex — where the heap ceiling actually binds, and where the
chunked workaround stops helping
Common Gotchas (see references/gotchas.md)
- Orphan ContentVersion — the insert succeeds, the file is invisible, nothing errors
FirstPublishLocationIdis one-shot — and it rewrites itself when ownership changesShareTypeis Required andCis trigger-hostile — a documented, not incidental, limit- Base64 costs ~37 %, not 33 % — the heap budget is smaller than the arithmetic suggests
- Unfiltered
ContentDocumentLinkqueries need View All Data — from API 59.0
Top LLM Anti-Patterns (full list in references/llm-anti-patterns.md)
- Hand-rolling a base64 uploader when the base component already does the job
- Quoting a per-file size ceiling from memory
- Inserting
ContentVersionand calling the file attached - Treating
acceptas validation
Reference Files
| File | Read it when |
|---|---|
references/code-examples.md | You are writing the bundle: Bundle A (lightning-file-upload + uploadfinished + toast), Bundle B (custom input → FileReader → Apex → ContentVersion + ContentDocumentLink), both Jest suites, the Apex test, js-meta.xml, package.xml, deploy order and the verification SOQL |
references/gotchas.md | A file uploaded successfully and something about it is wrong — invisible on the record, visible to the wrong audience, failing on heap, or unqueryable |
references/llm-anti-patterns.md | You are reviewing generated upload code, or want the detection hints the checker implements |
references/well-architected.md | You need the pillar framing, the tier tradeoff argument, or the sourced claims list |
templates/file-upload-patterns-template.md | You are recording the tier decision, the size budget and the sharing choice for review |
scripts/check_file_upload_patterns.py | Before deploy: run it with --manifest-dir over the LWC and Apex source tree, --strict in CI |
Related Skills
data/salesforce-files-architecture— use for the storage model itself:ContentDocument, versioning, libraries, and querying files that already exist.security/file-upload-virus-scanning— use for the scanning design this skill deliberately does not cover.lwc/lwc-forms-and-validation— use when the upload is one field inside a larger form; that skill owns the submit lifecycle and validation feedback.lwc/lwc-error-boundaries— use to decide how an upload failure degrades in the surrounding page rather than blanking it.lwc/lwc-testing— use for the broader Jest setup this skill's tests assume.data/attachment-to-files-migration— use when the requirement is moving legacyAttachmentrecords into Files rather than accepting new uploads.
Official Sources Used
See references/well-architected.md § Official Sources Used for the full list with the claim each source supports.