The store you pick is the one you live with
Most Power Apps builds start with a decision nobody writes down. Screens get discussed, fields get agreed, and somebody picks a data source in the first ten minutes. That pick decides how much of the app's behaviour you can rely on three years later, and it is the part of the design that costs real money to change.
The honest answer to Dataverse or SharePoint lists is that both are correct some of the time. A list already sits in the tenant, the people who run your team sites can run it, and plenty of working apps never need more. Dataverse buys enforcement that holds whichever client wrote the row, and charges you in licensing, capacity and governance effort.
One difference deserves more weight than the rest. A list that has grown past a certain size will answer some queries with fewer records than match, and it does that without an error, a warning or a failed run. Wrong answers arrive looking the same as right ones.
Lists are the correct answer more often than architects admit
Start with what lists get right, because people who spend their days in Dataverse skip past it. A list costs nothing extra in a tenant that already pays for Microsoft 365. Version history is on, the data is readable in a browser without a maker licence, and somebody in the business can fix a typo without raising a change.
That covers a lot of genuine work. A reference table of cost centres that changes twice a year. A request log where every row stands alone and nobody minds who reads it. Point Power Apps at either of those and spend the saved budget where the business will notice.
Lists also fail where people can see the failure. A missing column throws an error. A deleted row sits in the recycle bin with a name against it. Teams recover from that on their own. The problems worth your attention are the ones nobody gets told about.
Delegation hands you wrong answers instead of errors
Power Apps does not drag a whole data source into the client. It translates your filter, sort and lookup into something the server can run, then brings back the matching rows. Anything it can translate is delegable. Anything it cannot gets handled the other way round, with the app pulling a capped page of rows and applying the condition locally.
That cap is a setting, and the number matters far less than what happens either side of it. While the list holds fewer rows than the cap, local filtering returns what the server would have returned, so everything looks correct in testing. Past the cap, the app filters a slice rather than all of the rows, and the gallery shows a subset of the records that match. No error appears, and a flow reading the same list reports success.
SharePoint makes this sharper than it needs to be, because the set of operations that delegate against a list is narrow and full of exceptions. Several column types and operators drop out of delegation, and the ones that catch people are the lookup and calculated columns you reach for when a list starts carrying relationships.
The permit register that hid its newest rows
I ran into this on a facilities project a couple of years ago. A contractor permit register, built on a list by a capable analyst in the estates team, live for eighteen months across eleven sites. Each site had a screen of open permits, filtered on site and status. Everyone liked it.
Then the managers at the two busiest sites said the counts felt low. The permits were all there in the browser and missing in the app. The filter ran against a choice column and a lookup to the site list, the lookup put the query outside delegation, and the app had been filtering a capped page of a list that kept growing. The oldest rows survived the slice and the newest permits fell off the end.
Nobody had done anything wrong. The delegation warning had appeared in the studio eighteen months earlier, when the list held a few hundred rows and the results were correct, so it got dismissed as noise. Moving the register to Dataverse took three weeks, most of it spent reconciling permits raised twice because the app had hidden the first one.
What a list will never enforce, whatever you build on top
Past delegation, the gaps are structural. A list holds rows, and a lookup column holds a pointer with nothing stopping anyone deleting the row it aims at. Referential integrity and cascade behaviour on delete become things you write yourself in flows and hope every future maker reproduces.
Security stops at the row, and it stops awkwardly. Item level permissions get fragile at volume and around inheritance, and there is no way to tell SharePoint that one column of a row is readable by two people while the rest is readable by twenty. Dataverse does both below the app, and the explainer on what Dataverse actually is covers the mechanics.
Server-side rules are the other gap. Validation written into a canvas screen holds for that screen. It does not hold for the nightly import, the partner API call, the second app somebody builds next year, or an agent nobody has proposed yet. A rule that must survive every route into the data has to live under all of them, which is the argument in where business logic should live.
The honest cost of moving to Dataverse
Dataverse sits behind premium licensing, and the shape of that licensing changes often enough that you should price it against current terms rather than what you remember from the last project. Get a real number before the design review. An app saving two hours a week for six people rarely survives a per user premium line.
Storage is the second surprise, and row counts are not what drives it. Attachments, audit history and log tables grow faster than anyone forecasts, and capacity carries a charge. The guide on Dataverse capacity before it surprises you is worth reading while you are still deciding.
The third cost never appears on an invoice. A Dataverse build wants environments, a solution, a repeatable way of moving changes, and a named owner. Skip that and you edit production directly, which throws away most of the reason you paid for a governed store. The low-code app that outgrew its maker tells the same story from the people side.
One test before you build, one test for what you already have
Before the first screen exists, put four questions to whoever is paying. How many rows will this hold in three years rather than this month? Does any rule have to hold when a record arrives from an import, an API or a second app? Will two groups share a table while one must not see the other's rows? Does one record point at another in a way that matters if somebody deletes one side?
One yes and a modest row count still leaves you in list territory, and you should take it. Two or more, or a row count heading past the delegation cap under normal use, and you are building on Dataverse whether you pay now or pay to migrate later. The row count question carries the most weight, because people answer it with today's number.
For something already on a list, the second test takes an afternoon. Open the app in the studio, find every delegation warning, count the rows in the lists those warnings touch, then compare the app's filter result against the same filter run in the browser. A gap means the app has already been wrong, and the only question is how long for.
Run that comparison on your busiest list this week. If the numbers match, write the row count next to the warning and set a reminder for six months. If they do not, stop adding features and count the duplicate records the gap has already created.



