Skip to content
duarteclemente
Go back

Building a 3M-File Document Management System on Power Apps Code Apps: What the SharePoint Connector Doesn't Tell You

A 200+ person CPA practice needed a document management system handling more than three million files, tightly bound to engagement and billing records, with reliable bulk operations, usable daily by non-technical staff. We built it as a Power Apps Code App, wiring together SharePoint document libraries and Dataverse business data. On paper this looked like a connector-wiring exercise: drop in the SharePoint connector, drop in the Dataverse connector, build the UI on top, ship it.

That is not how it went. Nearly every typed, abstracted SharePoint operation broke somewhere on the way to three million files, and each failure had to be diagnosed, evidenced, and fixed with the smallest change that actually worked. This is the story of what broke, why, and what we built instead — and why the Dataverse side of the integration ended up being the simplest part of the system.

Table of contents

Open Table of contents

The Brief

The firm didn’t want file storage. It wanted a practice-management document system: every file had to be addressable from the same engagement, client, and billing records the rest of the practice already ran on. SharePoint document libraries (Engagements and Clients) would hold the files, Dataverse would hold the structured business data, and the two had to stay linked without becoming two systems pretending to be one.

The Default Path

The starting design was the one anyone building on Power Apps Code Apps would reach for first: the platform’s typed SharePoint connector for file operations, the typed Dataverse connector for business records, both generated through pac code add-data-source. Both connectors are fully codegen’d — every generated file carries a Microsoft “autogenerated, do not edit” header, with zero hand-written authentication code anywhere. The Power Apps host SDK handles Entra ID token acquisition for both connectors automatically, through host-managed OAuth connection references. That part of the platform held up exactly as advertised, end to end.

The file operations did not.

Reality, in the Order We Discovered It

The first typed write action we exercised — PatchItem, to update file metadata — failed. So did MoveFileAsync. So did RenameFile. Not intermittently: these connector actions returned 404 and 500 responses in this environment, consistently, for operations core to a working DMS. The fix wasn’t to retry harder or wrap the typed actions in more error handling. It was to drop down a layer, to the one mechanism that did reliably reach the SharePoint REST API: a raw HttpRequest passthrough, built once as a thin shared wrapper module and routed through for every write operation instead of the typed actions sitting on top of it.

Six more failures surfaced after that, roughly in the order a growing file count would expose them — each diagnosed, evidenced, and fixed with the smallest change that worked rather than a rewrite:

That Azure Function ended up doing exactly three things: single-shot uploads under the 4MB ceiling, the chunked-upload path for files above it, and calling Microsoft Graph directly to convert Office documents to PDF server-side — a conversion capability the SharePoint connector doesn’t expose at all, and which powers the system’s combine-to-PDF feature. The Function is the only piece of genuinely custom infrastructure in the system. Everything else that looks like custom work is business logic sitting on top of a REST passthrough, not a new service.

The Dataverse Half

The Dataverse side integrates 15 entities — accounts, engagements, engagement tasks, AR transactions, billing info, deliverables, expense entries and reports, invoices and invoice items, service lines, staff, users, and environment variable definitions and values — generated the same way as the SharePoint connector, through pac code add-data-source. The data model, built on a custom ts_-prefixed solution, reflects a practice-management vocabulary at home next to established CPA-practice schemas.

The interesting decision wasn’t in the entity list. It was in how files relate to Dataverse records at all. The obvious, “proper” answer is a GUID foreign key: give every SharePoint file a Dataverse record GUID and join on that. We rejected that as premature. Both systems already had business keys that uniquely identified the things that mattered — an Account Number and an Engagement Number — so those became the actual join keys, stored as denormalized string metadata (Account_x0020_Number, Engagement_x0020_Number) directly on the SharePoint file and folder items, and used directly in queries across the file operations, file listing, and metadata services. A Dataverse_x0020_GUID field is reserved in the schema for a tighter future link, but it’s unused anywhere in the application code today — there for if the business-key join ever proves insufficient, not because anyone has evidence yet that it will.

The payoff is easy to understate. There’s nothing to keep in sync between two systems, because there’s no separate sync process: the join key is metadata sitting on the file, readable by a human looking directly at a SharePoint column, no Dataverse lookup or sync job required. Debugging a mismatched file means reading two strings side by side, not tracing a relationship through an ID that means nothing on its own.

What Fell Out of Getting the Plumbing Right

Once the REST passthrough layer, the retry logic, and the stable addressing scheme were solid, a long list of features became straightforward that would otherwise have required their own bespoke infrastructure: bulk metadata editing, bulk retention-label application, bulk portal publishing, combine-to-PDF, a recycle bin with restore, version restore, deep links into desktop Office applications, virtualized rendering for huge file lists, and client-side ZIP download of bulk selections. None needed new infrastructure of their own — they needed the infrastructure above to be reliable enough to build on. The hard part of this project wasn’t any individual feature; it was making the foundation trustworthy enough that features stopped being hard.

Numbers and What’s Next

The MVP shipped in roughly one month, a production-hardening pass followed at roughly two months in, and feature velocity has continued steadily since, against a system now operating against more than three million files in production. The roadmap extends well past file management: a dashboard with overdue-item alerts, a full billing workflow, document request lists, e-signature engagement letters, and staff scheduling are all on the table. This DMS is becoming the firm’s practice-management system of record, not staying a file manager that happens to talk to Dataverse.

Takeaway

Power Platform connectors are abstractions, not guarantees. At small scale, the typed SharePoint connector actions look like they cover everything a DMS needs. At three million files, several simply don’t work, and no amount of error handling around the typed call fixes that — the call itself fails. The right response isn’t a bigger abstraction on top of the broken one. It’s dropping to the primitive underneath it, working out which failures are transient and which are structural, and building the smallest possible bridge for the part genuinely impossible without one. Everything else in this system — the bulk operations, the recycle bin, the version restore, the combine-to-PDF — was downstream of getting that foundation right.


I’m a Power Platform consultant specializing in Dataverse and Power Apps Code Apps. Building something similar, or stuck on something similar? I’m happy to talk shop.


Share this post:

Previous Post
I deleted the server that merged my PDFs
Next Post
When Copilot Studio is over-engineering