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

Issue:
v0.3 - SEO Friendly Frameless Support
# Steps
- [x] Read good starting doc [Multiple Next.js Apps on Same Domain](https://dev.to/arihantverma/workaround-for-next-data-urls-throwing-404-for-multiple-next-js-apps-running-on-same-domain-3pok)
- [x] Get app loading locally without iframe under `/appName` path
  - [x] Confirm this works under Express too 
- [x] Get static resources, api, and _next/data requests to route to `/appName/semVer/resource`
- [x] Add command line args to `microapps-publish` for `iframeless` mode
- [x] Add parameter to deployer-lib DeployVersion for `iframeless` mode
- [x] Add startupType to datalib

# Details
- Search Engines strongly dislike iframes and often will not render them at all
- It is desired for microapps to work for both private logged in pages as well as public pages that need to be crawled by search engines
- Frameworks like Next.js can work fine with frameless version targeting:
  - `_next/data/` requests have a version built into the path (granted, it comes after the `_next/data/` part of the path, but that's ok, we can register another route in API Gateway)
    - The version can be controlled so it can be predictable
    - We can register API Gateway routes to send these to the right version
    - We may be able to route requests for deleted versions to the default version
  - `static/` files are immutable and hashed, so these can just pass through to S3 without any special handling - These will all get dumped into a shared directory, making cleanup nearly impossible... but that's not a big issue as S3 is nearly free
  - Page requests that hit API Gateway have a few options to send to the correct version per user
    - Proxy the requests with a fast / low memory request lambda
      - Benefits
        - This allows 1 RTT to Origin application startup
        - This is simple to implement if cost is no issue
      - Drawbacks
        - Cost can be an issue and will increase app lambda costs by ~20% to 100% using the simple approach
        - The low cost high-volume solution is available but is more complex to implement and maintain
      - This needs to be a very efficient framework so it can be configured with low RAM to keep cost down
      - Cost is an issue here because this proxy will be "running" for the entire duration of the Lambda that is proxied to (thus if both had 1,769 MB of RAM it would 2x the cost of every page load)
      - If the RAM is kept to 128 MB or 256 MB then it should keep the cost of proxying these requests low enough to be acceptable
      - An alternative is to send the requests to a non-Lambda proxy such as EKS or ECS as these can handle waiting for multiple requests in parallel without pushing costs through the roof
    - Rewrite URLs Using Origin Request Lambda @ Edge
      - Benefits
        - Lower cost than proxying
        - 1 RTT to Origin application startup
        - Eliminates the 2x cost of proxying
        - Avoids needing to use an alternative to Lambda (e.g. EKS or ECS)
      - Drawbacks
        - Requires stripping versions out of the received path
        - Requires a globally replicated DynamoDB table
        - Will be a little slower in Origin Request regions that do not have the DynamoDB replicated closely
      - Globally replicate the MicroApps DynamoDB table (not necessarily to every single region but to at least some "close" regions" for each Origin Request handling region
      - Move the `router` app logic up to the Origin Request handler
      - Lookup the version and write it into the path sent to the origin
      - Let API Gateway perform the version-based routing
      - Strip the version out of the received path in the app
    - Route the request to the `router` app and use a cookie to pick the version
      - Benefits
        - Lower cost than proxying
      - Drawbacks
        - 2 RTT to Origin application startup
        - Adds a 302 once per session so impacts the TTFB / LCP / TTI CoreWebVitals scores
        - Pretty mind-blowing to follow 😄 
        - Requires stripping versions out of the received path
      - When the user has no cookie the request hits the `router`
      - The `router` looks up the version and returns a 302 back to the same URL with a session `Set-Cookie` for that app path specifying the version
      - The browser receives the 302 and re-sends the request with the cookie
      - A CloudFront Origin Request function rewrites the origin URL with the version stitched into the path
      - API Gateway receives the request and routes using the stitched path to the correct version
      - The application *may* need to strip the version out of the path so that the application framework (like Next.js) does not get confused and think that the path does not exist
      - Subsequent requests for other pages in the same app all get rewritten in the same way

# Early Thoughts - No Longer Applicable
- Evaluate options for removing the iframe for both SEO compatibility and slightly improved application startup speeds
  - Using a session cookie (should stay per window) - This may require rewriting paths using Lambda @ Edge to hide the version
  - Using HTML5 attributes that make all requests from a page relative to a base path
  - Publishing 2 versions of apps that require re-rooting (e.g. Next.js)
    - /appName Version - Used when the versioned cookie is present
    - /appName/version - Used when the versioned cookie is not present
    - This would ensure that all links work, URLs in address bar stay normal, etc.
    - Apps that can be re-rooted on the fly would not need to publish two versions
    - Alternatively, the publisher can swap the version codes at publish time and transparently publish two versions (of both static assets and the Lambdas)
    - The Router that runs on /appName would be modified to look for the cookie and forward to the appropriate versionless Lambda for the corresponding cookie version (yeah, this could work - the proxying sucks, but it would work)
    - Better Alternative: CloudFront Lambda@Edge rewrites the server-side URL from /appName to /appName/rootlessVersion when it sees the cookie present - This would allow API Gateway to route directly to that version but all URLs from that version would be returned without the version, and the Lambda@Edge would continue to rewrite those server-side URLs when it sees the cookie present for them



Discussion / hints:

Fix patch:
diff --git a/.demo-app-replace.config.js b/.demo-app-replace.config.js
new file mode 100644
index 00000000..65eb652b
--- /dev/null
+++ b/.demo-app-replace.config.js
@@ -0,0 +1,4 @@
+// https://www.npmjs.com/package/replace-in-file
+module.exports = {
+  files: 'packages/demp-app/static_files/**/*',
+};
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba2a1a76..b4ec334f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,6 @@
-# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
-
-# Workflow syntax reference:
+# Workflow references:
 # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
+# https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 name: Build / Deploy - CI
 
 on:
@@ -30,7 +28,7 @@ jobs:
       releaseAppPackageVersion: ${{ steps.appVersions.outputs.releaseAppPackageVersion }}
       nextjsDemoAppPackageVersion: ${{ steps.appVersions.outputs.nextjsDemoAppPackageVersion }}
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Compute PR Suffix
         id: prSuffix
@@ -46,7 +44,7 @@ jobs:
           fi
 
       - name: Use Node.js 16
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -85,8 +83,8 @@ jobs:
       matrix:
         deployName:
           - microapps-core
-          - microapps-basic
-          - microapps-basic-prefix
+          # - microapps-basic
+          # - microapps-basic-prefix
     name: deploy-${{ matrix.deployName }}
     concurrency:
       group: deploy-${{ matrix.deployName }}-${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -101,7 +99,7 @@ jobs:
       id-token: write
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Extract Current NPM Version
         run: |
@@ -120,7 +118,7 @@ jobs:
           fi
 
       - name: Use Node.js 16
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -142,6 +140,10 @@ jobs:
           role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
           aws-region: ${{ env.AWS_REGION }}
 
+      - name: Stitch Version and Prefix into Simple Demo App
+        run: |
+            npx replace-in-file "/\/${DEMO_APP_NAME}\/0\.0\.0/g" ${PREFIX}/${DEMO_APP_NAME}/${PACKAGE_VERSION} --configFile=.demo-app-replace.config.js --isRegex
+
       - name: Stitch Prefix into Nextjs Demo App
         run: |
           if [ -n "${PREFIX}" ] ; then
@@ -166,7 +168,7 @@ jobs:
         working-directory: .
         run: zip -r cdk-out.zip cdk.out
       - name: Upload Zip
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: cdk_out_${{ matrix.deployName }}
           path: cdk-out.zip
@@ -198,7 +200,14 @@ jobs:
 
       - name: Publish Demo App to MicroApps
         run: |
-          npx microapps-publish publish -a ${DEMO_APP_NAME} -n ${PACKAGE_VERSION} -d ${DEPLOYER_LAMBDA_NAME} -l ${DEMO_APP_LAMBDA_NAME} -s packages/demo-app/static_files -i index.html --overwrite --noCache
+          npx microapps-publish publish -a ${DEMO_APP_NAME} \
+            -t lambda-url \
+            --startup-type direct \
+            -n ${PACKAGE_VERSION} \
+            -d ${DEPLOYER_LAMBDA_NAME} \
+            -l ${DEMO_APP_LAMBDA_NAME} \
+            -s packages/demo-app/static_files \
+            -i index.html --overwrite --noCache
 
       - name: Test Demo App
         run: |
@@ -211,7 +220,14 @@ jobs:
 
       - name: Publish Nextjs Demo App to MicroApps
         run: |
-          npx microapps-publish publish -a ${NEXTJS_DEMO_APP_NAME} -n ${{ needs.build.outputs.nextjsDemoAppPackageVersion }} -d ${DEPLOYER_LAMBDA_NAME} -l ${NEXTJS_DEMO_APP_LAMBDA_NAME} -s packages/cdk/node_modules/@pwrdrvr/microapps-app-nextjs-demo-cdk/lib/.static_files/nextjs-demo/${{ needs.build.outputs.nextjsDemoAppPackageVersion }}/ --overwrite --noCache
+          npx microapps-publish publish \
+            -t lambda-url \
+            -a ${NEXTJS_DEMO_APP_NAME} \
+            -n ${{ needs.build.outputs.nextjsDemoAppPackageVersion }} \
+            -d ${DEPLOYER_LAMBDA_NAME} \
+            -l ${NEXTJS_DEMO_APP_LAMBDA_NAME} \
+            -s packages/cdk/node_modules/@pwrdrvr/microapps-app-nextjs-demo-cdk/lib/.static_files/nextjs-demo/${{ needs.build.outputs.nextjsDemoAppPackageVersion }}/ \
+            --overwrite --noCache
 
       - name: Test Nextjs Demo App
         run: |
@@ -226,7 +242,15 @@ jobs:
 
       - name: Publish Release App to MicroApps
         run: |
-          npx microapps-publish publish -a ${RELEASE_APP_NAME} -n ${{ needs.build.outputs.releaseAppPackageVersion }} -d ${DEPLOYER_LAMBDA_NAME} -l ${RELEASE_APP_LAMBDA_NAME} -s packages/cdk/node_modules/@pwrdrvr/microapps-app-release-cdk/lib/.static_files/release/${{ needs.build.outputs.releaseAppPackageVersion }}/ --overwrite --noCache
+          npx microapps-publish publish \
+            -t lambda-url \
+            --startup-type iframe \
+            -a ${RELEASE_APP_NAME} \
+            -n ${{ needs.build.outputs.releaseAppPackageVersion }} \
+            -d ${DEPLOYER_LAMBDA_NAME} \
+            -l ${RELEASE_APP_LAMBDA_NAME} \
+            -s packages/cdk/node_modules/@pwrdrvr/microapps-app-release-cdk/lib/.static_files/release/${{ needs.build.outputs.releaseAppPackageVersion }}/ \
+            --overwrite --noCache
 
       - name: Test Release App
         run: |
@@ -237,73 +261,14 @@ jobs:
           echo Testing App Method Invocation
           curl --fail https://${EDGE_DOMAIN}${PREFIX}/${RELEASE_APP_NAME}/${{ needs.build.outputs.releaseAppPackageVersion }}/api/refresh/${DEMO_APP_NAME}
 
-  delete-apps:
-    if: github.event_name == 'pull_request' && false
-    concurrency:
-      group: delete-apps-${{ github.workflow }}-${{ github.event.pull_request.number }}
-      cancel-in-progress: false
-    needs:
-      - build
-      - deploy
-    environment:
-      name: ghpublic
-    permissions:
-      contents: read
-      id-token: write
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-
-      - name: Extract Current NPM Version
-        run: |
-          echo 'PACKAGE_VERSION_ORIG='$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
-
-      - name: Compute NPM Version and App Lambda Name
-        run: |
-          echo 'APP_HOST_NAME=apps'${{ needs.build.outputs.prSuffix }} >> $GITHUB_ENV
-          echo 'DEPLOYER_LAMBDA_NAME=microapps-ghpublic-deployer-prod'${{ needs.build.outputs.prSuffix }} >> $GITHUB_ENV
-          echo 'PACKAGE_VERSION='${PACKAGE_VERSION_ORIG}${{ needs.build.outputs.prSuffixPackageVersion }} >> $GITHUB_ENV
-          echo 'DEMO_APP_LAMBDA_NAME=microapps-ghpublic-app-'${DEMO_APP_NAME}-${NODE_ENV}${{ needs.build.outputs.prSuffix }} >> $GITHUB_ENV
-
-      - name: Use Node.js 16
-        uses: actions/setup-node@v2
-        with:
-          node-version: 16
-          cache: 'yarn'
-          cache-dependency-path: '**/yarn.lock'
-
-      - name: Install Node Modules
-        run: yarn install --frozen-lockfile
-
-      - name: Build All TypeScript
-        run: yarn build 
-    
-      - name: Configure AWS Credentials
-        uses: aws-actions/configure-aws-credentials@v1
-        with:
-          role-session-name: microapps-ghpublic-build
-          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
-          aws-region: ${{ env.AWS_REGION }}
-
-      - name: Delete Demo App from MicroApps
-        run: |
-          npx microapps-publish delete -a ${DEMO_APP_NAME} -n ${PACKAGE_VERSION} -d ${DEPLOYER_LAMBDA_NAME}
-
-      - name: Delete Nextjs Demo App from MicroApps
-        run: |
-          npx microapps-publish delete -a ${NEXTJS_DEMO_APP_NAME} -n ${{ needs.build.outputs.nextjsDemoAppPackageVersion }} -d ${DEPLOYER_LAMBDA_NAME}
-
-      - name: Delete Release App from MicroApps
-        run: |
-          npx microapps-publish delete -a ${RELEASE_APP_NAME} -n ${{ needs.build.outputs.releaseAppPackageVersion }} -d ${DEPLOYER_LAMBDA_NAME}
-  
   build-jsii:
+    if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'BUILD-JSII'))
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Use Node.js 16
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml
index 2b31feea..a636ecd6 100644
--- a/.github/workflows/main-build.yml
+++ b/.github/workflows/main-build.yml
@@ -24,12 +24,12 @@ jobs:
       CI: "true"
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           ref: ${{ github.event.pull_request.head.ref }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Setup Node.js
-        uses: actions/setup-node@v2.2.0
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -58,7 +58,7 @@ jobs:
           git diff --staged --patch --exit-code > .repo.patch || echo "::set-output name=self_mutation_happened::true"
       - if: steps.self_mutation.outputs.self_mutation_happened
         name: Upload patch
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: .repo.patch
           path: .repo.patch
@@ -69,7 +69,7 @@ jobs:
           cat .repo.patch
           exit 1
       - name: Upload CDK Construct artifact
-        uses: actions/upload-artifact@v2.1.1
+        uses: actions/upload-artifact@v3
         with:
           name: build-artifact
           path: packages/microapps-cdk/dist
@@ -81,13 +81,13 @@ jobs:
     if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository)
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
           ref: ${{ github.event.pull_request.head.ref }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Download patch
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: .repo.patch
           path: ${{ runner.temp }}
@@ -112,9 +112,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
       - name: Use Node.js 16
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -129,14 +129,14 @@ jobs:
       - name: Run Lint
         run: yarn lint
       - name: Upload Publish Tool artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: publish-tool-dist
           path: |
             packages/microapps-publish/
             !packages/microapps-publish/node_modules/
       - name: Upload Datalib artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: datalib-dist
           path: |
@@ -151,16 +151,16 @@ jobs:
     permissions: {}
     if: "! needs.build.outputs.self_mutation_happened"
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download Publish Tool build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: publish-tool-dist
           path: packages/microapps-publish
       - name: Download Datalib build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: datalib-dist
           path: packages/microapps-datalib
@@ -181,11 +181,11 @@ jobs:
     permissions: {}
     if: "! needs.build.outputs.self_mutation_happened"
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -207,11 +207,11 @@ jobs:
         with:
           distribution: temurin
           java-version: 11.x
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -229,14 +229,14 @@ jobs:
     permissions: {}
     if: "! needs.build.outputs.self_mutation_happened"
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - uses: actions/setup-python@v2
         with:
           python-version: 3.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -254,14 +254,14 @@ jobs:
     permissions: {}
     if: "! needs.build.outputs.self_mutation_happened"
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - uses: actions/setup-dotnet@v1
         with:
           dotnet-version: 3.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6f4e8468..e04a1b1b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
       CI: "true"
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
       - name: Set git identity
@@ -27,7 +27,7 @@ jobs:
           git config user.name "github-actions"
           git config user.email "github-actions@github.com"
       - name: Setup Node.js
-        uses: actions/setup-node@v2.2.0
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -55,13 +55,13 @@ jobs:
         run: echo ::set-output name=latest_commit::"$(git ls-remote origin -h ${{ github.ref }} | cut -f1)"
       - name: Upload version.txt
         if: ${{ steps.git_remote.outputs.latest_commit == github.sha }}
-        uses: actions/upload-artifact@v2.1.1
+        uses: actions/upload-artifact@v3
         with:
           name: version-txt
           path: packages/microapps-cdk/dist/version.txt
       - name: Upload CDK Construct Artifact
         if: ${{ steps.git_remote.outputs.latest_commit == github.sha }}
-        uses: actions/upload-artifact@v2.1.1
+        uses: actions/upload-artifact@v3
         with:
           name: build-artifact
           path: packages/microapps-cdk/dist
@@ -73,11 +73,11 @@ jobs:
       contents: write
     if: needs.release.outputs.latest_commit == github.sha
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -100,9 +100,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
       - name: Use Node.js 16
-        uses: actions/setup-node@v2
+        uses: actions/setup-node@v3
         with:
           node-version: 16
           cache: 'yarn'
@@ -111,7 +111,7 @@ jobs:
         id: git_remote
         run: echo ::set-output name=latest_commit::"$(git ls-remote origin -h ${{ github.ref }} | cut -f1)"
       - name: Download version.txt
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: version-txt
           path: dist
@@ -126,14 +126,14 @@ jobs:
       - name: Run Lint
         run: yarn lint
       - name: Upload Publish Tool artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: publish-tool-dist
           path: |
             packages/microapps-publish/
             !packages/microapps-publish/node_modules/
       - name: Upload Datalib artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: datalib-dist
           path: |
@@ -147,11 +147,11 @@ jobs:
       contents: read
     if: needs.release.outputs.latest_commit == github.sha
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -177,16 +177,16 @@ jobs:
       contents: read
     if: needs.release.outputs.latest_commit == github.sha
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download Publish Tool build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: publish-tool-dist
           path: packages/microapps-publish
       - name: Download Datalib build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: datalib-dist
           path: packages/microapps-datalib
@@ -215,11 +215,11 @@ jobs:
         with:
           distribution: temurin
           java-version: 11.x
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -248,14 +248,14 @@ jobs:
       contents: read
     if: needs.release.outputs.latest_commit == github.sha
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - uses: actions/setup-python@v2
         with:
           python-version: 3.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
@@ -280,14 +280,14 @@ jobs:
       contents: read
     if: needs.release.outputs.latest_commit == github.sha
     steps:
-      - uses: actions/setup-node@v2
+      - uses: actions/setup-node@v3
         with:
           node-version: 14.x
       - uses: actions/setup-dotnet@v1
         with:
           dotnet-version: 3.x
       - name: Download build artifacts
-        uses: actions/download-artifact@v2
+        uses: actions/download-artifact@v3
         with:
           name: build-artifact
           path: dist
diff --git a/packages/cdk/lib/DemoApp.ts b/packages/cdk/lib/DemoApp.ts
index 74fc43bb..5b2669c8 100644
--- a/packages/cdk/lib/DemoApp.ts
+++ b/packages/cdk/lib/DemoApp.ts
@@ -3,6 +3,8 @@ import { Duration, RemovalPolicy } from 'aws-cdk-lib';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
 import * as logs from 'aws-cdk-lib/aws-logs';
+import * as os from 'os';
+import * as path from 'path';
 
 /**
  * Properties to initialize an instance of `DemoApp`.
@@ -78,6 +80,21 @@ export class DemoApp extends Construct implements IDemoApp {
       bundling: {
         minify: true,
         sourceMap: true,
+        commandHooks: {
+          beforeInstall: () => [],
+          beforeBundling: () => [],
+          afterBundling: (_inputDir: string, outputDir: string) => {
+            return [
+              `${os.platform() === 'win32' ? 'copy' : 'cp'} ${path.join(
+                '.',
+                'packages',
+                'demo-app',
+                'static_files',
+                '*',
+              )} ${outputDir}`,
+            ];
+          },
+        },
       },
     });
     if (removalPolicy !== undefined) {
diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts
index 460bbe05..fc847604 100644
--- a/packages/cdk/lib/MicroApps.ts
+++ b/packages/cdk/lib/MicroApps.ts
@@ -3,7 +3,7 @@ import { Construct } from 'constructs';
 import * as acm from 'aws-cdk-lib/aws-certificatemanager';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as r53 from 'aws-cdk-lib/aws-route53';
-import { MicroApps, MicroAppsProps } from '@pwrdrvr/microapps-cdk';
+import { MicroApps, MicroAppsProps, MicroAppsTable } from '@pwrdrvr/microapps-cdk';
 import { DemoApp } from './DemoApp';
 import { Env } from './Types';
 import { MicroAppsAppRelease } from '@pwrdrvr/microapps-app-release-cdk';
@@ -237,11 +237,20 @@ export class MicroAppsStack extends Stack {
       assetNameSuffix,
     };
 
+    // The table has to be created with a defined name for
+    // the 2nd generation routing that needs the table name
+    // in the edge lambda function
+    const table = new MicroAppsTable(this, 'microapps-table', {
+      ...optionalAssetNameOpts,
+    });
+
     const microapps = new MicroApps(this, 'microapps', {
       removalPolicy,
       appEnv: nodeEnv,
       rootPathPrefix,
       originRegion,
+      table: table.table,
+      tableNameForEdgeToOrigin: `${assetNameRoot}${assetNameSuffix}`,
       ...optionalAssetNameOpts,
       ...optionals3PolicyOpts,
       ...optionalCustomDomainOpts,
diff --git a/packages/demo-app/src/index.ts b/packages/demo-app/src/index.ts
index cdcbe4fa..72b2f041 100644
--- a/packages/demo-app/src/index.ts
+++ b/packages/demo-app/src/index.ts
@@ -1,5 +1,8 @@
 import 'source-map-support/register';
 import type * as lambda from 'aws-lambda';
+import { readFileSync } from 'fs';
+
+const html = readFileSync(`./index.html`, 'utf8');
 
 // eslint-disable-next-line @typescript-eslint/require-await
 export async function handler(
@@ -8,17 +11,34 @@ export async function handler(
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   context?: lambda.Context,
 ): Promise<unknown> {
-  // return {
-  //   statusCode: 200,
-  //   headers: {
-  //     'Content-Type': 'application/json',
-  //   },
-  //   isBase64Encoded: false,
-  //   body: JSON.stringify({
-  //     source: 'demo-app',
-  //     timestamp: `${new Date().toUTCString()}`,
-  //   }),
-  // };
+  if (event.rawPath === '/demo-app') {
+    return {
+      statusCode: 200,
+      headers: {
+        'Content-Type': 'text/html',
+      },
+      isBase64Encoded: false,
+      body: html,
+    };
+  }
+
+  if (event.rawPath.endsWith('/serverIncrement')) {
+    const currValue = parseInt(event.queryStringParameters?.currValue ?? '0', 10);
+    const newValue = currValue + 1;
+
+    return {
+      statusCode: 200,
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      isBase64Encoded: false,
+      body: {
+        newValue,
+        source: 'demo-app',
+        timestamp: `${new Date().toUTCString()}`,
+      },
+    };
+  }
 
   return {
     source: 'demo-app',
diff --git a/packages/demo-app/static_files/index.html b/packages/demo-app/static_files/index.html
index 8bf4a405..f871c66b 100644
--- a/packages/demo-app/static_files/index.html
+++ b/packages/demo-app/static_files/index.html
@@ -3,7 +3,16 @@
   <head>
     <title>Demo App</title>
   </head>
+
   <body>
-    This is a simple demo app
+    <div>This is a simple demo app</div>
+    <div>
+      <button
+        onclick="fetch(`/demo-app/0.0.0/serverIncrement?currValue=${document.getElementById('output').innerText}`).then((response) => response.json()).then((data) => {document.getElementById('output').innerText = data.newValue })"
+      >
+        Run Server Increment
+      </button>
+    </div>
+    <div id="output">1</div>
   </body>
 </html>
diff --git a/packages/microapps-cdk/.projen/tasks.json b/packages/microapps-cdk/.projen/tasks.json
index 131e845a..d252c4da 100644
--- a/packages/microapps-cdk/.projen/tasks.json
+++ b/packages/microapps-cdk/.projen/tasks.json
@@ -100,6 +100,9 @@
         },
         {
           "exec": "cp -R ../microapps-router/templates lib/microapps-router/"
+        },
+        {
+          "exec": "cp ../microapps-router/templates/* lib/microapps-edge-to-origin/"
         }
       ]
     },
diff --git a/packages/microapps-cdk/.projenrc.js b/packages/microapps-cdk/.projenrc.js
index d63ba04d..72e5223d 100644
--- a/packages/microapps-cdk/.projenrc.js
+++ b/packages/microapps-cdk/.projenrc.js
@@ -111,5 +111,6 @@ project.compileTask.exec(
   'esbuild ../microapps-router/src/index.ts --bundle --minify --sourcemap --platform=node --target=node14 --external:aws-sdk --outfile=lib/microapps-router/index.js',
 );
 project.compileTask.exec('cp -R ../microapps-router/templates lib/microapps-router/');
+project.compileTask.exec('cp ../microapps-router/templates/* lib/microapps-edge-to-origin/');
 
 project.synth();
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index 7a819e62..ff026303 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -646,6 +646,12 @@ const generateEdgeToOriginConfigOptions: GenerateEdgeToOriginConfigOptions = { .
 
 ---
 
+##### `tableName`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.GenerateEdgeToOriginConfigOptions.tableName"></a>
+
+- *Type:* `string`
+
+---
+
 ### MicroAppsAPIGwyProps <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps"></a>
 
 Properties to initialize an instance of `MicroAppsAPIGwy`.
@@ -977,6 +983,18 @@ Note: if 'sign' or 'presign', creates OriginRequest Lambda @ Edge function for A
 
 ---
 
+##### `tableRulesArn`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.tableRulesArn"></a>
+
+- *Type:* `string`
+
+DynamoDB Table Name for apps/versions/rules.
+
+Must be a full ARN as this can be cross region.
+
+Implies that 2nd generation routing is enabled.
+
+---
+
 ### MicroAppsProps <a name="@pwrdrvr/microapps-cdk.MicroAppsProps"></a>
 
 Properties to initialize an instance of `MicroApps`.
@@ -1228,6 +1246,16 @@ Existing table for apps/versions/rules.
 
 ---
 
+##### `tableNameForEdgeToOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.tableNameForEdgeToOrigin"></a>
+
+- *Type:* `string`
+
+Pre-set table name for apps/versions/rules.
+
+This is required when using v2 routing
+
+---
+
 ### MicroAppsS3Props <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props"></a>
 
 Properties to initialize an instance of `MicroAppsS3`.
@@ -1489,14 +1517,6 @@ import { MicroAppsTableProps } from '@pwrdrvr/microapps-cdk'
 const microAppsTableProps: MicroAppsTableProps = { ... }
 ```
 
-##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps.appEnv"></a>
-
-- *Type:* `string`
-
-Application environment, passed as `NODE_ENV` to the Router and Deployer Lambda functions.
-
----
-
 ##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps.assetNameRoot"></a>
 
 - *Type:* `string`
@@ -1526,15 +1546,6 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
-##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps.rootPathPrefix"></a>
-
-- *Type:* `string`
-- *Default:* none
-
-Path prefix on the root of the deployment.
-
----
-
 
 ## Protocols <a name="Protocols"></a>
 
diff --git a/packages/microapps-cdk/src/MicroApps.ts b/packages/microapps-cdk/src/MicroApps.ts
index 8ef915bb..23522d2f 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -250,6 +250,13 @@ export interface MicroAppsProps {
    * @default created by construct
    */
   readonly table?: dynamodb.ITable;
+
+  /**
+   * Pre-set table name for apps/versions/rules
+   *
+   * This is required when using v2 routing
+   */
+  readonly tableNameForEdgeToOrigin?: string;
 }
 
 /**
@@ -345,6 +352,7 @@ export class MicroApps extends Construct implements IMicroApps {
       signingMode = 'sign',
       originRegion,
       table,
+      tableNameForEdgeToOrigin,
     } = props;
 
     this._s3 = new MicroAppsS3(this, 's3', {
@@ -368,6 +376,22 @@ export class MicroApps extends Construct implements IMicroApps {
       rootPathPrefix,
       requireIAMAuthorization: signingMode !== 'none',
     });
+    this._svcs = new MicroAppsSvcs(this, 'svcs', {
+      httpApi: this.apigwy.httpApi,
+      removalPolicy,
+      bucketApps: this._s3.bucketApps,
+      bucketAppsOAI: this._s3.bucketAppsOAI,
+      bucketAppsStaging: this._s3.bucketAppsStaging,
+      assetNameRoot,
+      assetNameSuffix,
+      appEnv,
+      s3PolicyBypassAROAs,
+      s3PolicyBypassPrincipalARNs,
+      s3StrictBucketPolicy,
+      rootPathPrefix,
+      requireIAMAuthorization: signingMode !== 'none',
+      table,
+    });
     if (signingMode !== 'none' || replaceHostHeader || addXForwardedHostHeader) {
       this._edgeToOrigin = new MicroAppsEdgeToOrigin(this, 'edgeToOrigin', {
         assetNameRoot,
@@ -377,6 +401,7 @@ export class MicroApps extends Construct implements IMicroApps {
         replaceHostHeader,
         originRegion,
         signingMode,
+        tableRulesArn: tableNameForEdgeToOrigin || this._svcs.table.tableName,
       });
     }
     this._cf = new MicroAppsCF(this, 'cft', {
@@ -394,21 +419,5 @@ export class MicroApps extends Construct implements IMicroApps {
       createAPIPathRoute,
       edgeToOriginLambdas: this._edgeToOrigin ? this._edgeToOrigin.edgeToOriginLambdas : undefined,
     });
-    this._svcs = new MicroAppsSvcs(this, 'svcs', {
-      httpApi: this.apigwy.httpApi,
-      removalPolicy,
-      bucketApps: this._s3.bucketApps,
-      bucketAppsOAI: this._s3.bucketAppsOAI,
-      bucketAppsStaging: this._s3.bucketAppsStaging,
-      assetNameRoot,
-      assetNameSuffix,
-      appEnv,
-      s3PolicyBypassAROAs,
-      s3PolicyBypassPrincipalARNs,
-      s3StrictBucketPolicy,
-      rootPathPrefix,
-      requireIAMAuthorization: signingMode !== 'none',
-      table,
-    });
   }
 }
diff --git a/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
index f3c015ea..d305cce4 100644
--- a/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
+++ b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
@@ -1,9 +1,11 @@
+/* eslint-disable @typescript-eslint/indent */
 import * as crypto from 'crypto';
-import { existsSync, writeFileSync } from 'fs';
+import { copyFileSync, existsSync, writeFileSync } from 'fs';
 import * as os from 'os';
 import * as path from 'path';
 import { Aws, Duration, RemovalPolicy, Stack, Tags } from 'aws-cdk-lib';
 import * as cf from 'aws-cdk-lib/aws-cloudfront';
+import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import * as iam from 'aws-cdk-lib/aws-iam';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
@@ -98,6 +100,15 @@ export interface MicroAppsEdgeToOriginProps {
    * @default undefined
    */
   readonly originRegion?: string;
+
+  /**
+   * DynamoDB Table Name for apps/versions/rules.
+   *
+   * Must be a full ARN as this can be cross region.
+   *
+   * Implies that 2nd generation routing is enabled.
+   */
+  readonly tableRulesArn?: string;
 }
 
 export interface GenerateEdgeToOriginConfigOptions {
@@ -105,6 +116,7 @@ export interface GenerateEdgeToOriginConfigOptions {
   readonly signingMode: 'sign' | 'presign' | '';
   readonly addXForwardedHostHeader: boolean;
   readonly replaceHostHeader: boolean;
+  readonly tableName?: string;
 }
 
 /**
@@ -120,7 +132,8 @@ export class MicroAppsEdgeToOrigin extends Construct implements IMicroAppsEdgeTo
     return `originRegion: ${props.originRegion}
 ${props.signingMode === '' ? '' : `signingMode: ${props.signingMode}`}
 addXForwardedHostHeader: ${props.addXForwardedHostHeader}
-replaceHostHeader: ${props.replaceHostHeader}`;
+replaceHostHeader: ${props.replaceHostHeader}
+${props.tableName ? `tableName: '${props.tableName}'` : ''}`;
   }
 
   private _edgeToOriginFunction: lambda.Function | cf.experimental.EdgeFunction;
@@ -141,13 +154,14 @@ replaceHostHeader: ${props.replaceHostHeader}`;
     }
 
     const {
-      removalPolicy,
+      addXForwardedHostHeader = true,
       assetNameRoot,
       assetNameSuffix,
+      originRegion,
       signingMode = 'sign',
-      addXForwardedHostHeader = true,
+      removalPolicy,
       replaceHostHeader = true,
-      originRegion,
+      tableRulesArn,
     } = props;
 
     // Create the edge function config file from the construct options
@@ -156,6 +170,11 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       addXForwardedHostHeader,
       replaceHostHeader,
       signingMode: signingMode === 'none' ? '' : signingMode,
+      ...(tableRulesArn
+        ? {
+            tableName: tableRulesArn,
+          }
+        : {}),
     });
 
     //
@@ -187,6 +206,16 @@ replaceHostHeader: ${props.replaceHostHeader}`;
           //   StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
           // },
         }),
+        //
+        // Grant permission to invoke tagged Function URLs
+        //
+        new iam.PolicyStatement({
+          actions: ['lambda:InvokeFunctionUrl'],
+          resources: [`arn:aws:lambda:*:${Aws.ACCOUNT_ID}:*`],
+          conditions: {
+            StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
+          },
+        }),
       ],
       ...(removalPolicy ? { removalPolicy } : {}),
     };
@@ -217,19 +246,19 @@ replaceHostHeader: ${props.replaceHostHeader}`;
         edgeToOriginFuncProps,
       );
     } else {
-      // 2022-07-30 - Does this actually get used at all anymore?
-
-      // 2022-10-02 - This is broken - it's emitting a config file but then
-      // usinga different config file in the bundling below.
-      // This may be ok if this is only used for the construct packaging
-      // as the consuming stack should select a different above which will
-      // use the correct config file.
-      // Emit the config file from the construct options
+      // This is used when bundling the app and building the CDK module
+      // for distribution.
       writeFileSync(
         path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'config.yml'),
         edgeToOriginConfigYaml,
       );
 
+      // Copy the appFrame.html to the place where the bundling will find it
+      copyFileSync(
+        path.join(__dirname, '..', '..', 'microapps-router', 'appFrame.html'),
+        path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'appFrame.html'),
+      );
+
       // This builds the function for distribution with the CDK Construct
       // and will be used during local builds and PR builds of microapps-core
       // if the microapps-edge-to-origin function is not already bundled.
@@ -256,6 +285,13 @@ replaceHostHeader: ${props.replaceHostHeader}`;
                   'microapps-edge-to-origin',
                   'config.yml',
                 )} ${outputDir}`,
+                `${os.platform() === 'win32' ? 'copy' : 'cp'} ${path.join(
+                  __dirname,
+                  '..',
+                  '..',
+                  'microapps-router',
+                  'appFrame.html',
+                )} ${outputDir}`,
               ];
             },
           },
@@ -271,6 +307,12 @@ replaceHostHeader: ${props.replaceHostHeader}`;
         includeBody: true,
       },
     ];
+
+    // Grant access to the rules table
+    if (tableRulesArn) {
+      const tableRules = dynamodb.Table.fromTableName(this, 'tableRules', tableRulesArn);
+      tableRules.grantReadData(this._edgeToOriginFunction);
+    }
   }
 
   /**
@@ -289,6 +331,10 @@ replaceHostHeader: ${props.replaceHostHeader}`;
     edgeToOriginFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'>,
   ) {
     writeFileSync(path.join(distPath, 'config.yml'), edgeToOriginConfigYaml);
+    copyFileSync(
+      path.join(__dirname, '..', '..', 'microapps-router', 'appFrame.html'),
+      path.join(distPath, 'appFrame.html'),
+    );
 
     // EdgeFunction has a bug where it will generate the same parameter
     // name across multiple stacks in the same region if the id param is constant
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index b4918f17..ebc735ac 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
 import * as apigwy from '@aws-cdk/aws-apigatewayv2-alpha';
 import * as apigwyAuth from '@aws-cdk/aws-apigatewayv2-authorizers-alpha';
 import * as apigwyint from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
-import { Aws, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
+import { Aws, Duration, PhysicalName, RemovalPolicy, Stack } from 'aws-cdk-lib';
 import * as cf from 'aws-cdk-lib/aws-cloudfront';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import * as iam from 'aws-cdk-lib/aws-iam';
@@ -249,7 +249,9 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
     if (props.table === undefined) {
       // Create able if none passed
       this._ownedTable = new dynamodb.Table(this, 'table', {
-        tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
+        tableName: assetNameRoot
+          ? `${assetNameRoot}${assetNameSuffix}`
+          : PhysicalName.GENERATE_IF_NEEDED,
         billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
         partitionKey: {
           name: 'PK',
diff --git a/packages/microapps-cdk/src/MicroAppsTable.ts b/packages/microapps-cdk/src/MicroAppsTable.ts
index f2611152..b7fc245f 100644
--- a/packages/microapps-cdk/src/MicroAppsTable.ts
+++ b/packages/microapps-cdk/src/MicroAppsTable.ts
@@ -1,4 +1,4 @@
-import { RemovalPolicy } from 'aws-cdk-lib';
+import { PhysicalName, RemovalPolicy } from 'aws-cdk-lib';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import { Construct } from 'constructs';
 
@@ -15,12 +15,6 @@ export interface MicroAppsTableProps {
    */
   readonly removalPolicy?: RemovalPolicy;
 
-  /**
-   * Application environment, passed as `NODE_ENV`
-   * to the Router and Deployer Lambda functions
-   */
-  readonly appEnv: string;
-
   /**
    * Optional asset name root
    *
@@ -36,14 +30,6 @@ export interface MicroAppsTableProps {
    * @default none
    */
   readonly assetNameSuffix?: string;
-
-  /**
-   * Path prefix on the root of the deployment
-   *
-   * @example dev/
-   * @default none
-   */
-  readonly rootPathPrefix?: string;
 }
 
 /**
@@ -58,6 +44,12 @@ export interface IMicroAppsTable {
 
 /**
  * Create a new MicroApps Table for apps / versions / rules
+ *
+ * @warning This construct is not intended for production use.
+ * In a production stack the DynamoDB Table, API Gateway, S3 Buckets,
+ * etc. should be created in a "durable" stack where the IDs will not
+ * change and where changes to the MicroApps construct will not
+ * cause failures to deploy or data to be deleted.
  */
 export class MicroAppsTable extends Construct implements IMicroAppsTable {
   private _table: dynamodb.Table;
@@ -78,7 +70,9 @@ export class MicroAppsTable extends Construct implements IMicroAppsTable {
     // DynamoDB Table
     //
     this._table = new dynamodb.Table(this, 'table', {
-      tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
+      tableName: assetNameRoot
+        ? `${assetNameRoot}${assetNameSuffix}`
+        : PhysicalName.GENERATE_IF_NEEDED,
       billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
       partitionKey: {
         name: 'PK',
diff --git a/packages/microapps-datalib/src/index.ts b/packages/microapps-datalib/src/index.ts
index bdadc24a..fd95527f 100644
--- a/packages/microapps-datalib/src/index.ts
+++ b/packages/microapps-datalib/src/index.ts
@@ -1,6 +1,4 @@
-import { DBManager } from './manager';
-import { Application, IVersionsAndRules } from './models/application';
-import { Rules } from './models/rules';
-import { Version } from './models/version';
-
-export { Application, DBManager, Version, Rules, IVersionsAndRules };
+export { DBManager } from './manager';
+export { Application, IVersionsAndRules } from './models/application';
+export { Rules } from './models/rules';
+export { Version } from './models/version';
diff --git a/packages/microapps-datalib/src/models/version.spec.ts b/packages/microapps-datalib/src/models/version.spec.ts
index 09fc17f5..84aecec5 100644
--- a/packages/microapps-datalib/src/models/version.spec.ts
+++ b/packages/microapps-datalib/src/models/version.spec.ts
@@ -24,119 +24,148 @@ describe('version records', () => {
     dynamoClient.destroy();
   }, 20000);
 
-  it('saving a version should create one record', async () => {
-    const version = new Version();
-    version.AppName = 'Cat';
-    version.SemVer = '1.2.3-Beta4';
-    version.Status = 'pending';
-    version.Type = 'lambda';
-    version.DefaultFile = 'index.html';
-    version.IntegrationID = 'abcd';
-    version.RouteIDAppVersion = '123';
-    version.RouteIDAppVersionSplat = '456';
-
-    await version.Save(dbManager);
-
-    const { Item } = await dbManager.ddbDocClient.get({
-      TableName: dbManager.tableName,
-      Key: { PK: 'appname#cat', SK: 'version#1.2.3-beta4' },
+  describe('Type API Gateway', () => {
+    it('saving a version should create one record', async () => {
+      const version = new Version();
+      version.AppName = 'Cat';
+      version.SemVer = '1.2.3-Beta4';
+      version.Status = 'pending';
+      version.Type = 'lambda';
+      version.DefaultFile = 'index.html';
+      version.IntegrationID = 'abcd';
+      version.RouteIDAppVersion = '123';
+      version.RouteIDAppVersionSplat = '456';
+
+      await version.Save(dbManager);
+
+      const { Item } = await dbManager.ddbDocClient.get({
+        TableName: dbManager.tableName,
+        Key: { PK: 'appname#cat', SK: 'version#1.2.3-beta4' },
+      });
+      expect(Item).toBeDefined();
+      expect(Item?.PK).toBe('appname#cat');
+      expect(Item?.SK).toBe('version#1.2.3-beta4');
+      expect(Item?.AppName).toBe('cat');
+      expect(Item?.SemVer).toBe('1.2.3-Beta4');
+      expect(Item?.Status).toBe('pending');
+      expect(Item?.Type).toBe('lambda');
+      expect(Item?.DefaultFile).toBe('index.html');
+      expect(Item?.IntegrationID).toBe('abcd');
+      expect(Item?.RouteIDAppVersion).toEqual('123');
+      expect(Item?.RouteIDAppVersionSplat).toEqual('456');
     });
-    expect(Item).toBeDefined();
-    expect(Item?.PK).toBe('appname#cat');
-    expect(Item?.SK).toBe('version#1.2.3-beta4');
-    expect(Item?.AppName).toBe('cat');
-    expect(Item?.SemVer).toBe('1.2.3-Beta4');
-    expect(Item?.Status).toBe('pending');
-    expect(Item?.Type).toBe('lambda');
-    expect(Item?.DefaultFile).toBe('index.html');
-    expect(Item?.IntegrationID).toBe('abcd');
-    expect(Item?.RouteIDAppVersion).toEqual('123');
-    expect(Item?.RouteIDAppVersionSplat).toEqual('456');
-  });
 
-  it('load 1 version should load 1 version', async () => {
-    let version = new Version();
-    version.AppName = 'Dog';
-    version.SemVer = '1.2.3-Beta5';
-    version.Status = 'pending';
-    version.Type = 'lambda';
-    version.DefaultFile = 'index.html';
-    version.IntegrationID = 'abcd';
-
-    await version.Save(dbManager);
-
-    version = new Version();
-    version.AppName = 'Dog';
-    version.SemVer = '1.2.3-Beta6';
-    version.Status = 'pending';
-    version.Type = 'lambda';
-    version.DefaultFile = 'index.html';
-    version.IntegrationID = 'abcd';
-
-    await version.Save(dbManager);
-
-    const version1 = await Version.LoadVersion({
-      dbManager,
-      key: {
-        AppName: 'Dog',
-        SemVer: '1.2.3-Beta5',
-      },
+    it('load 1 version should load 1 version', async () => {
+      let version = new Version();
+      version.AppName = 'Dog';
+      version.SemVer = '1.2.3-Beta5';
+      version.Status = 'pending';
+      version.Type = 'lambda';
+      version.DefaultFile = 'index.html';
+      version.IntegrationID = 'abcd';
+
+      await version.Save(dbManager);
+
+      version = new Version();
+      version.AppName = 'Dog';
+      version.SemVer = '1.2.3-Beta6';
+      version.Status = 'pending';
+      version.Type = 'lambda';
+      version.DefaultFile = 'index.html';
+      version.IntegrationID = 'abcd';
+
+      await version.Save(dbManager);
+
+      const version1 = await Version.LoadVersion({
+        dbManager,
+        key: {
+          AppName: 'Dog',
+          SemVer: '1.2.3-Beta5',
+        },
+      });
+
+      expect(version1.AppName).toBe('dog');
+      expect(version1.SK).toBe('version#1.2.3-beta5');
+      expect(version1.SemVer).toBe('1.2.3-Beta5');
+
+      const version2 = await Version.LoadVersion({
+        dbManager,
+        key: { AppName: 'Dog', SemVer: '1.2.3-Beta6' },
+      });
+
+      expect(version2.AppName).toBe('dog');
+      expect(version2.SK).toBe('version#1.2.3-beta6');
+      expect(version2.SemVer).toBe('1.2.3-Beta6');
     });
 
-    expect(version1.AppName).toBe('dog');
-    expect(version1.SK).toBe('version#1.2.3-beta5');
-    expect(version1.SemVer).toBe('1.2.3-Beta5');
-
-    const version2 = await Version.LoadVersion({
-      dbManager,
-      key: { AppName: 'Dog', SemVer: '1.2.3-Beta6' },
+    it('load all app versions should load all versions for 1 app', async () => {
+      let version = new Version();
+      version.AppName = 'Frog';
+      version.SemVer = '2.2.3-Beta5';
+      version.Status = 'pending';
+      version.Type = 'lambda';
+      version.DefaultFile = 'index1.html';
+      version.IntegrationID = 'abcd1';
+      version.RouteIDAppVersion = 'routeAppVersion1';
+      version.RouteIDAppVersionSplat = 'routeAppVersionSplat1';
+
+      await version.Save(dbManager);
+
+      version = new Version();
+      version.AppName = 'Frog';
+      version.SemVer = '2.2.3-Beta6';
+      version.Status = 'pending';
+      version.Type = 'lambda';
+      version.DefaultFile = 'index2.html';
+      version.IntegrationID = 'abcd2';
+      version.RouteIDAppVersion = 'routeAppVersion2';
+      version.RouteIDAppVersionSplat = 'routeAppVersionSplat2';
+
+      await version.Save(dbManager);
+
+      const versions = await Version.LoadVersions({ dbManager, key: { AppName: 'Frog' } });
+
+      expect(versions.length).toBe(2);
+
+      expect(versions[0].AppName).toBe('frog');
+      expect(versions[0].SK).toBe('version#2.2.3-beta5');
+      expect(versions[0].SemVer).toBe('2.2.3-Beta5');
+      expect(versions[0].DefaultFile).toEqual('index1.html');
+      expect(versions[0].RouteIDAppVersion).toEqual('routeAppVersion1');
+      expect(versions[0].RouteIDAppVersionSplat).toEqual('routeAppVersionSplat1');
+      expect(versions[1].AppName).toBe('frog');
+      expect(versions[1].SK).toBe('version#2.2.3-beta6');
+      expect(versions[1].SemVer).toBe('2.2.3-Beta6');
+      expect(versions[1].DefaultFile).toEqual('index2.html');
+      expect(versions[1].RouteIDAppVersion).toEqual('routeAppVersion2');
+      expect(versions[1].RouteIDAppVersionSplat).toEqual('routeAppVersionSplat2');
     });
-
-    expect(version2.AppName).toBe('dog');
-    expect(version2.SK).toBe('version#1.2.3-beta6');
-    expect(version2.SemVer).toBe('1.2.3-Beta6');
   });
 
-  it('load all app versions should load all versions for 1 app', async () => {
-    let version = new Version();
-    version.AppName = 'Frog';
-    version.SemVer = '2.2.3-Beta5';
-    version.Status = 'pending';
-    version.Type = 'lambda';
-    version.DefaultFile = 'index1.html';
-    version.IntegrationID = 'abcd1';
-    version.RouteIDAppVersion = 'routeAppVersion1';
-    version.RouteIDAppVersionSplat = 'routeAppVersionSplat1';
-
-    await version.Save(dbManager);
-
-    version = new Version();
-    version.AppName = 'Frog';
-    version.SemVer = '2.2.3-Beta6';
-    version.Status = 'pending';
-    version.Type = 'lambda';
-    version.DefaultFile = 'index2.html';
-    version.IntegrationID = 'abcd2';
-    version.RouteIDAppVersion = 'routeAppVersion2';
-    version.RouteIDAppVersionSplat = 'routeAppVersionSplat2';
-
-    await version.Save(dbManager);
-
-    const versions = await Version.LoadVersions({ dbManager, key: { AppName: 'Frog' } });
-
-    expect(versions.length).toBe(2);
-
-    expect(versions[0].AppName).toBe('frog');
-    expect(versions[0].SK).toBe('version#2.2.3-beta5');
-    expect(versions[0].SemVer).toBe('2.2.3-Beta5');
-    expect(versions[0].DefaultFile).toEqual('index1.html');
-    expect(versions[0].RouteIDAppVersion).toEqual('routeAppVersion1');
-    expect(versions[0].RouteIDAppVersionSplat).toEqual('routeAppVersionSplat1');
-    expect(versions[1].AppName).toBe('frog');
-    expect(versions[1].SK).toBe('version#2.2.3-beta6');
-    expect(versions[1].SemVer).toBe('2.2.3-Beta6');
-    expect(versions[1].DefaultFile).toEqual('index2.html');
-    expect(versions[1].RouteIDAppVersion).toEqual('routeAppVersion2');
-    expect(versions[1].RouteIDAppVersionSplat).toEqual('routeAppVersionSplat2');
+  describe('Type Lambda URL', () => {
+    it('saving and loading should return same record', async () => {
+      const version = new Version();
+      version.AppName = 'Cat';
+      version.SemVer = '1.2.3-Beta4';
+      version.Status = 'pending';
+      version.Type = 'lambda-url';
+      version.DefaultFile = 'index.html';
+      version.URL = 'https://abc123.lambda-url.us-east-1.on.aws/';
+
+      await version.Save(dbManager);
+
+      const { Item } = await dbManager.ddbDocClient.get({
+        TableName: dbManager.tableName,
+        Key: { PK: 'appname#cat', SK: 'version#1.2.3-beta4' },
+      });
+      expect(Item).toBeDefined();
+      expect(Item?.PK).toBe('appname#cat');
+      expect(Item?.SK).toBe('version#1.2.3-beta4');
+      expect(Item?.AppName).toBe('cat');
+      expect(Item?.SemVer).toBe('1.2.3-Beta4');
+      expect(Item?.Status).toBe('pending');
+      expect(Item?.Type).toBe('lambda-url');
+      expect(Item?.URL).toEqual('https://abc123.lambda-url.us-east-1.on.aws/');
+    });
   });
 });
diff --git a/packages/microapps-datalib/src/models/version.ts b/packages/microapps-datalib/src/models/version.ts
index 8ecdf476..7053bf25 100644
--- a/packages/microapps-datalib/src/models/version.ts
+++ b/packages/microapps-datalib/src/models/version.ts
@@ -13,7 +13,9 @@ export type VersionStatus =
   | 'routed'
   | 'deployed';
 
-export type AppTypes = 'static' | 'lambda';
+export type AppTypes = 'static' | 'lambda' | 'lambda-url' | 'url';
+
+export type AppStartupTypes = 'iframe' | 'direct';
 
 /**
  * Represents a Version Record
@@ -21,14 +23,69 @@ export type AppTypes = 'static' | 'lambda';
 export interface IVersionRecord {
   PK: string;
   SK: string;
+  /**
+   * Name of the app
+   */
   AppName: string;
+  /**
+   * SemVer for this version
+   */
   SemVer: string;
-  Type: 'lambda' | 'static';
+  /**
+   * Type of app (which implies how it's routed)
+   *
+   * @enum {string} static - Files only
+   * @enum {string} lambda - Lambda integrated via API Gateway
+   * @enum {string} lambda-url - Lambda integrated via Function URL w/IAM Auth
+   * @enum {string} url - Other URL without IAM Auth
+   */
+  Type: AppTypes;
+  /**
+   * Startup type of the app
+   * This currently indicates whether `/appName` will render an iframe
+   * that points to `/appName/semVer` or if `/appName` will proxy
+   * directly to a specific version of the app, which will render itself at
+   * `/appName` but will write all of it's static/api requests as
+   * `/appName/semVer/...`
+   * @enum {string} iframe - Render an iframe that points to the version
+   * @enum {string} direct - Proxy directly to the version, incompatible with `static` and `apigwy` (`lambda`)
+   */
+  StartupType: AppStartupTypes;
+  /**
+   * @enum {string} pending - Version is being created
+   * @enum {string} assets-copied - Version assets have been copied to S3
+   * @enum {string} permissioned - API Gatewy - Permissions to invoke have been granted
+   * @enum {string} integrated - API Gateway - Lambda integration has been created
+   * @enum {string} routed - API Gateway - Lambe integration has been added to routes
+   * @enum {string} deployed - Version has been fully deployed
+   */
   Status: VersionStatus;
+  /**
+   * Default file or path to redirect to if the app is started without
+   * a specific file or path below the appName or appName/semVer,
+   * particularly useful for `static` apps.
+   *
+   * @example 'index.html'
+   */
   DefaultFile: string;
+  /**
+   * API Gateway (type=lambda) only
+   */
   IntegrationID: string;
+  /**
+   * API Gateway (type=lambda) only
+   */
   RouteIDAppVersion: string;
+  /**
+   * API Gateway (type=lambda) only
+   */
   RouteIDAppVersionSplat: string;
+  /**
+   * Lambda Function URL (type=lambda-url) or URL (type=url) only
+   * @example https://[lambda-url-id].lambda-url.us-east-1.on.aws/
+   * @example https://www.example.com/
+   */
+  URL: string;
 }
 
 export type IVersionRecordNoKeysLoose = Partial<
@@ -121,19 +178,25 @@ export class Version implements IVersionRecord {
   private _appName: string | undefined;
   private _semVer: string | undefined;
   private _type: AppTypes | undefined;
+  private _startupType: AppStartupTypes | undefined;
   private _status: VersionStatus;
   private _defaultFile: string;
+  // API Gateway Integration Properties
   private _integrationID: string;
   private _routeIDAppVersion: string;
   private _routeIDAppVersionSplat: string | undefined;
+  // Lambda URL and URL Properties
+  private _url: string;
 
   public constructor(init?: Partial<IVersionRecordNoKeysLoose>) {
     this._keyBy = SaveBy.AppName;
     this._status = 'pending';
+    this._startupType = 'iframe';
     this._defaultFile = '';
     this._integrationID = '';
     this._routeIDAppVersion = '';
     this._routeIDAppVersionSplat = '';
+    this._url = '';
 
     // Save any passed in values over the defaults
     Object.assign(this, init);
@@ -146,11 +209,13 @@ export class Version implements IVersionRecord {
       AppName: this.AppName,
       SemVer: this.SemVer,
       Type: this.Type,
+      StartupType: this.StartupType,
       Status: this.Status,
       DefaultFile: this.DefaultFile,
       IntegrationID: this.IntegrationID,
       RouteIDAppVersion: this.RouteIDAppVersion,
       RouteIDAppVersionSplat: this.RouteIDAppVersionSplat,
+      URL: this.URL,
     };
   }
 
@@ -199,6 +264,13 @@ export class Version implements IVersionRecord {
     this._type = value;
   }
 
+  public get StartupType(): AppStartupTypes {
+    return this._startupType as AppStartupTypes;
+  }
+  public set StartupType(value: AppStartupTypes) {
+    this._startupType = value;
+  }
+
   public get Status(): VersionStatus {
     return this._status;
   }
@@ -233,4 +305,11 @@ export class Version implements IVersionRecord {
   public set RouteIDAppVersionSplat(value: string) {
     this._routeIDAppVersionSplat = value;
   }
+
+  public get URL(): string {
+    return this._url as string;
+  }
+  public set URL(value: string) {
+    this._url = value;
+  }
 }
diff --git a/packages/microapps-deployer-lib/src/index.ts b/packages/microapps-deployer-lib/src/index.ts
index f9f47f04..ea9955f9 100644
--- a/packages/microapps-deployer-lib/src/index.ts
+++ b/packages/microapps-deployer-lib/src/index.ts
@@ -63,9 +63,31 @@ export interface IDeployVersionPreflightRequest extends IDeployVersionRequestBas
  */
 export interface IDeployVersionRequest extends IDeployVersionRequestBase {
   readonly type: 'deployVersion';
-  readonly appType?: 'lambda' | 'static';
+  /**
+   * Type of the app (which implies how it's routed)
+   */
+  readonly appType?: 'lambda' | 'lambda-url' | 'url' | 'static';
+  /**
+   * Render the `/appName` route as an iframe pointing to `/appName/semVer`
+   * or proxy directly to `/appName/semVer` and render the app at `/appName`
+   *
+   * @default 'iframe'
+   */
+  readonly startupType?: 'iframe' | 'direct';
+  /**
+   * Used for `lambda` and `lambda-url` apps
+   */
   readonly lambdaARN?: string;
+  /**
+   * Default file or path
+   * Typically used for `static` apps, but can be used for non-static apps
+   */
   readonly defaultFile: string;
+  /**
+   * Used for `url` not for `lambda-url` (as the Function URL is created by the
+   * DeployVersion action in that case)
+   */
+  readonly url?: string;
 }
 
 /**
diff --git a/packages/microapps-deployer/src/controllers/VersionController.spec.ts b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
index c69b5d07..1de267c1 100644
--- a/packages/microapps-deployer/src/controllers/VersionController.spec.ts
+++ b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
@@ -19,6 +19,7 @@ const theConfig: Writeable<IConfig> = {
   },
   uploadRoleName: 'microapps-upload-test-role',
   rootPathPrefix: 'dev',
+  requireIAMAuthorization: true,
 };
 const origConfig = { ...theConfig };
 Object.defineProperty(Config, 'instance', {
diff --git a/packages/microapps-deployer/src/controllers/VersionController.ts b/packages/microapps-deployer/src/controllers/VersionController.ts
index dfdc1dd0..dfa4bd3a 100644
--- a/packages/microapps-deployer/src/controllers/VersionController.ts
+++ b/packages/microapps-deployer/src/controllers/VersionController.ts
@@ -146,7 +146,7 @@ export default class VersionController {
     config: IConfig;
   }): Promise<IDeployerResponse> {
     const { dbManager, request, config } = opts;
-    const { overwrite = false } = request;
+    const { appType = 'lambda', overwrite = false, startupType = 'iframe' } = request;
 
     Log.Instance.debug('Got Body:', request);
 
@@ -171,17 +171,34 @@ export default class VersionController {
       }
     }
 
-    const { appType = 'lambda' } = request;
+    // Check for incompatible app types and startup types
+    if (startupType === 'direct' && ['lambda'].includes(appType)) {
+      // 'lambda' (aka 'apigwy') cannot have direct routing because
+      // we don't deploy a proxy to API Gateway on the /appName routes
+      Log.Instance.info('Error: Incompatible app type and startup type', {
+        appType,
+        startupType,
+      });
+
+      return { statusCode: 400 };
+    }
+
+    // Update the version record if overwriting
+    if (overwrite && record) {
+      record.DefaultFile = request.defaultFile;
+      record.Type = appType;
+      record.StartupType = startupType;
+    }
 
     // Create the version record
     if (record === undefined) {
       record = new Version({
         AppName: request.appName,
         SemVer: request.semVer,
-        IntegrationID: '',
         Type: appType,
         Status: 'pending',
         DefaultFile: request.defaultFile,
+        StartupType: startupType,
       });
 
       // Save record with pending status
@@ -210,6 +227,9 @@ export default class VersionController {
       await record.Save(dbManager);
     }
 
+    //
+    // BEGIN: Type-specific handling
+    //
     if (appType === 'lambda') {
       // TODO: Confirm the Lambda Function exists
 
@@ -395,13 +415,82 @@ export default class VersionController {
         record.Status = 'routed';
         await record.Save(dbManager);
       }
-    } else {
+    } else if (appType === 'lambda-url') {
+      // Get base of lambda arn
+      const lambdaARNBase = request.lambdaARN?.substring(0, request.lambdaARN.lastIndexOf(':'));
+      const lambdaARNVersion = request.lambdaARN?.substring(request.lambdaARN.lastIndexOf(':') + 1);
+
+      if (overwrite || record.Status === 'assets-copied') {
+        // Check if the lambda function has the microapp-managed tag
+        const tags = await lambdaClient.send(
+          new lambda.ListTagsCommand({
+            Resource: lambdaARNBase,
+          }),
+        );
+        // Add the tag if it is missing
+        if (tags.Tags === undefined || tags.Tags['microapp-managed'] !== 'true') {
+          await lambdaClient.send(
+            new lambda.TagResourceCommand({
+              Resource: lambdaARNBase,
+              Tags: {
+                'microapp-managed': 'true',
+              },
+            }),
+          );
+        }
+
+        record.Status = 'permissioned';
+        await record.Save(dbManager);
+      }
+
+      if (overwrite || record.Status === 'permissioned') {
+        let url: string | undefined = undefined;
+        let functionUrl: lambda.GetFunctionUrlConfigResponse | undefined = undefined;
+        try {
+          functionUrl = await lambdaClient.send(
+            new lambda.GetFunctionUrlConfigCommand({
+              FunctionName: lambdaARNBase,
+              Qualifier: lambdaARNVersion,
+            }),
+          );
+          // Create the FunctionUrl if it doesn't already exist
+          if (functionUrl.FunctionUrl) {
+            url = functionUrl.FunctionUrl;
+          }
+        } catch (error: any) {
+          if (error.name !== 'ResourceNotFoundException') {
+            throw error;
+          }
+        }
+
+        // Create the FunctionUrl if it doesn't already exist
+        if (!functionUrl?.FunctionUrl) {
+          const functionUrlNew = await lambdaClient.send(
+            new lambda.CreateFunctionUrlConfigCommand({
+              FunctionName: lambdaARNBase,
+              Qualifier: lambdaARNVersion,
+              AuthType: 'AWS_IAM',
+            }),
+          );
+          url = functionUrlNew.FunctionUrl;
+        }
+
+        // Update the status - Final status
+        record.Status = 'routed';
+        if (url) {
+          record.URL = url;
+        }
+        await record.Save(dbManager);
+      }
+    } else if (appType === 'static') {
       // static app
       if (record.Status === 'assets-copied') {
         // Update the status - Final status
         record.Status = 'routed';
         await record.Save(dbManager);
       }
+    } else {
+      throw new Error(`Unknown app type: ${appType}`);
     }
 
     // Check if there are any release rules
@@ -546,7 +635,12 @@ export default class VersionController {
             });
           }
         }
+
+        // TODO: Remove the Alias from the Lambda Function
       }
+    } else if (record.Type === 'lambda-url') {
+      // TODO: Remove the Function URL and Alias from the Lambda Function
+      throw new Error('Not implemented');
     }
 
     // Delete DynamoDB record
diff --git a/packages/microapps-edge-to-origin/appFrame.html b/packages/microapps-edge-to-origin/appFrame.html
new file mode 120000
index 00000000..bda74f25
--- /dev/null
+++ b/packages/microapps-edge-to-origin/appFrame.html
@@ -0,0 +1,1 @@
+../microapps-router/appFrame.html
\ No newline at end of file
diff --git a/packages/microapps-edge-to-origin/package.json b/packages/microapps-edge-to-origin/package.json
index 323e718e..b520a1f3 100644
--- a/packages/microapps-edge-to-origin/package.json
+++ b/packages/microapps-edge-to-origin/package.json
@@ -5,6 +5,7 @@
   "private": true,
   "license": "MIT",
   "main": "dist/index.js",
+  "types": "dist/index.d.ts",
   "devDependencies": {
     "@types/aws-lambda": "^8.10.77",
     "@types/convict": "^6.0.2",
diff --git a/packages/microapps-edge-to-origin/src/config/config.spec.ts b/packages/microapps-edge-to-origin/src/config/config.spec.ts
index e94b8eb9..e4a8a355 100644
--- a/packages/microapps-edge-to-origin/src/config/config.spec.ts
+++ b/packages/microapps-edge-to-origin/src/config/config.spec.ts
@@ -16,6 +16,7 @@ describe('config', () => {
       addXForwardedHostHeader: true,
       replaceHostHeader: true,
       signingMode: 'sign',
+      tableName: 'microapps',
     };
     const loader = new TSConvict<Config>(Config);
     const config = loader.load(rawConfig);
diff --git a/packages/microapps-edge-to-origin/src/config/config.ts b/packages/microapps-edge-to-origin/src/config/config.ts
index cbcab35f..6d47afac 100644
--- a/packages/microapps-edge-to-origin/src/config/config.ts
+++ b/packages/microapps-edge-to-origin/src/config/config.ts
@@ -9,12 +9,47 @@ import { FilesExist } from '../lib/files-exist';
 export interface IConfig {
   readonly awsAccountID: number;
   readonly awsRegion: string;
+
+  /**
+   * AWS Origin Region for signing requests with SigV4
+   */
   readonly originRegion: string;
+
+  /**
+   * SigV4 signing mode for origin requests
+   */
   readonly signingMode: 'sign' | 'presign' | '';
+
+  /**
+   * Add X-Forwarded-Host header with incoming Host header
+   * received by CloudFront
+   */
   readonly addXForwardedHostHeader: boolean;
+
+  /**
+   * Replace the Host header with the origin host to prevent rejection by API Gateway
+   * when forwarding all headers to origin
+   */
   readonly replaceHostHeader: boolean;
+
+  /**
+   * Allow 2nd generation routing by forwarding the request
+   * directly to the Lambda Function URL of the target version,
+   * using the rules in the rules table.
+   *
+   * This resolves issues due to the 300 route limit in API Gateway
+   * and allows optional SEO-friendly routing without iFrames.
+   *
+   * @default none
+   */
+  readonly tableName: string;
 }
 
+/**
+ * Config fields written to the edge-to-origin config.yml file
+ */
+export type IConfigFile = Partial<Omit<IConfig, 'awsAccountID' | 'awsRegion'>>;
+
 @convict.Config({
   // optional default file to load, no errors if it doesn't exist
   file: 'config.yml', // relative to NODE_PATH or cwd()
@@ -117,4 +152,11 @@ export class Config implements IConfig {
     env: 'REPLACE_HOST_HEADER',
   })
   public replaceHostHeader!: boolean;
+
+  @convict.Property({
+    doc: `Allow 2nd generation routing by forwarding the request to the Lambda Function URL of the target version.`,
+    env: 'TABLE_NAME',
+    default: '',
+  })
+  public tableName!: string;
 }
diff --git a/packages/microapps-edge-to-origin/src/index.route.spec.ts b/packages/microapps-edge-to-origin/src/index.route.spec.ts
new file mode 100644
index 00000000..d34c7d2b
--- /dev/null
+++ b/packages/microapps-edge-to-origin/src/index.route.spec.ts
@@ -0,0 +1,759 @@
+/// <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');
+type Writeable<T> = { -readonly [P in keyof T]: T[P] };
+const theConfig: Writeable<IConfig> = {
+  awsAccountID: 123456,
+  awsRegion: 'mock',
+  addXForwardedHostHeader: false,
+  originRegion: 'us-west-1',
+  replaceHostHeader: false,
+  signingMode: 'sign',
+  tableName: '',
+};
+const origConfig = { ...theConfig };
+Object.defineProperty(Config, 'instance', {
+  configurable: false,
+  enumerable: false,
+  get: jest.fn((): IConfig => {
+    return theConfig;
+  }),
+});
+import { handler, overrideDBManager } from './index';
+
+let dynamoClient: dynamodb.DynamoDBClient;
+let dbManager: DBManager;
+
+const TEST_TABLE_NAME = 'microapps';
+
+describe('edge-to-origin - routing - without 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 });
+
+    // Reset the config that's visible to the handler back to defaults
+    Object.keys(origConfig).forEach((key) => {
+      (theConfig as { [index: string]: unknown })[key] = (
+        origConfig as { [index: string]: unknown }
+      )[key];
+    });
+  });
+
+  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
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toBeDefined();
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse?.status).toBe('200');
+    expect(responseResponse).toHaveProperty('body');
+    expect(responseResponse.body?.length).toBeGreaterThan(80);
+    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta.1/bat.html" seamless');
+  });
+
+  it('should route `iframe` app request with appName/version to origin', async () => {
+    theConfig.replaceHostHeader = true;
+
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      IntegrationID: 'abcd',
+      SemVer: '4.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'lambda-url',
+      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '4.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {
+                  host: [
+                    {
+                      key: 'Host',
+                      value: 'zyz.cloudfront.net',
+                    },
+                  ],
+                },
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/4.2.1-beta.1',
+                origin: {
+                  custom: {
+                    customHeaders: {},
+                    domainName: 'zyz.cloudfront.net',
+                    keepaliveTimeout: 5,
+                    path: '',
+                    port: 443,
+                    protocol: 'https',
+                    readTimeout: 30,
+                    sslProtocols: ['TLSv1.2'],
+                  },
+                },
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const requestResponse = response as lambda.CloudFrontRequest;
+    expect(requestResponse).toBeDefined();
+    expect(requestResponse).not.toHaveProperty('status');
+    expect(requestResponse).not.toHaveProperty('body');
+    expect(requestResponse).toHaveProperty('headers');
+    expect(requestResponse.headers).toHaveProperty('host');
+    expect(requestResponse.headers.host).toHaveLength(1);
+    expect(requestResponse.headers.host[0].key).toBe('Host');
+    expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+    expect(requestResponse).toHaveProperty('origin');
+    expect(requestResponse.origin).toHaveProperty('custom');
+    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+    expect(requestResponse?.origin?.custom?.domainName).toBe('abc123.lambda-url.us-east-1.on.aws');
+  });
+
+  it('should route `direct` app request with appName to origin', async () => {
+    theConfig.replaceHostHeader = true;
+
+    const app = new Application({
+      AppName: 'BatDirect',
+      DisplayName: 'Direct Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'BatDirect',
+      SemVer: '1.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'lambda-url',
+      StartupType: 'direct',
+      URL: 'https://abc123.lambda-url.us-east-1.on.aws/',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'BatDirect',
+      Version: 0,
+      RuleSet: { default: { SemVer: '1.2.1-beta.1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {
+                  host: [
+                    {
+                      key: 'Host',
+                      value: 'zyz.cloudfront.net',
+                    },
+                  ],
+                },
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/batdirect',
+                origin: {
+                  custom: {
+                    customHeaders: {},
+                    domainName: 'zyz.cloudfront.net',
+                    keepaliveTimeout: 5,
+                    path: '',
+                    port: 443,
+                    protocol: 'https',
+                    readTimeout: 30,
+                    sslProtocols: ['TLSv1.2'],
+                  },
+                },
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const requestResponse = response as lambda.CloudFrontRequest;
+    expect(requestResponse).toBeDefined();
+    expect(requestResponse).not.toHaveProperty('status');
+    expect(requestResponse).not.toHaveProperty('body');
+    expect(requestResponse).toHaveProperty('headers');
+    expect(requestResponse.headers).toHaveProperty('host');
+    expect(requestResponse.headers.host).toHaveLength(1);
+    expect(requestResponse.headers.host[0].key).toBe('Host');
+    expect(requestResponse.headers.host[0].value).toBe('abc123.lambda-url.us-east-1.on.aws');
+    expect(requestResponse).toHaveProperty('origin');
+    expect(requestResponse.origin).toHaveProperty('custom');
+    expect(requestResponse?.origin?.custom).toHaveProperty('domainName');
+    expect(requestResponse?.origin?.custom?.domainName).toBe('abc123.lambda-url.us-east-1.on.aws');
+  });
+
+  it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'static',
+    });
+    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
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/3.2.1-beta.1',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('404');
+    expect(responseResponse).not.toHaveProperty('headers');
+  });
+
+  it('static app - request to app/x.y.z should redirect to defaultFile', 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: 'static',
+    });
+    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
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/3.2.1-beta.1',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('302');
+    expect(responseResponse.headers).toHaveProperty('location');
+    expect(responseResponse.headers?.location[0].value).toContain('/bat/3.2.1-beta.1/bat.html');
+  });
+
+  it('static app - request to app/x.y.z/ should redirect to defaultFile', 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: 'static',
+    });
+    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
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/3.2.1-beta.1',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('302');
+    expect(responseResponse.headers).toHaveProperty('location');
+    expect(responseResponse.headers?.location[0].value).toBe('/bat/3.2.1-beta.1/bat.html');
+  });
+
+  it('static app - request to app/notVersion should load app frame with defaultFile', 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: 'static',
+    });
+    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
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/notVersion',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('200');
+    expect(responseResponse).toHaveProperty('body');
+    expect(responseResponse.headers).not.toHaveProperty('location');
+    expect(responseResponse.body?.length).toBeGreaterThan(80);
+    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta.1/bat.html" seamless');
+  });
+
+  it('should serve appframe with no default file', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: '',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta1',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('200');
+    expect(responseResponse).toHaveProperty('body');
+    expect(responseResponse.headers).not.toHaveProperty('location');
+    expect(responseResponse.body?.length).toBeGreaterThan(80);
+    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta1" seamless');
+  });
+
+  it('should serve appframe with sub-sub-route - beta2', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: '',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta2',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/demo/grid',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('200');
+    expect(responseResponse).toHaveProperty('body');
+    expect(responseResponse.headers).not.toHaveProperty('location');
+    expect(responseResponse.body?.length).toBeGreaterThan(80);
+    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta2/demo/grid" seamless');
+  });
+
+  it('should serve appframe with sub-route - beta3', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'someFile.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta3',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/bat/demo',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('200');
+    expect(responseResponse).toHaveProperty('body');
+    expect(responseResponse.headers).not.toHaveProperty('location');
+    expect(responseResponse.body?.length).toBeGreaterThan(80);
+    expect(responseResponse.body).toContain('<iframe src="/bat/3.2.1-beta3/demo" seamless');
+  });
+
+  it('should return 404 for /favicon.ico', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'someFile.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta3',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    // @ts-expect-error no callback
+    const response = await handler(
+      {
+        Records: [
+          {
+            cf: {
+              config: {
+                distributionDomainName: 'zyz.cloudfront.net',
+                distributionId: '123',
+                eventType: 'origin-request',
+                requestId: '123',
+              },
+              request: {
+                headers: {},
+                method: 'GET',
+                querystring: '',
+                clientIp: '1.1.1.1',
+                uri: '/favicon.ico',
+              },
+            },
+          },
+        ],
+      } as lambda.CloudFrontRequestEvent,
+      {} as lambda.Context,
+    );
+
+    const responseResponse = response as lambda.CloudFrontResultResponse;
+    expect(responseResponse).toHaveProperty('status');
+    expect(responseResponse.status).toBe('404');
+    expect(responseResponse).not.toHaveProperty('body');
+  });
+});
diff --git a/packages/microapps-edge-to-origin/src/index.spec.ts b/packages/microapps-edge-to-origin/src/index.spec.ts
index 64c423f5..3b2ca18e 100644
--- a/packages/microapps-edge-to-origin/src/index.spec.ts
+++ b/packages/microapps-edge-to-origin/src/index.spec.ts
@@ -11,6 +11,7 @@ const theConfig: Writeable<IConfig> = {
   originRegion: 'us-west-1',
   replaceHostHeader: false,
   signingMode: 'sign',
+  tableName: '',
 };
 const origConfig = { ...theConfig };
 Object.defineProperty(Config, 'instance', {
@@ -191,6 +192,10 @@ describe('handler', () => {
         ],
       },
       method: 'GET',
+      origin: {
+        // @ts-expect-error do not need full object for this test
+        custom: { domainName: 'random-but-account-specific-name.lambda-url.us-east-2.on.aws' },
+      },
       querystring: 'param1=value1&param2=value2',
       uri: '/test/test.html',
     };
@@ -249,6 +254,10 @@ describe('handler', () => {
       method: 'GET',
       querystring: 'param1=value1&param2=value2',
       uri: '/test/test.html',
+      origin: {
+        // @ts-expect-error do not need full object for this test
+        custom: { domainName: 'random-but-account-specific-name.lambda-url.us-east-2.on.aws' },
+      },
     };
 
     const signedRequest = await handler(
@@ -306,6 +315,10 @@ describe('handler', () => {
       method: 'GET',
       querystring: 'param1=value1&param2=value2',
       uri: '/test/test.html',
+      origin: {
+        // @ts-expect-error do not need full object for this test
+        custom: { domainName: 'random-but-account-specific-name.lambda-url.us-east-2.on.aws' },
+      },
     };
 
     const unsignedRequest = await handler(
diff --git a/packages/microapps-edge-to-origin/src/index.ts b/packages/microapps-edge-to-origin/src/index.ts
index 3f882c4b..85fbbf3c 100644
--- a/packages/microapps-edge-to-origin/src/index.ts
+++ b/packages/microapps-edge-to-origin/src/index.ts
@@ -2,19 +2,43 @@ import 'source-map-support/register';
 // Used by ts-convict
 import 'reflect-metadata';
 import type * as lambda from 'aws-lambda';
+import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
 import { defaultProvider } from '@aws-sdk/credential-provider-node';
 import { SignatureV4 } from '@aws-sdk/signature-v4';
 import { Sha256 } from '@aws-crypto/sha256-browser';
+import { DBManager } from '@pwrdrvr/microapps-datalib';
+import { GetRoute, loadAppFrame } from '@pwrdrvr/microapps-router-lib';
 import { signRequest, presignRequest } from './sign-request';
 import { Config } from './config/config';
+export { IConfigFile } from './config/config';
 import Log from './lib/log';
 
 const log = Log.Instance;
 const config = Config.instance;
+const appFrame = loadAppFrame({ basePath: __dirname });
 
-log.info('loading config', { config });
+log.info('loaded config', { config });
 
-// TODO: get the target region from the config file
+let dbManager: DBManager | undefined;
+let dynamoClient = new DynamoDBClient({
+  maxAttempts: 8,
+  region: config.originRegion,
+});
+
+export function overrideDBManager(opts: {
+  dbManager: DBManager;
+  dynamoClient: DynamoDBClient;
+}): void {
+  dbManager = opts.dbManager;
+  dynamoClient = opts.dynamoClient;
+}
+dbManager =
+  config.tableName && dynamoClient
+    ? new DBManager({
+        dynamoClient,
+        tableName: config.tableName,
+      })
+    : undefined;
 
 // NOTE: signing requires that we know the target region so R53 names with balancing
 // across regions is not supported.  We could instead modify the target
@@ -55,21 +79,121 @@ export const handler: lambda.CloudFrontRequestHandler = async (
       ];
     }
 
+    // Get the target if using direct to version routing
+    // This can use API Gateway per app or Lambda per version
+    // or ALBs or whatever you want (but it assumes IAM auth).
+    let originHost = request.origin?.custom?.domainName;
+    let routeType = 'api-gateway';
+    if (dbManager) {
+      // TODO: Get the prefix from the config file
+
+      const route = await GetRoute({
+        dbManager,
+        rawPath: event.Records[0].cf.request.uri,
+      });
+
+      log.info('got route info', { route });
+
+      // Write the app iframe to start an iframe-based app
+      if (route.startupType === 'iframe' && route.iFrameAppVersionPath) {
+        const frameHTML = appFrame.replace('{{iframeSrc}}', route.iFrameAppVersionPath);
+
+        log.info('returning app frame', { frameHTML });
+
+        return {
+          status: '200',
+          headers: {
+            'content-type': [{ key: 'Content-Type', value: 'text/html; charset=UTF-8' }],
+            'cache-control': [{ key: 'Cache-Control', value: 'no-store; private' }],
+          },
+          body: frameHTML,
+          bodyEncoding: 'text',
+        };
+      }
+
+      // Handle redirect if we got one
+      if (route.redirectLocation) {
+        return {
+          status: `${route.statusCode}`,
+          headers: {
+            location: [{ key: 'Location', value: route.redirectLocation }],
+          },
+        };
+      }
+
+      // Fall through to apigwy handling if type is not url
+      if (route.url && (route.type === 'url' || route.type === 'lambda-url')) {
+        log.info('rewriting to url', { url: route.url });
+
+        routeType = route.type;
+
+        const url = new URL(route.url);
+
+        // Set the origin host to point to the Lambda Function URL for this version
+        originHost = url.hostname;
+      }
+
+      // We've got a table name to lookup targets
+      // const appName = 'release';
+      // const semVer = '0.2.4';
+
+      // TODO: If there is a verion in the path, bail out of this check
+      // (e.g. /app1/0.3.4/...)
+
+      // TODO: If there is a version in a cookie for this app and the request is not
+      // for the root of the app, route to the version in the cookie
+
+      // TODO: If the path has a version in a sub-path, route to that version
+      // (e.g. /app1/_next/data/0.3.4/...)
+
+      // TODO: Rules should have the version info denormalized onto each Rule
+      // to make it one hit to the DB to get the target info.
+
+      // const rules = await Rules.Load({ dbManager, key: { AppName: appName } });
+
+      // // Lookup the URL for this specific version (or fall through to API Gateway)
+      // const version = await Version.LoadVersion({
+      //   dbManager,
+      //   key: { AppName: appName, SemVer: semVer },
+      // });
+
+      // Dump the info
+      // log.info('rules and version', { rules, version, event });
+
+      // TODO: Check the rule - If it points to a Function URL, then change the origin
+      // to the Function URL
+
+      // Just pass everything through to this one app
+      // originHost = version?.URL;
+    }
+
+    // Can't do anything without an origin
+    if (!originHost) {
+      log.error('No origin found', { request });
+      return {
+        status: '404',
+        statusDescription: 'Origin Domain Missing',
+      };
+    }
+
     // Replace the Host header with the target origin host
     // This prevents API Gateway and Function URLs from rejecting
     // the request when the OriginRequestPolicy is forwarding all
     // headers to the origin
-    if (config.replaceHostHeader && request.origin?.custom?.domainName) {
-      request.headers['host'] = [{ key: 'host', value: request.origin.custom.domainName }];
+    if (
+      config.tableName ||
+      (config.replaceHostHeader && originHost) ||
+      routeType === 'lambda-url' ||
+      routeType === 'url'
+    ) {
+      request.headers['host'] = [{ key: 'Host', value: originHost }];
     }
 
     // Lambda Function URLs cannot have a custom domain name
     // Function URLs will always contain `.lambda-url.`
     // API Gateway URLs can contain '.execute-api.' but will not
     // when customized, so we can only rely on the Lambda URL check.
-    const signer = request.origin?.custom?.domainName.includes('.lambda-url.')
-      ? lambdaSigner
-      : executeApiSigner;
+    const signer = originHost.includes('.lambda-url.') ? lambdaSigner : executeApiSigner;
     if (config.signingMode === 'sign') {
       log.info('signing request');
       const signedRequest = await signRequest(request, context, signer);
@@ -87,6 +211,12 @@ export const handler: lambda.CloudFrontRequestHandler = async (
       requestToReturn,
     });
 
+    // Overwrite the origin
+    if (request.origin?.custom?.domainName) {
+      requestToReturn.origin = { ...request.origin };
+      requestToReturn.origin.custom.domainName = originHost;
+    }
+
     return requestToReturn as unknown as lambda.CloudFrontResultResponse;
   } catch (error) {
     log.error('caught exception at top level', { error });
diff --git a/packages/microapps-publish/src/commands/publish.ts b/packages/microapps-publish/src/commands/publish.ts
index ef926641..ddc4fee7 100644
--- a/packages/microapps-publish/src/commands/publish.ts
+++ b/packages/microapps-publish/src/commands/publish.ts
@@ -3,7 +3,7 @@ import * as util from 'util';
 import * as lambda from '@aws-sdk/client-lambda';
 import * as s3 from '@aws-sdk/client-s3';
 import * as sts from '@aws-sdk/client-sts';
-import { Command, flags as flagsParser } from '@oclif/command';
+import { Command, flags, flags as flagsParser } from '@oclif/command';
 import * as path from 'path';
 import { pathExists, createReadStream } from 'fs-extra';
 import { Listr, ListrTask } from 'listr2';
@@ -99,6 +99,21 @@ export class PublishCommand extends Command {
       default: false,
       description: 'Force revalidation of CloudFront and browser caching of static assets',
     }),
+    'startup-type': flagsParser.enum({
+      multiple: false,
+      required: false,
+      options: ['iframe', 'direct'],
+      default: 'iframe',
+      description: 'How the app should be loaded',
+    }),
+    type: flagsParser.enum({
+      char: 't',
+      multiple: false,
+      required: false,
+      options: ['apigwy', 'lambda-url'],
+      default: 'apigwy',
+      description: 'Type of the application and how its requests are routed',
+    }),
   };
 
   private VersionAndAlias: IVersions;
@@ -331,10 +346,18 @@ export class PublishCommand extends Command {
             const origTitle = task.title;
             task.title = RUNNING + origTitle;
 
+            const appType =
+              parsedFlags.type === 'apigwy'
+                ? 'lambda'
+                : parsedFlags.type === 'lambda-url'
+                ? 'lambda-url'
+                : 'url';
+
             // Call Deployer to Deploy AppName/Version
             await DeployClient.DeployVersion({
               config,
-              appType: 'lambda',
+              appType,
+              startupType: parsedFlags['startup-type'] as 'iframe' | 'direct',
               overwrite,
               output: (message: string) => (task.output = message),
             });
diff --git a/packages/microapps-publish/src/lib/DeployClient.ts b/packages/microapps-publish/src/lib/DeployClient.ts
index 5e85240a..47140e09 100644
--- a/packages/microapps-publish/src/lib/DeployClient.ts
+++ b/packages/microapps-publish/src/lib/DeployClient.ts
@@ -148,18 +148,20 @@ export default class DeployClient {
    */
   public static async DeployVersion(opts: {
     config: IConfig;
-    appType: 'lambda' | 'static';
+    appType: 'lambda' | 'static' | 'lambda-url' | 'url';
+    startupType?: 'iframe' | 'direct';
     overwrite: boolean;
     output: (message: string) => void;
   }): Promise<void> {
-    const { config, appType, overwrite, output } = opts;
+    const { config, appType, startupType = 'iframe', overwrite, output } = opts;
     const request = {
       type: 'deployVersion',
       appType,
+      startupType,
       appName: config.app.name,
       semVer: config.app.semVer,
       defaultFile: config.app.defaultFile,
-      lambdaARN: appType === 'lambda' ? config.app.lambdaARN : undefined,
+      lambdaARN: ['lambda', 'lambda-url'].includes(appType) ? config.app.lambdaARN : undefined,
       overwrite,
     } as IDeployVersionRequest;
     const response = await this._client.send(
diff --git a/packages/microapps-router-lib/LICENSE b/packages/microapps-router-lib/LICENSE
new file mode 120000
index 00000000..30cff740
--- /dev/null
+++ b/packages/microapps-router-lib/LICENSE
@@ -0,0 +1,1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/packages/microapps-router-lib/package.json b/packages/microapps-router-lib/package.json
new file mode 100644
index 00000000..b77a94d5
--- /dev/null
+++ b/packages/microapps-router-lib/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "@pwrdrvr/microapps-router-lib",
+  "version": "0.0.0",
+  "private": true,
+  "description": "Router library for the microapps framework",
+  "main": "dist/index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/pwrdrvr/microapps-core.git"
+  },
+  "author": "PwrDrvr LLC",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/pwrdrvr/microapps-core/issues"
+  },
+  "homepage": "https://github.com/pwrdrvr/microapps-core#readme",
+  "dependencies": {
+    "@aws-sdk/client-dynamodb": "^3.21.0",
+    "fs-extra": "^9.1.0",
+    "lambda-log": "^3.1.0",
+    "source-map-support": "^0.5.19",
+    "tslib": "^2.1.0"
+  },
+  "devDependencies": {
+    "@types/aws-lambda": "^8.10.72",
+    "@types/lambda-log": "^2.2.0",
+    "@types/node": "^14.17.34",
+    "@types/source-map-support": "^0.5.4"
+  }
+}
diff --git a/packages/microapps-router-lib/src/index.prefix.spec.ts b/packages/microapps-router-lib/src/index.prefix.spec.ts
new file mode 100644
index 00000000..9c48335b
--- /dev/null
+++ b/packages/microapps-router-lib/src/index.prefix.spec.ts
@@ -0,0 +1,94 @@
+/// <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 { GetRoute } 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 });
+  });
+
+  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 GetRoute({
+      dbManager,
+      normalizedPathPrefix: '/qa',
+      rawPath: '/qa/bat/',
+    });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response).toBeDefined();
+    expect(response.iFrameAppVersionPath).toBe('/qa/bat/3.2.1-beta.1/bat.html');
+  });
+
+  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 GetRoute({ dbManager, normalizedPathPrefix: '/qa', rawPath: '/bat/' });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(404);
+    expect(response.errorMessage).toBe('Request not routable');
+  });
+});
diff --git a/packages/microapps-router-lib/src/index.spec.ts b/packages/microapps-router-lib/src/index.spec.ts
new file mode 100644
index 00000000..4bd2455a
--- /dev/null
+++ b/packages/microapps-router-lib/src/index.spec.ts
@@ -0,0 +1,318 @@
+/// <reference types="jest" />
+import 'jest-dynalite/withDb';
+import * as dynamodb from '@aws-sdk/client-dynamodb';
+import { Application, DBManager, Version, Rules } from '@pwrdrvr/microapps-datalib';
+import { GetRoute } from './index';
+
+let dynamoClient: dynamodb.DynamoDBClient;
+let dbManager: DBManager;
+
+const TEST_TABLE_NAME = 'microapps';
+
+describe('router - without 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 });
+  });
+
+  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 GetRoute({ dbManager, rawPath: '/bat/' });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response).toBeDefined();
+    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
+  });
+
+  it('static app - request to app/x.y.z should redirect to defaultFile', 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: 'static',
+    });
+    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 GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1' });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(302);
+    expect(response.redirectLocation).toBeDefined();
+    expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
+  }, 60000);
+
+  it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'static',
+    });
+    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 GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
+
+    expect(response).toHaveProperty('appName');
+    expect(response.appName).toBe('bat');
+    expect(response).toHaveProperty('semVer');
+    expect(response.semVer).toBe('3.2.1-beta.1');
+  });
+
+  it('static app - request to app/x.y.z/ should redirect to defaultFile', 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: 'static',
+    });
+    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 GetRoute({ dbManager, rawPath: '/bat/3.2.1-beta.1/' });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(302);
+    expect(response.redirectLocation).toBeDefined();
+    expect(response.redirectLocation).toBe('/bat/3.2.1-beta.1/bat.html');
+  });
+
+  it('static app - request to app/notVersion should load app frame with defaultFile', 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: 'static',
+    });
+    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 GetRoute({ dbManager, rawPath: '/bat/notVersion' });
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response).toBeDefined();
+    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta.1/bat.html');
+  });
+
+  it('should serve appframe with no default file', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: '',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta1',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta1', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await GetRoute({ dbManager, rawPath: '/bat/' });
+
+    expect(response).toBeDefined();
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta1');
+  });
+
+  it('should serve appframe with sub-route', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: '',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta2',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta2', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await GetRoute({ dbManager, rawPath: '/bat/demo/grid' });
+
+    expect(response).toBeDefined();
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta2/demo/grid');
+  });
+
+  it('should serve appframe with sub-route', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'someFile.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta3',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await GetRoute({ dbManager, rawPath: '/bat/demo' });
+
+    expect(response).toBeDefined();
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response.iFrameAppVersionPath).toBe('/bat/3.2.1-beta3/demo');
+  });
+
+  it('should return 404 for /favicon.ico', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      DefaultFile: 'someFile.html',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta3',
+      Status: 'deployed',
+      Type: 'lambda',
+    });
+    await version.Save(dbManager);
+
+    const rules = new Rules({
+      AppName: 'Bat',
+      Version: 0,
+      RuleSet: { default: { SemVer: '3.2.1-beta3', AttributeName: '', AttributeValue: '' } },
+    });
+    await rules.Save(dbManager);
+
+    // Call the handler
+    const response = await GetRoute({ dbManager, rawPath: '/favicon.ico' });
+
+    expect(response).toBeDefined();
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(404);
+  });
+});
diff --git a/packages/microapps-router-lib/src/index.ts b/packages/microapps-router-lib/src/index.ts
new file mode 100644
index 00000000..b7cba8ed
--- /dev/null
+++ b/packages/microapps-router-lib/src/index.ts
@@ -0,0 +1,428 @@
+// Used by ts-convict
+import 'source-map-support/register';
+import path from 'path';
+import { pathExistsSync, readFileSync } from 'fs-extra';
+import { Application, DBManager, IVersionsAndRules, Version } from '@pwrdrvr/microapps-datalib';
+import Log from './lib/log';
+
+const log = Log.Instance;
+
+/**
+ * Find and load the appFrame file
+ * @returns
+ */
+export function loadAppFrame({ basePath = '.' }: { basePath?: string }): string {
+  const paths = [
+    basePath,
+    path.join(basePath, '..'),
+    path.join(basePath, 'templates'),
+    basePath,
+    '/opt',
+    '/opt/templates',
+  ];
+
+  for (const pathRoot of paths) {
+    const fullPath = path.join(pathRoot, 'appFrame.html');
+    try {
+      if (pathExistsSync(fullPath)) {
+        log.info('found html file', { fullPath });
+        return readFileSync(fullPath, 'utf-8');
+      }
+    } catch {
+      // Don't care - we get here if stat throws because the file does not exist
+    }
+  }
+
+  log.error('appFrame.html not found');
+  throw new Error('appFrame.html not found');
+}
+
+/**
+ * Ensure that the path starts with a / and does not end with a /
+ *
+ * @param pathPrefix
+ * @returns
+ */
+export 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;
+}
+
+export interface IGetRouteResult {
+  /**
+   * HTTP status code for immediate response, immediate redirect, and errors
+   */
+  readonly statusCode?: number;
+
+  /**
+   * Error message for errors
+   */
+  readonly errorMessage?: string;
+
+  /**
+   * Location to redirect to
+   */
+  readonly redirectLocation?: string;
+
+  /**
+   * Optional headers for immediate response, immediate redirect, and errors
+   */
+  readonly headers?: Record<string, string>;
+
+  /**
+   *
+   *
+   * @example /myapp/1.0.0/index.html
+   * @example /myapp/1.0.1
+   * @example /myapp/1.0.2/some/path?query=string
+   */
+  readonly iFrameAppVersionPath?: string;
+
+  /**
+   * Name of the app if resolved
+   */
+  readonly appName?: string;
+
+  /**
+   * Version of the app if resolved
+   */
+  readonly semVer?: string;
+
+  /**
+   * Type of the app
+   */
+  readonly type?: 'apigwy' | 'lambda-url' | 'url' | 'static';
+
+  /**
+   * Startup type of the app (indirect with iframe or direct)
+   */
+  readonly startupType?: 'iframe' | 'direct';
+
+  /**
+   * URL to the app if resolved
+   */
+  readonly url?: string;
+}
+
+export interface IGetRouteEvent {
+  readonly dbManager: DBManager;
+
+  /**
+   * rawPath from the Lambda event
+   */
+  readonly rawPath: string;
+
+  /**
+   * Configured prefix of the deployment, must start with a / and not end with a /
+   */
+  readonly normalizedPathPrefix?: string;
+}
+
+/**
+ * Get information about immediate redirect, immediate response,
+ * or which host to route the request to.
+ *
+ * @param event
+ *
+ * @returns IGetRouteResult
+ */
+export async function GetRoute(event: IGetRouteEvent): Promise<IGetRouteResult> {
+  const { dbManager, normalizedPathPrefix = '' } = event;
+
+  try {
+    if (normalizedPathPrefix && !event.rawPath.startsWith(normalizedPathPrefix)) {
+      // The prefix is required if configured, if missing we cannot serve this app
+      return { statusCode: 404, errorMessage: 'Request not routable' };
+    }
+
+    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 = pathAfterPrefix.split('/');
+
+    // Pass any parts after the appName/Version to the route handler
+    let additionalParts = '';
+    if (parts.length >= 3 && parts[2] !== '') {
+      additionalParts = parts.slice(2).join('/');
+    }
+
+    // Route an app and version (only) to include the defaultFile
+    // If the second part is not a version that exists, fall through to
+    // routing the app and glomming the rest of the path on to the end
+    if (parts.length === 3 || (parts.length === 4 && !parts[3])) {
+      //   / appName / semVer /
+      // ^   ^^^^^^^   ^^^^^^   ^
+      // 0         1        2   3
+      // 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)
+      const response = await RedirectToDefaultFile({
+        dbManager,
+        appName: parts[1],
+        normalizedPathPrefix,
+        semVer: parts[2],
+      });
+      if (response) {
+        return response;
+      }
+    }
+
+    // Remember the first element is '' (nothing to the left of /)
+    if (parts.length >= 2) {
+      //  / appName (/ something)?
+      // ^  ^^^^^^^    ^^^^^^^^^
+      // 0        1            2
+      // Got at least an application name, try to route it
+      const response = await RouteApp({
+        dbManager,
+        normalizedPathPrefix,
+        event,
+        appName: parts[1],
+        possibleSemVer: parts[2],
+        additionalParts,
+      });
+      if (response) {
+        return response;
+      }
+    }
+
+    return {
+      statusCode: 599,
+      errorMessage: `Router - Could not route: ${event.rawPath}, no matching route`,
+    };
+  } catch (error: any) {
+    log.error('unexpected exception - returning 599', { statusCode: 599, error });
+    return {
+      statusCode: 599,
+      errorMessage: `Router - Could not route: ${event.rawPath}, ${error.message}`,
+    };
+  }
+}
+
+/**
+ * Lookup the version of the app to run
+ * @param event
+ * @param response
+ * @param appName
+ * @param additionalParts
+ * @param log
+ * @returns
+ */
+async function RouteApp(opts: {
+  dbManager: DBManager;
+  event: IGetRouteEvent;
+  appName: string;
+  possibleSemVer: string;
+  additionalParts: string;
+  normalizedPathPrefix?: string;
+}): Promise<IGetRouteResult> {
+  const {
+    dbManager,
+    event,
+    normalizedPathPrefix = '',
+    appName,
+    possibleSemVer,
+    additionalParts,
+  } = opts;
+  let versionsAndRules: IVersionsAndRules;
+
+  try {
+    versionsAndRules = await Application.GetVersionsAndRules({
+      dbManager,
+      key: { AppName: appName },
+    });
+  } catch (error) {
+    // 2021-03-10 - NOTE: This isn't clean - DocumentClient.get throws if the item is not found
+    // It's not easily detectable either.  When the lib is updated we can improve this
+    // Assume this means "succeeded but not found for now"
+    log.info(`GetVersionsAndRules threw for '${appName}', assuming not found - returning 404`, {
+      appName,
+      statusCode: 404,
+      error,
+    });
+
+    return {
+      statusCode: 404,
+      errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
+    };
+  }
+
+  // Check if the semver placeholder is actually a defined version
+  const possibleSemVerVersionInfo = versionsAndRules.Versions.find(
+    (item) => item.SemVer === possibleSemVer,
+  );
+  if (possibleSemVerVersionInfo) {
+    // This is a version, route the request to it
+    return {
+      appName,
+      semVer: possibleSemVer,
+      ...(possibleSemVerVersionInfo?.URL ? { url: possibleSemVerVersionInfo?.URL } : {}),
+      ...(possibleSemVerVersionInfo?.Type
+        ? {
+            type:
+              possibleSemVerVersionInfo?.Type === 'lambda'
+                ? 'apigwy'
+                : possibleSemVerVersionInfo?.Type,
+          }
+        : {}),
+    };
+  }
+
+  //
+  // TODO: Get the incoming attributes of user
+  // For logged in users, these can be: department, product type,
+  //  employee, office, division, etc.
+  // For anonymous users this can be: geo region, language,
+  // browser, IP, CIDR, ASIN, etc.
+  //
+  // The Rules can be either a version or a distribution of versions,
+  // including default, for example:
+  //     80% to 1.1.0, 20% to default (1.0.3)
+  //
+
+  const defaultVersion = versionsAndRules.Rules?.RuleSet.default?.SemVer;
+
+  if (defaultVersion == null) {
+    log.error(`could not find app ${appName}, for path ${event.rawPath} - returning 404`, {
+      statusCode: 404,
+    });
+
+    return {
+      statusCode: 404,
+      errorMessage: `Router - Could not find app: ${event.rawPath}, ${appName}`,
+    };
+  }
+
+  // TODO: Yeah, this is lame - We should save these in a dictionary keyed by SemVer
+  const defaultVersionInfo = versionsAndRules.Versions.find(
+    (item) => item.SemVer === defaultVersion,
+  );
+
+  if (defaultVersionInfo?.StartupType === 'iframe' || !defaultVersionInfo?.StartupType) {
+    // Prepare the iframe contents
+    let appVersionPath: string;
+    if (
+      defaultVersionInfo?.Type !== 'static' &&
+      (defaultVersionInfo?.DefaultFile === undefined ||
+        defaultVersionInfo?.DefaultFile === '' ||
+        additionalParts !== '')
+    ) {
+      // 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 = `${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 = `${normalizedPathPrefix}/${appName}/${defaultVersion}/${defaultVersionInfo.DefaultFile}`;
+    }
+
+    return {
+      statusCode: 200,
+      appName,
+      semVer: defaultVersion,
+      startupType: 'iframe',
+      ...(defaultVersionInfo?.URL ? { url: defaultVersionInfo?.URL } : {}),
+      ...(defaultVersionInfo?.Type
+        ? { type: defaultVersionInfo?.Type === 'lambda' ? 'apigwy' : defaultVersionInfo?.Type }
+        : {}),
+      iFrameAppVersionPath: appVersionPath,
+    };
+  } else {
+    // This is a direct app version, no iframe needed
+
+    if (defaultVersionInfo?.Type === 'lambda') {
+      throw new Error('Invalid type for direct app version');
+    }
+    if (['apigwy', 'static'].includes(defaultVersionInfo?.Type || '')) {
+      throw new Error('Invalid type for direct app version');
+    }
+
+    return {
+      appName,
+      semVer: possibleSemVer,
+      startupType: 'direct',
+      ...(defaultVersionInfo?.URL ? { url: defaultVersionInfo?.URL } : {}),
+      ...(defaultVersionInfo?.Type ? { type: defaultVersionInfo?.Type } : {}),
+    };
+  }
+}
+
+/**
+ * Redirect the request to app/x.y.z/? to app/x.y.z/{defaultFile}
+ * @param response
+ * @param normalizedPathPrefix
+ * @param appName
+ * @param semVer
+ * @returns
+ */
+async function RedirectToDefaultFile(opts: {
+  dbManager: DBManager;
+  normalizedPathPrefix?: string;
+  appName: string;
+  semVer: string;
+}): Promise<IGetRouteResult | undefined> {
+  const { dbManager, normalizedPathPrefix = '', appName, semVer } = opts;
+  let versionInfo: Version;
+
+  try {
+    versionInfo = await Version.LoadVersion({
+      dbManager,
+      key: { AppName: appName, SemVer: semVer },
+    });
+  } catch (error) {
+    log.info(
+      `LoadVersion threw for '${normalizedPathPrefix}/${appName}/${semVer}' - falling through to app routing'`,
+      {
+        appName,
+        semVer,
+        error,
+      },
+    );
+    return undefined;
+  }
+
+  if (versionInfo === undefined) {
+    log.info(
+      `LoadVersion returned undefined for '${normalizedPathPrefix}/${appName}/${semVer}', assuming not found - falling through to app routing'`,
+      {
+        appName,
+        semVer,
+      },
+    );
+    return undefined;
+  }
+
+  if (!versionInfo.DefaultFile) {
+    return undefined;
+  }
+
+  log.info(
+    `found '${normalizedPathPrefix}/${appName}/${semVer}' - returning 302 to ${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`,
+    {
+      statusCode: 302,
+      routedPath: `${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`,
+    },
+  );
+
+  return {
+    statusCode: 302,
+    redirectLocation: `${normalizedPathPrefix}/${appName}/${semVer}/${versionInfo.DefaultFile}`,
+  };
+}
diff --git a/packages/microapps-router-lib/src/lib/log.ts b/packages/microapps-router-lib/src/lib/log.ts
new file mode 100644
index 00000000..363df927
--- /dev/null
+++ b/packages/microapps-router-lib/src/lib/log.ts
@@ -0,0 +1,8 @@
+import { LambdaLog } from 'lambda-log';
+
+export default class Log {
+  public static Instance = new LambdaLog({
+    silent: process.env.JEST_WORKER_ID !== undefined,
+    // debug: process.env.DEBUG ? true : false,
+  });
+}
diff --git a/packages/microapps-router-lib/tsconfig.json b/packages/microapps-router-lib/tsconfig.json
new file mode 100644
index 00000000..c57192ad
--- /dev/null
+++ b/packages/microapps-router-lib/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "extends": "../../tsconfig.packages.json",
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "dist",
+    "resolveJsonModule": true
+  },
+  "references": [
+    {
+      "path": "../microapps-datalib/"
+    }
+  ],
+  "exclude": ["**/*.spec.ts", "dist"]
+}
diff --git a/packages/microapps-router/src/index.spec.ts b/packages/microapps-router/src/index.spec.ts
index e66e5308..7994c787 100644
--- a/packages/microapps-router/src/index.spec.ts
+++ b/packages/microapps-router/src/index.spec.ts
@@ -80,6 +80,40 @@ describe('router - without prefix', () => {
     expect(response.body).toContain('<iframe src="/bat/3.2.1-beta.1/bat.html" seamless');
   });
 
+  it('static app - request to app/x.y.z/ should not redirect if no defaultFile', async () => {
+    const app = new Application({
+      AppName: 'Bat',
+      DisplayName: 'Bat App',
+    });
+    await app.Save(dbManager);
+
+    const version = new Version({
+      AppName: 'Bat',
+      IntegrationID: 'abcd',
+      SemVer: '3.2.1-beta.1',
+      Status: 'deployed',
+      Type: 'static',
+    });
+    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/3.2.1-beta.1' } as lambda.APIGatewayProxyEventV2,
+      {} as lambda.Context,
+    );
+
+    expect(response).toHaveProperty('statusCode');
+    expect(response.statusCode).toBe(200);
+    expect(response.headers?.Location).not.toBeDefined();
+  });
+
   it('static app - request to app/x.y.z should redirect to defaultFile', async () => {
     const app = new Application({
       AppName: 'Bat',
diff --git a/packages/microapps-router/src/index.ts b/packages/microapps-router/src/index.ts
index 0ae18671..84e95d79 100644
--- a/packages/microapps-router/src/index.ts
+++ b/packages/microapps-router/src/index.ts
@@ -1,11 +1,10 @@
 // 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 { loadAppFrame } from '@pwrdrvr/microapps-router-lib';
 import type * as lambda from 'aws-lambda';
-import { pathExistsSync, readFileSync } from 'fs-extra';
 import { Config } from './config/Config';
 import Log from './lib/log';
 
@@ -42,36 +41,7 @@ function normalizePathPrefix(pathPrefix: string): string {
   return normalizedPathPrefix;
 }
 
-/**
- * Find and load the appFrame file
- * @returns
- */
-function loadAppFrame(): string {
-  const paths = [
-    __dirname,
-    path.join(__dirname, '..'),
-    path.join(__dirname, 'templates'),
-    '/opt',
-    '/opt/templates',
-  ];
-
-  for (const pathRoot of paths) {
-    const fullPath = path.join(pathRoot, 'appFrame.html');
-    try {
-      if (pathExistsSync(fullPath)) {
-        log.info('found html file', { fullPath });
-        return readFileSync(fullPath, 'utf-8');
-      }
-    } catch {
-      // Don't care - we get here if stat throws because the file does not exist
-    }
-  }
-
-  log.error('appFrame.html not found');
-  throw new Error('appFrame.html not found');
-}
-
-const appFrame = loadAppFrame();
+const appFrame = loadAppFrame({ basePath: __dirname });
 const normalizedPathPrefix = normalizePathPrefix(Config.instance.rootPathPrefix);
 
 /**
@@ -322,6 +292,10 @@ async function RedirectToDefaultFile(opts: {
     return false;
   }
 
+  if (!versionInfo.DefaultFile) {
+    return false;
+  }
+
   response.statusCode = 302;
   response.headers['Cache-Control'] = 'no-store; private';
   response.headers[
diff --git a/tsconfig.json b/tsconfig.json
index bf70e30b..47bd5723 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,11 +13,14 @@
     {
       "path": "packages/microapps-deployer/"
     },
+    {
+      "path": "packages/microapps-router-lib/"
+    },
     {
       "path": "packages/microapps-router/"
     },
     {
-      "path": "packages/microapps-edge-to-origin/",
+      "path": "packages/microapps-edge-to-origin/"
     },
     {
       "path": "packages/microapps-cdk/"


Test patch:
diff --git a/packages/cdk/test/MicroApps.spec.ts b/packages/cdk/test/MicroApps.spec.ts
index e0492118..603f8bb3 100644
--- a/packages/cdk/test/MicroApps.spec.ts
+++ b/packages/cdk/test/MicroApps.spec.ts
@@ -21,7 +21,8 @@ describe('MicroAppsStack', () => {
       Export: { Name: `${stackName}-edge-domain-name` },
     });
     Template.fromStack(stack).hasOutput('dynamodbtablename', {
-      Value: { Ref: 'microappssvcstable1C50DC7E' },
+      Value: { Ref: 'microappstableAA1718EA' },
+      Export: { Name: `${stackName}-dynamodb-table-name` },
     });
   });
 
@@ -51,7 +52,7 @@ describe('MicroAppsStack', () => {
       Export: { Name: `${stackName}-edge-domain-name` },
     });
     Template.fromStack(stack).hasOutput('dynamodbtablename', {
-      Value: { Ref: 'microappssvcstable1C50DC7E' },
+      Value: { Ref: 'microappstableAA1718EA' },
       Export: { Name: `${stackName}-dynamodb-table-name` },
     });
   });
diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts
index c03abd6b..81604d3a 100644
--- a/packages/microapps-cdk/test/MicroApps.test.ts
+++ b/packages/microapps-cdk/test/MicroApps.test.ts
@@ -10,6 +10,7 @@ describe('MicroApps', () => {
     const app = new App({});
     const stack = new Stack(app, 'stack', {
       env: {
+        account: '123456789012',
         region: 'us-east-2',
       },
     });
@@ -64,6 +65,7 @@ describe('MicroApps', () => {
     const app = new App({});
     const stack = new Stack(app, 'stack', {
       env: {
+        account: '123456789012',
         region: 'us-east-2',
       },
     });