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

Issue:
Edge - Simplify Routes
## Motivations
- There are way too many routes
- These are not all strictly necessary anymore

## To-Do
- [ ] Determine which routes can be cleaned up or consolidated before moving static file routing to the Origin Request function
- [ ] Implement the cleanups from above
- [ ] Determine if another route of cleanups can be made after static file routing is moved to the Origin Request function


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 114f6192..01114716 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -223,7 +223,7 @@ jobs:
       #       packages/microapps-cdk/lib
       #     key: typescript-build-${{ hashFiles('package.json', 'yarn.lock', 'tsconfig.json', 'tsconfig.packages.json', 'packages/**/tsconfig.json', 'packages/**/*.ts') }}
 
-      - name: Optionally Build All TypeScript
+      - name: Build All TypeScript
         # if: steps.cache-typescript-build.outputs.cache-hit != 'true'
         run: yarn build
 
diff --git a/packages/demo-app/src/index.ts b/packages/demo-app/src/index.ts
index 58959598..f4a068bb 100644
--- a/packages/demo-app/src/index.ts
+++ b/packages/demo-app/src/index.ts
@@ -3,6 +3,7 @@ import type * as lambda from 'aws-lambda';
 import { readFileSync } from 'fs';
 
 const html = readFileSync(`./index.html`, 'utf8');
+const file = readFileSync(`./file.html`, 'utf8');
 
 const buildTrigger = '2023-01-24-01';
 console.info('Demo-app build trigger', { buildTrigger });
@@ -25,6 +26,7 @@ export async function handler(
       statusCode: 200,
       headers: {
         'Content-Type': 'application/json',
+        'Powered-By': 'demo-app',
       },
       isBase64Encoded: false,
       body: {
@@ -35,6 +37,30 @@ export async function handler(
     };
   }
 
+  // Route for testing files served by the app
+  if (event.rawPath.endsWith('/file.html')) {
+    return {
+      statusCode: 200,
+      headers: {
+        'Content-Type': 'text/html',
+        'Powered-By': 'demo-app',
+      },
+      isBase64Encoded: false,
+      body: file,
+    };
+  }
+
+  // Route for testing files served by the app
+  if (event.rawPath.endsWith('/notfound.html')) {
+    return {
+      statusCode: 404,
+      headers: {
+        'Powered-By': 'demo-app',
+      },
+      isBase64Encoded: false,
+    };
+  }
+
   if (event.headers?.accept && event.headers.accept.includes('text/html')) {
     return {
       statusCode: 200,
diff --git a/packages/demo-app/static_files/file.html b/packages/demo-app/static_files/file.html
new file mode 100644
index 00000000..abb22100
--- /dev/null
+++ b/packages/demo-app/static_files/file.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Demo App</title>
+  </head>
+
+  <body>
+    <div>`file.html`</div>
+  </body>
+</html>
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index c7ddc7ca..2f731331 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -406,11 +406,19 @@ CloudFront Origin Access Identity for the deployed applications bucket.
 
 ---
 
-##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsOrigin"></a>
+##### `bucketAppsOriginApp`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsOriginApp"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
 
-CloudFront Origin for the deployed applications bucket.
+CloudFront Origin for the deployed applications bucket Marked with `x-microapps-origin: app` so the OriginRequest function knows to send the request to the application origin first, if configured for a particular application.
+
+---
+
+##### `bucketAppsOriginS3`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsOriginS3"></a>
+
+- *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
+
+CloudFront Origin for the deployed applications bucket Marked with `x-microapps-origin: s3` so the OriginRequest function knows to NOT send the request to the application origin and instead let it fall through to the S3 bucket.
 
 ---
 
@@ -551,12 +559,15 @@ import { AddRoutesOptions } from '@pwrdrvr/microapps-cdk'
 const addRoutesOptions: AddRoutesOptions = { ... }
 ```
 
-##### `appOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.appOrigin"></a>
+##### `appOnlyOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.appOnlyOrigin"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront.IOrigin`](#aws-cdk-lib.aws_cloudfront.IOrigin)
-- *Default:* invalid URL (never used)
 
-Default origin (invalid URL or API Gateway).
+Application origin.
+
+Typically an S3 bucket with a `x-microapps-origin: app` custom header
+
+The request never actually falls through to the S3 bucket.
 
 ---
 
@@ -568,11 +579,11 @@ Origin Request policy for API Gateway Origin.
 
 ---
 
-##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.bucketAppsOrigin"></a>
+##### `bucketOriginFallbackToApp`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.bucketOriginFallbackToApp"></a>
 
-- *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
+- *Type:* [`aws-cdk-lib.aws_cloudfront_origins.OriginGroup`](#aws-cdk-lib.aws_cloudfront_origins.OriginGroup)
 
-S3 Bucket CloudFront Origin for static assets.
+Origin Group with Primary of S3 bucket with `x-microapps-origin: s3` custom header and Fallback of `appOnlyOrigin`.
 
 ---
 
@@ -584,34 +595,6 @@ CloudFront Distribution to add the Behaviors (Routes) to.
 
 ---
 
-##### `createAPIPathRoute`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.createAPIPathRoute"></a>
-
-- *Type:* `boolean`
-- *Default:* false
-
-Create an extra Behavior (Route) for /api/ that allows API routes to have a period in them.
-
-When false API routes with a period in the path will get routed to S3.
-
-When true API routes that contain /api/ in the path will get routed to API Gateway
-even if they have a period in the path.
-
----
-
-##### `createNextDataPathRoute`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.createNextDataPathRoute"></a>
-
-- *Type:* `boolean`
-- *Default:* false
-
-Create an extra Behavior (Route) for /_next/data/ This route is used by Next.js to load data from the API Gateway on `getServerSideProps` calls.  The requests can end in `.json`, which would cause them to be routed to S3 if this route is not created.
-
-When false API routes with a period in the path will get routed to S3.
-
-When true API routes that contain /_next/data/ in the path will get routed to API Gateway
-even if they have a period in the path.
-
----
-
 ##### `edgeLambdas`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.edgeLambdas"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront.EdgeLambda`](#aws-cdk-lib.aws_cloudfront.EdgeLambda)[]
@@ -818,11 +801,19 @@ import { MicroAppsCFProps } from '@pwrdrvr/microapps-cdk'
 const microAppsCFProps: MicroAppsCFProps = { ... }
 ```
 
-##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.bucketAppsOrigin"></a>
+##### `bucketAppsOriginApp`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.bucketAppsOriginApp"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
 
-S3 bucket origin for deployed applications.
+S3 bucket origin for deployed applications Marked with `x-microapps-origin: app`.
+
+---
+
+##### `bucketAppsOriginS3`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.bucketAppsOriginS3"></a>
+
+- *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
+
+S3 bucket origin for deployed applications Marked with `x-microapps-origin: s3`.
 
 ---
 
@@ -1971,11 +1962,19 @@ CloudFront Origin Access Identity for the deployed applications bucket.
 
 ---
 
-##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsOrigin"></a>
+##### `bucketAppsOriginApp`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsOriginApp"></a>
+
+- *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
+
+CloudFront Origin for the deployed applications bucket Marked with `x-microapps-origin: app` so the OriginRequest function knows to send the request to the application origin first, if configured for a particular application.
+
+---
+
+##### `bucketAppsOriginS3`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsOriginS3"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront_origins.S3Origin`](#aws-cdk-lib.aws_cloudfront_origins.S3Origin)
 
-CloudFront Origin for the deployed applications bucket.
+CloudFront Origin for the deployed applications bucket Marked with `x-microapps-origin: s3` so the OriginRequest function knows to NOT send the request to the application origin and instead let it fall through to the S3 bucket.
 
 ---
 
diff --git a/packages/microapps-cdk/README.md b/packages/microapps-cdk/README.md
index bdfc000c..13dc2d07 100644
--- a/packages/microapps-cdk/README.md
+++ b/packages/microapps-cdk/README.md
@@ -6,22 +6,22 @@ The MicroApps project enables rapidly deploying many web apps to AWS on a single
 
 MicroApps allows many versions of an application to be deployed either as ephemeral deploys (e.g. for pull request builds) or as semi-permanent deploys. The `microapps-router` Lambda function handled routing requests to apps to the current version targeted for a particular application start request using rules as complex as one is interested in implementing (e.g. A/B testing integration, canary releases, per-user rules for logged in users, per-group, per-deparment, and default rules).
 
-2023-01-01 NOTE: The next paragraph is dated as the `iframe` is no longer required for frameworks that write absolute URLs for their static resources and API requests.
+Users start applications via a URL such as `[/{prefix}]/{appname}/`, which hits the `microapps-router` Lambda@Edge OriginRequest handler that looks up the version of the application to be run, and either forwards the request to the target Lambda Function URL (`--startupType direct` invoke mode) or returns a transparent `iframe` (`--startupType iframe`) with a link to that version.  `direct` mode works with frameworks, like Next.js, that can return pages that have build-time computed relative URLs to static resources and API calls.  `iframe` mode works with frameworks that do not write computed relative URLs at build time and/or that do not use URLs that are completely relative to wherever the applications is rooted at runtime; this mode is primarily for quick prototyping as it has other complications (such as indirect access to query strings). The URL seen by the user in the browser (and available for bookmarking) has no version in it, so subsequent launches (e.g. the next day or just in another tab) will lookup the version again. All relative URL API requests (e.g. `some/api/path`) will go to the corresponding API version that matches the version of the loaded static files, eliminating issues of incompatibility between static files and API deployments.
 
-Users start applications via a URL such as `[/{prefix}]/{appname}/`, which hits the `microapps-router` that looks up the version of the application to be run, then renders a transparent `iframe` with a link to that version. The URL seen by the user in the browser (and available for bookmarking) has no version in it, so subsequent launches (e.g. the next day or just in another tab) will lookup the version again. All relative URL API requests (e.g. `some/api/path`) will go to the corresponding API version that matches the version of the loaded static files, eliminating issues of incompatibility between static files and API deployments.
-
-For development / testing purposes only, each version of an applicaton can be accessed directly via a URL of the pattern `[/{prefix}]/{appname}/{semver}/`. These "versioned" URLs are not intended to be advertised to end users as they would cause a user to be stuck on a particular version of the app if the URL was bookmarked. Note that the system does not limit access to particular versions of an application, as of 2022-01-26, but that can be added as a feature.
+For development / testing purposes only, each version of an applicaton can be accessed directly via a URL of the patterns `[/{prefix}]/{appname}?appver={semver}` for `direct` mode or `[/{prefix}]/{appname}/{semver}/` for `iframe` mode. These "versioned" URLs are not intended to be advertised to end users as they would cause a user to be stuck on a particular version of the app if the URL was bookmarked. Note that the system does not limit access to particular versions of an application, as of 2023-03-04, but that can be added as a feature.
 
 # Table of Contents <!-- omit in toc -->
 
 - [Overview](#overview)
+- [Why MicroApps](#why-microapps)
+- [Request Routing for Static Assets / App - Diagram](#request-routing-for-static-assets--app---diagram)
 - [Request Dispatch Model for Multi-Account Deployments](#request-dispatch-model-for-multi-account-deployments)
 - [Video Preview of the Deploying CDK Construct](#video-preview-of-the-deploying-cdk-construct)
 - [Installation / CDK Constructs](#installation--cdk-constructs)
 - [Tutorial - Bootstrapping a Deploy](#tutorial---bootstrapping-a-deploy)
-- [Why MicroApps](#why-microapps)
 - [Limitations / Future Development](#limitations--future-development)
 - [Related Projects / Components](#related-projects--components)
+- [Why Lambda @ Origin and Not Lambda @ Edge for Apps](#why-lambda--origin-and-not-lambda--edge-for-apps)
 - [Architecure Diagram](#architecure-diagram)
 - [Project Layout](#project-layout)
 - [Creating a MicroApp Using Zip Lambda Functions](#creating-a-microapp-using-zip-lambda-functions)
@@ -31,17 +31,29 @@ For development / testing purposes only, each version of an applicaton can be ac
     - [Install Dependencies](#install-dependencies)
     - [Dockerfile](#dockerfile)
     - [next.config.js](#nextconfigjs)
-    - [deploy.json](#deployjson)
-    - [serverless.yaml](#serverlessyaml)
 - [Troubleshooting](#troubleshooting)
   - [CloudFront Requests to API Gateway are Rejected with 403 Forbidden](#cloudfront-requests-to-api-gateway-are-rejected-with-403-forbidden)
     - [SignatureV4 Headers](#signaturev4-headers)
 
+# Why MicroApps
+
+MicroApps are like micro services, but for Web UIs. A MicroApp allows a single functional site to be developed by many independent teams within an organization. Teams must coordinate deployments and agree upon one implementation technology and framework when building a monolithic, or even a monorepo, web application.
+
+Teams using MicroApps can deploy independently of each other with coordination being required only at points of intentional integration (e.g. adding a feature to pass context from one MicroApp to another or coordination of a major feature release to users) and sharing UI styles, if desired (it is possible to build styles that look the same across many different UI frameworks).
+
+MicroApps also allow each team to use a UI framework and backend language that is most appropriate for their solving their business problem. Not every app has to use React or Next.js or even Node on the backend, but instead they can use whatever framework they want and Java, Go, C#, Python, etc. for UI API calls.
+
+For internal sites, or logged-in-customer sites, different tools or products can be hosted in entirely independent MicroApps. A menuing system / toolbar application can be created as a MicroApp and that menu app can open the apps in the system within a transparent iframe. For externally facing sites, such as for an e-commerce site, it is possible to have a MicroApp serving `/product/...`, another serving `/search/...`, another serving `/`, etc.
+
+# Request Routing for Static Assets / App - Diagram
+
+![Request Routing for Static Assets and App](https://user-images.githubusercontent.com/5617868/222913451-0e6ed906-b6ee-461f-99a7-61db13135ce1.png)
+
 # Request Dispatch Model for Multi-Account Deployments
 
 Note: requests can also be dispatched into the same account, but this model is more likely to be used by organizations with many AWS accounts.
 
-![211132720-604510fa-de44-4ac6-a79b-c28c829d2490](https://user-images.githubusercontent.com/5617868/218237120-65b3ae44-31ba-4b6d-8722-4d3fb7da5577.png)
+![Request Dispatch Model for Mulit-Account Deployments](https://user-images.githubusercontent.com/5617868/218237120-65b3ae44-31ba-4b6d-8722-4d3fb7da5577.png)
 
 # Video Preview of the Deploying CDK Construct
 
@@ -68,10 +80,6 @@ Note: requests can also be dispatched into the same account, but this model is m
   - Of course, there are other methods of setting env vars
 - `aws sso login`
   - Establish an AWS SSO session
-- `cdk-sso-sync`
-  - Using `npm i -g cdk-sso-sync`
-  - Sets AWS SSO credentials in a way that CDK can use them
-  - Not necessary if not using AWS SSO
 - `export AWS_REGION=us-east-2`
   - Region needs to be set for the Lambda invoke - This can be done other ways in `~/.aws/config` as well
 - `./deploy.sh`
@@ -81,33 +89,8 @@ Note: requests can also be dispatched into the same account, but this model is m
     - `npx microapps-publish publish -a release -n ${RELEASE_APP_PACKAGE_VERSION} -d ${DEPLOYER_LAMBDA_NAME} -l ${RELEASE_APP_LAMBDA_NAME} -s node_modules/@pwrdrvr/microapps-app-release-cdk/lib/static_files/release/${RELEASE_APP_PACKAGE_VERSION}/ --overwrite --noCache`
   - URL will be printed as last output
 
-# Why MicroApps
-
-MicroApps are like micro services, but for Web UIs. A MicroApp allows a single functional site to be developed by many independent teams within an organization. Teams must coordinate deployments and agree upon one implementation technology and framework when building a monolithic, or even a monorepo, web application.
-
-Teams using MicroApps can deploy independently of each other with coordination being required only at points of intentional integration (e.g. adding a feature to pass context from one MicroApp to another or coordination of a major feature release to users) and sharing UI styles, if desired (it is possible to build styles that look the same across many different UI frameworks).
-
-MicroApps also allow each team to use a UI framework and backend language that is most appropriate for their solving their business problem. Not every app has to use React or Next.js or even Node on the backend, but instead they can use whatever framework they want and Java, Go, C#, Python, etc. for UI API calls.
-
-For internal sites, or logged-in-customer sites, different tools or products can be hosted in entirely independent MicroApps. A menuing system / toolbar application can be created as a MicroApp and that menu app can open the apps in the system within a transparent iframe. For externally facing sites, such as for an e-commerce site, it is possible to have a MicroApp serving `/product/...`, another serving `/search/...`, another serving `/`, etc.
-
 # Limitations / Future Development
 
-- `iframes`
-  - Yeah, yeah: `iframes` are not framesets and most of the hate about iframes is probably better directed at framesets
-  - The iframe serves a purpose but it stinks that it is there, primarily because it will cause issues with search bot indexing (SEO)
-  - There are other options available to implement that have their own drabacks:
-    - Using the `microapps-router` to proxy the "app start" request to a particular version of an app that then renders all of it's API resource requests to versioned URLs
-      - Works only with frameworks that support hashing filenams for each deploy to unique names
-      - This page would need to be marked as non-cachable
-      - This may work well with Next.js which wants to know the explicit path that it will be running at (it writes that path into all resource and API requests)
-      - Possible issue: the app would need to work ok being displayed at `[/{prefix}]/{appname}` when it may think that it's being displayed at `[/{prefix}]/{appname}/{semver}`
-      - Disadvantage: requires some level of UI framework features (e.g. writing the absolute resource paths) to work correctly - may not work as easily for all UI frameworks
-    - HTML5 added features to allow setting the relative path of all subsequent requests to be different than that displayed in the address bar
-      - Gotta see if this works in modern browsers
-    - Option to ditch the multiple-versions feature
-      - Works only with frameworks that support hashing filenams for each deploy to unique names
-      - Allows usage of the deploy and routing tooling without advantages and disadvantages of multiple-versions support
 - AWS Only
   - For the time being this has only been implemented for AWS technologies and APIs
   - It is possible that Azure and GCP have sufficient support to enable porting the framework
@@ -134,30 +117,39 @@ For internal sites, or logged-in-customer sites, different tools or products can
 - Next.js Demo App
   - The Next.js Tutorial application deployed as a MicroApp
   - [pwrdrvr/serverless-nextjs-demo](https://github.com/pwrdrvr/serverless-nextjs-demo)
-- Serverless Next.js Router
-  - [pwrdrvr/serverless-nextjs-router](https://github.com/pwrdrvr/serverless-nextjs-router)
-  - Complementary to [@sls-next/serverless-component](https://github.com/serverless-nextjs/serverless-next.js)
-  - Allows Next.js apps to run as Lambda @ Origin for speed and cost improvements vs Lambda@Edge
-  - Essentially the router translates CloudFront Lambda events to API Gateway Lambda events and vice versa for responses
-  - The `serverless-nextjs` project allows Next.js apps to run as Lambda functions without Express, but there was a design change to make the Lambda functions run at Edge (note: need to recheck if this changed after early 2021)
-    - Lambda@Edge is _at least_ 3x more expensive than Lambda at the origin:
-      - In US East 1, the price per GB-Second is $0.00005001 for Lambda@Edge vs $0.0000166667 for Lambda at the origin
-    - Additionally, any DB or services calls from Lambda@Edge back to the origin will pay that 3x higher per GB-Second cost for any time spent waiting to send the request and get a response. Example:
-      - Lambda@Edge
-        - 0.250s Round Trip Time (RTT) for EU-zone edge request to hit US-East 1 Origin
-        - 0.200s DB lookup time
-        - 0.050s CPU usage to process the DB response
-        - 0.500s total billed time @ $0.00005001 @ 128 MB
-        - $0.000003125625 total charge
-      - Lambda at Origin
-        - RTT does not apply (it's effectively 1-2 ms to hit a DB in the same region)
-        - 0.200s DB lookup time
-        - 0.050s CPU usage to process the DB response
-        - 0.250s total billed time @ $0.0000166667 @ 128 MB
-        - Half the billed time of running on Lambda@Edge
-        - 1/6th the cost of running on Lambda@Edge:
-          - $0.000000520834375 total charge (assuming no CPU time to process the response)
-          - $0.000003125625 / $0.000000520834375 = 6x more expensive in Lambda@Edge
+
+
+# Why Lambda @ Origin and Not Lambda @ Edge for Apps
+
+Calling resources (DBs and other services) and waiting for a long synchronous response is an anti-pattern in Lambda as the Lambda function will be billed for the time spent waiting for the response. This is especially true for Lambda@Edge as the cost is 3x higher than Lambda at the origin.
+
+With Lambda@Edge (even with Origin Requests) the cost is 3x higher per GB-second and the time spent waiting for a 1 ms service response from an origin that is 250 ms away is 750x higher (250 ms / 1ms * 3x higher cost) than making that same request within the region where the resource resides.
+
+- Lambda@Edge is _at least_ 3x more expensive than Lambda at the origin:
+  - In US East 1, the price per GB-Second is $0.00005001 for Lambda@Edge
+    - Source: https://aws.amazon.com/lambda/pricing/ (bottom of page)
+    - Updated: 2023-03-04
+  - In US East 1, the price per GB-Second is $0.0000166667 for Lambda at the origin on x86
+    - Source: https://aws.amazon.com/lambda/pricing/
+    - Updated: 2023-03-04
+  - Ratio
+    - Lambda@Edge / Lambda@Origin = $0.00005001 / $0.0000166667 = 3.0006x
+- Any DB or services calls from Lambda@Edge back to the origin will pay that 3x higher per GB-Second cost for any time spent waiting to send the request and get a response. Example:
+  - Lambda@Edge
+    - 0.250s Round Trip Time (RTT) for EU-zone edge request to hit US-East 1 Origin
+    - 0.200s DB lookup time
+    - 0.050s CPU usage to process the DB response
+    - 0.500s total billed time @ $0.00005001 @ 128 MB
+    - $0.000003125625 total charge
+  - Lambda at Origin
+    - RTT does not apply (it's effectively 1-2 ms to hit a DB in the same region)
+    - 0.200s DB lookup time
+    - 0.050s CPU usage to process the DB response
+    - 0.250s total billed time @ $0.0000166667 @ 128 MB
+    - Half the billed time of running on Lambda@Edge
+    - 1/6th the cost of running on Lambda@Edge:
+      - $0.000000520834375 total charge (assuming no CPU time to process the response)
+      - $0.000003125625 / $0.000000520834375 = 6x more expensive in Lambda@Edge
 
 # Architecure Diagram
 
@@ -221,11 +213,13 @@ Replace the version with `0.0.0` so it can be modified by the `microapps-publish
 ### Install Dependencies
 
 ```
-npm i --save-dev @sls-next/serverless-component@1.19.0 @pwrdrvr/serverless-nextjs-router @pwrdrvr/microapps-publish
+npm i --save-dev @pwrdrvr/microapps-publish
 ```
 
 ### Dockerfile
 
+FIXME: Out of date 2023-03-04
+
 Add this file to the root of the app.
 
 ```Dockerfile
@@ -258,6 +252,8 @@ CMD [ "./index.handler" ]
 
 ### next.config.js
 
+FIXME: Out of date 2023-03-04
+
 Add this file to the root of the app.
 
 Replace `appname` with your URL path-compatible application name.
@@ -279,37 +275,6 @@ module.exports = {
 };
 ```
 
-### deploy.json
-
-Add this file to the root of the app.
-
-Replace `appname` with your URL path-compatible application name.
-
-```json
-{
-  "AppName": "appname",
-  "SemVer": "0.0.0",
-  "DefaultFile": "",
-  "StaticAssetsPath": "./.serverless_nextjs/assets/appname/0.0.0/",
-  "LambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:appname:v0_0_0",
-  "AWSAccountID": "123456789012",
-  "AWSRegion": "us-east-2",
-  "ServerlessNextRouterPath": "./node_modules/@pwrdrvr/serverless-nextjs-router/dist/index.js"
-}
-```
-
-### serverless.yaml
-
-Add this file to the root of the app.
-
-```yaml
-nextApp:
-  component: './node_modules/@sls-next/serverless-component'
-  inputs:
-    deploy: false
-    uploadStaticAssetsFromBuild: false
-```
-
 # Troubleshooting
 
 ## CloudFront Requests to API Gateway are Rejected with 403 Forbidden
diff --git a/packages/microapps-cdk/src/MicroApps.ts b/packages/microapps-cdk/src/MicroApps.ts
index 584859a4..33a46170 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -476,7 +476,8 @@ export class MicroApps extends Construct implements IMicroApps {
       ...(this._apigwy ? { httpApi: this._apigwy.httpApi } : {}),
       r53Zone,
       certEdge,
-      bucketAppsOrigin: this._s3.bucketAppsOrigin,
+      bucketAppsOriginS3: this._s3.bucketAppsOriginS3,
+      bucketAppsOriginApp: this._s3.bucketAppsOriginApp,
       bucketLogs: this._s3.bucketLogs,
       rootPathPrefix,
       createAPIPathRoute,
diff --git a/packages/microapps-cdk/src/MicroAppsCF.ts b/packages/microapps-cdk/src/MicroAppsCF.ts
index 540d7cfc..67e09238 100644
--- a/packages/microapps-cdk/src/MicroAppsCF.ts
+++ b/packages/microapps-cdk/src/MicroAppsCF.ts
@@ -35,8 +35,15 @@ export interface MicroAppsCFProps {
 
   /**
    * S3 bucket origin for deployed applications
+   * Marked with `x-microapps-origin: s3`
    */
-  readonly bucketAppsOrigin: cforigins.S3Origin;
+  readonly bucketAppsOriginS3: cforigins.S3Origin;
+
+  /**
+   * S3 bucket origin for deployed applications
+   * Marked with `x-microapps-origin: app`
+   */
+  readonly bucketAppsOriginApp: cforigins.S3Origin;
 
   /**
    * S3 bucket for CloudFront logs
@@ -176,16 +183,19 @@ export interface CreateAPIOriginPolicyOptions {
  */
 export interface AddRoutesOptions {
   /**
-   * Default origin (invalid URL or API Gateway)
+   * Application origin
    *
-   * @default invalid URL (never used)
+   * Typically an S3 bucket with a `x-microapps-origin: app` custom header
+   *
+   * The request never actually falls through to the S3 bucket.
    */
-  readonly appOrigin: cf.IOrigin;
+  readonly appOnlyOrigin: cf.IOrigin;
 
   /**
-   * S3 Bucket CloudFront Origin for static assets
+   * Origin Group with Primary of S3 bucket with `x-microapps-origin: s3` custom header
+   * and Fallback of `appOnlyOrigin`
    */
-  readonly bucketAppsOrigin: cforigins.S3Origin;
+  readonly bucketOriginFallbackToApp: cforigins.OriginGroup;
 
   /**
    * CloudFront Distribution to add the Behaviors (Routes) to
@@ -204,34 +214,6 @@ export interface AddRoutesOptions {
    */
   readonly rootPathPrefix?: string;
 
-  /**
-   * Create an extra Behavior (Route) for /api/ that allows
-   * API routes to have a period in them.
-   *
-   * When false API routes with a period in the path will get routed to S3.
-   *
-   * When true API routes that contain /api/ in the path will get routed to API Gateway
-   * even if they have a period in the path.
-   *
-   * @default false
-   */
-  readonly createAPIPathRoute?: boolean;
-
-  /**
-   * Create an extra Behavior (Route) for /_next/data/
-   * This route is used by Next.js to load data from the API Gateway
-   * on `getServerSideProps` calls.  The requests can end in `.json`,
-   * which would cause them to be routed to S3 if this route is not created.
-   *
-   * When false API routes with a period in the path will get routed to S3.
-   *
-   * When true API routes that contain /_next/data/ in the path will get routed to API Gateway
-   * even if they have a period in the path.
-   *
-   * @default false
-   */
-  readonly createNextDataPathRoute?: boolean;
-
   /**
    * Edge lambdas to associate with the API Gateway routes
    */
@@ -302,13 +284,11 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
    */
   public static addRoutes(_scope: Construct, props: AddRoutesOptions) {
     const {
-      appOrigin: defaultOrigin,
-      bucketAppsOrigin,
+      appOnlyOrigin,
+      bucketOriginFallbackToApp,
       distro,
       appOriginRequestPolicy,
       rootPathPrefix = '',
-      createAPIPathRoute = false,
-      createNextDataPathRoute = false,
     } = props;
 
     //
@@ -318,10 +298,20 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
       allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
       cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
       compress: true,
-      originRequestPolicy: cf.OriginRequestPolicy.CORS_S3_ORIGIN,
+      originRequestPolicy: cf.OriginRequestPolicy.ALL_VIEWER,
       viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+      edgeLambdas: props.edgeLambdas,
     };
-    const appBehaviorOptions: cf.AddBehaviorOptions = {
+    const s3FallbackToAppOptions: cf.AddBehaviorOptions = {
+      allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
+      // TODO: Caching needs to be set by the app response
+      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
+      compress: true,
+      originRequestPolicy: appOriginRequestPolicy,
+      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+      edgeLambdas: props.edgeLambdas,
+    };
+    const appOnlyBehaviorOptions: cf.AddBehaviorOptions = {
       allowedMethods: cf.AllowedMethods.ALLOW_ALL,
       // TODO: Caching needs to be set by the app response
       cachePolicy: cf.CachePolicy.CACHING_DISABLED,
@@ -332,73 +322,30 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
     };
 
     //
-    // If a route specifically has `/api/` in it, send it to API Gateway
-    // This is needed to catch routes that have periods in the API path data,
-    // such as: /release/0.0.0/api/update/default/release/0.0.0
-    //
-    if (createAPIPathRoute) {
-      distro.addBehavior(
-        posixPath.join(rootPathPrefix, '*/api/*'),
-        defaultOrigin,
-        appBehaviorOptions,
-      );
-
-      distro.addBehavior(
-        posixPath.join(rootPathPrefix, 'api/*'),
-        defaultOrigin,
-        appBehaviorOptions,
-      );
-    }
-
-    //
-    // If a route specifically has `/_next/data/` in it, send it to API Gateway
-    // This is needed to catch routes that have periods in the API path data,
-    // such as: /release/0.0.0/_next/data/app.json
-    //
-    if (createNextDataPathRoute) {
-      distro.addBehavior(
-        // Note: send anything with _next/data after the appName (and optional version)
-        // to the app origin as iframe-less will have no version before _next/data
-        // in the path
-        posixPath.join(rootPathPrefix, '*/_next/data/*'),
-        defaultOrigin,
-        appBehaviorOptions,
-      );
-
-      distro.addBehavior(
-        // Note: send anything with _next/data after the appName (and optional version)
-        // to the app origin as iframe-less will have no version before _next/data
-        // in the path
-        posixPath.join(rootPathPrefix, '_next/data/*'),
-        defaultOrigin,
-        appBehaviorOptions,
-      );
-    }
-
-    //
-    // All static assets are assumed to have a dot in them
+    // Handle designated static assets
+    // Falls back to the app on 403/404
     //
     distro.addBehavior(
-      posixPath.join(rootPathPrefix, '/*/*/*.*'),
-      bucketAppsOrigin,
+      posixPath.join(rootPathPrefix, '*/static/*.*'),
+      // TODO: 2023-03-04 - This could be the bucket origin without fallback, at least as an option
+      // which would allow skipping the OriginRequest (which has invoke cost and throughtput limits).
+      // This would be a distinct config from the `*/*.*` route below, which always has to have
+      // the OriginRequest function.
+      bucketOriginFallbackToApp,
       s3BehaviorOptions,
     );
-
-    //
-    // Root app static resources
-    //
     distro.addBehavior(
-      posixPath.join(rootPathPrefix, '/*.*.*/*.*'),
-      bucketAppsOrigin,
-      s3BehaviorOptions,
+      posixPath.join(rootPathPrefix, '*/*.*'),
+      bucketOriginFallbackToApp,
+      s3FallbackToAppOptions,
     );
 
     //
-    // Everything that isn't a static asset is going to API Gateway
-    // There is no trailing slash because Serverless Next.js wants
-    // go load pages at /release/0.0.3 (with no trailing slash).
+    // Default to sending everything else to the app only
+    // This is necessary because we allow all methods for the app but that is not
+    // allowed for an OriginGroup
     //
-    distro.addBehavior(posixPath.join(rootPathPrefix, '/*'), defaultOrigin, appBehaviorOptions);
+    distro.addBehavior(posixPath.join(rootPathPrefix, '/*'), appOnlyOrigin, appOnlyBehaviorOptions);
   }
 
   private _cloudFrontDistro: cf.Distribution;
@@ -430,10 +377,9 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
       assetNameSuffix,
       r53Zone,
       bucketLogs,
-      bucketAppsOrigin,
+      bucketAppsOriginS3,
+      bucketAppsOriginApp,
       rootPathPrefix,
-      createAPIPathRoute = !!props.httpApi,
-      createNextDataPathRoute = !!props.httpApi,
       edgeLambdas,
       originShieldRegion,
     } = props;
@@ -455,11 +401,7 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
     }
 
     //
-    // Get the Edge to Origin Lambdas
-    //
-
-    //
-    // CloudFront Distro
+    // Create fallback to S3 origin group
     //
     const appOrigin = httpApi
       ? new cforigins.HttpOrigin(httpOriginFQDN, {
@@ -467,7 +409,16 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
         originSslProtocols: [cf.OriginSslPolicy.TLS_V1_2],
         originShieldRegion,
       })
-      : bucketAppsOrigin;
+      : bucketAppsOriginApp ?? bucketAppsOriginS3;
+    const bucketOriginFallbackToApp = new cforigins.OriginGroup({
+      primaryOrigin: bucketAppsOriginS3,
+      fallbackOrigin: appOrigin,
+      fallbackStatusCodes: [403, 404],
+    });
+
+    //
+    // CloudFront Distro
+    //
     this._cloudFrontDistro = new cf.Distribution(this, 'cft', {
       comment: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : domainNameEdge,
       domainNames: domainNameEdge !== undefined ? [domainNameEdge] : undefined,
@@ -495,13 +446,11 @@ export class MicroAppsCF extends Construct implements IMicroAppsCF {
 
     // Add routes to the CloudFront Distribution
     MicroAppsCF.addRoutes(scope, {
-      appOrigin,
-      bucketAppsOrigin,
+      appOnlyOrigin: appOrigin,
+      bucketOriginFallbackToApp,
       distro: this._cloudFrontDistro,
       appOriginRequestPolicy,
       rootPathPrefix,
-      createAPIPathRoute,
-      createNextDataPathRoute,
       edgeLambdas,
     });
 
diff --git a/packages/microapps-cdk/src/MicroAppsS3.ts b/packages/microapps-cdk/src/MicroAppsS3.ts
index 980084b0..a63dec26 100644
--- a/packages/microapps-cdk/src/MicroAppsS3.ts
+++ b/packages/microapps-cdk/src/MicroAppsS3.ts
@@ -20,8 +20,19 @@ export interface IMicroAppsS3 {
 
   /**
    * CloudFront Origin for the deployed applications bucket
+   * Marked with `x-microapps-origin: app` so the OriginRequest function
+   * knows to send the request to the application origin first, if configured
+   * for a particular application.
    */
-  readonly bucketAppsOrigin: cforigins.S3Origin;
+  readonly bucketAppsOriginApp: cforigins.S3Origin;
+
+  /**
+   * CloudFront Origin for the deployed applications bucket
+   * Marked with `x-microapps-origin: s3` so the OriginRequest function
+   * knows to NOT send the request to the application origin and instead
+   * let it fall through to the S3 bucket.
+   */
+  readonly bucketAppsOriginS3: cforigins.S3Origin;
 
   /**
    * S3 bucket for staged applications (prior to deploy)
@@ -112,9 +123,14 @@ export class MicroAppsS3 extends Construct implements IMicroAppsS3 {
     return this._bucketAppsOAI;
   }
 
-  private _bucketAppsOrigin: cforigins.S3Origin;
-  public get bucketAppsOrigin(): cforigins.S3Origin {
-    return this._bucketAppsOrigin;
+  private _bucketAppsOriginApp: cforigins.S3Origin;
+  public get bucketAppsOriginApp(): cforigins.S3Origin {
+    return this._bucketAppsOriginApp;
+  }
+
+  private _bucketAppsOriginS3: cforigins.S3Origin;
+  public get bucketAppsOriginS3(): cforigins.S3Origin {
+    return this._bucketAppsOriginS3;
   }
 
   private _bucketAppsStaging: s3.IBucket;
@@ -171,9 +187,20 @@ export class MicroAppsS3 extends Construct implements IMicroAppsS3 {
     }
 
     // Add Origin for CloudFront
-    this._bucketAppsOrigin = new cforigins.S3Origin(this._bucketApps, {
+    this._bucketAppsOriginS3 = new cforigins.S3Origin(this._bucketApps, {
+      originAccessIdentity: this.bucketAppsOAI,
+      originShieldRegion,
+      customHeaders: {
+        'x-microapps-origin': 's3',
+      },
+    });
+
+    this._bucketAppsOriginApp = new cforigins.S3Origin(this._bucketApps, {
       originAccessIdentity: this.bucketAppsOAI,
       originShieldRegion,
+      customHeaders: {
+        'x-microapps-origin': 'app',
+      },
     });
   }
 }
diff --git a/packages/microapps-edge-to-origin/src/index.ts b/packages/microapps-edge-to-origin/src/index.ts
index b9bee221..c0c7410f 100644
--- a/packages/microapps-edge-to-origin/src/index.ts
+++ b/packages/microapps-edge-to-origin/src/index.ts
@@ -72,6 +72,34 @@ export const handler: lambda.CloudFrontRequestHandler = async (
   try {
     let requestToReturn = request;
 
+    // eslint-disable-next-line no-console
+    log.debug('got request', { event, context });
+
+    // If the origin is S3 but the `x-microapps-origin` header is set to
+    // `s3`, then we let the request fall through to S3
+    if (
+      request.origin?.s3?.domainName &&
+      request.origin?.s3?.customHeaders['x-microapps-origin']?.[0]?.value === 's3'
+    ) {
+      log.info('request is for S3 origin', { request });
+
+      if (
+        request.headers['host']?.[0]?.value &&
+        request.headers['host']?.[0]?.value !== request.origin?.s3?.domainName
+      ) {
+        // Overwrite the host because it gets wrong value after the Primary
+        // Origin in the OriginGroup overwrites it and we fallback here.
+        request.headers['host'] = [
+          {
+            key: 'Host',
+            value: request.origin?.s3?.domainName,
+          },
+        ];
+      }
+
+      return request as unknown as lambda.CloudFrontResultResponse;
+    }
+
     // Add x-forwarded-host before signing
     if (config.addXForwardedHostHeader && request.headers['host']) {
       // Overwrite to prevent spoofed value from getting through
@@ -246,9 +274,9 @@ export const handler: lambda.CloudFrontRequestHandler = async (
       log.info('not signing request');
     }
 
-    log.info('returning request', {
-      requestToReturn,
-    });
+    // log.debug('returning request', {
+    //   requestToReturn,
+    // });
 
     return requestToReturn as unknown as lambda.CloudFrontResultResponse;
   } catch (error) {
diff --git a/packages/microapps-publish/src/commands/delete.ts b/packages/microapps-publish/src/commands/delete.ts
index 0faae1b6..61868204 100644
--- a/packages/microapps-publish/src/commands/delete.ts
+++ b/packages/microapps-publish/src/commands/delete.ts
@@ -19,22 +19,40 @@ export class DeleteCommand extends Command {
       char: 'v',
     }),
     help: flagsParser.help(),
+    // Deprecated
     appName: flagsParser.string({
-      char: 'a',
       multiple: false,
       required: false,
-      description: 'Name of the MicroApp',
+      hidden: true,
     }),
+    'app-name': flagsParser.string({
+      char: 'a',
+      multiple: false,
+      exactlyOne: ['app-name', 'appName'],
+      description: 'MicroApps app name (this becomes the path the app is rooted at)',
+    }),
+    // Deprecated
     newVersion: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'new-version': flagsParser.string({
       char: 'n',
       multiple: false,
-      required: true,
+      exactlyOne: ['new-version', 'newVersion'],
       description: 'New semantic version to apply',
     }),
+    // Deprecated
     deployerLambdaName: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'deployer-lambda-name': flagsParser.string({
       char: 'd',
       multiple: false,
-      required: true,
+      exactlyOne: ['deployer-lambda-name', 'deployerLambdaName'],
       description: 'Name of the deployer lambda function',
     }),
   };
@@ -47,10 +65,12 @@ export class DeleteCommand extends Command {
     const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
 
     const { flags: parsedFlags } = this.parse(DeleteCommand);
-    const version = parsedFlags.newVersion;
-    const appName = parsedFlags.appName ?? config.app.name;
-    const deployerLambdaName = parsedFlags.deployerLambdaName ?? config.deployer.lambdaName;
-    const semVer = parsedFlags.newVersion ?? config.app.semVer;
+    const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'] ?? config.app.semVer;
+    const appName = parsedFlags.appName ?? parsedFlags['app-name'] ?? config.app.name;
+    const deployerLambdaName =
+      parsedFlags.deployerLambdaName ??
+      parsedFlags['deployer-lambda-name'] ??
+      config.deployer.lambdaName;
 
     // Override the config value
     config.deployer.lambdaName = deployerLambdaName;
@@ -87,7 +107,7 @@ export class DeleteCommand extends Command {
             task.title = RUNNING + origTitle;
 
             // Confirm the Version Does Not Exist in Published State
-            task.output = `Deleting app/version ${config.app.name}/${version}`;
+            task.output = `Deleting app/version ${config.app.name}/${semVer}`;
             const result = await DeployClient.DeleteVersion({
               config,
               output: (message: string) => (task.output = message),
diff --git a/packages/microapps-publish/src/commands/nextjs-version.ts b/packages/microapps-publish/src/commands/nextjs-version.ts
index b1ad74e4..5904e3b5 100644
--- a/packages/microapps-publish/src/commands/nextjs-version.ts
+++ b/packages/microapps-publish/src/commands/nextjs-version.ts
@@ -18,13 +18,27 @@ export class NextJSVersionCommand extends Command {
       char: 'v',
     }),
     help: flagsParser.help(),
+    // Deprecated
     newVersion: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'new-version': flagsParser.string({
       char: 'n',
       multiple: false,
-      required: true,
+      exactlyOne: ['new-version', 'newVersion'],
       description: 'New semantic version to apply',
     }),
+    // Deprecated
     leaveCopy: flagsParser.boolean({
+      char: 'l',
+      default: false,
+      required: false,
+      hidden: true,
+      exclusive: ['leave-copy'],
+    }),
+    'leave-copy': flagsParser.boolean({
       char: 'l',
       default: false,
       required: false,
@@ -44,14 +58,14 @@ export class NextJSVersionCommand extends Command {
     const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
 
     const { flags: parsedFlags } = this.parse(NextJSVersionCommand);
-    const version = parsedFlags.newVersion;
-    const leaveFiles = parsedFlags.leaveCopy;
+    const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'];
+    const leaveFiles = parsedFlags.leaveCopy ?? parsedFlags['leave-copy'];
 
     // Override the config value
     const config = Config.instance;
-    config.app.semVer = version;
+    config.app.semVer = semVer;
 
-    this.VersionAndAlias = createVersions(version);
+    this.VersionAndAlias = createVersions(semVer);
     const versionOnly = { version: this.VersionAndAlias.version };
 
     this.FILES_TO_MODIFY = [{ path: 'next.config.js', versions: versionOnly }];
diff --git a/packages/microapps-publish/src/commands/preflight.ts b/packages/microapps-publish/src/commands/preflight.ts
index 167a686c..fd4c7407 100644
--- a/packages/microapps-publish/src/commands/preflight.ts
+++ b/packages/microapps-publish/src/commands/preflight.ts
@@ -19,22 +19,40 @@ export class PreflightCommand extends Command {
       char: 'v',
     }),
     help: flagsParser.help(),
+    // Deprecated
     appName: flagsParser.string({
-      char: 'a',
       multiple: false,
       required: false,
-      description: 'Name of the MicroApp',
+      hidden: true,
     }),
+    'app-name': flagsParser.string({
+      char: 'a',
+      multiple: false,
+      exactlyOne: ['app-name', 'appName'],
+      description: 'MicroApps app name (this becomes the path the app is rooted at)',
+    }),
+    // Deprecated
     newVersion: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'new-version': flagsParser.string({
       char: 'n',
       multiple: false,
-      required: true,
+      exactlyOne: ['new-version', 'newVersion'],
       description: 'New semantic version to apply',
     }),
+    // Deprecated
     deployerLambdaName: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'deployer-lambda-name': flagsParser.string({
       char: 'd',
       multiple: false,
-      required: true,
+      exactlyOne: ['deployer-lambda-name', 'deployerLambdaName'],
       description: 'Name of the deployer lambda function',
     }),
     overwrite: flagsParser.boolean({
@@ -54,10 +72,12 @@ export class PreflightCommand extends Command {
     const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
 
     const { flags: parsedFlags } = this.parse(PreflightCommand);
-    const version = parsedFlags.newVersion;
-    const appName = parsedFlags.appName ?? config.app.name;
-    const deployerLambdaName = parsedFlags.deployerLambdaName ?? config.deployer.lambdaName;
-    const semVer = parsedFlags.newVersion ?? config.app.semVer;
+    const appName = parsedFlags.appName ?? parsedFlags['app-name'] ?? config.app.name;
+    const deployerLambdaName =
+      parsedFlags.deployerLambdaName ??
+      parsedFlags['deployer-lambda-name'] ??
+      config.deployer.lambdaName;
+    const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'] ?? config.app.semVer;
     const overwrite = parsedFlags.overwrite;
 
     // Override the config value
@@ -95,7 +115,7 @@ export class PreflightCommand extends Command {
             task.title = RUNNING + origTitle;
 
             // Confirm the Version Does Not Exist in Published State
-            task.output = `Checking if deployed app/version already exists for ${config.app.name}/${version}`;
+            task.output = `Checking if deployed app/version already exists for ${config.app.name}/${semVer}`;
             const preflightResult = await DeployClient.DeployVersionPreflight({
               config,
               needS3Creds: false,
diff --git a/packages/microapps-publish/src/commands/publish-static.ts b/packages/microapps-publish/src/commands/publish-static.ts
index 1242f98f..fa3136e9 100644
--- a/packages/microapps-publish/src/commands/publish-static.ts
+++ b/packages/microapps-publish/src/commands/publish-static.ts
@@ -43,32 +43,64 @@ export class PublishCommand extends Command {
       char: 'v',
     }),
     help: flagsParser.help(),
+    // Deprecated
     deployerLambdaName: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'deployer-lambda-name': flagsParser.string({
       char: 'd',
       multiple: false,
-      required: true,
+      exactlyOne: ['deployer-lambda-name', 'deployerLambdaName'],
       description: 'Name of the deployer lambda function',
     }),
+    // Deprecated
     newVersion: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'new-version': flagsParser.string({
       char: 'n',
       multiple: false,
-      required: true,
+      exactlyOne: ['new-version', 'newVersion'],
       description: 'New semantic version to apply',
     }),
+    // Deprecated
     appName: flagsParser.string({
-      char: 'a',
       multiple: false,
       required: false,
-      description: 'MicroApps app name',
+      hidden: true,
     }),
+    'app-name': flagsParser.string({
+      char: 'a',
+      multiple: false,
+      exactlyOne: ['app-name', 'appName'],
+      description: 'MicroApps app name (this becomes the path the app is rooted at)',
+    }),
+    // Deprecated
     staticAssetsPath: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'static-assets-path': flagsParser.string({
       char: 's',
       multiple: false,
       required: false,
+      exactlyOne: ['static-assets-path', 'staticAssetsPath'],
       description:
         'Path to files to be uploaded to S3 static bucket at app/version/ path.  Do include app/version/ in path if files are already "rooted" under that path locally.',
     }),
+    // Deprecated
     defaultFile: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+      exclusive: ['default-file'],
+    }),
+    'default-file': flagsParser.string({
       char: 'i',
       multiple: false,
       required: false,
@@ -82,15 +114,19 @@ export class PublishCommand extends Command {
       description:
         'Allow overwrite - Warn but do not fail if version exists. Discouraged outside of test envs if cacheable static files have changed.',
     }),
+    // Deprecated
     noCache: flagsParser.boolean({
+      required: false,
+      default: false,
+      hidden: true,
+    }),
+    'no-cache': flagsParser.boolean({
       required: false,
       default: false,
       description: 'Force revalidation of CloudFront and browser caching of static assets',
     }),
   };
 
-  private VersionAndAlias: IVersions;
-
   async run(): Promise<void> {
     const config = Config.instance;
 
@@ -99,11 +135,18 @@ export class PublishCommand extends Command {
     const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
 
     const { flags: parsedFlags } = this.parse(PublishCommand);
-    const appName = parsedFlags.appName ?? config.app.name;
-    const deployerLambdaName = parsedFlags.deployerLambdaName ?? config.deployer.lambdaName;
-    const semVer = parsedFlags.newVersion ?? config.app.semVer;
-    const staticAssetsPath = parsedFlags.staticAssetsPath ?? config.app.staticAssetsPath;
-    const defaultFile = parsedFlags.defaultFile ?? config.app.defaultFile;
+    const appName = parsedFlags.appName ?? parsedFlags['app-name'] ?? config.app.name;
+    const deployerLambdaName =
+      parsedFlags.deployerLambdaName ??
+      parsedFlags['deployer-lambda-name'] ??
+      config.deployer.lambdaName;
+    const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'] ?? config.app.semVer;
+    const staticAssetsPath =
+      parsedFlags.staticAssetsPath ??
+      parsedFlags['static-assets-path'] ??
+      config.app.staticAssetsPath;
+    const defaultFile =
+      parsedFlags.defaultFile ?? parsedFlags['default-file'] ?? config.app.defaultFile;
     const overwrite = parsedFlags.overwrite;
     const noCache = parsedFlags.noCache;
 
@@ -130,8 +173,6 @@ export class PublishCommand extends Command {
       }
     }
 
-    this.VersionAndAlias = createVersions(semVer);
-
     if (config.app.staticAssetsPath === undefined) {
       this.error('staticAssetsPath must be specified');
     }
diff --git a/packages/microapps-publish/src/commands/publish.ts b/packages/microapps-publish/src/commands/publish.ts
index ca1d6a09..7cf3e286 100644
--- a/packages/microapps-publish/src/commands/publish.ts
+++ b/packages/microapps-publish/src/commands/publish.ts
@@ -69,38 +69,78 @@ export class PublishCommand extends Command {
       char: 'v',
     }),
     help: flagsParser.help(),
+    // Deprecated
     deployerLambdaName: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'deployer-lambda-name': flagsParser.string({
       char: 'd',
       multiple: false,
-      required: true,
+      exactlyOne: ['deployer-lambda-name', 'deployerLambdaName'],
       description: 'Name of the deployer lambda function',
     }),
+    // Deprecated
     newVersion: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+    }),
+    'new-version': flagsParser.string({
       char: 'n',
       multiple: false,
-      required: true,
+      exactlyOne: ['new-version', 'newVersion'],
       description: 'New semantic version to apply',
     }),
+    // Deprecated
     appLambdaName: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+      exclusive: ['app-lambda-name'],
+    }),
+    'app-lambda-name': flagsParser.string({
       char: 'l',
       multiple: false,
       required: false,
       description: 'ARN of lambda version, alias, or function (name or ARN) to deploy',
     }),
+    // Deprecated
     appName: flagsParser.string({
-      char: 'a',
       multiple: false,
       required: false,
+      hidden: true,
+      exclusive: ['app-name'],
+    }),
+    'app-name': flagsParser.string({
+      char: 'a',
+      multiple: false,
+      exactlyOne: ['app-name', 'appName'],
       description: 'MicroApps app name (this becomes the path the app is rooted at)',
     }),
+    // Deprecated
     staticAssetsPath: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+      exclusive: ['static-assets-path'],
+    }),
+    'static-assets-path': flagsParser.string({
       char: 's',
       multiple: false,
       required: false,
       description:
         'Path to files to be uploaded to S3 static bucket at app/version/ path.  Do include app/version/ in path if files are already "rooted" under that path locally.',
     }),
+    // Deprecated
     defaultFile: flagsParser.string({
+      multiple: false,
+      required: false,
+      hidden: true,
+      exclusive: ['default-file'],
+    }),
+    'default-file': flagsParser.string({
       char: 'i',
       multiple: false,
       required: false,
@@ -114,7 +154,13 @@ export class PublishCommand extends Command {
       description:
         'Allow overwrite - Warn but do not fail if version exists. Discouraged outside of test envs if cacheable static files have changed.',
     }),
+    // Deprecated
     noCache: flagsParser.boolean({
+      required: false,
+      default: false,
+      hidden: true,
+    }),
+    'no-cache': flagsParser.boolean({
       required: false,
       default: false,
       description: 'Force revalidation of CloudFront and browser caching of static assets',
@@ -152,14 +198,22 @@ export class PublishCommand extends Command {
     const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
 
     const { flags: parsedFlags } = this.parse(PublishCommand);
-    const appLambdaName = parsedFlags.appLambdaName ?? config.app.lambdaName;
-    const appName = parsedFlags.appName ?? config.app.name;
-    const deployerLambdaName = parsedFlags.deployerLambdaName ?? config.deployer.lambdaName;
-    const semVer = parsedFlags.newVersion ?? config.app.semVer;
-    const staticAssetsPath = parsedFlags.staticAssetsPath ?? config.app.staticAssetsPath;
-    const defaultFile = parsedFlags.defaultFile ?? config.app.defaultFile;
+    const appLambdaName =
+      parsedFlags.appLambdaName ?? parsedFlags['app-lambda-name'] ?? config.app.lambdaName;
+    const appName = parsedFlags.appName ?? parsedFlags['app-name'] ?? config.app.name;
+    const deployerLambdaName =
+      parsedFlags.deployerLambdaName ??
+      parsedFlags['deployer-lambda-name'] ??
+      config.deployer.lambdaName;
+    const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'] ?? config.app.semVer;
+    const staticAssetsPath =
+      parsedFlags.staticAssetsPath ??
+      parsedFlags['static-assets-path'] ??
+      config.app.staticAssetsPath;
+    const defaultFile =
+      parsedFlags.defaultFile ?? parsedFlags['default-file'] ?? config.app.defaultFile;
     const overwrite = parsedFlags.overwrite;
-    const noCache = parsedFlags.noCache;
+    const noCache = parsedFlags.noCache ?? parsedFlags['no-cache'];
 
     // Override the config value
     config.deployer.lambdaName = deployerLambdaName;


Test patch:
diff --git a/packages/microapps-cdk/test/MicroAppsCF.test.ts b/packages/microapps-cdk/test/MicroAppsCF.test.ts
index 36ef7021..aaf4b0df 100644
--- a/packages/microapps-cdk/test/MicroAppsCF.test.ts
+++ b/packages/microapps-cdk/test/MicroAppsCF.test.ts
@@ -21,30 +21,30 @@ describe('MicroAppsCF', () => {
 
     const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
       apiName: 'some-api',
-      // defaultDomainMapping: {
-      //   domainName: new apigwy.DomainName(stack, 'domain-name', {
-      //     domainName: 'apps.pwrdrvr.com',
-      //     certificate: new acm.Certificate(stack, 'cert', {
-      //       domainName: 'apps.pwrdrvr.com',
-      //     }),
-      //   }),
-      // },
     });
+    const bucket = new s3.Bucket(stack, 'bucket-apps', {});
+    const oai = new cf.OriginAccessIdentity(stack, 'oai', {});
     const construct = new MicroAppsCF(stack, 'construct', {
       httpApi,
-      bucketAppsOrigin: new cforigins.S3Origin(new s3.Bucket(stack, 'bucket-apps', {}), {
-        originAccessIdentity: new cf.OriginAccessIdentity(stack, 'oai', {}),
+      bucketAppsOriginApp: new cforigins.S3Origin(bucket, {
+        customHeaders: {
+          'x-microapps-origin': 'app',
+        },
+        originAccessIdentity: oai,
+      }),
+      bucketAppsOriginS3: new cforigins.S3Origin(bucket, {
+        customHeaders: {
+          'x-microapps-origin': 's3',
+        },
+        originAccessIdentity: oai,
       }),
     });
 
     expect(construct).toBeDefined();
     expect(construct.cloudFrontDistro).toBeDefined();
     expect(construct.node).toBeDefined();
-    // expect(stack).toHaveResource('AWS::S3::Bucket');
-    // expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
 
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    // Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginRequestPolicy', 1);
   });
 
   it('works with params', () => {
@@ -56,14 +56,6 @@ describe('MicroAppsCF', () => {
     });
     const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
       apiName: 'some-api',
-      // defaultDomainMapping: {
-      //   domainName: new apigwy.DomainName(stack, 'domain-name', {
-      //     domainName: 'apps.pwrdrvr.com',
-      //     certificate: new acm.Certificate(stack, 'cert', {
-      //       domainName: 'apps.pwrdrvr.com',
-      //     }),
-      //   }),
-      // },
     });
     const r53Zone = new r53.HostedZone(stack, 'zone', {
       zoneName: 'test.pwrdrvr.com',
@@ -71,10 +63,21 @@ describe('MicroAppsCF', () => {
     const certOrigin = new acm.Certificate(stack, 'cert', {
       domainName: '*.test.pwrdrvr.com',
     });
+    const bucket = new s3.Bucket(stack, 'bucket-apps', {});
+    const oai = new cf.OriginAccessIdentity(stack, 'oai', {});
     const construct = new MicroAppsCF(stack, 'construct', {
       httpApi,
-      bucketAppsOrigin: new cforigins.S3Origin(new s3.Bucket(stack, 'bucket-apps', {}), {
-        originAccessIdentity: new cf.OriginAccessIdentity(stack, 'oai', {}),
+      bucketAppsOriginApp: new cforigins.S3Origin(bucket, {
+        customHeaders: {
+          'x-microapps-origin': 'app',
+        },
+        originAccessIdentity: oai,
+      }),
+      bucketAppsOriginS3: new cforigins.S3Origin(bucket, {
+        customHeaders: {
+          'x-microapps-origin': 's3',
+        },
+        originAccessIdentity: oai,
       }),
       domainNameEdge: 'some.test.pwrdrvr.com',
       domainNameOrigin: 'some-origin.test.pwrdvr.com',
diff --git a/packages/microapps-cdk/test/MicroAppsS3.test.ts b/packages/microapps-cdk/test/MicroAppsS3.test.ts
index c4fb6727..3447eece 100644
--- a/packages/microapps-cdk/test/MicroAppsS3.test.ts
+++ b/packages/microapps-cdk/test/MicroAppsS3.test.ts
@@ -15,7 +15,8 @@ describe('MicroAppsS3', () => {
     expect(construct.bucketAppsStaging).toBeDefined();
     expect(construct.bucketLogs).toBeDefined();
     expect(construct.bucketAppsOAI).toBeDefined();
-    expect(construct.bucketAppsOrigin).toBeDefined();
+    expect(construct.bucketAppsOriginApp).toBeDefined();
+    expect(construct.bucketAppsOriginS3).toBeDefined();
 
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::S3::Bucket', 3);