Code App team setup: environments, data and app access, ALM and Git branching

Code App team setup: access, ALM and Git. Power Apps for the app, Power Platform for the Dev, Test and Prod environments, GitHub or GitLab for the repo, on recursion.no's black and green design

A project team is building a Power Apps code app for a department. The department gets its own environments, and the app is planned with its own custom Dataverse tables. During development, only the team should be able to build and see those tables, as several other projects in the future will co-exist on the same environment. In test and production, users should reach the app and the tables behind it, and nothing else.

Below is the recommended setup I usually have in mind when advising customers, but demonstrated in my own personal tenant. First, let me explain the whole picture, then the steps in details, then how two or more developers work together day to day over time.

The whole picture

Overview: an Entra security group per environment, a group team with roles inside Dev, Test and Prod, one solution moved by the pipeline, and code pushed from main into Dev

By following best practice setup; users needs a secured way into the environment, a security role access on the data, and access to the app before the app works for them. Each comes from different settings, and most access problems come from mixing them up, this is why I feel it is important for readers to grasp the concept of this setup below:

  • Who can enter the environment? A Microsoft Entra security group linked to the environment.
  • What can they do with the data? A Dataverse security role, given to that group through a group team (defined from environment settings).
  • Who can open the app? Sharing the app with the same group from Code Apps CLI.

Dev, Test and Prod each get all of this, with a different group in each. The developers’ group gets maker roles in Dev. The testers’ group and the users’ group get the app’s own custom role in Test and Prod.

The code goes from a Git branch through a pull request to main, and from main into Dev with pa app push. The tables, the app role and the app itself go from Dev to Test to Prod in one solution, deployed by a Power Platform pipeline. Nobody edits anything in Test or Prod by hand.

Setting it up

Step 1. Create Dev, Test and Prod

Give the department a Dev, a Test and a Prod environment. Dev is where the team builds and changes tables. Test is where testers run the managed solution with the same security role as the end users. Prod is where the department works.

Power Platform admin center: the environment list with dept-dev as a Developer environment and dept-prod as a managed Trial environment

An environment is the security boundary in Power Platform. A license alone does not give anyone access to an environment that is not the default environment, so each environment is closed until you open it.

I just want to note that in my tenant; Dev is a developer environment and Prod is a trial, because the tenant has no paid Dataverse capacity. The screenshots in this post come from those two, and I point out where they behave differently from a sandbox or a production environment.

Before developing any code app, check these pre-requisited settings in the Power Platform admin center (PPAC) for Dev, Test and Prod:

  1. Code apps are turned on under Settings > Product > Features > Power Apps code apps.
  2. The Dataverse connector is allowed by the data loss prevention (DLP) policy that covers the environment.
  3. Code App users have a Power Apps Premium license, an App Pass or pay-as-you-go. Dataverse makes the app premium, and the documentation lists one of these as a requirement for anyone who runs a code app. Without one of these, the player shows “Start a Power Apps trial?” instead of the app.
Environment settings, Product, Features: the Enable code apps toggle turned on, with Save highlighted
Power Platform admin center, Security, Data policies: the tenant data policy that covers the environments
The Power Apps player in the production environment asking the user to start a Power Apps trial because the account has no Power Apps plan

Step 2. Create a security group per environment and link it

The groups decides who can enter the environment. Link a Microsoft Entra security group to each environment. Only members of the group can be added as users in that environment. Power Platform administrators keep access to every environment regardless of the group.

Microsoft Entra admin center: the three security groups rec-dept-test, rec-dept-dev and rec-dept-prod
EnvironmentSecurity groupMembers
Devrec-<dept>-<env>-devThe developers
Testrec-<dept>-<env>-testThe developers and the testers or super-users
Prodrec-<dept>-<env>-prodThe app users
Edit security group on the dept-prod environment: search for rec-dept-prod, select it, Done
Environment details for dept-prod with Security group set to rec-dept-prod

A developer environment cannot have a security group. The Power Platform admin center shows “Can’t be assigned to this environment” when you try. If your Dev is a developer environment, control access there with the group team and its roles in the next step, and link the groups to Test and Prod.

Edit details on a Developer environment: Security group shows Can't be assigned to this environment

Step 3. Give the developers maker roles in Dev

The roles decide what a developer can build. A developer needs Environment Maker and System Customizer in Dev. Environment Maker lets them create apps and connections. System Customizer lets them create and change tables. Environment Maker has no privileges on the environment’s data, so a developer with only that role can publish a code app that cannot read its own tables.

System Customizer gives full access to all custom tables by default. Full access is fine in Dev, where the data is test data and the developers own the schema. Do not give developers System Administrator. They do not need it to build, and it gives them read access to most records in the environment.

Assign the roles to a Dataverse group team linked to rec-<dept>-<env>-dev, not to each developer. When someone joins or leaves the team, you change the Entra group, and the group team updates its members in Dataverse.

New team in dept-dev: team type Microsoft Entra ID Security Group, group name rec-dept-dev, membership type Members
Manage security roles for the rec-dept-dev team with System Customizer ticked
The Teams list in dept-dev with rec-dept-dev as a Microsoft Entra ID Security Group team

Step 4. Create the solution

The solution is the box that the pipeline moves from Dev to Test to Prod. Create one solution for the app with its own publisher prefix, and set it as the preferred solution in Dev. Everything the app needs goes in it: the tables, the app role, the environment variables, the connection references and the code app itself.

New solution Dept Code App with publisher Recursion Dept, prefix dept, and Set as your preferred solution ticked

Step 5. Build the tables and the app role in the solution

The app role is what users get in Test and Prod. Create a custom security role for the app, for example <App> User. Copy it from App Opener, which Learn describes as the role with the minimum privileges to run an app. Then add privileges on the app’s custom tables only, at the level each table needs:

  • Organization when everyone in the department should see every record.
  • User when people should only see the records they own.
Create New Role in the solution: role name Dept App User with App Opener privileges included
Permission Settings for the Dept Case table: the Full Access preset selected, then Save
Security role Dept App User with Organization level on every privilege for the Dept Case table

Build the role in Dev inside the solution. The pipeline deploys it to Test and Prod together with the tables, so the privileges are identical in every environment.

Solution objects: the Dept App User security role and the Dept Case table

Step 6. Push the app into the solution

Set the app up and build it locally first, with the front-end framework your organisation already uses. A code app is a Vite app, so React, Vue, Svelte, Solid and Preact all work, and I showed the recipe for each of them in Power Apps Code App: Pick Any Vite Framework. When the app runs locally against the tables in Dev, publish it:

pa app push publishes the app to Dev and adds it to the preferred solution. The first push needs no solution ID when the preferred solution is set. After the push, the solution holds the tables, the role and the app, and that is the unit the pipeline deploys.

Terminal: pa app push with a solution ID adds the app to the solution and returns the play URL
Solution objects after pa app push: the Dept Case App code app added next to the role and the table

Step 7. Deploy with a pipeline

Use Power Platform Pipelines to deploy the solution from Dev to Test and from Test to Prod. Pipelines import it as a managed solution, so nobody can edit the tables or the role in Test or Prod. Developers do not need a maker role in Test or Prod.

Create pipeline from dept-dev with dept-prod selected as the target environment
Deploying Solution summary: pipeline Dept Dev to Prod, stage dept-prod, version 1.0.0.1, with Deploy highlighted
The dept-prod stage after the run: last successfully installed version 1.0.0.1

Pipelines only list target environments in the same geography as the pipelines host, unless Cross-Geo Solution Deployment is on. The admin center places a new environment by available capacity, so it can end up in another region than the host. In my tenant the new environments were placed in Switzerland while the host is in Norway, and dept-prod only appeared as a target after I turned the setting on under Manage pipelines and Advanced Settings.

Deployment Pipeline Configuration, Advanced Settings: the Cross-Geo Solution Deployment toggle set to Yes, then Save

If the app reads a SharePoint list or anything else that differs per environment, add the data source with an @envvar: reference. The value comes from the target environment on import, and the code stays the same:

pa app add data-source --connector shared_sharepointonline \
  --connection-id <connection-id> \
  --dataset "@envvar:<prefix>_SiteUrl" --table "@envvar:<prefix>_ListName"

Step 8. Give the users the role and the app in Test and Prod

The group team and the role assignment are not solution components, so this step is done by hand once per environment. Create a Dataverse group team for rec-<dept>-<env>-test in Test and one for rec-<dept>-<env>-prod in Prod, and assign <App> User to each.

Manage security roles for the rec-dept-prod team in dept-prod with Dept App User ticked

Then share the app in Power Apps with the same group. The Share dialog for a code app has the same options as for a canvas app: User, Co-owner and More security roles, where <App> User is listed. I use the group team instead, so the role follows the group membership and not the sharing.

Share dialog for the code app with the security group rec-dept-prod selected and the access options User, Co-owner and More security roles

Sharing gives access to the app, the security role gives access to the data, and a user needs both. After this step, adding a person to the Entra group opens the environment, gives them the app role through the group team, and gives them the app. Removing them takes all of it away.

When the environment gets a second app with other users, create a group per app, for example rec-<dept>-<app>-users. Link it to a group team with that app’s role and share the app with it. The environment group then only opens the environment. For one app, one group per environment is enough.

Step 9. Test the role with a test user

A code app runs under the signed-in user’s own permissions. A user without <App> User can open the app and gets an error on the first call to Dataverse. Test the role before go-live:

  1. Sign in as a test user who is only a member of rec-<dept>-<env>-test.
  2. Open the app and create, read, update and delete a record in each table.
  3. Open a table that is not part of the app in the same environment. The user should get no access.

Day to day for the developers

Only ‘main’ branch publishes to Dev

Code apps do not support Dataverse Git integration (as Managed Apps do). The source code is stored in your own GitHub or GitLab repository. The tables, the role and the solution are stored in the Dev environment. A pull request covers the code. It does not cover a column that someone added in Dev.

pa app push replaces the published version of the app in the environment. If two developers push from two branches to the same app in Dev, the last push wins, and the other person’s work disappears from the app until they push again. So I recommend that only one job publishes to Dev: the GitHub Actions or GitLab CI job that runs when a pull request is merged to main.

Each developer runs the app on their own machine with pa app run against the tables in Dev, so they work with real data without publishing anything.

That job has no person who can sign in through a browser, so it needs its own identity in Microsoft Entra. Register an application for it in Entra. The CLI calls this identity a service principal. Access to the environment alone does not let it update an existing app, and it cannot share the app with itself. So before the first push, the maker who owns the app shares it with the service principal once, with edit access, from the app folder. Use the Object ID from Enterprise applications in Entra, not the one from App registrations:

pa app share --principal <enterprise-application-object-id> --access edit

After that, the job on main builds and publishes without a browser:

# GitHub Actions job on push to main (GitLab CI uses the same variables)
env:
  PA_CLI_USE_SP_AUTH: "true"
  PA_CLI_SP_CLIENT_ID: ${{ secrets.PA_CLIENT_ID }}
  PA_CLI_SP_CLIENT_SECRET: ${{ secrets.PA_CLIENT_SECRET }}
  PA_CLI_SP_TENANT_ID: ${{ secrets.PA_TENANT_ID }}
steps:
  - run: npm ci && npm run build
  - run: npx pa app push --non-interactive
# .gitlab-ci.yml: the same job on the main branch
publish:
  stage: deploy
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  variables:
    PA_CLI_USE_SP_AUTH: "true"
    PA_CLI_SP_CLIENT_ID: $PA_CLIENT_ID
    PA_CLI_SP_CLIENT_SECRET: $PA_CLIENT_SECRET
    PA_CLI_SP_TENANT_ID: $PA_TENANT_ID
  script:
    - npm ci && npm run build
    - npx pa app push --non-interactive

Store the client secret as an Actions secret in GitHub or as a masked and protected CI/CD variable in GitLab, never in the repository.

A feature from branch to merge

Below is what a developer types for the feature “register a new case” on the existing app, in this order. pa auth login opens the browser once, and the CLI keeps the account signed in for the commands that follow.

git switch main
git pull
npm ci
pa auth login --account <[email protected]>
pa auth status
Terminal: git switch main, git pull, npm ci, pa auth login and pa auth status, ending with Signed in as the developer

Create the branch from main and start the app against the tables in Dev. The URL labelled Local Play opens the app in the browser with your own account:

git switch -c feature/register-case
pa app run
Terminal: git switch -c feature/register-case and pa app run, with the Local Play URL from the Power Apps Vite plugin

The feature needs a Location on each case, and the Dept Case table has no such column yet. This is the app before the change, running locally against Dev:

The Dept Case App running locally in the Power Apps player: a title field, the Register case button and one case, Broken printer on floor 3

The person who owns the table adds the column Location to Dept Case in Dev, in the maker portal:

New column panel on the Dept Case table in dept-dev: display name Location, data type Single line of text, format Text

Then regenerate the model and the service so the code sees the column:

pa app refresh data-source --name deptcases
Terminal: pa app refresh data-source --name deptcases reports deptcases refreshed successfully

A new table is added with pa app add data-source --connector dataverse --table <table-logical-name>. Both commands change power.config.json and the files under src/generated, and both belong in the commit.

Add the field to the form and to the list. Vite reloads the app in the browser while pa app run is running, so the result shows up at once:

The Dept Case App after the change: a Location field next to the title, and the new case Water leak in the kitchen with Floor 2, east wing

Build before you push so you see the same errors as the pull request check does:

npm run build
git add -A
git commit -m "Register a new case"
git push -u origin feature/register-case
Terminal: npm run build, git add, git commit and git push of the feature branch, with GitHub offering to create a pull request

Open the pull request from the terminal or in the browser. Say in the description which columns the code expects in Dev. On GitLab the command is glab mr create --fill:

gh pr create --fill
Terminal: gh pr create --fill opens pull request 1 for feature/register-case into main

After the review and the green build, merge. The job on main publishes to Dev. Then clean up on your machine:

git switch main
git pull
git branch -d feature/register-case

Nobody runs pa app push on their own machine.

Branches and pull requests

Short feature branches from main work well for a small team. A branch exists for a few days, ends in a pull request and is deleted after the merge. Protect main so a merge needs one approved review and a green build. GitLab calls the pull request a merge request, and a protected main with an approval rule gives the same result.

Terminal: the GitHub API shows branch protection on main with one required review, code owner reviews, the build check and no force push

Split the work by feature, not by layer. One developer takes “register a new case” from table to screen, and the other takes “case overview”. Two people who share the same screen and the same service file get merge conflicts often.

Give these parts of the project extra care:

  • power.config.json binds the repository to the app and its data sources. Commit it, and let pa app add data-source change it rather than editing it manually. Add it to CODEOWNERS so one person reviews every change. (On GitLab the same file only enforces a review in the paid Premium and Ultimate tiers. On Free, require one approval on every merge request instead.)
  • The generated services and models come from the CLI. When two branches add data sources, do not resolve the conflict line by line. Merge one branch, then run pa app add data-source or pa app refresh data-source again on the other.
  • Table changes happen in Dev, outside Git. Let one person own schema changes in each sprint, and say in the pull request which columns the code expects. A reviewer can then check that the column exists in Dev before approving.

Pair programming fits best where the code and the schema change together. One developer adds the column in Dev while the other writes the code that uses it, and both review the pull request against the same environment.

When the shared Dev environment is not enough

If two developers often change tables at the same time, give each of them a developer environment. Each one imports the solution as unmanaged and builds there. When a change is ready, the developer exports the solution from their environment and imports it into the shared Dev environment. The pipeline deploys from shared Dev as before. Each schema change then costs an extra export and import, so I would only do it when collisions in Dev become a regular problem.

If you want the table definitions in Git as well, export the unmanaged solution from Dev with pac solution export and unpack it with pac solution unpack into a folder in the same repository. You then get a history of the schema next to the code, but you still deploy with the pipeline from Dev.

Status today

The recommendation to publish only from main is my own conclusion from how pa app push behaves. The rest is documented on Microsoft Learn as of October 2026. Check the code apps ALM page before you set this up, because Microsoft changes the CLI and the ALM support often.

Got a setup that works better for your team? I’d love to hear about it. Drop a comment or find me on LinkedIn.


Posted

in

,

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *