diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index cf2a25a516a96790bc20cbb32246600d34aa97bb..1fe620c1de9bf943636378c055878bfa4b16aa3d 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -251,3 +251,11 @@ jobs:
     steps:
       - run: |
           echo "Didn't run due to conditional filtering"
+
+  pr-env:
+    needs: [build]
+    if: |
+      !cancelled() &&
+      contains(github.event.pull_request.labels.*.name, 'PR-Env')
+    uses: ./.github/workflows/pr-env.yml
+    secrets: inherit
diff --git a/.github/workflows/pr-env-close-unlabel.yml b/.github/workflows/pr-env-close-unlabel.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0d2ff1673b007d53fa7ac7b1f3ef0fa4539b19f1
--- /dev/null
+++ b/.github/workflows/pr-env-close-unlabel.yml
@@ -0,0 +1,15 @@
+name: PR Closed or Unlabeled
+
+on:
+  pull_request:
+    types: [ closed, unlabeled ]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  pr-env:
+    if: ${{ github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR-Env') || github.event.label.name == 'PR-Env' }}
+    uses: ./.github/workflows/pr-env-destroy.yml
+    secrets: inherit
diff --git a/.github/workflows/pr-env-destroy.yml b/.github/workflows/pr-env-destroy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..273fdb81d21a6926bb88fc82a3f36ebacdf58a27
--- /dev/null
+++ b/.github/workflows/pr-env-destroy.yml
@@ -0,0 +1,71 @@
+name: CI for PR Review ENV Destroy
+run-name: Destroying Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }}
+
+on:
+  workflow_call:
+
+jobs:
+  destroy_pr:
+    runs-on: ubuntu-latest
+    name: PR Review ENV Destroy
+    permissions:
+      id-token: write
+      contents: read
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@v4
+      - name: Tailscale
+        uses: tailscale/github-action@v2
+        with:
+          oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }}
+          oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }}
+          tags: tag:ci
+          version: 1.50.1
+          sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576
+      - name: Create OIDC Token
+        id: create-oidc-token
+        shell: bash
+        run: |
+          export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.PR_ENV_K8S_AUDIENCE }}"
+          IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value)
+          echo "::add-mask::${IDTOKEN}"
+          echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT
+      - name: Setup Kube Context
+        uses: azure/k8s-set-context@v2
+        with:
+          method: kubeconfig
+          kubeconfig: |
+            kind: Config
+            apiVersion: v1
+            current-context: default
+            clusters:
+            - name: default
+              cluster:
+                certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }}
+                server: ${{ secrets.PR_ENV_K8S_SERVER }}
+            users:
+            - name: oidc-token
+              user:
+                token: ${{ steps.create-oidc-token.outputs.IDTOKEN }}
+            contexts:
+            - name: default
+              context:
+                cluster: default
+                namespace: default
+                user: oidc-token
+      - name: Destroy PR Review ENV
+        run: |
+          kubectl delete metabase -n hosting-pr${{ github.event.number }} hosting-pr${{ github.event.number }}
+          kubectl delete ns hosting-pr${{ github.event.number }}
+      - name: Setup psql client
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y postgresql-client
+      - name: Drop app database if exists
+        run: |
+          PGPASSWORD='${{ secrets.PR_ENV_DB_PASSWORD }}' \
+          psql \
+          -h ${{ secrets.PR_ENV_DB_HOST }} \
+          -U ${{ secrets.PR_ENV_DB_USER }} \
+          -d ${{ secrets.PR_ENV_DB_NAME }} \
+          -c "DROP DATABASE IF EXISTS hosting_pr${{ github.event.number }};"
diff --git a/.github/workflows/pr-env-labeled.yml b/.github/workflows/pr-env-labeled.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6c1aa0ae9bd83591d43ecf79aafbda6fc85e4041
--- /dev/null
+++ b/.github/workflows/pr-env-labeled.yml
@@ -0,0 +1,17 @@
+name: PR
+
+on:
+  pull_request:
+    types: [ labeled ]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  pr-env:
+    if: ${{ github.event.label.name == 'PR-Env' }}
+    uses: ./.github/workflows/pr-env.yml
+    with:
+      wait_for_uberjar: true
+    secrets: inherit
diff --git a/.github/workflows/pr-env.yml b/.github/workflows/pr-env.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4af86f580ee554c36b3289055e0d95dda0a41da
--- /dev/null
+++ b/.github/workflows/pr-env.yml
@@ -0,0 +1,167 @@
+name: CI for PR Review ENV
+run-name: Building Dynamic PR Environment for ${{ github.ref_name }} by @${{ github.actor }}
+
+on:
+  workflow_call:
+    inputs:
+      wait_for_uberjar:
+        description: "Wait for uberjar build"
+        required: false
+        type: boolean
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    name: Build Metabase Docker image
+    timeout-minutes: 60
+    permissions:
+      id-token: write
+      contents: read
+      actions: read
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@v4
+      - name: Configure AWS credentials
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
+          role-session-name: GitHub_to_AWS_via_FederatedOIDC
+          aws-region: us-east-1
+      - name: Login to Amazon ECR
+        uses: aws-actions/amazon-ecr-login@v2
+        with:
+          registries: "${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}"
+      - name: Wait for uberjar
+        id: wait_for_uberjar
+        if: ${{ inputs.wait_for_uberjar == true }}
+        run: |
+          ## Get workflow run id for uberjar build
+          curl -Ls --output e2e-tests.json \
+          -H "Accept: application/vnd.github+json" \
+          -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
+          -H "X-GitHub-Api-Version: 2022-11-28" \
+          https://api.github.com/repos/${{ github.repository }}/actions/workflows/e2e-tests.yml/runs?head_sha=${{ github.event.pull_request.head.sha || github.sha }}
+          ID=$(jq -r '.workflow_runs[0].id' e2e-tests.json)
+          ## Wait for uberjar build to complete
+          while [ true ]; do
+            curl -Ls --output uberjar.json \
+            -H "Accept: application/vnd.github+json" \
+            -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
+            -H "X-GitHub-Api-Version: 2022-11-28" \
+            https://api.github.com/repos/${{ github.repository }}/actions/runs/${ID}/jobs?filter=latest
+            jq -r '.jobs[] | select(.name == "build (ee)") | .steps[] | select(.name == "Prepare uberjar artifact") | .status' uberjar.json | grep -q "completed" && break
+            echo "Waiting for uberjar build..."
+            sleep 10
+          done
+          echo "run_id=$(jq -r '.workflow_runs[0].id' e2e-tests.json)" >> $GITHUB_OUTPUT
+      - name: Retrieve uberjar artifact for ee
+        uses: actions/download-artifact@v4
+        with:
+          name: metabase-ee-${{ github.event.pull_request.head.sha || github.sha }}-uberjar
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          run-id: ${{ inputs.wait_for_uberjar && steps.wait_for_uberjar.outputs.run_id || github.run_id }}
+      - name: Move uberjar to bin/docker
+        run: |
+          jar xf target/uberjar/metabase.jar
+          mv target/uberjar/metabase.jar bin/docker/metabase.jar
+      - name: Build container
+        uses: docker/build-push-action@v6
+        with:
+          context: bin/docker/
+          platforms: linux/amd64
+          network: host
+          tags: ${{ secrets.PR_ENV_AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/metabase-enterprise:pr${{ github.event.number }}
+          push: true
+  deploy_pr:
+    needs: [ build ]
+    runs-on: ubuntu-latest
+    name: PR Review ENV
+    permissions:
+      id-token: write
+      contents: read
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@v4
+      - name: Tailscale
+        uses: tailscale/github-action@v2
+        with:
+          oauth-client-id: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_CLIENT_ID }}
+          oauth-secret: ${{ secrets.PR_ENV_TAILSCALE_OAUTH_SECRET }}
+          tags: tag:ci
+          version: 1.50.1
+          sha256sum: d9fe6b480fb5078f0aa57dace686898dda7e2a768884271159faa74846bfb576
+      - name: Create OIDC Token
+        id: create-oidc-token
+        shell: bash
+        run: |
+          export OIDC_URL_WITH_AUDIENCE="$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${{ secrets.PR_ENV_K8S_AUDIENCE }}"
+          IDTOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" -H "Accept: application/json; api-version=2.0" "$OIDC_URL_WITH_AUDIENCE" | jq -r .value)
+          echo "::add-mask::${IDTOKEN}"
+          echo "idToken=${IDTOKEN}" >>$GITHUB_OUTPUT
+      - name: Setup Kube Context
+        uses: azure/k8s-set-context@v2
+        with:
+          method: kubeconfig
+          kubeconfig: |
+            kind: Config
+            apiVersion: v1
+            current-context: default
+            clusters:
+            - name: default
+              cluster:
+                certificate-authority-data: ${{ secrets.PR_ENV_K8S_CERTIFICATE_AUTHORITY_DATA }}
+                server: ${{ secrets.PR_ENV_K8S_SERVER }}
+            users:
+            - name: oidc-token
+              user:
+                token: ${{ steps.create-oidc-token.outputs.IDTOKEN }}
+            contexts:
+            - name: default
+              context:
+                cluster: default
+                namespace: default
+                user: oidc-token
+      - name: Configure AWS credentials
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          role-to-assume: ${{ secrets.PR_ENV_IAM_ROLE }}
+          role-session-name: GitHub_to_AWS_via_FederatedOIDC
+          aws-region: us-east-1
+      - name: Setup psql client
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y postgresql-client
+      - name: Create app database if not exists
+        run: |
+          export PGPASSWORD='${{ secrets.PR_ENV_DB_PASSWORD }}'
+          export PGUSER=${{ secrets.PR_ENV_DB_USER }}
+          export PGHOST=${{ secrets.PR_ENV_DB_HOST }}
+          export PGDATABASE=${{ secrets.PR_ENV_DB_NAME }}
+          psql -tc "SELECT 1 FROM pg_database WHERE datname = 'hosting_pr${{ github.event.number }}'" \
+          | grep -q 1 \
+          || psql -c "CREATE DATABASE hosting_pr${{ github.event.number }}"
+      - name: Download Deployment YAML template
+        run: aws s3 cp s3://metabase-pr-env/metabase.yml.tmpl ./metabase.yml.tmpl
+      - name: Render Deployment YAML
+        uses: nowactions/envsubst@v1
+        with:
+          input: ./metabase.yml.tmpl
+          output: ./metabase.yml
+        env:
+          IMAGE_TAG: pr${{ github.event.number }}
+          PR_NUMBER: ${{ github.event.number }}
+          MB_PREMIUM_EMBEDDING_TOKEN: ${{ secrets.PR_ENV_MB_PREMIUM_EMBEDDING_TOKEN }}
+          SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+      - name: Deploy PR Review ENV
+        run: |
+          kubectl apply -f ./metabase.yml
+  preview_links:
+    runs-on: ubuntu-latest
+    needs: [ deploy_pr ]
+    steps:
+      - uses: marocchino/sticky-pull-request-comment@v2
+        with:
+          recreate: true
+          message: |
+            👋 Deploying a preview environment for commit ${{ github.event.pull_request.head.sha || github.sha }}.
+            ✅ Preview:
+                https://pr${{ github.event.number }}.coredev.metabase.com