Microsoft Power Platform, in depth: environments, Dataverse, connectors, DLP, solution ALM and API limits

By Sandeep Belgavi · 2026-10-03 · Category: Multi-Cloud (OCI/Azure/Other)
Advertisement
Power Appscanvas, model-drivenPower Automatecloud + desktop flowsCopilot StudioagentsPower BI / Pagesreports, sitesEnvironmentregion, security group, DLP scope, Managed EnvDataversetables, security roles, plug-ins, Web APIConnectorsstandard, premium, custom (OpenAPI)SaaS APIsM365, SalesforceYour APIsFunctions, on-prem gatewayMicrosoft Entra IDusers, groups, app usersidentity
Power Platform as an engineering system: every product runs inside an environment, stores data in Dataverse or reaches it through connectors, and authenticates through Entra ID. DLP policies and ALM both operate at the environment boundary.

Microsoft Power Platform is usually sold as low-code: drag controls onto a canvas, wire a flow, ship an app by Friday. That description is accurate and also the reason so many Power Platform estates become unmanageable. Underneath the designers there is a real distributed system with a multi-tenant database (Dataverse), an integration layer of hundreds of connectors, an identity model borrowed from Entra ID, rate limits that behave like any other SaaS API, and an application lifecycle built around a packaging format called a solution.

This article treats the platform the way an engineer would treat any runtime. It explains what each part is for, how data and identity flow through it, how to build and deploy a small but realistic application, how to call the same data from ordinary code, and where things break. The goal is that you can design a Power Platform workload that survives its author leaving the company, its first throttling incident and its first audit.

What the platform actually is

The platform is a family of products sharing one substrate. Power Apps builds two kinds of application: canvas apps, where you lay out every control and write formulas in Power Fx, and model-driven apps, which are generated from a Dataverse data model and give you forms, views and business process flows almost for free. Power Automate runs cloud flows (event or schedule triggered workflows that call connectors) and desktop flows (robotic process automation that drives Windows user interfaces where no API exists). Copilot Studio builds conversational agents. Power BI is the analytics product and Power Pages builds external-facing websites over Dataverse.

What they share matters more than what separates them. All of them live inside an environment, all of them authenticate users through Microsoft Entra ID, all of them reach data either in Dataverse or through connectors, and all of them can be packaged into solutions for deployment. When you design a workload, you are really making four decisions: which environment it lives in, where its data lives, which connectors it is allowed to use, and how it moves from development to production. The product choice usually follows from those.

Advertisement

Environments and Dataverse

An environment is the unit of isolation. It has a region, an optional Dataverse database, an optional security group that restricts who can be a member, and a type: production, sandbox, developer, trial, or the special default environment that every tenant gets and every licensed user can create things in. Apps, flows, connections and agents belong to exactly one environment and cannot see resources in another one except through connectors.

Dataverse is the platform's database. It stores data in tables with typed columns, relationships and choice lists, and wraps them in a security model of business units, security roles and record ownership by user or team. A role grants privileges such as read, write or delete at a depth of user, business unit, parent-child business unit or organisation, so the same table can expose a salesperson's own records to them and the whole region's records to a manager without application code. Server-side logic runs as plug-ins (.NET code in a sandbox, synchronous or asynchronous, registered on messages like Create and Update) or as low-code business rules and flows.

Dataverse also exposes everything through an OData v4 Web API. The practical rule: if a workload needs relational data, row-level security, auditing or more than a few thousand records, put it in Dataverse rather than in a SharePoint list or Excel file, which make poor systems of record.

Connectors, connections and whose credentials run

A connector is a typed wrapper around an API: an OpenAPI description plus authentication metadata. Microsoft and partners publish hundreds of them, split into standard connectors (for example SharePoint, Outlook, Teams) and premium connectors (for example SQL Server, HTTP, Dataverse in many licensing contexts, and every custom connector), and premium usage changes what each user must be licensed for. A custom connector is one you define from your own OpenAPI document, which is how a flow calls your Azure Function or internal REST service. Services that sit on a private network are reached through the on-premises data gateway, an agent you install inside the network that relays requests outbound.

The concept engineers most often miss is the connection: an authenticated instance of a connector, owned by someone. When a maker builds a flow with the SharePoint connector, the flow runs with that maker's SharePoint credentials unless designed otherwise. If they leave and their account is disabled, every flow running on their connections fails. Production workloads should run on connections owned by a service account or, where the connector supports it, a service principal, and solutions should use connection references so the binding to a concrete connection is made per environment at import time rather than baked into the flow definition.

Canvas apps differ: most connectors run with the signed-in user's own credentials, so sharing an app does not share its data.

Governance: DLP policies and Managed Environments

Governance is where a hobby estate becomes an operable one. Data loss prevention (DLP) policies classify connectors into three groups: Business, Non-business and Blocked. An app or flow may combine connectors from the Business group or from the Non-business group, but not across them, and may not use Blocked connectors at all. Policies apply at tenant scope or to specific environments, and several policies can apply at once with the most restrictive outcome winning. Some connectors also support endpoint filtering and action-level control, for example allowing the HTTP connector only to specific hosts. When a new policy makes an existing flow non-compliant, the flow is suspended, which is good for safety and bad if nobody tested the policy first.

Managed Environments is a premium set of governance features switched on per environment: limits on sharing canvas apps, usage insights, solution checker enforcement on import, and the ability to target the environment with platform pipelines. Pair it with an environment strategy: a locked-down default environment for personal productivity, and dedicated development, test and production environments per workload.

Worked example: an expense approval workload

Take a concrete workload: employees submit expenses, managers approve them, approved claims are posted to a finance API. In Dataverse create an Expense table with Title, Amount (currency), Submitted By (lookup to user), Manager (lookup to user), Stage (choice: Draft, Submitted, Approved, Rejected, Posted) and Finance Reference (text). Grant employees a role with user-level read and write on Expense and managers a role with business-unit-level read.

The canvas app gives employees a form. Its submit button writes a row with Power Fx:

Patch(
    Expenses,
    Defaults(Expenses),
    {
        Title: txtTitle.Text,
        Amount: Value(txtAmount.Text),
        Manager: LookUp(Users, 'Primary Email' = txtManager.Text),
        Stage: 'Stage (Expenses)'.Submitted
    }
);
Notify("Expense submitted", NotificationType.Success)

A cloud flow uses the Dataverse trigger When a row is added, modified or deleted, filtered to the Stage column so it does not fire on unrelated edits, and with a trigger condition so it only runs when Stage is Submitted. It starts an approval assigned to the manager, waits, and updates the row to Approved or Rejected. A second flow fires when Stage becomes Approved, calls the finance API through a custom connector whose OpenAPI document points at an Azure Function, writes the returned reference into Finance Reference and sets Stage to Posted.

Three details make this robust. First, the posting flow is idempotent: the Function receives the Dataverse row ID and refuses to post the same ID twice, because flows retry and a timeout after a successful call would otherwise double-post. Second, the Stage column is a state machine and each flow acts on one transition, so no flow can retrigger itself. Third, every component (table, app, both flows, the custom connector and its connection reference, an environment variable holding the Function base URL) lives in one solution, so the whole thing moves between environments as a unit.

One more canvas-app trap belongs here: delegation. A formula like Filter(Expenses, Amount > 500) is translated into a server-side query when the data source supports that operation. When it cannot be delegated, the app downloads only the first 500 rows (configurable up to 2,000) and filters locally, silently returning incomplete results. Treat delegation warnings in the editor as bugs.

Calling Dataverse from code, and service protection limits

Because Dataverse speaks OData, ordinary code can read and write the same tables. Register an app in Entra ID, add it to the environment as an application user with a security role, and use the client credentials flow. The scope is the environment URL followed by /.default.

import time, msal, requests

ORG = "https://contoso.crm.dynamics.com"
app = msal.ConfidentialClientApplication(
    CLIENT_ID, authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET)
token = app.acquire_token_for_client(scopes=[f"{ORG}/.default"])["access_token"]
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {token}",
                  "Accept": "application/json",
                  "OData-Version": "4.0",
                  "Prefer": "odata.maxpagesize=500"})

def get(url):
    while True:
        r = s.get(url)
        if r.status_code == 429:                  # service protection limit
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        return r.json()

url = f"{ORG}/api/data/v9.2/accounts?$select=name,revenue&$filter=revenue gt 1000000"
while url:
    page = get(url)
    for row in page["value"]:
        print(row["name"], row["revenue"])
    url = page.get("@odata.nextLink")           # server-driven paging

The retry loop is not optional. Dataverse enforces service protection limits per user, evaluated independently on each web server serving the environment. The documented defaults are 6,000 requests and 1,200 seconds of combined execution time within a 300-second sliding window, and 52 or more concurrent requests. Exceeding them returns HTTP 429 with a Retry-After header. Plug-in operations triggered by your request do not count as extra requests, but their execution time is charged to the request that triggered them. Start at a modest rate, raise parallelism until 429s appear, and let Retry-After pace you; large batches mostly turn a request-count problem into an execution-time problem. Daily per-licence request entitlements are a separate, licensing matter.

Application lifecycle with solutions and the pac CLI

A solution is a package of components with a publisher and a prefix. Development happens in an unmanaged solution in a development environment; downstream environments receive a managed solution, which cannot be edited in place and can be cleanly upgraded or uninstalled. Components from multiple managed solutions stack in layers, and an unmanaged change made directly in production sits on top of every managed layer and silently masks later upgrades. That is why production must be read-only for humans.

The Power Platform CLI turns this into a source-controlled pipeline. Export, unpack into readable files, commit, and in CI pack and import with a deployment settings file that binds connection references and environment variables for the target environment:

# developer loop: capture the dev solution into source control
pac auth create --environment https://contoso-dev.crm.dynamics.com
pac solution export --name ExpenseApp --path out/ExpenseApp.zip
pac solution unpack --zipfile out/ExpenseApp.zip --folder src/ExpenseApp

# CI: build the managed artifact and a settings template
pac solution export --name ExpenseApp --managed --path out/ExpenseApp_managed.zip
pac solution create-settings --solution-zip out/ExpenseApp_managed.zip --settings-file test.settings.json

# CD: deploy to test with environment-specific bindings
pac auth create --environment https://contoso-test.crm.dynamics.com
pac solution import --path out/ExpenseApp_managed.zip --settings-file test.settings.json --async

If you do not want to run your own CI, pipelines in Power Platform provide the same flow inside the service: an admin defines stages from development to test to production, targets are Managed Environments, and makers request deployments that admins can gate with approvals. Dataverse also has native Git integration that commits solutions in a YAML source format. Either way, version the solution and keep per-environment settings files in the repository.

Failure modes that recur

Most incidents in Power Platform estates come from a short list.

Trade-offs: when low-code fits

Power Platform is strongest for internal, form-and-workflow applications over business data, where time to value and maintainability by a mixed team matter more than custom user experience or extreme throughput. It is weakest for high-volume transaction processing, complex algorithms, low-latency public APIs and anything that needs fine control over the runtime. The healthy pattern is fusion: low-code for the user interface, data model and orchestration, pro-code behind custom connectors for heavy logic.

NeedLow-code fitPrefer pro-code when
Internal CRUD app with approvalsExcellent: model-driven or canvas plus flowsUX must be pixel-perfect or offline-first at scale
Integration between SaaS systemsGood: connectors and cloud flowsVolumes reach millions of events per day or need ordering guarantees
Business logicFine for rules and simple calculationsLogic is algorithmic, needs unit tests and code review discipline

Price premium connectors and Dataverse capacity before building: licensing is a design input.

What to do next

  1. Draw your environment strategy: restrict the default environment with a strict DLP policy and create dev, test and prod environments per workload.
  2. Write DLP policies that place your internal connectors in Business and block risky ones, and run impact analysis before applying them.
  3. Model one workload in Dataverse with security roles instead of SharePoint lists, and enable auditing on sensitive tables.
  4. Put every component in a named solution with your own publisher prefix, using connection references and environment variables.
  5. Move production flows onto service-account connections and add co-owners.
  6. Set up pac solution export, unpack and import --settings-file in CI, or configure platform pipelines with approvals.
  7. Make every integration client honour HTTP 429 and Retry-After, and load-test bulk jobs against a sandbox.
  8. Read the related articles on Microsoft Entra ID, Azure Functions for custom connector back ends, Azure DevOps for pipelines and Azure SQL Database for data that outgrows Dataverse.
Key takeaway: Treat Power Platform as a runtime, not a toy: isolate workloads in environments, keep data in Dataverse with security roles, govern connectors with DLP, run production on service-owned connections, ship everything as managed solutions through a pipeline, and make every client honour 429 and Retry-After.