The short version: Files (ContentDocument) is the current model, Attachments and Documents are the legacy ones, and new work should use Files exclusively. The migration matters because Attachments cannot be shared to more than the one record they hang off, and that limitation is usually what sends people looking.
The three models
Attachment belongs to exactly one parent record. Delete the parent, the attachment goes. It cannot be shared, versioned, or previewed well. It is the oldest model and it still exists because deleting it would break a great deal of code.
Document lives in a folder, not on a record. It was the place for logos and templates before Files existed. Mostly obsolete, still used by some email templates and Visualforce.
File, meaning ContentDocument and its versions, is the modern model. One file can be linked to many records, has version history, previews properly, and works in Chatter, communities and the mobile app.
The object model behind Files, which is three objects
ContentDocument the file's identity, one per file
ContentVersion each uploaded version; the newest is IsLatest
ContentDocumentLink one row per record the file is linked to
You insert a ContentVersion, and the platform creates the ContentDocument for you. To attach it to a record you then insert a ContentDocumentLink. Two DML operations, and the second one is what people forget.
ContentVersion cv = new ContentVersion(
Title = 'Signed contract',
PathOnClient = 'contract.pdf',
VersionData = fileBody
);
insert cv;
Id docId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId;
insert new ContentDocumentLink(
ContentDocumentId = docId,
LinkedEntityId = accountId,
ShareType = 'V',
Visibility = 'AllUsers'
);
ShareType and Visibility decide who sees it
This is the part that produces surprises. ShareType is V for viewer, C for collaborator, I for inferred from the record. Visibility is AllUsers, InternalUsers or SharedUsers.
The combination that catches people is a link with ShareType = 'I', which infers permission from the linked record. That sounds right and it means anyone who can see the account can see the file, including community users if the account is shared to them. A contract linked to an account with broad sharing becomes broadly visible, and nothing about the file itself says so.
Check what a file is linked to before assuming its access:
SELECT ContentDocumentId, LinkedEntityId, LinkedEntity.Name, ShareType, Visibility
FROM ContentDocumentLink
WHERE ContentDocumentId = :docId
That query needs a filter on ContentDocumentId or LinkedEntityId. It cannot be run unfiltered, which is a deliberate platform restriction.
Migrating from Attachments
There is no button. The pattern is to read each Attachment, create a ContentVersion from its Body, create a ContentDocumentLink to the same parent, verify, then delete the Attachment.
Three things to plan for. Heap: a file’s body counts against the 6 MB synchronous heap, so this is batch work with a small scope size. Storage: during the migration both copies exist and file storage doubles. And references: any code, formula or integration that reads Attachment breaks, so find them before the delete step and not after.
Storage counts differently
Files and Attachments consume file storage, not data storage, and the two have separate allowances. A scratch or developer org has very little file storage, which is why a migration rehearsal there tells you nothing about capacity.
Versions each consume storage. A file uploaded ten times holds ten versions and ten times the space, and nothing prunes them automatically.
Frequently asked
Can one file be on several records? Yes, that is the main advantage. Insert one ContentDocumentLink per record.
What happens when I delete the last link? The ContentDocument remains, owned by whoever uploaded it, visible in their Files. It is not deleted.
Are Notes the same thing? Enhanced Notes are stored as ContentNote, which is a ContentDocument subtype. Old Notes are a separate legacy object alongside Attachment.
