Learn how Dataverse actually stores file attachments, full-size images, and binary content — and how architectural decisions you make at design time determine whether your app scales or hits a storage wall. This lesson covers Image columns, File columns, Notes attachments, SharePoint integration, and Azure Blob patterns with realistic capacity planning examples.

Picture this: your organization has been running a model-driven app for field inspections for about six months. Users are uploading photos of equipment defects, signed documents, and scanned certificates directly from their phones. The app works great — until one morning your environment hits its storage capacity limit and every record save starts throwing errors. Nobody flagged it as a risk because nobody fully understood how Dataverse stores files, how those files count against your tenant's storage quota, or how to architect file and image handling in a way that scales.
This scenario is more common than it should be, and it's almost entirely preventable. The problem isn't that Dataverse can't handle files — it absolutely can, and with considerable sophistication. The problem is that most makers treat image and file columns as a drop-in feature without understanding the underlying blob storage architecture, the behavioral differences between image column types, and the tradeoffs involved in storing binary content inside Dataverse versus referencing it from external blob storage.
By the end of this lesson, you'll have a complete, architectural understanding of how Dataverse manages file attachments, image columns, and binary storage. You'll be able to configure image and file columns correctly, understand how full-size images differ from thumbnail images, reason about your storage consumption, and make deliberate design decisions about when to store binary content in Dataverse and when to reach for Azure Blob Storage or SharePoint integration instead.
What you'll learn:
Can Store Full Image setting and its behavioral implications on formsYou should be comfortable with Dataverse table and column fundamentals before tackling this lesson. If you need a refresher on how Dataverse organizes data and what column types are available, work through Dataverse Fundamentals: Tables, Columns, and Rows Explained for Power Apps Makers first. You should also have hands-on experience building model-driven forms, since we'll be configuring how these columns surface on forms — Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms covers the mechanics if you need them.
You'll need a Power Apps environment with System Administrator or System Customizer privileges, access to the Power Platform Admin Center to view storage analytics, and ideally an environment where you can experiment without affecting production data.
Before touching a single column setting, you need a mental model of where files actually live when you store them in Dataverse. This isn't just academic — getting this wrong is what leads to the storage crisis scenario from the introduction.
Dataverse operates with three distinct storage tiers, each billed and measured separately in the Power Platform Admin Center:
Database storage holds the relational data: rows, column values, metadata, audit logs, and importantly, file metadata (names, sizes, content types) but not file content itself. This is your "traditional" database storage.
File storage holds the actual binary content of File columns and the full-size content of Image columns. This is Azure Blob Storage behind the scenes, fully managed by Microsoft, scoped to your environment. When you create a File column on a table, writes go here.
Log storage holds audit records and plug-in trace logs. This matters for compliance discussions but isn't our focus today.
The critical insight is that file content never lives in database storage. When a user uploads a photo to an Image column or attaches a PDF to a File column, the binary payload routes to file storage. What sits in your database row is metadata: the file name, content type, size in bytes, and an internal reference to the blob. This is efficient by design, but it means your file storage quota and your database storage quota can balloon completely independently of each other.
Key insight
Many makers look at their database storage consumption, see it's fine, and conclude they don't have a storage problem — right up until the file storage quota fills up and uploads start failing. Always monitor all three storage tiers in the Power Platform Admin Center, not just database.
Dataverse allocates storage capacity to your tenant based on licensed users. As of current pricing: every tenant gets 10 GB of database capacity base, plus 2 GB per Power Apps per-user plan license, plus 250 MB per seeded license. For file storage, the base is 20 GB plus 400 MB per Power Apps per-user plan license. If you're operating a field inspection app with 50 licensed users uploading photos, you can do the rough math — and it fills up faster than expected when full-size images are involved.
Dataverse has supported image columns for a long time, but the feature has evolved in ways that create confusion because the underlying behavior changed significantly with the introduction of "Full Image" support. You need to understand both modes.
Every table in Dataverse can have exactly one primary image column — one column designated as the "entity image." Historically, Dataverse automatically resized any image uploaded to this column to a maximum of 144×144 pixels and stored only that thumbnail. The original, full-resolution image was silently discarded.
This was pragmatic for the era — it kept storage predictable — but it makes primary image columns essentially useless for any scenario where image fidelity matters: product photography, document scans, medical images, engineering diagrams, inspection photos.
The thumbnail is what renders in views as the record's avatar image, and it's what you see in the upper-left corner of many out-of-the-box entity forms. It's still there, it still works that way if you don't opt into full-image storage, and for its intended purpose (a small visual identifier on a record) it's fine.
Microsoft introduced full-size image storage as an opt-in capability. When you enable Can Store Full Image on an Image column, Dataverse stores both the resized thumbnail AND the original full-resolution binary in file storage. The two blobs are linked to the same column but retrieved via different endpoints.
To configure this in the Power Apps maker portal:
When Can Store Full Image is enabled, the column's API schema exposes two content endpoints: one for the thumbnail (used implicitly by form headers and views) and one for the full-size binary. Power Apps model-driven forms will render an image control that, when clicked, can display the full-resolution version. The form renders the thumbnail for performance and loads the full binary on demand.
Warning
Each table can only have one primary image column (the one that drives the entity image in views and form headers). If you need to store multiple images per record — say, multiple photos of an inspection site — you cannot do this with Image columns alone. You'll need File columns, Notes attachments, or a related table. This is one of the most common architectural mistakes.
Full-size images stored via Image columns are subject to a maximum file size of 30 MB per image. This is a hard platform limit, not configurable. For most photograph use cases this is adequate — a 30 MB JPEG is a very large photo — but for engineering diagrams, medical scans, or raw camera files, 30 MB can be insufficient.
Additionally, the MIME type accepted by Image columns is restricted to standard image formats: JPEG, PNG, GIF, TIFF, BMP, and WebP. If users need to upload PDFs, Office documents, or other non-image binaries, Image columns are the wrong tool regardless of the size.
File columns are the more powerful and flexible binary storage mechanism in Dataverse. Unlike Image columns, which are limited to one per table and image MIME types, you can have multiple File columns on a single table, and each can accept any MIME type you specify.
In the maker portal, when you add a new column to a table:
The maximum file size field deserves emphasis. The 131,072 KB ceiling is a hard platform limit that applies to each individual file upload. It is not a per-record, per-table, or per-environment limit — it's per-file. You cannot configure your way past this limit for a single file stored in a File column. If your use case involves files larger than 128 MB (CAD files, video recordings, large databases), you need a different architectural approach, which we'll address later.
Tip
Even if your individual files are small, be deliberate about the maximum size you configure. Setting it to 131,072 KB by default "just in case" invites users to upload unnecessarily large files. If your column stores signed PDFs that average 500 KB, setting the limit to 5,120 KB (5 MB) is a reasonable guard while still accommodating edge cases. This isn't just about storage — large file uploads affect API throughput and user experience.
The Accepted file types setting takes a comma-separated list of file extensions (e.g., .pdf,.docx,.xlsx) or MIME type strings. This filtering happens client-side in the form control and is enforced by the model-driven app's file picker dialog, which grays out non-matching files.
However — and this is important — MIME type filtering on a File column is not enforced at the API layer. A developer making a direct API call with the correct authorization can upload any file type to a File column regardless of the accepted types configuration. If your security model requires strict file type enforcement, you need a plug-in step on the Create or Update message to validate the content type server-side.
This is a meaningful distinction for compliance scenarios. Imagine a regulated industry where only specific document types should be attached to records. Relying solely on the File column's accepted types setting is insufficient — a plug-in that reads the Content-Type header from the file upload request and rejects non-compliant types is required.
For implementing server-side validation logic at the data layer, review how Configuring Dataverse Plug-in Steps and Business Event Handlers: Triggering Server-Side Logic on Table Operations works before treating the File column's MIME filter as a security control.
When you add a File column to a model-driven form, it renders as a file attachment control with upload and download affordances. The control shows the current file's name and size when a file is stored, and presents an upload button when the field is empty.
One behavioral nuance: each File column stores exactly one file at a time. Uploading a new file to a File column that already contains a file replaces the existing file — the previous version is deleted from blob storage. There is no built-in versioning. If you need version history for attached documents, you need either Notes/Annotations (which don't have native versioning either, but each annotation is its own record) or SharePoint integration (which provides native document versioning).
Note
File column values are not included in standard Dataverse export/import operations (via Excel or the import wizard). The file metadata (name, size) can sometimes appear in exports but the binary content is not portable this way. For data migration scenarios involving File column content, you need to use the Dataverse API to download each file individually and re-upload it to the target environment. This is frequently overlooked during environment migrations.
Long before Image and File columns existed, Dataverse (then CRM) handled file attachments through the Annotation table (also known as Notes). Every standard Dataverse table that has "Notes & Attachments" enabled gets a system relationship to the Annotation table, and file attachments in that system are records in the Annotation table with a binary payload.
The Notes/Annotations approach is different from File columns in several critical ways:
Multiple attachments per record: Each note record is independent, so a single parent record can have many file attachments — each is its own Annotation row.
Maximum file size: Notes attachments are governed by an organization-level setting: MaxUploadFileSize (also surfaced as Maximum file size in the legacy settings area at Settings > Administration > System Settings > Email > Maximum file size). The default is 5 MB (5,120 KB) and the platform maximum is 128 MB — same ceiling as File columns.
Storage billing: Annotation attachments store their binary content in database storage, not file storage. This is the legacy behavior from when Dataverse didn't have a separate file storage tier. This means Notes attachments count against your database quota, not your file storage quota. This has significant cost implications and is a common source of surprise — organizations using Notes attachments heavily are burning through database storage (the more expensive tier) rather than file storage.
Key insight
This is one of the strongest architectural reasons to prefer File columns over Notes attachments for new development. Notes store binaries in database storage. File columns store binaries in file storage. Database storage is more expensive per GB than file storage in Power Platform capacity pricing. For new implementations, default to File columns unless you specifically need the multi-attachment-per-record behavior that Notes provide.
If your table has Notes enabled, you add the Notes timeline or a Notes subgrid to your form in the form designer. The Notes section typically appears as a timeline component that shows both notes (text) and attachments (files). You can configure the timeline's display behavior in the form component properties — including whether to show only notes, only activities, or all of them.
For more detail on timeline and subgrid configuration in form design, see Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms.
Understanding the column types is necessary but not sufficient — you also need to understand how these columns behave on model-driven app forms specifically, because the form layer has its own configuration concerns.
When you designate an Image column as the primary image for a table (done in the table properties, not the column properties), model-driven forms automatically display that image in the form header area — the small circular avatar next to the record name. This uses the thumbnail version regardless of whether you've enabled full-size storage.
Users can click this header image to replace it. The upload dialog they see is the same standard file picker, but it's triggered by clicking the image placeholder, not a labeled field on the form. Many users don't realize this is interactive, which leads to complaints that "the photo feature doesn't work." Consider adding instructional text near the form header or creating a custom form notification that guides users on this interaction.
You add Image and File columns to form sections exactly like any other column: drag them from the column panel in the form designer onto a section. However, the rendered controls have behaviors worth knowing:
For Image columns, the form renders an image display area. If Can Store Full Image is enabled, there's typically a control to upload and a way to view the full-size version. The size of the image control on the form is determined by the section layout and column span, not the image's actual dimensions. You can give the column a 2-column span to make it render larger.
For File columns, the form renders a simple attachment widget: filename when populated, upload button when empty, and download affordance. There's no preview of file content in the form — not even for image MIME types. If you want image previews for content stored in File columns, you need a custom PCF control. This is a meaningful UX gap that surprises makers who expect File columns storing images to behave like Image columns.
Tip
If you want image preview behavior for multiple images per record — a common requirement in inspection, real estate, or product apps — the cleanest architecture is a related table with an Image column as its primary image, combined with a subgrid on the parent form showing that related table's records. Each related record holds one image, displayed as thumbnails in the subgrid. This gives you unlimited images per parent record with native image rendering, without hitting the one-primary-image-per-table constraint.
File and Image columns participate in column-level security (CLS) just like other column types. You can configure a field security profile to restrict read or write access to a File or Image column to specific users or teams. For a file column storing sensitive documents — say, employee contracts or financial statements — this is an important control.
When a user lacks read access to a File column due to CLS, the form renders the field as empty even if a file exists. They also cannot download the file via API. Write restrictions prevent uploading new files.
For a detailed treatment of how column-level security profiles work and how to configure them, see Column-Level Security and Record Sharing in Dataverse.
Let's get practical about the numbers. Understanding consumption calculations helps you avoid crises and design systems that stay within budget.
In the Power Platform Admin Center (admin.powerplatform.microsoft.com):
For per-table storage breakdowns, you have two options. The built-in storage report (under Capacity > Storage capacity usage within an environment) shows per-table database storage. However, it doesn't give you a direct per-table breakdown of file storage — that's a known gap in the tooling. For file storage forensics, you need to query the msdyn_storageusage table or export blob storage analytics, which requires a more advanced approach.
Here's a realistic estimation framework:
Suppose you have a table called contoso_inspection with:
Daily file storage addition:
Images: 15 records × 4 MB = 60 MB (full-size)
15 records × ~20 KB = ~300 KB (thumbnails, stored separately)
PDF: 15 records × 800 KB = 12 MB
Checklist: 15 records × 200 KB = 3 MB
---
Daily total: ~75 MB/day in file storage
Over 90 days, that's ~6.75 GB from this one table alone — before accounting for older records or other tables. With a base file storage allocation of 20 GB and 50 licensed users contributing ~20 GB of capacity, you have room, but only if you're actively managing it. Leave this running for a year without a data retention strategy and you're looking at ~27 GB from this table alone.
Warning
Dataverse does not have built-in automated file deletion policies tied to record age. There is no "delete file attachments older than N days" configuration you can switch on. Retention and cleanup must be implemented intentionally: either through Power Automate flows that query old records and delete file content, through plug-in logic triggered on date conditions, or through Azure Data Factory pipelines for bulk operations. Failing to plan for this is how organizations end up in emergency storage situations.
When your requirements exceed what native File and Image columns can deliver — whether due to the 128 MB per-file ceiling, the need for versioning, the desire to reduce storage costs, or integration with external systems — you have several architectural patterns to consider.
Dataverse environments can be integrated with SharePoint to enable document management for specific tables. When SharePoint integration is configured for a table, the model-driven form gains a Documents tab (or section) that directly surfaces a SharePoint document library scoped to each record. Files stored here live in SharePoint, not in Dataverse file storage — so they don't consume your Dataverse storage quota at all.
SharePoint integration gives you:
The tradeoff: SharePoint-stored files are outside Dataverse's security model. A user with SharePoint access can reach those documents through SharePoint directly, bypassing whatever row-level or column-level security you've configured in Dataverse. For sensitive records, this is a meaningful security consideration. You'd need SharePoint permission inheritance and site-level security to compensate.
To configure SharePoint integration for a model-driven app: in the Power Platform Admin Center, navigate to the environment settings and enable SharePoint integration, then specify the SharePoint site URL. In the maker portal, you then enable document management for each specific table under Settings > Document Management.
Note
Once SharePoint integration is enabled for a table, any existing Notes attachments for that table remain in Dataverse — they're not automatically migrated to SharePoint. You'll have a period where attachments exist in both systems, which confuses users and complicates support. Plan a migration period and communicate clearly about where to store new documents.
For files exceeding 128 MB, or for use cases where you need external processing pipelines, a common pattern is to store the file in Azure Blob Storage directly and store only the reference URL in Dataverse. You'd use a Text or URL column to hold the blob URL, and optionally a separate Text column for the file name.
This pattern requires that:
For generating SAS tokens at read time in a model-driven form, a PCF control is essentially required because the built-in form components have no mechanism to call an external API dynamically during render. A PCF control can call a custom API (registered as a Dataverse custom API or a direct Azure Function HTTPS call) to retrieve a time-limited SAS URL and then present a download link.
For building and deploying PCF controls for this kind of custom form behavior, see Extending Model-Driven Apps with PCF Controls: Building and Deploying Custom Field and Dataset Components for Dataverse Forms and Views.
For large-file scenarios that still need to live within the Dataverse ecosystem (to benefit from Dataverse security, auditing, and search), the Dataverse Web API supports chunked file upload and download for File columns. The native API allows uploading files in chunks, which is how it achieves the 128 MB limit in practice — the client sends multiple HTTP requests, each carrying a chunk of the binary.
The relevant API endpoints are:
# Initiate an upload
PATCH [org]/api/data/v9.2/[entityset]([id])/[filecolumnname]
Content-Type: application/octet-stream
x-ms-transfer-mode: chunked
x-ms-file-name: filename.pdf
# Upload a chunk
PATCH [org]/api/data/v9.2/[entityset]([id])/[filecolumnname]
Content-Range: bytes 0-4194303/10485760
Content-Type: application/octet-stream
[binary chunk data]
# Download the file
GET [org]/api/data/v9.2/[entityset]([id])/[filecolumnname]/$value
This chunked transfer approach is what Power Apps model-driven forms use internally when a user uploads through the file control. If you're building automation that programmatically populates File columns (e.g., a Power Automate flow that saves email attachments to related records), you need to use this chunked approach for any file over ~4 MB. The standard "set column value" action in Power Automate connectors handles small files, but for larger files, you need the HTTP with Microsoft Entra connector making direct API calls.
If your organization requires change tracking on when files were uploaded, replaced, or deleted, you need to plan for this explicitly because Dataverse's standard audit log handles file columns differently from regular column values.
Dataverse auditing can be enabled for File and Image columns. When enabled, the audit log records that a file was uploaded or deleted, along with the timestamp and user. However, the audit log does not store a copy of the file contents — it cannot do a "diff" on binary content. You get an audit trail of file operations but not file versioning.
For full file version history, your options are:
For enabling Dataverse auditing on specific columns and understanding the audit record structure, Configuring Dataverse Auditing and Field-Level Change History in Model-Driven Apps walks through the full configuration process.
This exercise walks you through a complete, realistic configuration for a property management scenario. You'll configure a contoso_property table to support multiple inspection images, a primary profile photo with full-size storage, and a signed lease document — then you'll verify the form behavior and estimate storage impact.
You need a Dataverse environment with System Customizer or System Administrator access. Create an unmanaged solution called PropertyManagement and a new table called Property with display name Property and schema name contoso_property.
Property table and select Columns.Entity Image column (every table has this; if it's not visible, enable display of all columns including system columns).Entity Image column is set as the Primary image. This setting is on the table editor's main properties, not on the column itself.Tip
The "Primary image" designation in table properties and the "Can store full-size image" setting on the column are two separate things that work together. A column can store full-size images without being the primary image, but only the primary image column drives the form header avatar. Don't conflate them.
Property table.Lease Document.File.10240 (10 MB — reasonable for a signed PDF)..pdf (restricts the file picker to PDFs).Since a property needs multiple inspection photos and Image columns are limited to one per table:
Property Inspection Photo (schema: contoso_propertyinspectionphoto).Entity Image column.Entity Image column of this new table.Property Inspection Photo to Property (each photo belongs to one property).Caption for brief descriptions.For understanding how to set up this relationship correctly, including cascade behaviors, Configuring Dataverse Table Relationships in Model-Driven Apps: One-to-Many, Many-to-Many, and Cascade Behaviors Explained covers the pattern.
Property table.Documents & Media.Lease Document and drag in the Lease Document File column.Inspection Photos and add a subgrid showing Property Inspection Photo records related to the current property. Configure the subgrid to show the contoso_propertyinspectionphoto table records filtered by the relationship to the current property.Create a test Property record:
Property Inspection Photo record via the subgrid. Upload an image to that record's primary image. Return to the parent form and verify the inspection photo appears in the subgrid.Suppose you manage 200 properties, each with:
Calculations:
Primary images: 200 × 3 MB = 600 MB (full-size)
200 × 0.02 MB = 4 MB (thumbnails)
Lease documents: 200 × 1.5 MB = 300 MB
Inspection photos: 200 × 8 × 3.5 MB = 5,600 MB = 5.6 GB
---
Total file storage: ~6.5 GB for 200 properties (initial load)
With 25 new properties per month and similar photo volumes, you're adding ~813 MB/month. At this rate, a 20 GB file storage base is consumed in about 17 months from baseline — well within range for a production system to hit if you never archive.
As discussed earlier, the accepted file types setting on File columns is enforced only by the UI file picker. A determined user or external integration can bypass it entirely via the API. If you've configured a .pdf restriction on a column and you're seeing .docx files appear, a user or flow is uploading via the API without the UI constraint. Add a plug-in on the table's Create and Update messages to validate the uploaded file's content type header.
Organizations migrating from Dynamics 365 sales or service apps often continue using Notes attachments because that's what they've always done. For new Dataverse development or when planning a modernization, explicitly audit whether file storage should shift to File columns. The database storage billing difference is real — especially when database storage overages are priced higher than file storage add-ons.
When you enable Can Store Full Image, every upload creates two blobs: thumbnail and full-size. Your storage calculations should account for both. In practice, the thumbnail is small enough to be negligible (~5–30 KB per image), but in high-volume systems it adds up. More importantly, understand that both blobs are deleted when the record is deleted or the column value is cleared — there's no orphan blob scenario with native Image columns.
When you export a solution containing a table with a File column, the solution package contains the column's schema definition but zero file data. This is expected — solutions are for metadata/schema, not data. New makers sometimes assume their test files travel with the solution to a target environment. They don't.
Dataverse environment backups do include file storage content. When you restore a backup, file content is restored too. This is generally what you want, but it means backup restore is slower for environments with significant file storage — and the backup window and restore time scale with total file volume, not just database size. Factor this into your RTO/RPO planning.
Three possible causes:
Maximum file size setting is smaller than the file being uploaded — check and adjust the column configuration if appropriate.MaxUploadFileSize setting (visible in legacy System Settings) is set lower than the column's setting — this organization-level setting applies to Notes attachments but can affect some upload paths. Verify it's set appropriately (up to 131072 KB).Check two things. First, verify the Can Store Full Image column setting was published after being enabled — unpublished changes have no effect. Second, verify that images were uploaded after the setting was enabled. Enabling full-size storage doesn't retroactively generate full-size blobs for previously uploaded thumbnails. Users need to re-upload those images to get full-size storage.
If your database storage is higher than your table row counts and standard column data would suggest, investigate the Annotation table. Run a query on the Annotation table filtered to records with ObjectTypeCode matching your table(s) and with a non-null DocumentBody. These are your Notes attachments, and their binary content is sitting in database storage. Migrating those attachments to File columns or SharePoint can significantly reduce database storage consumption.
You now have a complete architectural picture of how Dataverse handles binary content. Let's consolidate the key decisions you'll make when designing file and image storage for a model-driven app:
Choose Image columns when: you need a single identifying image per record (like a product photo or person's headshot), the image will be displayed as a visual element on the form and in views, and the file will always be an image MIME type under 30 MB. Enable Can Store Full Image whenever the original resolution matters — don't leave it at thumbnail-only.
Choose File columns when: you need multiple file columns per table (you can have many), the content is not an image (PDFs, Office documents, etc.), or you need per-column file type or size limits. Remember the 128 MB ceiling per file.
Use Notes/Annotations when: you need multiple attachments per record without a related table pattern, you're working with a table that already has Notes enabled and migration isn't feasible, or the attachments are supplementary comments-with-files rather than structured document fields. Accept the database storage cost consciously.
Extend to SharePoint when: files need to be collaborated on in Microsoft 365, file size exceeds 128 MB, native document versioning is required, or you want to keep file storage costs out of Dataverse capacity.
Use Azure Blob with URL references when: file sizes exceed all Dataverse limits, you need integration with external processing pipelines, or you require more sophisticated access control than Dataverse column-level security provides.
For your next steps, if you're building solutions where storage capacity, performance at scale, and long-term data management are concerns, explore Configuring Dataverse Table Capacity and Storage Partitioning: Managing Large-Table Performance, Elastic Tables, and Time-Series Data Strategies in Model-Driven Apps — it builds directly on the storage fundamentals covered here and takes you into partitioning strategies for high-volume tables.
If your file columns contain sensitive content that needs precise access control, you'll also want to review Model-Driven App Security: Configuring Security Roles, Field Permissions, and Team-Based Access for Table Data to ensure your column-level security profiles are correctly configured for your File and Image columns.