Repository: pwrdrvr/microapps-core (language: python)

Issue:
Demo - CDK - Allow bring your own assets
- [x] Allow passing in a CloudFront distro
- [x] Add static helper methods
  - [x] CloudFront - Add Behaviors for the API Gateway and S3 Origins with optional rooting directory
  - [x] API Gateway - Add Router default or rooted default path integration?
  - [x] S3 - None necessary as creating custom buckets is straightforward
- [x] Allow passing in an API Gateway HTTPv2 to CloudFront
- [x] Allow passing in an existing S3 bucket for Prod and Staging to CloudFront and Svcs
- [ ] Create test deployment using custom assets and static helpers


Discussion / hints:
This was mostly completed in #199 

The remaining tasks are to add the static helper functions to configure CloudFront and API Gateway routes on user-provided resources.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index af5e08f7..ea0af455 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,6 +67,7 @@ jobs:
         deployName:
           - microapps-core
           - microapps-basic
+          - microapps-basic-prefix
     name: deploy-${{ matrix.deployName }}
     concurrency:
       group: deploy-${{ matrix.deployName }}-${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -91,6 +92,14 @@ jobs:
         run: |
           echo 'PACKAGE_VERSION='${PACKAGE_VERSION_ORIG}${{ needs.build.outputs.prSuffixPackageVersion }} >> $GITHUB_ENV
 
+      - name: Set Prefix Path
+        run: |
+          if [ "${{ matrix.deployName }}" == "microapps-basic-prefix" ]; then
+            echo 'PREFIX=/prefix' >> $GITHUB_ENV
+          else
+            echo 'PREFIX=' >> $GITHUB_ENV
+          fi
+
       - name: Use Node.js 16
         uses: actions/setup-node@v2
         with:
@@ -115,6 +124,12 @@ jobs:
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
           aws-region: ${{ env.AWS_REGION }}
 
+      - name: Stitch Prefix into Release App
+        run: |
+          if [ -n "${PREFIX}" ] ; then
+            npx replace-in-file "/\/${RELEASE_APP_NAME}\/${RELEASE_APP_PACKAGE_VERSION}/g" ${PREFIX}/${RELEASE_APP_NAME}/${RELEASE_APP_PACKAGE_VERSION} --configFile=.release-replace.config.js --isRegex
+          fi
+
       - name: Deploy CDK Stack
         run: npx cdk deploy --context @pwrdrvr/microapps:stackName=${{ matrix.deployName }}-ghpublic --context @pwrdrvr/microapps:deployDemoApp=true --context @pwrdrvr/microapps:deployReleaseApp=true --hotswap --require-approval never ${{ matrix.deployName }}
 
@@ -134,11 +149,11 @@ jobs:
       - name: Test Demo App
         run: |
           echo Testing App Frame Loading
-          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/
           echo Testing App HTML Loading
-          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
           echo Testing App Method Invocation
-          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/someMethod
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/someMethod
 
       - name: Publish Release App to MicroApps
         run: |
@@ -147,11 +162,11 @@ jobs:
       - name: Test Release App
         run: |
           echo Testing App Frame Loading
-          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${RELEASE_APP_NAME}/
           echo Testing App HTML Loading
-          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${RELEASE_APP_NAME}/${RELEASE_APP_PACKAGE_VERSION}
           echo Testing App Method Invocation
-          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/api/refresh/${DEMO_APP_NAME}
+          curl --fail https://${EDGE_DOMAIN}${PREFIX}/${RELEASE_APP_NAME}/${RELEASE_APP_PACKAGE_VERSION}/api/refresh/${DEMO_APP_NAME}
 
   delete-apps:
     if: github.event_name == 'pull_request' && false
diff --git a/.github/workflows/pr-closed.yml b/.github/workflows/pr-closed.yml
index c4123966..4b4fff3f 100644
--- a/.github/workflows/pr-closed.yml
+++ b/.github/workflows/pr-closed.yml
@@ -27,6 +27,7 @@ jobs:
         deployName:
           - microapps-core
           - microapps-basic
+          - microapps-basic-prefix
     name: deploy-${{ matrix.deployName }}
     permissions:
       contents: read
diff --git a/.release-replace.config.js b/.release-replace.config.js
new file mode 100644
index 00000000..932cfce0
--- /dev/null
+++ b/.release-replace.config.js
@@ -0,0 +1,4 @@
+// https://www.npmjs.com/package/replace-in-file
+module.exports = {
+  files: 'node_modules/@pwrdrvr/microapps-app-release-cdk/lib/.serverless_nextjs/**/*,node_modules/@pwrdrvr/microapps-app-release-cdk/lib/.static_files/**/*',
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0f358924..55f292c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
         "jest-dynalite": "^3.4.4",
         "prettier": "^2.2.1",
         "projen": "~0.34.20",
+        "replace-in-file": "^6.3.2",
         "rimraf": "^3.0.2",
         "rollup": "^2.39.0",
         "rollup-plugin-node-externals": "^2.2.0",
@@ -12824,9 +12825,9 @@
       }
     },
     "node_modules/glob": {
-      "version": "7.1.7",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
-      "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -18709,6 +18710,50 @@
         "node": ">=0.10"
       }
     },
+    "node_modules/replace-in-file": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz",
+      "integrity": "sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.2",
+        "glob": "^7.2.0",
+        "yargs": "^17.2.1"
+      },
+      "bin": {
+        "replace-in-file": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/replace-in-file/node_modules/yargs": {
+      "version": "17.3.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
+      "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/replace-in-file/node_modules/yargs-parser": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
+      "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/request": {
       "version": "2.88.2",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -20373,13 +20418,13 @@
       }
     },
     "node_modules/string-width": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
-      "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.0"
+        "strip-ansi": "^6.0.1"
       },
       "engines": {
         "node": ">=8"
@@ -20424,11 +20469,11 @@
       "dev": true
     },
     "node_modules/strip-ansi": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-      "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
       "dependencies": {
-        "ansi-regex": "^5.0.0"
+        "ansi-regex": "^5.0.1"
       },
       "engines": {
         "node": ">=8"
@@ -32512,9 +32557,9 @@
       }
     },
     "glob": {
-      "version": "7.1.7",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
-      "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
       "requires": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -36931,6 +36976,40 @@
       "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
       "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
     },
+    "replace-in-file": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz",
+      "integrity": "sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.2",
+        "glob": "^7.2.0",
+        "yargs": "^17.2.1"
+      },
+      "dependencies": {
+        "yargs": {
+          "version": "17.3.1",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
+          "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+          "dev": true,
+          "requires": {
+            "cliui": "^7.0.2",
+            "escalade": "^3.1.1",
+            "get-caller-file": "^2.0.5",
+            "require-directory": "^2.1.1",
+            "string-width": "^4.2.3",
+            "y18n": "^5.0.5",
+            "yargs-parser": "^21.0.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "21.0.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
+          "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
+          "dev": true
+        }
+      }
+    },
     "request": {
       "version": "2.88.2",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -38242,13 +38321,13 @@
       }
     },
     "string-width": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
-      "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
       "requires": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
-        "strip-ansi": "^6.0.0"
+        "strip-ansi": "^6.0.1"
       }
     },
     "string.prototype.repeat": {
@@ -38284,11 +38363,11 @@
       "dev": true
     },
     "strip-ansi": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
-      "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
       "requires": {
-        "ansi-regex": "^5.0.0"
+        "ansi-regex": "^5.0.1"
       }
     },
     "strip-eof": {
diff --git a/package.json b/package.json
index 45dcff0a..0bc9ac8c 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
     "jest-dynalite": "^3.4.4",
     "prettier": "^2.2.1",
     "projen": "~0.34.20",
+    "replace-in-file": "^6.3.2",
     "rimraf": "^3.0.2",
     "rollup": "^2.39.0",
     "rollup-plugin-node-externals": "^2.2.0",
diff --git a/packages/cdk/bin/cdk.ts b/packages/cdk/bin/cdk.ts
index 6dd83ced..29c62670 100644
--- a/packages/cdk/bin/cdk.ts
+++ b/packages/cdk/bin/cdk.ts
@@ -47,6 +47,16 @@ new MicroAppsStack(app, 'microapps-basic', {
   deployReleaseApp: shared.deployReleaseApp,
 });
 
+new MicroAppsStack(app, 'microapps-basic-prefix', {
+  env,
+  stackName: `${shared.stackName}${shared.envSuffix}${shared.prSuffix}`,
+  ttl: shared.isPR ? shared.ttlBase : undefined,
+  autoDeleteEverything: true,
+  deployDemoApp: shared.deployDemoApp,
+  deployReleaseApp: shared.deployReleaseApp,
+  rootPathPrefix: 'prefix',
+});
+
 // Note: This is only run manually once per env to create build user
 new MicroAppsBuilder(app, 'microapps-builder', {
   stackName: `${shared.stackName}-builder${shared.envSuffix}`,
diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts
index 2001ac14..03264c60 100644
--- a/packages/cdk/lib/MicroApps.ts
+++ b/packages/cdk/lib/MicroApps.ts
@@ -115,6 +115,13 @@ export interface MicroAppsStackProps extends cdk.StackProps {
    * @default dev
    */
   readonly nodeEnv?: Env;
+
+  /**
+   * Path prefix on the root of the CloudFront distribution
+   *
+   * @example dev/
+   */
+  readonly rootPathPrefix?: string;
 }
 
 export class MicroAppsStack extends cdk.Stack {
@@ -149,6 +156,7 @@ export class MicroAppsStack extends cdk.Stack {
       r53ZoneName,
       certARNEdge,
       certARNOrigin,
+      rootPathPrefix,
     } = props;
 
     let removalPolicy: cdk.RemovalPolicy | undefined = undefined;
@@ -220,6 +228,7 @@ export class MicroAppsStack extends cdk.Stack {
     const microapps = new MicroAppsCDK(this, 'microapps', {
       removalPolicy,
       appEnv: nodeEnv,
+      rootPathPrefix,
       ...optionalAssetNameOpts,
       ...optionals3PolicyOpts,
       ...optionalCustomDomainOpts,
diff --git a/packages/cdk/lib/MicroAppsBuilder.ts b/packages/cdk/lib/MicroAppsBuilder.ts
index 061b1b50..5347cb8f 100644
--- a/packages/cdk/lib/MicroAppsBuilder.ts
+++ b/packages/cdk/lib/MicroAppsBuilder.ts
@@ -70,7 +70,7 @@ export class MicroAppsBuilder extends cdk.Stack {
     //   }),
     // );
 
-    const builderRole = new iam.Role(this, 'microapps-builder-role', {
+    const builderRole = new iam.Role(this, 'builder-role', {
       managedPolicies: [
         iam.ManagedPolicy.fromAwsManagedPolicyName('AWSCertificateManagerReadOnly'),
         iam.ManagedPolicy.fromAwsManagedPolicyName('CloudFrontFullAccess'),
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index 6ed2ce46..72196042 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -123,7 +123,7 @@ Domain Name applied to API Gateway origin.
 ```typescript
 import { MicroAppsCF } from '@pwrdrvr/microapps-cdk'
 
-new MicroAppsCF(scope: Construct, id: string, props?: MicroAppsCFProps)
+new MicroAppsCF(scope: Construct, id: string, props: MicroAppsCFProps)
 ```
 
 ##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.scope"></a>
@@ -138,13 +138,54 @@ new MicroAppsCF(scope: Construct, id: string, props?: MicroAppsCFProps)
 
 ---
 
-##### `props`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
+##### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
 
 - *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsCFProps`](#@pwrdrvr/microapps-cdk.MicroAppsCFProps)
 
 ---
 
 
+#### Static Functions <a name="Static Functions"></a>
+
+##### `addRoutes` <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.addRoutes"></a>
+
+```typescript
+import { MicroAppsCF } from '@pwrdrvr/microapps-cdk'
+
+MicroAppsCF.addRoutes(_scope: Construct, props: AddRoutesOptions)
+```
+
+###### `_scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF._scope"></a>
+
+- *Type:* [`@aws-cdk/core.Construct`](#@aws-cdk/core.Construct)
+
+---
+
+###### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.AddRoutesOptions`](#@pwrdrvr/microapps-cdk.AddRoutesOptions)
+
+---
+
+##### `createAPIOriginPolicy` <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.createAPIOriginPolicy"></a>
+
+```typescript
+import { MicroAppsCF } from '@pwrdrvr/microapps-cdk'
+
+MicroAppsCF.createAPIOriginPolicy(scope: Construct, props: CreateAPIOriginPolicyOptions)
+```
+
+###### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.scope"></a>
+
+- *Type:* [`@aws-cdk/core.Construct`](#@aws-cdk/core.Construct)
+
+---
+
+###### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions`](#@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions)
+
+---
 
 #### Properties <a name="Properties"></a>
 
@@ -283,6 +324,76 @@ DynamoDB table used by Router, Deployer, and Release console app.
 
 ## Structs <a name="Structs"></a>
 
+### AddRoutesOptions <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions"></a>
+
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+import { AddRoutesOptions } from '@pwrdrvr/microapps-cdk'
+
+const addRoutesOptions: AddRoutesOptions = { ... }
+```
+
+##### `apiGwyOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.apiGwyOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-cloudfront.IOrigin`](#@aws-cdk/aws-cloudfront.IOrigin)
+
+---
+
+##### `apigwyOriginRequestPolicy`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.apigwyOriginRequestPolicy"></a>
+
+- *Type:* [`@aws-cdk/aws-cloudfront.IOriginRequestPolicy`](#@aws-cdk/aws-cloudfront.IOriginRequestPolicy)
+
+---
+
+##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.bucketAppsOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-cloudfront-origins.S3Origin`](#@aws-cdk/aws-cloudfront-origins.S3Origin)
+
+---
+
+##### `distro`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.distro"></a>
+
+- *Type:* [`@aws-cdk/aws-cloudfront.Distribution`](#@aws-cdk/aws-cloudfront.Distribution)
+
+---
+
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.rootPathPrefix"></a>
+
+- *Type:* `string`
+
+---
+
+### CreateAPIOriginPolicyOptions <a name="@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions"></a>
+
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+import { CreateAPIOriginPolicyOptions } from '@pwrdrvr/microapps-cdk'
+
+const createAPIOriginPolicyOptions: CreateAPIOriginPolicyOptions = { ... }
+```
+
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions.assetNameRoot"></a>
+
+- *Type:* `string`
+
+---
+
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions.assetNameSuffix"></a>
+
+- *Type:* `string`
+
+---
+
+##### `domainNameEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions.domainNameEdge"></a>
+
+- *Type:* `string`
+
+Edge domain name used by CloudFront - If set a custom OriginRequestPolicy will be created that prevents the Host header from being passed to the origin.
+
+---
+
 ### MicroAppsAPIGwyProps <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps"></a>
 
 #### Initializer <a name="[object Object].Initializer"></a>
@@ -357,6 +468,15 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.rootPathPrefix"></a>
+
+- *Type:* `string`
+- *Default:* none
+
+Path prefix on the root of the API Gateway Stage.
+
+---
+
 ### MicroAppsCFProps <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps"></a>
 
 #### Initializer <a name="[object Object].Initializer"></a>
@@ -454,6 +574,14 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.rootPathPrefix"></a>
+
+- *Type:* `string`
+
+Path prefix on the root of the CloudFront distribution.
+
+---
+
 ### MicroAppsProps <a name="@pwrdrvr/microapps-cdk.MicroAppsProps"></a>
 
 Props for MicroApps.
@@ -546,6 +674,14 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.rootPathPrefix"></a>
+
+- *Type:* `string`
+
+Path prefix on the root of the CloudFront distribution.
+
+---
+
 ##### `s3PolicyBypassAROAs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.s3PolicyBypassAROAs"></a>
 
 - *Type:* `string`[]
@@ -762,6 +898,15 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.rootPathPrefix"></a>
+
+- *Type:* `string`
+- *Default:* none
+
+Path prefix on the root of the deployment.
+
+---
+
 ##### `s3PolicyBypassAROAs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.s3PolicyBypassAROAs"></a>
 
 - *Type:* `string`[]
diff --git a/packages/microapps-cdk/src/MicroApps.ts b/packages/microapps-cdk/src/MicroApps.ts
index 86be83d3..5fb9692c 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -147,6 +147,13 @@ export interface MicroAppsProps {
    * @default auto-assigned
    */
   readonly domainNameOrigin?: string;
+
+  /**
+   * Path prefix on the root of the CloudFront distribution
+   *
+   * @example dev/
+   */
+  readonly rootPathPrefix?: string;
 }
 
 export interface IMicroApps {
@@ -208,6 +215,7 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
       s3PolicyBypassAROAs,
       s3PolicyBypassPrincipalARNs,
       s3StrictBucketPolicy,
+      rootPathPrefix,
     } = props;
 
     this._s3 = new MicroAppsS3(this, 's3', {
@@ -228,6 +236,7 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
       domainNameOrigin,
       r53Zone,
       certOrigin,
+      rootPathPrefix,
     });
     this._cf = new MicroAppsCF(this, 'cft', {
       removalPolicy,
@@ -240,6 +249,7 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
       certEdge,
       bucketAppsOrigin: this._s3.bucketAppsOrigin,
       bucketLogs: this._s3.bucketLogs,
+      rootPathPrefix,
     });
     this._svcs = new MicroAppsSvcs(this, 'svcs', {
       httpApi: this.apigwy.httpApi,
@@ -253,6 +263,7 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
       s3PolicyBypassAROAs,
       s3PolicyBypassPrincipalARNs,
       s3StrictBucketPolicy,
+      rootPathPrefix,
     });
   }
 }
diff --git a/packages/microapps-cdk/src/MicroAppsAPIGwy.ts b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
index 308bb9f7..a83877a0 100644
--- a/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
+++ b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
@@ -60,6 +60,14 @@ export interface MicroAppsAPIGwyProps {
    * Route53 zone in which to create optional `domainNameEdge` record
    */
   readonly r53Zone?: r53.IHostedZone;
+
+  /**
+   * Path prefix on the root of the API Gateway Stage
+   *
+   * @example dev/
+   * @default none
+   */
+  readonly rootPathPrefix?: string;
 }
 
 export interface IMicroAppsAPIGwy {
@@ -131,6 +139,7 @@ export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
       removalPolicy,
       assetNameRoot,
       assetNameSuffix,
+      rootPathPrefix,
     } = props;
 
     // API Gateway uses the `id` string as the gateway name without
@@ -142,6 +151,22 @@ export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
     //
     // APIGateway domain names for CloudFront and origin
     //
+    this._httpApi = new apigwy.HttpApi(this, 'gwy', {
+      apiName: apigatewayName,
+      createDefaultStage: false,
+    });
+    if (removalPolicy !== undefined) {
+      this._httpApi.applyRemovalPolicy(removalPolicy);
+    }
+
+    // Create the stage
+    const stage = new apigwy.HttpStage(this, 'stage', {
+      httpApi: this._httpApi,
+      autoDeploy: true,
+      // If rootPathPrefix is not defined this will be the $default stage
+      stageName: rootPathPrefix,
+    });
+
     if (domainNameEdge !== undefined && certOrigin !== undefined) {
       // Create Custom Domains for API Gateway
       const dnAppsEdge = new apigwy.DomainName(this, 'microapps-apps-edge-dn', {
@@ -152,25 +177,14 @@ export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
         dnAppsEdge.applyRemovalPolicy(removalPolicy);
       }
 
-      // Create APIGateway for the Edge name
-      const httpApiDomainMapping: apigwy.DomainMappingOptions = {
+      // Create the edge domain name mapping for the API
+      new apigwy.ApiMapping(this, 'mapping', {
+        api: this._httpApi,
         domainName: dnAppsEdge,
-      };
-      this._httpApi = new apigwy.HttpApi(this, 'gwy', {
-        defaultDomainMapping: httpApiDomainMapping,
-        apiName: apigatewayName,
-      });
-    } else {
-      // Create HttpApi with default names
-      this._httpApi = new apigwy.HttpApi(this, 'gwy', {
-        apiName: apigatewayName,
+        stage,
       });
     }
 
-    if (removalPolicy !== undefined) {
-      this._httpApi.applyRemovalPolicy(removalPolicy);
-    }
-
     if (domainNameOrigin !== undefined && certOrigin !== undefined) {
       this._dnAppsOrigin = new apigwy.DomainName(this, 'origin-dn', {
         domainName: domainNameOrigin,
@@ -191,8 +205,8 @@ export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
     if (removalPolicy !== undefined) {
       apiAccessLogs.applyRemovalPolicy(removalPolicy);
     }
-    const stage = this._httpApi.defaultStage?.node.defaultChild as apigwy.CfnStage;
-    stage.accessLogSettings = {
+    // const stage = this._httpApi.defaultStage?.node.defaultChild as apigwy.CfnStage;
+    (stage as unknown as apigwy.CfnStage).accessLogSettings = {
       destinationArn: apiAccessLogs.logGroupArn,
       format: JSON.stringify({
         requestId: '$context.requestId',
@@ -231,6 +245,7 @@ export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
       const mappingAppsApis = new apigwy.ApiMapping(this, 'api-map-origin', {
         api: this._httpApi,
         domainName: this._dnAppsOrigin,
+        stage,
       });
       // 2021-12-12 - This should not be needed
       mappingAppsApis.node.addDependency(this._dnAppsOrigin);
diff --git a/packages/microapps-cdk/src/MicroAppsCF.ts b/packages/microapps-cdk/src/MicroAppsCF.ts
index d1f2e609..467e1d8c 100644
--- a/packages/microapps-cdk/src/MicroAppsCF.ts
+++ b/packages/microapps-cdk/src/MicroAppsCF.ts
@@ -1,3 +1,4 @@
+import { posix as posixPath } from 'path';
 import * as apigwy from '@aws-cdk/aws-apigatewayv2';
 import * as acm from '@aws-cdk/aws-certificatemanager';
 import * as cf from '@aws-cdk/aws-cloudfront';
@@ -78,9 +79,151 @@ export interface MicroAppsCFProps {
    * Route53 zone in which to create optional `domainNameEdge` record
    */
   readonly r53Zone?: r53.IHostedZone;
+
+  /**
+   * Path prefix on the root of the CloudFront distribution
+   *
+   * @example dev/
+   */
+  readonly rootPathPrefix?: string;
+}
+
+export interface CreateAPIOriginPolicyOptions {
+  readonly assetNameRoot?: string;
+  readonly assetNameSuffix?: string;
+
+  /**
+   * Edge domain name used by CloudFront - If set a custom
+   * OriginRequestPolicy will be created that prevents
+   * the Host header from being passed to the origin.
+   */
+  readonly domainNameEdge?: string;
+}
+
+export interface AddRoutesOptions {
+  readonly apiGwyOrigin: cf.IOrigin;
+  readonly bucketAppsOrigin: cforigins.S3Origin;
+  readonly distro: cf.Distribution;
+  readonly apigwyOriginRequestPolicy: cf.IOriginRequestPolicy;
+  readonly rootPathPrefix?: string;
 }
 
 export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
+  /**
+   * Create or get the origin request policy
+   *
+   * If a custom domain name is NOT used for the origin then a policy
+   * will be created.
+   *
+   * If a custom domain name IS used for the origin then the ALL_VIEWER
+   * policy will be returned.  This policy passes the Host header to the
+   * origin, which is fine when using a custom domain name on the origin.
+   *
+   * @param scope
+   * @param props
+   */
+  public static createAPIOriginPolicy(
+    scope: cdk.Construct,
+    props: CreateAPIOriginPolicyOptions,
+  ): cf.IOriginRequestPolicy {
+    const { assetNameRoot, assetNameSuffix, domainNameEdge } = props;
+
+    let apigwyOriginRequestPolicy: cf.IOriginRequestPolicy = cf.OriginRequestPolicy.ALL_VIEWER;
+    if (domainNameEdge === undefined) {
+      // When not using a custom domain name we must limit down the origin policy to
+      // prevent it from passing the Host header (distribution_id.cloudfront.net) to
+      // apigwy which will then reject it with a 403 because it does not match the
+      // execute-api name that apigwy is expecting.
+      //
+      // 2021-12-28 - There is a bug in the name generation that causes the same asset
+      // in different stacks to have the same generated name.  We have to make the id
+      // in all cases to ensure the generated name is unique.
+      apigwyOriginRequestPolicy = new cf.OriginRequestPolicy(
+        scope,
+        `apigwy-origin-policy-${cdk.Stack.of(scope).stackName}`,
+        {
+          comment: assetNameRoot ? `${assetNameRoot}-apigwy${assetNameSuffix}` : undefined,
+
+          originRequestPolicyName: assetNameRoot
+            ? `${assetNameRoot}-apigwy${assetNameSuffix}`
+            : undefined,
+          cookieBehavior: cf.OriginRequestCookieBehavior.all(),
+          queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(),
+          headerBehavior: cf.OriginRequestHeaderBehavior.allowList('user-agent', 'referer'),
+        },
+      );
+    }
+
+    return apigwyOriginRequestPolicy;
+  }
+
+  /**
+   * Add API Gateway and S3 routes to an existing CloudFront Distribution
+   * @param _scope
+   * @param props
+   */
+  public static addRoutes(_scope: cdk.Construct, props: AddRoutesOptions) {
+    const {
+      apiGwyOrigin,
+      bucketAppsOrigin,
+      distro,
+      apigwyOriginRequestPolicy,
+      rootPathPrefix = '',
+    } = props;
+
+    //
+    // Add Behaviors
+    //
+    const apiGwyBehaviorOptions: cf.AddBehaviorOptions = {
+      allowedMethods: cf.AllowedMethods.ALLOW_ALL,
+      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
+      compress: true,
+      originRequestPolicy: apigwyOriginRequestPolicy,
+      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+    };
+    const s3BehaviorOptions: cf.AddBehaviorOptions = {
+      allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
+      cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
+      compress: true,
+      originRequestPolicy: cf.OriginRequestPolicy.CORS_S3_ORIGIN,
+      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+    };
+    const apiGwyVersionRootBehaviorOptions: cf.AddBehaviorOptions = {
+      allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
+      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
+      compress: true,
+      originRequestPolicy: apigwyOriginRequestPolicy,
+      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+    };
+
+    //
+    // Setup Routes
+    // Pull to the API first, then pull to S3 if it contains /static/
+    // Pull anything under /appName/x.y.z/ folder with '.' in file name to S3
+    // Let everything else fall through to the API Gateway
+    //
+    distro.addBehavior(
+      posixPath.join(rootPathPrefix, '/*/*/api/*'),
+      apiGwyOrigin,
+      apiGwyBehaviorOptions,
+    );
+    distro.addBehavior(
+      posixPath.join(rootPathPrefix, '/*/*/static/*'),
+      bucketAppsOrigin,
+      s3BehaviorOptions,
+    );
+    distro.addBehavior(
+      posixPath.join(rootPathPrefix, '/*/*/*.*'),
+      bucketAppsOrigin,
+      s3BehaviorOptions,
+    );
+    distro.addBehavior(
+      posixPath.join(rootPathPrefix, '/*/*/'),
+      apiGwyOrigin,
+      apiGwyVersionRootBehaviorOptions,
+    );
+  }
+
   private _cloudFrontDistro: cf.Distribution;
   public get cloudFrontDistro(): cf.Distribution {
     return this._cloudFrontDistro;
@@ -92,7 +235,7 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
    * @param id
    * @param props
    */
-  constructor(scope: cdk.Construct, id: string, props?: MicroAppsCFProps) {
+  constructor(scope: cdk.Construct, id: string, props: MicroAppsCFProps) {
     super(scope, id);
 
     if (props === undefined) {
@@ -117,30 +260,28 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
       r53Zone,
       bucketLogs,
       bucketAppsOrigin,
+      rootPathPrefix,
     } = props;
 
+    const apigwyOriginRequestPolicy = MicroAppsCF.createAPIOriginPolicy(this, {
+      assetNameRoot,
+      assetNameSuffix,
+      domainNameEdge,
+    });
+
     //
-    // CloudFront Distro
+    // Determine URL of the origin FQDN
     //
     let httpOriginFQDN: string = 'invalid.pwrdrvr.com';
-    let apigwyOriginPolicy: cf.IOriginRequestPolicy = cf.OriginRequestPolicy.ALL_VIEWER;
     if (domainNameOrigin !== undefined) {
-      // When using a custom domain we can use the default apigwy origin policy of
-      // ALL_VIEWER (all header, all cookies, all query strings)
       httpOriginFQDN = domainNameOrigin;
     } else {
-      // When not using a custom domain name we must limit down the origin policy to
-      // prevent it from passing the Host header (distribution_id.cloudfront.net) to
-      // apigwy which will then reject it with a 403 because it does not match the
-      // execute-api name that apigwhy is expecting.
-      apigwyOriginPolicy = new cf.OriginRequestPolicy(this, 'apigwy-origin-policy', {
-        comment: assetNameRoot ? `${assetNameRoot}-apigwy${assetNameSuffix}` : undefined,
-        cookieBehavior: cf.OriginRequestCookieBehavior.all(),
-        queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(),
-        headerBehavior: cf.OriginRequestHeaderBehavior.allowList('user-agent', 'referer'),
-      });
       httpOriginFQDN = `${httpApi.apiId}.execute-api.${cdk.Aws.REGION}.amazonaws.com`;
     }
+
+    //
+    // CloudFront Distro
+    //
     const apiGwyOrigin = new cforigins.HttpOrigin(httpOriginFQDN, {
       protocolPolicy: cf.OriginProtocolPolicy.HTTPS_ONLY,
       originSslProtocols: [cf.OriginSslPolicy.TLS_V1_2],
@@ -154,7 +295,7 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
         allowedMethods: cf.AllowedMethods.ALLOW_ALL,
         cachePolicy: cf.CachePolicy.CACHING_DISABLED,
         compress: true,
-        originRequestPolicy: apigwyOriginPolicy,
+        originRequestPolicy: apigwyOriginRequestPolicy,
         origin: apiGwyOrigin,
         viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
       },
@@ -169,41 +310,14 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
       this._cloudFrontDistro.applyRemovalPolicy(removalPolicy);
     }
 
-    //
-    // Add Behaviors
-    //
-    const apiGwyBehavior: cf.AddBehaviorOptions = {
-      allowedMethods: cf.AllowedMethods.ALLOW_ALL,
-      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
-      compress: true,
-      originRequestPolicy: apigwyOriginPolicy,
-      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
-    };
-    const s3Behavior: cf.AddBehaviorOptions = {
-      allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
-      cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
-      compress: true,
-      originRequestPolicy: cf.OriginRequestPolicy.CORS_S3_ORIGIN,
-      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
-    };
-    const apiGwyVersionRootBehavior: cf.AddBehaviorOptions = {
-      allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
-      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
-      compress: true,
-      originRequestPolicy: apigwyOriginPolicy,
-      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
-    };
-
-    //
-    // Setup Routes
-    // Pull to the API first, then pull to S3 if it contains /static/
-    // Pull anything under /appName/x.y.z/ folder with '.' in file name to S3
-    // Let everything else fall through to the API Gateway
-    //
-    this._cloudFrontDistro.addBehavior('/*/*/api/*', apiGwyOrigin, apiGwyBehavior);
-    this._cloudFrontDistro.addBehavior('/*/*/static/*', bucketAppsOrigin, s3Behavior);
-    this._cloudFrontDistro.addBehavior('/*/*/*.*', bucketAppsOrigin, s3Behavior);
-    this._cloudFrontDistro.addBehavior('/*/*/', apiGwyOrigin, apiGwyVersionRootBehavior);
+    // Add routes to the CloudFront Distribution
+    MicroAppsCF.addRoutes(scope, {
+      apiGwyOrigin,
+      bucketAppsOrigin,
+      distro: this._cloudFrontDistro,
+      apigwyOriginRequestPolicy: apigwyOriginRequestPolicy,
+      rootPathPrefix,
+    });
 
     //
     // Create the edge name for the CloudFront distro
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index d5a6a5cd..ab914957 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -62,6 +62,14 @@ export interface MicroAppsSvcsProps {
   readonly s3StrictBucketPolicy?: boolean;
   readonly s3PolicyBypassAROAs?: string[];
   readonly s3PolicyBypassPrincipalARNs?: string[];
+
+  /**
+   * Path prefix on the root of the deployment
+   *
+   * @example dev/
+   * @default none
+   */
+  readonly rootPathPrefix?: string;
 }
 
 export interface IMicroAppsSvcs {
@@ -112,6 +120,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       removalPolicy,
       assetNameRoot,
       assetNameSuffix,
+      rootPathPrefix = '',
     } = props;
 
     if (s3StrictBucketPolicy === true) {
@@ -155,6 +164,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         NODE_ENV: appEnv,
         DATABASE_TABLE_NAME: this._table.tableName,
         AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
+        ROOT_PATH_PREFIX: rootPathPrefix,
       },
     };
     if (
@@ -233,6 +243,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         FILESTORE_STAGING_BUCKET: bucketAppsStaging.bucketName,
         FILESTORE_DEST_BUCKET: bucketApps.bucketName,
         AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
+        ROOT_PATH_PREFIX: rootPathPrefix,
       },
     };
     if (
@@ -392,13 +403,13 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     const policyCloudFrontAccess = new iam.PolicyStatement({
       sid: 'cloudfront-oai-access',
       effect: iam.Effect.ALLOW,
-      actions: ['s3:GetObject'],
+      actions: ['s3:GetObject', 's3:ListBucket'],
       principals: [
         new iam.CanonicalUserPrincipal(
           bucketAppsOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId,
         ),
       ],
-      resources: [`${bucketApps.bucketArn}/*`],
+      resources: [`${bucketApps.bucketArn}/*`, bucketApps.bucketArn],
     });
 
     if (bucketApps.policy === undefined) {
@@ -432,7 +443,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     // Allow the Lambda to write to the target bucket and delete
     const policyReadWriteListTarget = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
-      actions: ['s3:GetObject', 's3:PutObject', 's3:ListBucket', 's3:DeleteObject'],
+      actions: ['s3:DeleteObject', 's3:GetObject', 's3:PutObject', 's3:ListBucket'],
       resources: [`${bucketApps.bucketArn}/*`, bucketApps.bucketArn],
     });
     this._deployerFunc.addToRolePolicy(policyReadWriteListTarget);
diff --git a/packages/microapps-deployer/src/config/Config.ts b/packages/microapps-deployer/src/config/Config.ts
index fcff7a1a..d9d00ff6 100644
--- a/packages/microapps-deployer/src/config/Config.ts
+++ b/packages/microapps-deployer/src/config/Config.ts
@@ -16,6 +16,8 @@ export interface IConfig {
   awsRegion: string;
 
   uploadRoleName: string;
+
+  rootPathPrefix: string;
 }
 
 @convict.Config({
@@ -106,4 +108,11 @@ export class Config implements IConfig {
     env: 'UPLOAD_ROLE_NAME',
   })
   public uploadRoleName!: string;
+
+  @convict.Property({
+    doc: 'Path prefix for this deployment',
+    default: '',
+    env: 'ROOT_PATH_PREFIX',
+  })
+  public rootPathPrefix!: string;
 }
diff --git a/packages/microapps-deployer/src/controllers/VersionController.spec.ts b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
index 516f2a1e..b49f2242 100644
--- a/packages/microapps-deployer/src/controllers/VersionController.spec.ts
+++ b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
@@ -21,6 +21,7 @@ Object.defineProperty(Config, 'instance', {
         destinationBucket: 'microapps-test-destination',
       },
       uploadRoleName: 'microapps-upload-test-role',
+      rootPathPrefix: 'dev',
     };
   }),
 });
@@ -54,6 +55,7 @@ const TEST_TABLE_NAME = 'microapps';
 describe('VersionController', () => {
   const config = Config.instance;
   let sandbox: sinon.SinonSandbox;
+  const pathPrefix = `${config.rootPathPrefix}/`;
 
   beforeAll(() => {
     dynamoClient = new dynamodb.DynamoDBClient({
@@ -134,7 +136,7 @@ describe('VersionController', () => {
         // Mock S3 get for staging bucket - return one file name
         .on(s3.ListObjectsV2Command, {
           Bucket: config.filestore.destinationBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: true,
@@ -143,18 +145,18 @@ describe('VersionController', () => {
         .on(s3.ListObjectsV2Command, {
           ContinuationToken: 'nothing-to-see-here-yet',
           Bucket: config.filestore.destinationBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: false,
-          Contents: [{ Key: `${appName}/${semVer}/index.html` }],
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
         })
         .on(s3.DeleteObjectsCommand, {
           Bucket: config.filestore.destinationBucket,
           Delete: {
             Objects: [
               {
-                Key: `${appName}/${semVer}/index.html`,
+                Key: `${pathPrefix}${appName}/${semVer}/index.html`,
               },
             ],
           },
@@ -293,25 +295,35 @@ describe('VersionController', () => {
       s3Client
         .onAnyCommand()
         .rejects()
+        // .callsFake((input) => {
+        //   console.log(`received input: ${JSON.stringify(input)}`);
+        //   console.log(
+        //     `our matcher: ${JSON.stringify({
+        //       Bucket: config.filestore.destinationBucket,
+        //       CopySource: `${config.filestore.stagingBucket}${pathPrefix}/${appName}/${semVer}/index.html`,
+        //       Key: `${pathPrefix}${appName}/${semVer}/index.html`,
+        //     })}`,
+        //   );
+        // })
         // Mock S3 get for staging bucket - return one file name
         .on(s3.ListObjectsV2Command, {
           Bucket: config.filestore.stagingBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: false,
-          Contents: [{ Key: `${appName}/${semVer}/index.html` }],
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
         })
         // Mock S3 copy to prod bucket
         .on(s3.CopyObjectCommand, {
           Bucket: config.filestore.destinationBucket,
-          CopySource: `${config.filestore.stagingBucket}/${appName}/${semVer}/index.html`,
-          Key: `${appName}/${semVer}/index.html`,
+          CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({})
         .on(s3.DeleteObjectCommand, {
           Bucket: config.filestore.stagingBucket,
-          Key: `${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({});
 
@@ -412,22 +424,22 @@ describe('VersionController', () => {
         // Mock S3 get for staging bucket - return one file name
         .on(s3.ListObjectsV2Command, {
           Bucket: config.filestore.stagingBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: false,
-          Contents: [{ Key: `${appName}/${semVer}/index.html` }],
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
         })
         // Mock S3 copy to prod bucket
         .on(s3.CopyObjectCommand, {
           Bucket: config.filestore.destinationBucket,
-          CopySource: `${config.filestore.stagingBucket}/${appName}/${semVer}/index.html`,
-          Key: `${appName}/${semVer}/index.html`,
+          CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({})
         .on(s3.DeleteObjectCommand, {
           Bucket: config.filestore.stagingBucket,
-          Key: `${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({});
       lambdaClient
@@ -512,7 +524,7 @@ describe('VersionController', () => {
         // Mock S3 get for staging bucket - return one file name
         .on(s3.ListObjectsV2Command, {
           Bucket: config.filestore.stagingBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: true,
@@ -521,22 +533,22 @@ describe('VersionController', () => {
         .on(s3.ListObjectsV2Command, {
           ContinuationToken: 'nothing-to-see-here-yet',
           Bucket: config.filestore.stagingBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: false,
-          Contents: [{ Key: `${appName}/${semVer}/index.html` }],
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
         })
         // Mock S3 copy to prod bucket
         .on(s3.CopyObjectCommand, {
           Bucket: config.filestore.destinationBucket,
-          CopySource: `${config.filestore.stagingBucket}/${appName}/${semVer}/index.html`,
-          Key: `${appName}/${semVer}/index.html`,
+          CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({})
         .on(s3.DeleteObjectCommand, {
           Bucket: config.filestore.stagingBucket,
-          Key: `${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({});
 
@@ -652,22 +664,22 @@ describe('VersionController', () => {
         // Mock S3 get for staging bucket - return one file name
         .on(s3.ListObjectsV2Command, {
           Bucket: config.filestore.stagingBucket,
-          Prefix: `${appName}/${semVer}/`,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
         })
         .resolves({
           IsTruncated: false,
-          Contents: [{ Key: `${appName}/${semVer}/index.html` }],
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
         })
         // Mock S3 copy to prod bucket
         .on(s3.CopyObjectCommand, {
           Bucket: config.filestore.destinationBucket,
-          CopySource: `${config.filestore.stagingBucket}/${appName}/${semVer}/index.html`,
-          Key: `${appName}/${semVer}/index.html`,
+          CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({})
         .on(s3.DeleteObjectCommand, {
           Bucket: config.filestore.stagingBucket,
-          Key: `${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
         })
         .resolves({});
 
@@ -726,6 +738,7 @@ describe('VersionController', () => {
         // Mock create route for /*
         .on(apigwy.CreateRouteCommand, {
           ApiId: config.apigwy.apiId,
+
           Target: `integrations/${fakeIntegrationID}`,
           RouteKey: `ANY /${appName}/${semVer}/{proxy+}`,
         })
diff --git a/packages/microapps-deployer/src/controllers/VersionController.ts b/packages/microapps-deployer/src/controllers/VersionController.ts
index 480af35e..df9816b7 100644
--- a/packages/microapps-deployer/src/controllers/VersionController.ts
+++ b/packages/microapps-deployer/src/controllers/VersionController.ts
@@ -6,7 +6,7 @@ import * as s3 from '@aws-sdk/client-s3';
 import * as sts from '@aws-sdk/client-sts';
 import { DBManager, Rules, Version } from '@pwrdrvr/microapps-datalib';
 import pMap from 'p-map';
-import { IConfig } from '../config/Config';
+import { Config, IConfig } from '../config/Config';
 import {
   IDeleteVersionRequest,
   IDeployVersionRequest,
@@ -104,7 +104,9 @@ export default class VersionController {
         new sts.AssumeRoleCommand({
           RoleArn: `arn:aws:iam::${config.awsAccountID}:role/${config.uploadRoleName}`,
           DurationSeconds: 60 * 60,
-          RoleSessionName: VersionController.SHA1Hash(VersionController.GetBucketPrefix(request)),
+          RoleSessionName: VersionController.SHA1Hash(
+            VersionController.GetBucketPrefix(request, config),
+          ),
           Policy: JSON.stringify(iamPolicyDoc.toJSON()),
         }),
       );
@@ -115,6 +117,7 @@ export default class VersionController {
         statusCode: 404,
         s3UploadUrl: `s3://${config.filestore.stagingBucket}/${VersionController.GetBucketPrefix(
           request,
+          config,
         )}`,
 
         awsCredentials: {
@@ -147,8 +150,6 @@ export default class VersionController {
 
     Log.Instance.debug('Got Body:', request);
 
-    const destinationPrefix = VersionController.GetBucketPrefix(request);
-
     // Check if the version exists
     let record = await Version.LoadVersion({
       dbManager,
@@ -190,14 +191,14 @@ export default class VersionController {
     // Only copy the files if not copied yet
     if (overwrite || record.Status === 'pending') {
       const { stagingBucket } = config.filestore;
-      const sourcePrefix = VersionController.GetBucketPrefix(request) + '/';
+      const sourcePrefix = VersionController.GetBucketPrefix(request, config) + '/';
 
       // Example Source: s3://pwrdrvr-apps-staging/release/1.0.0/
       // Loop through all S3 source assets and copy to the destination
       await VersionController.CopyToProdBucket(
         stagingBucket,
         sourcePrefix,
-        destinationPrefix,
+        VersionController.GetBucketPrefix(request, config),
         config,
       );
 
@@ -448,7 +449,7 @@ export default class VersionController {
     }
 
     // Delete files in destinationBucket
-    const destinationPrefix = VersionController.GetBucketPrefix(request) + '/';
+    const destinationPrefix = VersionController.GetBucketPrefix(request, config) + '/';
     await this.DeleteFromDestinationBucket(destinationPrefix, config);
 
     if (record.Type === 'lambda') {
@@ -692,7 +693,9 @@ export default class VersionController {
 
   private static GetBucketPrefix(
     request: Pick<IDeployVersionRequestBase, 'appName' | 'semVer'>,
+    config: IConfig,
   ): string {
-    return `${request.appName}/${request.semVer}`.toLowerCase();
+    const pathPrefix = config.rootPathPrefix === '' ? '' : `${config.rootPathPrefix}/`;
+    return `${pathPrefix}${request.appName}/${request.semVer}`.toLowerCase();
   }
 }
diff --git a/packages/microapps-deployer/src/index.spec.ts b/packages/microapps-deployer/src/index.spec.ts
index 4669a887..d96284c2 100644
--- a/packages/microapps-deployer/src/index.spec.ts
+++ b/packages/microapps-deployer/src/index.spec.ts
@@ -22,11 +22,11 @@ Object.defineProperty(Config, 'instance', {
         destinationBucket: 'microapps-test-destination',
       },
       uploadRoleName: 'microapps-upload-test-role',
+      rootPathPrefix: 'qa',
     };
   }),
 });
 import * as dynamodb from '@aws-sdk/client-dynamodb';
-import { mockClient, AwsClientStub } from 'aws-sdk-client-mock';
 import { handler, overrideDBManager } from './index';
 import { Application, DBManager } from '@pwrdrvr/microapps-datalib';
 import { ICreateApplicationRequest } from '@pwrdrvr/microapps-deployer-lib';
diff --git a/packages/microapps-router/src/config/Config.ts b/packages/microapps-router/src/config/Config.ts
index 1f8710ae..6974e178 100644
--- a/packages/microapps-router/src/config/Config.ts
+++ b/packages/microapps-router/src/config/Config.ts
@@ -7,6 +7,7 @@ import { Database, IDatabase } from './Database';
 
 export interface IConfig {
   db: IDatabase;
+  rootPathPrefix: string;
 }
 
 @convict.Config({
@@ -70,4 +71,11 @@ export class Config implements IConfig {
 
   @convict.Property(Database)
   public db!: IDatabase;
+
+  @convict.Property({
+    doc: 'Path prefix for this deployment',
+    default: '',
+    env: 'ROOT_PATH_PREFIX',
+  })
+  public rootPathPrefix!: string;
 }
diff --git a/packages/microapps-router/src/index.prefix.spec.ts b/packages/microapps-router/src/index.prefix.spec.ts
new file mode 100644
index 00000000..9ba82426
--- /dev/null
+++ b/packages/microapps-router/src/index.prefix.spec.ts
@@ -0,0 +1,118 @@
+/// <reference types="jest" />
+import 'reflect-metadata';
+import 'jest-dynalite/withDb';
+import * as dynamodb from '@aws-sdk/client-dynamodb';
+import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
+import * as lambda from 'aws-lambda';
+import { Config, IConfig } from './config/Config';
+jest.mock('./config/Config');
+const configMock = jest.fn((): IConfig => {
+  return {
+    db: {
+      tableName: 'microapps',
+    },
+    rootPathPrefix: 'qa',
+  };
+});
+Object.defineProperty(Config, 'instance', {
+  configurable: false,
+  enumerable: false,
+  get: configMock,
+});
+import { handler, overrideDBManager } from './index';
+
+let dynamoClient: dynamodb.DynamoDBClient;
+let dbManager: DBManager;
+
+const TEST_TABLE_NAME = 'microapps';
+
+describe('router - with prefix', () => {
+  beforeAll(() => {
+    dynamoClient = new dynamodb.DynamoDBClient({
+      endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
+      tls: false,
+      region: 'local',
+    });
+
+    // Init the DB manager to point it at the right table
+    dbManager = new DBManager({ dynamoClient, tableName: TEST_TABLE_NAME });
+  });
+
+  beforeEach(() => {
+    overrideDBManager({ dbManager, dynamoClient });
+  });
+
+  it('should serve appframe with version and default file substitued', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'bat.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await handler(
+      { rawPath: '/qa/bat/' } as lambda.APIGatewayProxyEventV2,
+      {} as lambda.Context,
+    );
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response).toBeDefined();
+    expect(response).toHaveProperty('body');
+    expect(response.body?.length).toBeGreaterThan(80);
+    expect(response.body).toContain('<iframe src="/qa/bat/3.2.1-beta.1/bat.html" seamless');
+  });
+
+  it('should 404 appframe with version if the prefix is missing', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'bat.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await handler(
+      { rawPath: '/bat/' } as lambda.APIGatewayProxyEventV2,
+      {} as lambda.Context,
+    );
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(404);
+    expect(response).toBeDefined();
+    expect(response).not.toHaveProperty('body');
+  });
+});
diff --git a/packages/microapps-router/src/index.spec.ts b/packages/microapps-router/src/index.spec.ts
index 29473d0e..e66e5308 100644
--- a/packages/microapps-router/src/index.spec.ts
+++ b/packages/microapps-router/src/index.spec.ts
@@ -1,8 +1,24 @@
 /// <reference types="jest" />
+import 'reflect-metadata';
 import 'jest-dynalite/withDb';
 import * as dynamodb from '@aws-sdk/client-dynamodb';
 import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
 import * as lambda from 'aws-lambda';
+import { Config, IConfig } from './config/Config';
+jest.mock('./config/Config');
+const configMock = jest.fn((): IConfig => {
+  return {
+    db: {
+      tableName: 'microapps',
+    },
+    rootPathPrefix: '',
+  };
+});
+Object.defineProperty(Config, 'instance', {
+  configurable: false,
+  enumerable: false,
+  get: configMock,
+});
 import { handler, overrideDBManager } from './index';
 
 let dynamoClient: dynamodb.DynamoDBClient;
@@ -10,7 +26,7 @@ let dbManager: DBManager;
 
 const TEST_TABLE_NAME = 'microapps';
 
-describe('router', () => {
+describe('router - without prefix', () => {
   beforeAll(() => {
     dynamoClient = new dynamodb.DynamoDBClient({
       endpoint: process.env.MOCK_DYNAMODB_ENDPOINT,
diff --git a/packages/microapps-router/src/index.ts b/packages/microapps-router/src/index.ts
index a70bfad5..c6747bba 100644
--- a/packages/microapps-router/src/index.ts
+++ b/packages/microapps-router/src/index.ts
@@ -1,6 +1,7 @@
 // Used by ts-convict
 import 'source-map-support/register';
 import 'reflect-metadata';
+import path from 'path';
 import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
 import { Application, DBManager, IVersionsAndRules, Version } from '@pwrdrvr/microapps-datalib';
 import type * as lambda from 'aws-lambda';
@@ -24,12 +25,35 @@ export function overrideDBManager(opts: {
 }
 dbManager = new DBManager({ dynamoClient, tableName: Config.instance.db.tableName });
 
+/**
+ * Ensure that the path starts with a / and does not end with a /
+ * @param pathPrefix
+ * @returns
+ */
+function normalizePathPrefix(pathPrefix: string): string {
+  let normalizedPathPrefix = pathPrefix;
+  if (normalizedPathPrefix !== '' && !normalizedPathPrefix.startsWith('/')) {
+    normalizedPathPrefix = '/' + pathPrefix;
+  }
+  if (normalizedPathPrefix.endsWith('/')) {
+    normalizedPathPrefix.substring(0, normalizedPathPrefix.length - 1);
+  }
+
+  return normalizedPathPrefix;
+}
+
 /**
  * Find and load the appFrame file
  * @returns
  */
 function loadAppFrame(): string {
-  const paths = [__dirname, `${__dirname}/..`, `${__dirname}/templates`, '/opt', '/opt/templates'];
+  const paths = [
+    __dirname,
+    path.join(__dirname, '..'),
+    path.join(__dirname, 'templates'),
+    '/opt',
+    '/opt/templates',
+  ];
 
   // Change the logger on each request
   const log = new LambdaLog({
@@ -44,8 +68,8 @@ function loadAppFrame(): string {
     },
   });
 
-  for (const path of paths) {
-    const fullPath = `${path}/appFrame.html`;
+  for (const pathRoot of paths) {
+    const fullPath = path.join(pathRoot, 'appFrame.html');
     try {
       if (pathExistsSync(fullPath)) {
         log.info('found html file', { fullPath });
@@ -61,6 +85,7 @@ function loadAppFrame(): string {
 }
 
 const appFrame = loadAppFrame();
+const normalizedPathPrefix = normalizePathPrefix(Config.instance.rootPathPrefix);
 
 // Change the logger on each request
 const log = new LambdaLog({
@@ -102,13 +127,24 @@ export async function handler(
   };
 
   try {
+    if (normalizedPathPrefix !== '' && !event.rawPath.startsWith(normalizedPathPrefix)) {
+      // The prefix is required if configured, if missing we cannot serve this app
+      response.statusCode = 404;
+      return response;
+    }
+
+    const pathAfterPrefix =
+      normalizedPathPrefix !== '' && event.rawPath.startsWith(normalizedPathPrefix)
+        ? event.rawPath.slice(normalizedPathPrefix.length - 1)
+        : event.rawPath;
+
     // /someapp will split into length 2 with ["", "someapp"] as results
     // /someapp/somepath will split into length 3 with ["", "someapp", "somepath"] as results
     // /someapp/somepath/ will split into length 3 with ["", "someapp", "somepath", ""] as results
     // /someapp/somepath/somefile.foo will split into length 4 with ["", "someapp", "somepath", "somefile.foo", ""] as results
-    const parts = event.rawPath.split('/');
+    const parts = pathAfterPrefix.split('/');
 
-    // TODO: Pass any parts after the appName/Version to the route handler
+    // Pass any parts after the appName/Version to the route handler
     let additionalParts = '';
     if (parts.length >= 3 && parts[2] !== '') {
       additionalParts = parts.slice(2).join('/');
@@ -124,7 +160,7 @@ export async function handler(
       // This is an app and a version only
       // If the request got here it's likely a static app that has no
       // Lambda function (thus the API Gateway route fell through to the Router)
-      if (await RedirectToDefaultFile(event, response, parts[1], parts[2], log)) {
+      if (await RedirectToDefaultFile({ response, appName: parts[1], semVer: parts[2], log })) {
         return response;
       }
     }
@@ -135,7 +171,7 @@ export async function handler(
       // ^  ^^^^^^^    ^^^^^^^^^
       // 0        1            2
       // Got at least an application name, try to route it
-      await RouteApp(event, response, parts[1], additionalParts, log);
+      await RouteApp({ event, response, appName: parts[1], additionalParts, log });
     } else {
       throw new Error('Unmatched route');
     }
@@ -152,20 +188,21 @@ export async function handler(
 
 /**
  * Lookup the version of the app to run
- * @param request
+ * @param event
  * @param response
  * @param appName
  * @param additionalParts
  * @param log
  * @returns
  */
-async function RouteApp(
-  request: lambda.APIGatewayProxyEventV2,
-  response: lambda.APIGatewayProxyStructuredResultV2,
-  appName: string,
-  additionalParts: string,
-  log: LambdaLog,
-) {
+async function RouteApp(opts: {
+  event: lambda.APIGatewayProxyEventV2;
+  response: lambda.APIGatewayProxyStructuredResultV2;
+  appName: string;
+  additionalParts: string;
+  log: LambdaLog;
+}) {
+  const { event, response, appName, additionalParts, log } = opts;
   let versionsAndRules: IVersionsAndRules;
 
   if (response.headers === undefined) {
@@ -189,7 +226,7 @@ async function RouteApp(
     response.statusCode = 404;
     response.headers['Cache-Control'] = 'no-store; private';
     response.headers['Content-Type'] = 'text/plain; charset=UTF-8';
-    response.body = `Router - Could not find app: ${request.rawPath}, ${appName}`;
+    response.body = `Router - Could not find app: ${event.rawPath}, ${appName}`;
     return;
   }
 
@@ -208,13 +245,13 @@ async function RouteApp(
   const defaultVersion = versionsAndRules.Rules?.RuleSet.default?.SemVer;
 
   if (defaultVersion == null) {
-    log.error(`could not find app ${appName}, for path ${request.rawPath} - returning 404`, {
+    log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
       statusCode: 404,
     });
     response.statusCode = 404;
     response.headers['Cache-Control'] = 'no-store; private';
     response.headers['Content-Type'] = 'text/plain; charset=UTF-8';
-    response.body = `Router - Could not find app: ${request.rawPath}, ${appName}`;
+    response.body = `Router - Could not find app: ${event.rawPath}, ${appName}`;
     return;
   }
 
@@ -234,14 +271,14 @@ async function RouteApp(
     // KLUDGE: We're going to take a missing default file to mean that the
     // app type is Next.js (or similar) and that it wants no trailing slash after the version
     // TODO: Move this to an attribute of the version
-    appVersionPath = `/${appName}/${defaultVersion}`;
+    appVersionPath = `${normalizedPathPrefix}/${appName}/${defaultVersion}`;
     if (additionalParts !== '') {
       appVersionPath += `/${additionalParts}`;
     }
   } else {
     // Linking to the file directly means this will be peeled off by the S3 route
     // That means we won't have to proxy this from S3
-    appVersionPath = `/${appName}/${defaultVersion}/${defaultVersionInfo.DefaultFile}`;
+    appVersionPath = `${normalizedPathPrefix}/${appName}/${defaultVersion}/${defaultVersionInfo.DefaultFile}`;
   }
 
   //
@@ -255,7 +292,7 @@ async function RouteApp(
   response.statusCode = 200;
   response.body = frameHTML;
 
-  log.info(`found app ${appName}, for path ${request.rawPath} - returning 200`, {
+  log.info(`found app ${appName}, for path ${event.rawPath} - returning 200`, {
     statusCode: 200,
     routedPath: appVersionPath,
   });
@@ -270,13 +307,13 @@ async function RouteApp(
  * @param log
  * @returns
  */
-async function RedirectToDefaultFile(
-  request: lambda.APIGatewayProxyEventV2,
-  response: lambda.APIGatewayProxyStructuredResultV2,
-  appName: string,
-  semVer: string,
-  log: LambdaLog,
-): Promise<boolean> {
+async function RedirectToDefaultFile(opts: {
+  response: lambda.APIGatewayProxyStructuredResultV2;
+  appName: string;
+  semVer: string;
+  log: LambdaLog;
+}): Promise<boolean> {
+  const { response, appName, semVer, log } = opts;
   let versionInfo: Version;
 
   if (response.headers === undefined) {
@@ -289,17 +326,20 @@ async function RedirectToDefaultFile(
       key: { AppName: appName, SemVer: semVer },
     });
   } catch (error) {
-    log.info(`LoadVersion threw for '/${appName}/${semVer}' - falling through to app routing'`, {
-      appName,
-      semVer,
-      error,
-    });
+    log.info(
+      `LoadVersion threw for '${normalizedPathPrefix}/${appName}/${semVer}' - falling through to app routing'`,
+      {
+        appName,
+        semVer,
+        error,
+      },
+    );
     return false;
   }
 
   if (versionInfo === undefined) {
     log.info(
-      `LoadVersion returned undefined for '/${appName}/${semVer}', assuming not found - falling through to app routing'`,
+      `LoadVersion returned undefined for '${normalizedPathPrefix}/${appName}/${semVer}', assuming not found - falling through to app routing'`,
       {
         appName,
         semVer,
@@ -310,13 +350,15 @@ async function RedirectToDefaultFile(
 
   response.statusCode = 302;
   response.headers['Cache-Control'] = 'no-store; private';
-  response.headers['Location'] = `/${appName}/${semVer}/${versionInfo.DefaultFile}`;
+  response.headers[
+    'Location'
+  ] = `${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`;
 
   log.info(
-    `found '/${appName}/${semVer}' - returning 302 to /${appName}/${semVer}/${versionInfo.DefaultFile}`,
+    `found '${normalizedPathPrefix}/${appName}/${semVer}' - returning 302 to ${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`,
     {
       statusCode: 302,
-      routedPath: `/${appName}/${semVer}/${versionInfo.DefaultFile}`,
+      routedPath: `${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`,
     },
   );
 


Test patch:
diff --git a/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts b/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts
index c06d2a07..c449006b 100644
--- a/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts
+++ b/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts
@@ -1,23 +1,14 @@
 /// <reference types="jest" />
 import '@aws-cdk/assert/jest';
-import { App, Stack } from '@aws-cdk/core';
+import * as acm from '@aws-cdk/aws-certificatemanager';
+import * as r53 from '@aws-cdk/aws-route53';
+import { App, RemovalPolicy, Stack } from '@aws-cdk/core';
 import { MicroAppsAPIGwy } from '../src/MicroAppsAPIGwy';
 
 describe('MicroAppsAPIGwy', () => {
-  const app = new App({});
-
   it('works with no params', () => {
+    const app = new App({});
     const stack = new Stack(app, 'stack');
-    // const cloudFrontDistro = new cf.Distribution(stack, 'cf-distro', {
-    //   defaultBehavior: {
-    //     allowedMethods: cf.AllowedMethods.ALLOW_ALL,
-    //     cachePolicy: cf.CachePolicy.CACHING_DISABLED,
-    //     compress: true,
-    //     originRequestPolicy: cf.OriginRequestPolicy.ALL_VIEWER,
-    //     origin: bucketAppsOrigin, // Just a dummy to create this distro
-    //     viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
-    //   },
-    // });
     const construct = new MicroAppsAPIGwy(stack, 'construct', {});
 
     expect(construct).toBeDefined();
@@ -27,4 +18,32 @@ describe('MicroAppsAPIGwy', () => {
 
     expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
   });
+
+  it('works with params', () => {
+    const app = new App({});
+    const stack = new Stack(app, 'stack');
+    const r53Zone = new r53.HostedZone(stack, 'zone', {
+      zoneName: 'test.pwrdrvr.com',
+    });
+    const certOrigin = new acm.Certificate(stack, 'cert', {
+      domainName: '*.test.pwrdrvr.com',
+    });
+    const construct = new MicroAppsAPIGwy(stack, 'construct', {
+      assetNameRoot: 'mydeploy',
+      assetNameSuffix: '-mysuffix',
+      domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      domainNameOrigin: 'mydeploy-origin.test.pwrdrvr.com',
+      removalPolicy: RemovalPolicy.DESTROY,
+      r53Zone,
+      certOrigin,
+      rootPathPrefix: '/myprefix/',
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.dnAppsOrigin).toBeDefined();
+    expect(construct.httpApi).toBeDefined();
+    expect(construct.node).toBeDefined();
+
+    expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+  });
 });
diff --git a/packages/microapps-cdk/test/MicroAppsCF.test.ts b/packages/microapps-cdk/test/MicroAppsCF.test.ts
index a99fa390..d132b786 100644
--- a/packages/microapps-cdk/test/MicroAppsCF.test.ts
+++ b/packages/microapps-cdk/test/MicroAppsCF.test.ts
@@ -1,16 +1,49 @@
 /// <reference types="jest" />
 import '@aws-cdk/assert/jest';
 import * as apigwy from '@aws-cdk/aws-apigatewayv2';
+import * as acm from '@aws-cdk/aws-certificatemanager';
 import * as cf from '@aws-cdk/aws-cloudfront';
 import * as cforigins from '@aws-cdk/aws-cloudfront-origins';
+import * as r53 from '@aws-cdk/aws-route53';
 import * as s3 from '@aws-cdk/aws-s3';
 import { App, Stack } from '@aws-cdk/core';
 import { MicroAppsCF } from '../src/MicroAppsCF';
 
 describe('MicroAppsCF', () => {
-  const app = new App({});
-
   it('works with no params', () => {
+    const app = new App({});
+    const stack = new Stack(app, 'stack');
+    const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
+      apiName: 'some-api',
+      // defaultDomainMapping: {
+      //   domainName: new apigwy.DomainName(stack, 'domain-name', {
+      //     domainName: 'apps.pwrdrvr.com',
+      //     certificate: new acm.Certificate(stack, 'cert', {
+      //       domainName: 'apps.pwrdrvr.com',
+      //     }),
+      //   }),
+      // },
+    });
+    const construct = new MicroAppsCF(stack, 'construct', {
+      httpApi,
+      bucketAppsOrigin: new cforigins.S3Origin(new s3.Bucket(stack, 'bucket-apps', {}), {
+        originAccessIdentity: new cf.OriginAccessIdentity(stack, 'oai', {}),
+      }),
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.cloudFrontDistro).toBeDefined();
+    expect(construct.node).toBeDefined();
+    // expect(stack).toHaveResource('AWS::S3::Bucket');
+    expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+
+    const template = app.synth().getStackArtifact(stack.artifactId).template;
+    expect(template).toHaveResource('AWS::CloudFront::Distribution');
+    expect(template).toHaveResource('AWS::CloudFront::OriginRequestPolicy');
+  });
+
+  it('works with params', () => {
+    const app = new App({});
     const stack = new Stack(app, 'stack');
     const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
       apiName: 'some-api',
@@ -23,11 +56,21 @@ describe('MicroAppsCF', () => {
       //   }),
       // },
     });
+    const r53Zone = new r53.HostedZone(stack, 'zone', {
+      zoneName: 'test.pwrdrvr.com',
+    });
+    const certOrigin = new acm.Certificate(stack, 'cert', {
+      domainName: '*.test.pwrdrvr.com',
+    });
     const construct = new MicroAppsCF(stack, 'construct', {
       httpApi,
       bucketAppsOrigin: new cforigins.S3Origin(new s3.Bucket(stack, 'bucket-apps', {}), {
         originAccessIdentity: new cf.OriginAccessIdentity(stack, 'oai', {}),
       }),
+      domainNameEdge: 'some.test.pwrdrvr.com',
+      domainNameOrigin: 'some-origin.test.pwrdvr.com',
+      certEdge: certOrigin,
+      r53Zone,
     });
 
     expect(construct).toBeDefined();
@@ -38,5 +81,6 @@ describe('MicroAppsCF', () => {
 
     const template = app.synth().getStackArtifact(stack.artifactId).template;
     expect(template).toHaveResource('AWS::CloudFront::Distribution');
+    expect(template).not.toHaveResource('AWS::CloudFront::OriginRequestPolicy');
   });
 });
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap
index 52556fc8..6f88d583 100644
--- a/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap
@@ -3,17 +3,84 @@
 exports[`MicroAppsAPIGwy works with no params 1`] = `
 Object {
   "Resources": Object {
-    "constructgwyDefaultStageBB1CA110": Object {
+    "constructgwyE80472DA": Object {
       "Properties": Object {
-        "AccessLogSettings": Object {
-          "DestinationArn": Object {
-            "Fn::GetAtt": Array [
-              "constructlogsBC77618C",
-              "Arn",
+        "Name": "stack-microapps",
+        "ProtocolType": "HTTP",
+      },
+      "Type": "AWS::ApiGatewayV2::Api",
+    },
+    "constructlogsBC77618C": Object {
+      "DeletionPolicy": "Retain",
+      "Properties": Object {
+        "LogGroupName": "/aws/apigwy/stack-microapps",
+        "RetentionInDays": 14,
+      },
+      "Type": "AWS::Logs::LogGroup",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "constructlogsrole387D81E6": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "apigateway.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+              ],
             ],
           },
-          "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"userAgent\\":\\"$context.identity.userAgent\\",\\"sourceIp\\":\\"$context.identity.sourceIp\\",\\"requestTime\\":\\"$context.requestTime\\",\\"requestTimeEpoch\\":\\"$context.requestTimeEpoch\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"path\\":\\"$context.path\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\",\\"domainName\\":\\"$context.domainName\\"}",
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+    "constructlogsroleDefaultPolicyD4FA927F": Object {
+      "Properties": Object {
+        "PolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": Array [
+                "logs:CreateLogStream",
+                "logs:PutLogEvents",
+              ],
+              "Effect": "Allow",
+              "Resource": Object {
+                "Fn::GetAtt": Array [
+                  "constructlogsBC77618C",
+                  "Arn",
+                ],
+              },
+            },
+          ],
+          "Version": "2012-10-17",
         },
+        "PolicyName": "constructlogsroleDefaultPolicyD4FA927F",
+        "Roles": Array [
+          Object {
+            "Ref": "constructlogsrole387D81E6",
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Policy",
+    },
+    "constructstage3B55CA4F": Object {
+      "Properties": Object {
         "ApiId": Object {
           "Ref": "constructgwyE80472DA",
         },
@@ -22,21 +89,59 @@ Object {
       },
       "Type": "AWS::ApiGatewayV2::Stage",
     },
+  },
+}
+`;
+
+exports[`MicroAppsAPIGwy works with params 1`] = `
+Object {
+  "Resources": Object {
+    "cert56CA94EB": Object {
+      "Properties": Object {
+        "DomainName": "*.test.pwrdrvr.com",
+        "DomainValidationOptions": Array [
+          Object {
+            "DomainName": "*.test.pwrdrvr.com",
+            "ValidationDomain": "pwrdrvr.com",
+          },
+        ],
+        "ValidationMethod": "EMAIL",
+      },
+      "Type": "AWS::CertificateManager::Certificate",
+    },
+    "constructapimaporigin7C2B9F89": Object {
+      "DeletionPolicy": "Delete",
+      "DependsOn": Array [
+        "constructorigindnDD3F36CE",
+        "constructstage3B55CA4F",
+      ],
+      "Properties": Object {
+        "ApiId": Object {
+          "Ref": "constructgwyE80472DA",
+        },
+        "DomainName": "mydeploy-origin.test.pwrdrvr.com",
+        "Stage": "/myprefix/",
+      },
+      "Type": "AWS::ApiGatewayV2::ApiMapping",
+      "UpdateReplacePolicy": "Delete",
+    },
     "constructgwyE80472DA": Object {
+      "DeletionPolicy": "Delete",
       "Properties": Object {
-        "Name": "stack-microapps",
+        "Name": "mydeploy-mysuffix",
         "ProtocolType": "HTTP",
       },
       "Type": "AWS::ApiGatewayV2::Api",
+      "UpdateReplacePolicy": "Delete",
     },
     "constructlogsBC77618C": Object {
-      "DeletionPolicy": "Retain",
+      "DeletionPolicy": "Delete",
       "Properties": Object {
-        "LogGroupName": "/aws/apigwy/stack-microapps",
+        "LogGroupName": "/aws/apigwy/mydeploy-mysuffix",
         "RetentionInDays": 14,
       },
       "Type": "AWS::Logs::LogGroup",
-      "UpdateReplacePolicy": "Retain",
+      "UpdateReplacePolicy": "Delete",
     },
     "constructlogsrole387D81E6": Object {
       "Properties": Object {
@@ -98,6 +203,93 @@ Object {
       },
       "Type": "AWS::IAM::Policy",
     },
+    "constructmapping8BB481CC": Object {
+      "DependsOn": Array [
+        "constructstage3B55CA4F",
+      ],
+      "Properties": Object {
+        "ApiId": Object {
+          "Ref": "constructgwyE80472DA",
+        },
+        "DomainName": "mydeploy.test.pwrdrvr.com",
+        "Stage": "/myprefix/",
+      },
+      "Type": "AWS::ApiGatewayV2::ApiMapping",
+    },
+    "constructmicroappsappsedgedn308BF2C3": Object {
+      "DeletionPolicy": "Delete",
+      "Properties": Object {
+        "DomainName": "mydeploy.test.pwrdrvr.com",
+        "DomainNameConfigurations": Array [
+          Object {
+            "CertificateArn": Object {
+              "Ref": "cert56CA94EB",
+            },
+            "EndpointType": "REGIONAL",
+          },
+        ],
+      },
+      "Type": "AWS::ApiGatewayV2::DomainName",
+      "UpdateReplacePolicy": "Delete",
+    },
+    "constructoriginarecord70039239": Object {
+      "DeletionPolicy": "Delete",
+      "Properties": Object {
+        "AliasTarget": Object {
+          "DNSName": Object {
+            "Fn::GetAtt": Array [
+              "constructorigindnDD3F36CE",
+              "RegionalDomainName",
+            ],
+          },
+          "HostedZoneId": Object {
+            "Fn::GetAtt": Array [
+              "constructorigindnDD3F36CE",
+              "RegionalHostedZoneId",
+            ],
+          },
+        },
+        "HostedZoneId": Object {
+          "Ref": "zoneEB40FF1E",
+        },
+        "Name": "mydeploy-origin.test.pwrdrvr.com.",
+        "Type": "A",
+      },
+      "Type": "AWS::Route53::RecordSet",
+      "UpdateReplacePolicy": "Delete",
+    },
+    "constructorigindnDD3F36CE": Object {
+      "DeletionPolicy": "Delete",
+      "Properties": Object {
+        "DomainName": "mydeploy-origin.test.pwrdrvr.com",
+        "DomainNameConfigurations": Array [
+          Object {
+            "CertificateArn": Object {
+              "Ref": "cert56CA94EB",
+            },
+            "EndpointType": "REGIONAL",
+          },
+        ],
+      },
+      "Type": "AWS::ApiGatewayV2::DomainName",
+      "UpdateReplacePolicy": "Delete",
+    },
+    "constructstage3B55CA4F": Object {
+      "Properties": Object {
+        "ApiId": Object {
+          "Ref": "constructgwyE80472DA",
+        },
+        "AutoDeploy": true,
+        "StageName": "/myprefix/",
+      },
+      "Type": "AWS::ApiGatewayV2::Stage",
+    },
+    "zoneEB40FF1E": Object {
+      "Properties": Object {
+        "Name": "test.pwrdrvr.com.",
+      },
+      "Type": "AWS::Route53::HostedZone",
+    },
   },
 }
 `;
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap
index 2494e9e1..bba7cd37 100644
--- a/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap
@@ -8,7 +8,7 @@ Object {
       "Type": "AWS::S3::Bucket",
       "UpdateReplacePolicy": "Retain",
     },
-    "constructapigwyoriginpolicy9DEFDDA9": Object {
+    "constructapigwyoriginpolicystack77776C6F": Object {
       "Properties": Object {
         "OriginRequestPolicyConfig": Object {
           "CookiesConfig": Object {
@@ -21,7 +21,7 @@ Object {
               "referer",
             ],
           },
-          "Name": "stackconstructapigwyoriginpolicy94306769",
+          "Name": "stackconstructapigwyoriginpolicystack60399495",
           "QueryStringsConfig": Object {
             "QueryStringBehavior": "all",
           },
@@ -46,7 +46,7 @@ Object {
               "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
               "Compress": true,
               "OriginRequestPolicyId": Object {
-                "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+                "Ref": "constructapigwyoriginpolicystack77776C6F",
               },
               "PathPattern": "/*/*/api/*",
               "TargetOriginId": "stackconstructcftOrigin1D000029C",
@@ -87,7 +87,7 @@ Object {
               "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
               "Compress": true,
               "OriginRequestPolicyId": Object {
-                "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+                "Ref": "constructapigwyoriginpolicystack77776C6F",
               },
               "PathPattern": "/*/*/",
               "TargetOriginId": "stackconstructcftOrigin1D000029C",
@@ -107,7 +107,7 @@ Object {
             "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
             "Compress": true,
             "OriginRequestPolicyId": Object {
-              "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+              "Ref": "constructapigwyoriginpolicystack77776C6F",
             },
             "TargetOriginId": "stackconstructcftOrigin1D000029C",
             "ViewerProtocolPolicy": "redirect-to-https",
@@ -196,3 +196,226 @@ Object {
   },
 }
 `;
+
+exports[`MicroAppsCF works with params 1`] = `
+Object {
+  "Mappings": Object {
+    "AWSCloudFrontPartitionHostedZoneIdMap": Object {
+      "aws": Object {
+        "zoneId": "Z2FDTNDATAQYW2",
+      },
+      "aws-cn": Object {
+        "zoneId": "Z3RFFRIM2A3IF5",
+      },
+    },
+  },
+  "Resources": Object {
+    "bucketapps06013E35": Object {
+      "DeletionPolicy": "Retain",
+      "Type": "AWS::S3::Bucket",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "cert56CA94EB": Object {
+      "Properties": Object {
+        "DomainName": "*.test.pwrdrvr.com",
+        "DomainValidationOptions": Array [
+          Object {
+            "DomainName": "*.test.pwrdrvr.com",
+            "ValidationDomain": "pwrdrvr.com",
+          },
+        ],
+        "ValidationMethod": "EMAIL",
+      },
+      "Type": "AWS::CertificateManager::Certificate",
+    },
+    "constructcftBE9A3354": Object {
+      "Properties": Object {
+        "DistributionConfig": Object {
+          "Aliases": Array [
+            "some.test.pwrdrvr.com",
+          ],
+          "CacheBehaviors": Array [
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+                "PUT",
+                "PATCH",
+                "POST",
+                "DELETE",
+              ],
+              "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+              "Compress": true,
+              "OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
+              "PathPattern": "/*/*/api/*",
+              "TargetOriginId": "stackconstructcftOrigin1D000029C",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
+              "Compress": true,
+              "OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
+              "PathPattern": "/*/*/static/*",
+              "TargetOriginId": "stackconstructcftOrigin2BE69FFBB",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
+              "Compress": true,
+              "OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
+              "PathPattern": "/*/*/*.*",
+              "TargetOriginId": "stackconstructcftOrigin2BE69FFBB",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+              "Compress": true,
+              "OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
+              "PathPattern": "/*/*/",
+              "TargetOriginId": "stackconstructcftOrigin1D000029C",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+          ],
+          "Comment": "some.test.pwrdrvr.com",
+          "DefaultCacheBehavior": Object {
+            "AllowedMethods": Array [
+              "GET",
+              "HEAD",
+              "OPTIONS",
+              "PUT",
+              "PATCH",
+              "POST",
+              "DELETE",
+            ],
+            "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+            "Compress": true,
+            "OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
+            "TargetOriginId": "stackconstructcftOrigin1D000029C",
+            "ViewerProtocolPolicy": "redirect-to-https",
+          },
+          "Enabled": true,
+          "HttpVersion": "http2",
+          "IPV6Enabled": true,
+          "Origins": Array [
+            Object {
+              "CustomOriginConfig": Object {
+                "OriginProtocolPolicy": "https-only",
+                "OriginSSLProtocols": Array [
+                  "TLSv1.2",
+                ],
+              },
+              "DomainName": "some-origin.test.pwrdvr.com",
+              "Id": "stackconstructcftOrigin1D000029C",
+            },
+            Object {
+              "DomainName": Object {
+                "Fn::GetAtt": Array [
+                  "bucketapps06013E35",
+                  "RegionalDomainName",
+                ],
+              },
+              "Id": "stackconstructcftOrigin2BE69FFBB",
+              "S3OriginConfig": Object {
+                "OriginAccessIdentity": Object {
+                  "Fn::Join": Array [
+                    "",
+                    Array [
+                      "origin-access-identity/cloudfront/",
+                      Object {
+                        "Ref": "oai7A77E975",
+                      },
+                    ],
+                  ],
+                },
+              },
+            },
+          ],
+          "PriceClass": "PriceClass_100",
+          "ViewerCertificate": Object {
+            "AcmCertificateArn": Object {
+              "Ref": "cert56CA94EB",
+            },
+            "MinimumProtocolVersion": "TLSv1.2_2019",
+            "SslSupportMethod": "sni-only",
+          },
+        },
+      },
+      "Type": "AWS::CloudFront::Distribution",
+    },
+    "constructedgearecordF28A0EBD": Object {
+      "Properties": Object {
+        "AliasTarget": Object {
+          "DNSName": Object {
+            "Fn::GetAtt": Array [
+              "constructcftBE9A3354",
+              "DomainName",
+            ],
+          },
+          "HostedZoneId": Object {
+            "Fn::FindInMap": Array [
+              "AWSCloudFrontPartitionHostedZoneIdMap",
+              Object {
+                "Ref": "AWS::Partition",
+              },
+              "zoneId",
+            ],
+          },
+        },
+        "HostedZoneId": Object {
+          "Ref": "zoneEB40FF1E",
+        },
+        "Name": "some.test.pwrdrvr.com.",
+        "Type": "A",
+      },
+      "Type": "AWS::Route53::RecordSet",
+    },
+    "httpapi04200D5F": Object {
+      "Properties": Object {
+        "Name": "some-api",
+        "ProtocolType": "HTTP",
+      },
+      "Type": "AWS::ApiGatewayV2::Api",
+    },
+    "httpapiDefaultStageEEE9F4E2": Object {
+      "Properties": Object {
+        "ApiId": Object {
+          "Ref": "httpapi04200D5F",
+        },
+        "AutoDeploy": true,
+        "StageName": "$default",
+      },
+      "Type": "AWS::ApiGatewayV2::Stage",
+    },
+    "oai7A77E975": Object {
+      "Properties": Object {
+        "CloudFrontOriginAccessIdentityConfig": Object {
+          "Comment": "Allows CloudFront to reach the bucket",
+        },
+      },
+      "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
+    },
+    "zoneEB40FF1E": Object {
+      "Properties": Object {
+        "Name": "test.pwrdrvr.com.",
+      },
+      "Type": "AWS::Route53::HostedZone",
+    },
+  },
+}
+`;