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

Issue:
Next - CFront - Add _next/data Route to API Gateway
- `_next/data` is used for `getServerSideProps` calls
- Add route (or way to add additional custom routes) to CloudFront Behaviors
- Route this to API Gateway
- Currently this is going to S3


Discussion / hints:

Fix patch:
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index 89468a86..802db7dc 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -8,7 +8,7 @@
 
 Create a new MicroApps "turnkey" construct for simple deployments and for initial evaulation of the MicroApps framework.
 
-Use this construct to create a working entire stack.
+Use this construct to create a PoC working entire stack.
 
 Do not use this construct when adding MicroApps to an existing
 CloudFront, API Gateway, S3 Bucket, etc. or where access
@@ -80,12 +80,20 @@ new MicroApps(scope: Construct, id: string, props?: MicroAppsProps)
 
 ---
 
+##### `edgeToOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroApps.edgeToOrigin"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
+
+{@inheritdoc IMicroAppsEdgeToOrigin}.
+
+---
+
 
 ### MicroAppsAPIGwy <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy"></a>
 
 - *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy)
 
-Create a new MicroApps API Gateway HTTP API endpoint.
+Create a new MicroApps API Gateway HTTP API endpoint, optionally requiring IAM authorization.
 
 #### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.Initializer"></a>
 
@@ -194,30 +202,76 @@ MicroAppsCF.addRoutes(_scope: Construct, props: AddRoutesOptions)
 ```typescript
 import { MicroAppsCF } from '@pwrdrvr/microapps-cdk'
 
-MicroAppsCF.createAPIOriginPolicy(scope: Construct, props: CreateAPIOriginPolicyOptions)
+MicroAppsCF.createAPIOriginPolicy(_scope: Construct, _props: CreateAPIOriginPolicyOptions)
 ```
 
-###### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.scope"></a>
+###### `_scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF._scope"></a>
 
 - *Type:* [`constructs.Construct`](#constructs.Construct)
 
 ---
 
-###### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
+###### `_props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF._props"></a>
 
 - *Type:* [`@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions`](#@pwrdrvr/microapps-cdk.CreateAPIOriginPolicyOptions)
 
 ---
 
-##### `generateEdgeToOriginConfig` <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.generateEdgeToOriginConfig"></a>
+#### Properties <a name="Properties"></a>
+
+##### `cloudFrontDistro`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.cloudFrontDistro"></a>
+
+- *Type:* [`aws-cdk-lib.aws_cloudfront.Distribution`](#aws-cdk-lib.aws_cloudfront.Distribution)
+
+The CloudFront distribution.
+
+---
+
+
+### MicroAppsEdgeToOrigin <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin"></a>
+
+- *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
+
+Create a new MicroApps Edge to Origin Function w/ `config.yml`.
+
+#### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.Initializer"></a>
 
 ```typescript
-import { MicroAppsCF } from '@pwrdrvr/microapps-cdk'
+import { MicroAppsEdgeToOrigin } from '@pwrdrvr/microapps-cdk'
 
-MicroAppsCF.generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions)
+new MicroAppsEdgeToOrigin(scope: Construct, id: string, props: MicroAppsEdgeToOriginProps)
 ```
 
-###### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.props"></a>
+##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.scope"></a>
+
+- *Type:* [`constructs.Construct`](#constructs.Construct)
+
+---
+
+##### `id`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.id"></a>
+
+- *Type:* `string`
+
+---
+
+##### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps`](#@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps)
+
+---
+
+
+#### Static Functions <a name="Static Functions"></a>
+
+##### `generateEdgeToOriginConfig` <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.generateEdgeToOriginConfig"></a>
+
+```typescript
+import { MicroAppsEdgeToOrigin } from '@pwrdrvr/microapps-cdk'
+
+MicroAppsEdgeToOrigin.generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions)
+```
+
+###### `props`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.props"></a>
 
 - *Type:* [`@pwrdrvr/microapps-cdk.GenerateEdgeToOriginConfigOptions`](#@pwrdrvr/microapps-cdk.GenerateEdgeToOriginConfigOptions)
 
@@ -225,15 +279,21 @@ MicroAppsCF.generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions)
 
 #### Properties <a name="Properties"></a>
 
-##### `cloudFrontDistro`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.cloudFrontDistro"></a>
+##### `edgeToOriginFunction`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.edgeToOriginFunction"></a>
 
-- *Type:* [`aws-cdk-lib.aws_cloudfront.Distribution`](#aws-cdk-lib.aws_cloudfront.Distribution)
+- *Type:* [`aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction`](#aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction) | [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
+
+The edge to origin function for API Gateway Request Origin Edge Lambda.
+
+The generated `config.yml` is included in the Lambda's code.
 
 ---
 
-##### `edgeToOriginFunction`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCF.edgeToOriginFunction"></a>
+##### `edgeToOriginLambdas`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin.edgeToOriginLambdas"></a>
 
-- *Type:* [`aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction`](#aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction) | [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
+- *Type:* [`aws-cdk-lib.aws_cloudfront.EdgeLambda`](#aws-cdk-lib.aws_cloudfront.EdgeLambda)[]
+
+Configuration of the edge to origin lambda functions.
 
 ---
 
@@ -242,7 +302,10 @@ MicroAppsCF.generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions)
 
 - *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsS3`](#@pwrdrvr/microapps-cdk.IMicroAppsS3)
 
-Create a new MicroApps S3 Bucket.
+Create the durable MicroApps S3 Buckets.
+
+These should be created in a stack that will not be deleted if
+there are breaking changes to MicroApps in the future.
 
 #### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.Initializer"></a>
 
@@ -376,6 +439,51 @@ DynamoDB table used by Router, Deployer, and Release console app.
 ---
 
 
+### MicroAppsTable <a name="@pwrdrvr/microapps-cdk.MicroAppsTable"></a>
+
+- *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsTable`](#@pwrdrvr/microapps-cdk.IMicroAppsTable)
+
+Create a new MicroApps Table for apps / versions / rules.
+
+#### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsTable.Initializer"></a>
+
+```typescript
+import { MicroAppsTable } from '@pwrdrvr/microapps-cdk'
+
+new MicroAppsTable(scope: Construct, id: string, props?: MicroAppsTableProps)
+```
+
+##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTable.scope"></a>
+
+- *Type:* [`constructs.Construct`](#constructs.Construct)
+
+---
+
+##### `id`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTable.id"></a>
+
+- *Type:* `string`
+
+---
+
+##### `props`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTable.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsTableProps`](#@pwrdrvr/microapps-cdk.MicroAppsTableProps)
+
+---
+
+
+
+#### Properties <a name="Properties"></a>
+
+##### `table`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTable.table"></a>
+
+- *Type:* [`aws-cdk-lib.aws_dynamodb.Table`](#aws-cdk-lib.aws_dynamodb.Table)
+
+DynamoDB table used by Router, Deployer, and Release console app.
+
+---
+
+
 ## Structs <a name="Structs"></a>
 
 ### AddRoutesOptions <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions"></a>
@@ -444,6 +552,20 @@ even if they have a period in the path.
 
 ---
 
+##### `createNextDataPathRoute`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.createNextDataPathRoute"></a>
+
+- *Type:* `boolean`
+- *Default:* true
+
+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.
+
+---
+
 ##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.AddRoutesOptions.rootPathPrefix"></a>
 
 - *Type:* `string`
@@ -646,20 +768,6 @@ API Gateway v2 HTTP API for apps.
 
 ---
 
-##### `addXForwardedHostHeader`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.addXForwardedHostHeader"></a>
-
-- *Type:* `boolean`
-- *Default:* true
-
-Adds an X-Forwarded-Host-Header when calling API Gateway.
-
-Can only be trusted if `signingMode` is enabled, which restricts
-access to API Gateway to only IAM signed requests.
-
-Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
-
----
-
 ##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.assetNameRoot"></a>
 
 - *Type:* `string`
@@ -708,6 +816,20 @@ even if they have a period in the path.
 
 ---
 
+##### `createNextDataPathRoute`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.createNextDataPathRoute"></a>
+
+- *Type:* `boolean`
+- *Default:* true
+
+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.
+
+---
+
 ##### `domainNameEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainNameEdge"></a>
 
 - *Type:* `string`
@@ -726,12 +848,11 @@ API Gateway custom origin domain name.
 
 ---
 
-##### `originRegion`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.originRegion"></a>
+##### `edgeToOriginLambdas`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.edgeToOriginLambdas"></a>
 
-- *Type:* `string`
-- *Default:* undefined
+- *Type:* [`aws-cdk-lib.aws_cloudfront.EdgeLambda`](#aws-cdk-lib.aws_cloudfront.EdgeLambda)[]
 
-Origin region that API Gateway will be deployed to, used for the config.yml on the Edge function to sign requests for the correct region.
+Configuration of the edge to origin lambda functions.
 
 ---
 
@@ -754,31 +875,95 @@ Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true
 
 ---
 
-##### `replaceHostHeader`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.replaceHostHeader"></a>
+##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.rootPathPrefix"></a>
+
+- *Type:* `string`
+
+Path prefix on the root of the CloudFront distribution.
+
+---
+
+### MicroAppsEdgeToOriginProps <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps"></a>
+
+Properties to initialize an instance of `MicroAppsCF`.
+
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+import { MicroAppsEdgeToOriginProps } from '@pwrdrvr/microapps-cdk'
+
+const microAppsEdgeToOriginProps: MicroAppsEdgeToOriginProps = { ... }
+```
+
+##### `addXForwardedHostHeader`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.addXForwardedHostHeader"></a>
 
 - *Type:* `boolean`
 - *Default:* true
 
-Replaces Host header (which will be the Edge domain name) with the Origin domain name when enabled.
+Adds an X-Forwarded-Host-Header when calling API Gateway.
 
-This is necessary when API Gateway has not been configured
-with a custom domain name that matches the exact domain name used by the CloudFront
-Distribution AND when the OriginRequestPolicy.HeadersBehavior is set
-to pass all headers to the origin.
+Can only be trusted if `signingMode` is enabled, which restricts
+access to API Gateway to only IAM signed requests.
 
 Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
 
 ---
 
-##### `rootPathPrefix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.rootPathPrefix"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
 
-Path prefix on the root of the CloudFront distribution.
+Optional asset name root.
+
+---
+
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.assetNameSuffix"></a>
+
+- *Type:* `string`
+- *Default:* none
+
+Optional asset name suffix.
+
+---
+
+##### `originRegion`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.originRegion"></a>
+
+- *Type:* `string`
+- *Default:* undefined
+
+Origin region that API Gateway will be deployed to, used for the config.yml on the Edge function to sign requests for the correct region.
+
+---
+
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.removalPolicy"></a>
+
+- *Type:* [`aws-cdk-lib.RemovalPolicy`](#aws-cdk-lib.RemovalPolicy)
+- *Default:* per resource default
+
+RemovalPolicy override for child resources.
+
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
-##### `signingMode`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.signingMode"></a>
+##### `replaceHostHeader`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.replaceHostHeader"></a>
+
+- *Type:* `boolean`
+- *Default:* true
+
+Replaces Host header (which will be the Edge domain name) with the Origin domain name when enabled.
+
+This is necessary when API Gateway has not been configured
+with a custom domain name that matches the exact domain name used by the CloudFront
+Distribution AND when the OriginRequestPolicy.HeadersBehavior is set
+to pass all headers to the origin.
+
+Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
+
+---
+
+##### `signingMode`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.signingMode"></a>
 
 - *Type:* `string`
 - *Default:* 'sign'
@@ -1036,6 +1221,15 @@ Note: if 'sign' or 'presign', creates OriginRequest Lambda @ Edge function for A
 
 ---
 
+##### `table`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.table"></a>
+
+- *Type:* [`aws-cdk-lib.aws_dynamodb.ITable`](#aws-cdk-lib.aws_dynamodb.ITable)
+- *Default:* created by construct
+
+Existing table for apps/versions/rules.
+
+---
+
 ### MicroAppsS3Props <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props"></a>
 
 Properties to initialize an instance of `MicroAppsS3`.
@@ -1276,6 +1470,73 @@ fully trusted.
 
 ---
 
+##### `table`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.table"></a>
+
+- *Type:* [`aws-cdk-lib.aws_dynamodb.ITable`](#aws-cdk-lib.aws_dynamodb.ITable)
+- *Default:* created by construct
+
+Existing table for apps/versions/rules.
+
+---
+
+### MicroAppsTableProps <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps"></a>
+
+Properties to initialize an instance of `MicroAppsTable`.
+
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+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`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
+
+---
+
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps.assetNameSuffix"></a>
+
+- *Type:* `string`
+- *Default:* none
+
+Optional asset name suffix.
+
+---
+
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsTableProps.removalPolicy"></a>
+
+- *Type:* [`aws-cdk-lib.RemovalPolicy`](#aws-cdk-lib.RemovalPolicy)
+- *Default:* per resource default
+
+RemovalPolicy override for child resources.
+
+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>
 
@@ -1320,6 +1581,14 @@ Represents a MicroApps.
 
 ---
 
+##### `edgeToOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.IMicroApps.edgeToOrigin"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
+
+{@inheritdoc IMicroAppsEdgeToOrigin}.
+
+---
+
 ### IMicroAppsAPIGwy <a name="@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy"></a>
 
 - *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.MicroAppsAPIGwy), [`@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy)
@@ -1358,12 +1627,35 @@ Represents a MicroApps CloudFront.
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront.Distribution`](#aws-cdk-lib.aws_cloudfront.Distribution)
 
+The CloudFront distribution.
+
 ---
 
-##### `edgeToOriginFunction`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsCF.edgeToOriginFunction"></a>
+### IMicroAppsEdgeToOrigin <a name="@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin"></a>
+
+- *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin), [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
+
+Represents a MicroApps Edge to Origin Function.
+
+
+#### Properties <a name="Properties"></a>
+
+##### `edgeToOriginFunction`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin.edgeToOriginFunction"></a>
 
 - *Type:* [`aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction`](#aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction) | [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
 
+The edge to origin function for API Gateway Request Origin Edge Lambda.
+
+The generated `config.yml` is included in the Lambda's code.
+
+---
+
+##### `edgeToOriginLambdas`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin.edgeToOriginLambdas"></a>
+
+- *Type:* [`aws-cdk-lib.aws_cloudfront.EdgeLambda`](#aws-cdk-lib.aws_cloudfront.EdgeLambda)[]
+
+Configuration of the edge to origin lambda functions.
+
 ---
 
 ### IMicroAppsS3 <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3"></a>
@@ -1448,3 +1740,20 @@ DynamoDB table used by Router, Deployer, and Release console app.
 
 ---
 
+### IMicroAppsTable <a name="@pwrdrvr/microapps-cdk.IMicroAppsTable"></a>
+
+- *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsTable`](#@pwrdrvr/microapps-cdk.MicroAppsTable), [`@pwrdrvr/microapps-cdk.IMicroAppsTable`](#@pwrdrvr/microapps-cdk.IMicroAppsTable)
+
+Represents a MicroAppsTable.
+
+
+#### Properties <a name="Properties"></a>
+
+##### `table`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsTable.table"></a>
+
+- *Type:* [`aws-cdk-lib.aws_dynamodb.Table`](#aws-cdk-lib.aws_dynamodb.Table)
+
+DynamoDB table used by Router, Deployer, and Release console app.
+
+---
+
diff --git a/packages/microapps-cdk/src/MicroApps.ts b/packages/microapps-cdk/src/MicroApps.ts
index e79bebc1..8ef915bb 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -1,9 +1,11 @@
 import { RemovalPolicy } from 'aws-cdk-lib';
 import * as acm from 'aws-cdk-lib/aws-certificatemanager';
+import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import * as r53 from 'aws-cdk-lib/aws-route53';
 import { Construct } from 'constructs';
 import { IMicroAppsAPIGwy, MicroAppsAPIGwy } from './MicroAppsAPIGwy';
 import { IMicroAppsCF, MicroAppsCF } from './MicroAppsCF';
+import { IMicroAppsEdgeToOrigin, MicroAppsEdgeToOrigin } from './MicroAppsEdgeToOrigin';
 import { IMicroAppsS3, MicroAppsS3 } from './MicroAppsS3';
 import { IMicroAppsSvcs, MicroAppsSvcs } from './MicroAppsSvcs';
 import { reverseDomain } from './utils/ReverseDomain';
@@ -232,6 +234,22 @@ export interface MicroAppsProps {
    * @default undefined
    */
   readonly originRegion?: string;
+
+  /**
+   * Existing table for apps/versions/rules
+   *
+   * @warning - It is *strongly* suggested that production stacks create
+   * their own DynamoDB Table and pass it into this construct, for protection
+   * against data loss due to logical ID changes, the ability to configure
+   * Provisioned capacity with Auto Scaling, the ability to add additional indices, etc.
+   *
+   * Requirements:
+   * - Hash Key: `PK`
+   * - Sort Key: `SK`
+   *
+   * @default created by construct
+   */
+  readonly table?: dynamodb.ITable;
 }
 
 /**
@@ -241,6 +259,9 @@ export interface IMicroApps {
   /** {@inheritdoc IMicroAppsCF} */
   readonly cf: IMicroAppsCF;
 
+  /** {@inheritdoc IMicroAppsEdgeToOrigin} */
+  readonly edgeToOrigin?: IMicroAppsEdgeToOrigin;
+
   /** {@inheritdoc IMicroAppsS3} */
   readonly s3: IMicroAppsS3;
 
@@ -255,7 +276,7 @@ export interface IMicroApps {
  * Create a new MicroApps "turnkey" construct for simple
  * deployments and for initial evaulation of the MicroApps framework.
  *
- * Use this construct to create a working entire stack.
+ * Use this construct to create a PoC working entire stack.
  *
  * Do not use this construct when adding MicroApps to an existing
  * CloudFront, API Gateway, S3 Bucket, etc. or where access
@@ -263,6 +284,12 @@ export interface IMicroApps {
  * add additional Behaviors to the CloudFront distribution, set authorizors
  * on API Gateway, etc.).
  *
+ * @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.
+ *
  *  @see {@link https://github.com/pwrdrvr/microapps-core/blob/main/packages/cdk/lib/MicroApps.ts | example usage in a CDK Stack }
  */
 export class MicroApps extends Construct implements IMicroApps {
@@ -271,6 +298,11 @@ export class MicroApps extends Construct implements IMicroApps {
     return this._cf;
   }
 
+  private _edgeToOrigin?: MicroAppsEdgeToOrigin;
+  public get edgeToOrigin(): IMicroAppsEdgeToOrigin | undefined {
+    return this._edgeToOrigin;
+  }
+
   private _s3: MicroAppsS3;
   public get s3(): IMicroAppsS3 {
     return this._s3;
@@ -312,6 +344,7 @@ export class MicroApps extends Construct implements IMicroApps {
       replaceHostHeader = true,
       signingMode = 'sign',
       originRegion,
+      table,
     } = props;
 
     this._s3 = new MicroAppsS3(this, 's3', {
@@ -335,6 +368,17 @@ export class MicroApps extends Construct implements IMicroApps {
       rootPathPrefix,
       requireIAMAuthorization: signingMode !== 'none',
     });
+    if (signingMode !== 'none' || replaceHostHeader || addXForwardedHostHeader) {
+      this._edgeToOrigin = new MicroAppsEdgeToOrigin(this, 'edgeToOrigin', {
+        assetNameRoot,
+        assetNameSuffix,
+        removalPolicy,
+        addXForwardedHostHeader,
+        replaceHostHeader,
+        originRegion,
+        signingMode,
+      });
+    }
     this._cf = new MicroAppsCF(this, 'cft', {
       removalPolicy,
       assetNameRoot,
@@ -348,10 +392,7 @@ export class MicroApps extends Construct implements IMicroApps {
       bucketLogs: this._s3.bucketLogs,
       rootPathPrefix,
       createAPIPathRoute,
-      addXForwardedHostHeader,
-      replaceHostHeader,
-      signingMode,
-      originRegion,
+      edgeToOriginLambdas: this._edgeToOrigin ? this._edgeToOrigin.edgeToOriginLambdas : undefined,
     });
     this._svcs = new MicroAppsSvcs(this, 'svcs', {
       httpApi: this.apigwy.httpApi,
@@ -367,6 +408,7 @@ export class MicroApps extends Construct implements IMicroApps {
       s3StrictBucketPolicy,
       rootPathPrefix,
       requireIAMAuthorization: signingMode !== 'none',
+      table,
     });
   }
 }
diff --git a/packages/microapps-cdk/src/MicroAppsAPIGwy.ts b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
index de16b265..68f7b1e5 100644
--- a/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
+++ b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
@@ -99,7 +99,8 @@ export interface IMicroAppsAPIGwy {
 }
 
 /**
- * Create a new MicroApps API Gateway HTTP API endpoint.
+ * Create a new MicroApps API Gateway HTTP API endpoint, optionally
+ * requiring IAM authorization
  */
 export class MicroAppsAPIGwy extends Construct implements IMicroAppsAPIGwy {
   private _dnAppsOrigin: apigwy.DomainName | undefined;
diff --git a/packages/microapps-cdk/src/MicroAppsCF.ts b/packages/microapps-cdk/src/MicroAppsCF.ts
index 541b1366..fce676fe 100644
--- a/packages/microapps-cdk/src/MicroAppsCF.ts
+++ b/packages/microapps-cdk/src/MicroAppsCF.ts
@@ -1,17 +1,9 @@
-import { existsSync, writeFileSync } from 'fs';
-import * as os from 'os';
-import * as path from 'path';
 import { posix as posixPath } from 'path';
 import * as apigwy from '@aws-cdk/aws-apigatewayv2-alpha';
-import { Aws, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
-// import * as apigwycfn from 'aws-cdk-lib/aws-apigatewayv2';
+import { Aws, RemovalPolicy } from 'aws-cdk-lib';
 import * as acm from 'aws-cdk-lib/aws-certificatemanager';
 import * as cf from 'aws-cdk-lib/aws-cloudfront';
 import * as cforigins from 'aws-cdk-lib/aws-cloudfront-origins';
-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';
-import * as logs from 'aws-cdk-lib/aws-logs';
 import * as r53 from 'aws-cdk-lib/aws-route53';
 import * as r53targets from 'aws-cdk-lib/aws-route53-targets';
 import * as s3 from 'aws-cdk-lib/aws-s3';
@@ -22,9 +14,10 @@ import { reverseDomain } from './utils/ReverseDomain';
  * Represents a MicroApps CloudFront
  */
 export interface IMicroAppsCF {
+  /**
+   * The CloudFront distribution
+   */
   readonly cloudFrontDistro: cf.Distribution;
-
-  readonly edgeToOriginFunction?: lambda.Function | cf.experimental.EdgeFunction;
 }
 
 /**
@@ -118,56 +111,26 @@ export interface MicroAppsCFProps {
   readonly createAPIPathRoute?: boolean;
 
   /**
-   * Adds an X-Forwarded-Host-Header when calling API Gateway
+   * 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.
    *
-   * Can only be trusted if `signingMode` is enabled, which restricts
-   * access to API Gateway to only IAM signed requests.
+   * When false API routes with a period in the path will get routed to S3.
    *
-   * Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
-   * @default true
-   */
-  readonly addXForwardedHostHeader?: boolean;
-
-  /**
-   * Replaces Host header (which will be the Edge domain name) with the Origin domain name
-   * when enabled.  This is necessary when API Gateway has not been configured
-   * with a custom domain name that matches the exact domain name used by the CloudFront
-   * Distribution AND when the OriginRequestPolicy.HeadersBehavior is set
-   * to pass all headers to the origin.
+   * 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.
    *
-   * Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
    * @default true
    */
-  readonly replaceHostHeader?: boolean;
+  readonly createNextDataPathRoute?: boolean;
 
   /**
-   * Requires IAM auth on the API Gateway origin if not set to 'none'.
+   * Configuration of the edge to origin lambda functions
    *
-   * 'sign' - Uses request headers for auth.
-   * 'presign' - Uses query string for auth.
-   *
-   * If enabled,
-   *
-   * Note: if 'sign' or 'presign', creates OriginRequest Lambda @ Edge function for API Gateway Origin
-   * @default 'sign'
-   */
-  readonly signingMode?: 'sign' | 'presign' | 'none';
-
-  /**
-   * Origin region that API Gateway will be deployed to, used
-   * for the config.yml on the Edge function to sign requests for
-   * the correct region
-   *
-   * @default undefined
+   * @defaunt - no edge to API Gateway origin functions added
    */
-  readonly originRegion?: string;
-}
-
-export interface GenerateEdgeToOriginConfigOptions {
-  readonly originRegion: string;
-  readonly signingMode: 'sign' | 'presign' | '';
-  readonly addXForwardedHostHeader: boolean;
-  readonly replaceHostHeader: boolean;
+  readonly edgeToOriginLambdas?: cf.EdgeLambda[];
 }
 
 /**
@@ -242,6 +205,21 @@ export interface AddRoutesOptions {
    */
   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 true
+   */
+  readonly createNextDataPathRoute?: boolean;
+
   /**
    * Edge lambdas to associate with the API Gateway routes
    */
@@ -252,18 +230,6 @@ export interface AddRoutesOptions {
  * Create a new MicroApps CloudFront Distribution.
  */
 export class MicroAppsCF extends Construct implements IMicroAppsCF {
-  /**
-   * Generate the yaml config for the edge lambda
-   * @param props
-   * @returns
-   */
-  public static generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions) {
-    return `originRegion: ${props.originRegion}
-${props.signingMode === '' ? '' : `signingMode: ${props.signingMode}`}
-addXForwardedHostHeader: ${props.addXForwardedHostHeader}
-replaceHostHeader: ${props.replaceHostHeader}`;
-  }
-
   /**
    * Create or get the origin request policy
    *
@@ -274,47 +240,47 @@ replaceHostHeader: ${props.replaceHostHeader}`;
    * policy will be returned.  This policy passes the Host header to the
    * origin, which is fine when using a custom domain name on the origin.
    *
-   * @param scope
-   * @param props
+   * @param _scope
+   * @param _props
    */
   public static createAPIOriginPolicy(
-    scope: Construct,
-    props: CreateAPIOriginPolicyOptions,
+    _scope: Construct,
+    _props: CreateAPIOriginPolicyOptions,
   ): cf.IOriginRequestPolicy {
-    const { assetNameRoot, assetNameSuffix, domainNameEdge } = props;
-
-    let apigwyOriginRequestPolicy: cf.IOriginRequestPolicy = cf.OriginRequestPolicy.ALL_VIEWER;
-    if (domainNameEdge === undefined) {
-      // When not using a custom domain name we must limit down the origin policy to
-      // prevent it from passing the Host header (distribution_id.cloudfront.net) to
-      // apigwy which will then reject it with a 403 because it does not match the
-      // execute-api name that apigwy is expecting.
-      //
-      // 2021-12-28 - There is a bug in the name generation that causes the same asset
-      // in different stacks to have the same generated name.  We have to make the id
-      // in all cases to ensure the generated name is unique.
-      apigwyOriginRequestPolicy = new cf.OriginRequestPolicy(
-        scope,
-        `apigwy-origin-policy-${Stack.of(scope).stackName}`,
-        {
-          comment: assetNameRoot ? `${assetNameRoot}-apigwy${assetNameSuffix}` : undefined,
-
-          originRequestPolicyName: assetNameRoot
-            ? `${assetNameRoot}-apigwy${assetNameSuffix}`
-            : undefined,
-          cookieBehavior: cf.OriginRequestCookieBehavior.all(),
-          queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(),
-          // TODO: If signing is enabled this should forward all signature headers
-          // TODO: If set to "cfront.OriginRequestHeaderBehavior.all()" then
-          // `replaceHostHeader` must be set to true to prevent API Gateway from rejecting
-          // the request
-          // headerBehavior: cf.OriginRequestHeaderBehavior.allowList('user-agent', 'referer'),
-          headerBehavior: cf.OriginRequestHeaderBehavior.all(),
-        },
-      );
-    }
-
-    return apigwyOriginRequestPolicy;
+    // const { assetNameRoot, assetNameSuffix, domainNameEdge } = props;
+
+    // let apigwyOriginRequestPolicy: cf.IOriginRequestPolicy = cf.OriginRequestPolicy.ALL_VIEWER;
+    // if (domainNameEdge === undefined) {
+    //   // When not using a custom domain name we must limit down the origin policy to
+    //   // prevent it from passing the Host header (distribution_id.cloudfront.net) to
+    //   // apigwy which will then reject it with a 403 because it does not match the
+    //   // execute-api name that apigwy is expecting.
+    //   //
+    //   // 2021-12-28 - There is a bug in the name generation that causes the same asset
+    //   // in different stacks to have the same generated name.  We have to make the id
+    //   // in all cases to ensure the generated name is unique.
+    //   apigwyOriginRequestPolicy = new cf.OriginRequestPolicy(
+    //     scope,
+    //     `apigwy-origin-policy-${Stack.of(scope).stackName}`,
+    //     {
+    //       comment: assetNameRoot ? `${assetNameRoot}-apigwy${assetNameSuffix}` : undefined,
+
+    //       originRequestPolicyName: assetNameRoot
+    //         ? `${assetNameRoot}-apigwy${assetNameSuffix}`
+    //         : undefined,
+    //       cookieBehavior: cf.OriginRequestCookieBehavior.all(),
+    //       queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(),
+    //       // TODO: If signing is enabled this should forward all signature headers
+    //       // TODO: If set to "cfront.OriginRequestHeaderBehavior.all()" then
+    //       // `replaceHostHeader` must be set to true to prevent API Gateway from rejecting
+    //       // the request
+    //       // headerBehavior: cf.OriginRequestHeaderBehavior.allowList('user-agent', 'referer'),
+    //       headerBehavior: cf.OriginRequestHeaderBehavior.all(),
+    //     },
+    //   );
+    // }
+
+    return cf.OriginRequestPolicy.ALL_VIEWER;
   }
 
   /**
@@ -330,6 +296,7 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       apigwyOriginRequestPolicy,
       rootPathPrefix = '',
       createAPIPathRoute = true,
+      createNextDataPathRoute = true,
     } = props;
 
     //
@@ -365,6 +332,19 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       );
     }
 
+    //
+    // 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(
+        posixPath.join(rootPathPrefix, '/*/*/_next/data/*'),
+        apiGwyOrigin,
+        apiGwyBehaviorOptions,
+      );
+    }
+
     //
     // All static assets are assumed to have a dot in them
     //
@@ -387,11 +367,6 @@ replaceHostHeader: ${props.replaceHostHeader}`;
     return this._cloudFrontDistro;
   }
 
-  private _edgeToOriginFunction?: lambda.Function | cf.experimental.EdgeFunction;
-  public get edgeToOriginFunction(): lambda.Function | cf.experimental.EdgeFunction | undefined {
-    return this._edgeToOriginFunction;
-  }
-
   constructor(scope: Construct, id: string, props: MicroAppsCFProps) {
     super(scope, id);
 
@@ -419,10 +394,8 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       bucketAppsOrigin,
       rootPathPrefix,
       createAPIPathRoute = true,
-      signingMode = 'sign',
-      addXForwardedHostHeader = true,
-      replaceHostHeader = true,
-      originRegion,
+      createNextDataPathRoute = true,
+      edgeToOriginLambdas,
     } = props;
 
     const apigwyOriginRequestPolicy = MicroAppsCF.createAPIOriginPolicy(this, {
@@ -441,125 +414,9 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       httpOriginFQDN = `${httpApi.apiId}.execute-api.${Aws.REGION}.amazonaws.com`;
     }
 
-    // Create the edge function config file from the construct options
-    const edgeToOriginConfigYaml = MicroAppsCF.generateEdgeToOriginConfig({
-      originRegion: originRegion || Aws.REGION,
-      addXForwardedHostHeader,
-      replaceHostHeader,
-      signingMode: signingMode === 'none' ? '' : signingMode,
-    });
-
     //
-    // Create the Edge to Origin Function
+    // Get the Edge to Origin Lambdas
     //
-    const edgeToOriginFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
-      functionName: assetNameRoot ? `${assetNameRoot}-edge-to-origin${assetNameSuffix}` : undefined,
-      memorySize: 1769,
-      logRetention: logs.RetentionDays.ONE_MONTH,
-      runtime: lambda.Runtime.NODEJS_14_X,
-      timeout: Duration.seconds(5),
-      initialPolicy: [
-        // This can't have a reference to the httpApi because it would mean
-        // the parent stack (this stack) has to be created before the us-east-1
-        // child stack for the Edge Lambda Function.
-        // That's why we use a tag-based policy to allow the Edge Function
-        // to invoke any API Gateway API that we apply a tag to
-        // We allow the edge function to sign for all regions since
-        // we may use custom closest region in the future.
-        new iam.PolicyStatement({
-          actions: ['execute-api:Invoke'],
-          resources: [`arn:aws:execute-api:*:${Aws.ACCOUNT_ID}:*/*/*/*`],
-          // Unfortunately, API Gateway access cannot be restricted using
-          // tags on the target resource
-          // https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html
-          // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html#networking_svcs
-          // conditions: {
-          //   // TODO: Set this to a string unique to each stack
-          //   StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
-          // },
-        }),
-      ],
-    };
-    if (
-      process.env.NODE_ENV === 'test' ||
-      existsSync(path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'index.js'))
-    ) {
-      // Emit the config file from the construct options
-      writeFileSync(
-        path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'config.yml'),
-        edgeToOriginConfigYaml,
-      );
-      // copyFileSync(
-      //   path.join(__dirname, '..', '..', '..', 'configs', 'microapps-edge-to-origin', 'config.yml'),
-      //   path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'config.yml'),
-      // );
-      // This is for tests run under jest
-      // This is also for anytime when the edge function has already been bundled
-      this._edgeToOriginFunction = new cf.experimental.EdgeFunction(this, 'edge-to-apigwy-func', {
-        code: lambda.Code.fromAsset(
-          path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist'),
-        ),
-        handler: 'index.handler',
-        ...edgeToOriginFuncProps,
-      });
-    } else if (existsSync(path.join(__dirname, 'microapps-edge-to-origin', 'index.js'))) {
-      // Emit the config file from the construct options
-      writeFileSync(
-        path.join(__dirname, 'microapps-edge-to-origin', 'config.yml'),
-        edgeToOriginConfigYaml,
-      );
-
-      // This is for built apps packaged with the CDK construct
-      this._edgeToOriginFunction = new cf.experimental.EdgeFunction(this, 'edge-to-apigwy-func', {
-        code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-edge-to-origin')),
-        handler: 'index.handler',
-        ...edgeToOriginFuncProps,
-      });
-    } else {
-      // Emit the config file from the construct options
-      writeFileSync(
-        path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'config.yml'),
-        edgeToOriginConfigYaml,
-      );
-
-      // 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.
-      // This will fail to deploy in any region other than us-east-1
-      // We cannot use NodejsFunction because it will not create in us-east-1
-      this._edgeToOriginFunction = new lambdaNodejs.NodejsFunction(this, 'edge-to-apigwy-func', {
-        entry: path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'src', 'index.ts'),
-        handler: 'handler',
-        bundling: {
-          minify: true,
-          sourceMap: true,
-          commandHooks: {
-            beforeInstall: () => [],
-            beforeBundling: () => [],
-            afterBundling: (_inputDir: string, outputDir: string) => {
-              return [
-                `${os.platform() === 'win32' ? 'copy' : 'cp'} ${path.join(
-                  __dirname,
-                  '..',
-                  '..',
-                  '..',
-                  'configs',
-                  'microapps-edge-to-origin',
-                  'config.yml',
-                )} ${outputDir}`,
-              ];
-            },
-          },
-        },
-        ...edgeToOriginFuncProps,
-      });
-
-      if (removalPolicy) {
-        this._edgeToOriginFunction.applyRemovalPolicy(removalPolicy);
-      }
-    }
-
-    // const edgeToOriginFuncAlias = this._edgeToOriginFunction.addAlias('CloudfrontVersion');
 
     //
     // CloudFront Distro
@@ -568,13 +425,6 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       protocolPolicy: cf.OriginProtocolPolicy.HTTPS_ONLY,
       originSslProtocols: [cf.OriginSslPolicy.TLS_V1_2],
     });
-    const apiGwyOriginEdgeLambdas = [
-      {
-        eventType: cf.LambdaEdgeEventType.ORIGIN_REQUEST,
-        functionVersion: this._edgeToOriginFunction.currentVersion,
-        includeBody: true,
-      },
-    ];
     this._cloudFrontDistro = new cf.Distribution(this, 'cft', {
       comment: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : domainNameEdge,
       domainNames: domainNameEdge !== undefined ? [domainNameEdge] : undefined,
@@ -587,7 +437,7 @@ replaceHostHeader: ${props.replaceHostHeader}`;
         originRequestPolicy: apigwyOriginRequestPolicy,
         origin: apiGwyOrigin,
         viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
-        edgeLambdas: apiGwyOriginEdgeLambdas,
+        edgeLambdas: edgeToOriginLambdas,
       },
       enableIpv6: true,
       priceClass: cf.PriceClass.PRICE_CLASS_100,
@@ -608,7 +458,8 @@ replaceHostHeader: ${props.replaceHostHeader}`;
       apigwyOriginRequestPolicy: apigwyOriginRequestPolicy,
       rootPathPrefix,
       createAPIPathRoute,
-      apigwyEdgeFunctions: apiGwyOriginEdgeLambdas,
+      createNextDataPathRoute,
+      apigwyEdgeFunctions: edgeToOriginLambdas,
     });
 
     //
diff --git a/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
new file mode 100644
index 00000000..439daddb
--- /dev/null
+++ b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
@@ -0,0 +1,280 @@
+import { existsSync, writeFileSync } from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { Aws, Duration, RemovalPolicy } from 'aws-cdk-lib';
+import * as cf from 'aws-cdk-lib/aws-cloudfront';
+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';
+import * as logs from 'aws-cdk-lib/aws-logs';
+import { Construct } from 'constructs';
+
+/**
+ * Represents a MicroApps Edge to Origin Function
+ */
+export interface IMicroAppsEdgeToOrigin {
+  /**
+   * The edge to origin function for API Gateway Request Origin Edge Lambda
+   *
+   * The generated `config.yml` is included in the Lambda's code.
+   */
+  readonly edgeToOriginFunction: lambda.Function | cf.experimental.EdgeFunction;
+
+  /**
+   * Configuration of the edge to origin lambda functions
+   */
+  readonly edgeToOriginLambdas: cf.EdgeLambda[];
+}
+
+/**
+ * Properties to initialize an instance of `MicroAppsCF`.
+ */
+export interface MicroAppsEdgeToOriginProps {
+  /**
+   * RemovalPolicy override for child resources
+   *
+   * Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
+   *
+   * @default - per resource default
+   */
+  readonly removalPolicy?: RemovalPolicy;
+
+  /**
+   * Optional asset name root
+   *
+   * @example microapps
+   * @default - resource names auto assigned
+   */
+  readonly assetNameRoot?: string;
+
+  /**
+   * Optional asset name suffix
+   *
+   * @example -dev-pr-12
+   * @default none
+   */
+  readonly assetNameSuffix?: string;
+
+  /**
+   * Adds an X-Forwarded-Host-Header when calling API Gateway
+   *
+   * Can only be trusted if `signingMode` is enabled, which restricts
+   * access to API Gateway to only IAM signed requests.
+   *
+   * Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
+   * @default true
+   */
+  readonly addXForwardedHostHeader?: boolean;
+
+  /**
+   * Replaces Host header (which will be the Edge domain name) with the Origin domain name
+   * when enabled.  This is necessary when API Gateway has not been configured
+   * with a custom domain name that matches the exact domain name used by the CloudFront
+   * Distribution AND when the OriginRequestPolicy.HeadersBehavior is set
+   * to pass all headers to the origin.
+   *
+   * Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Origin
+   * @default true
+   */
+  readonly replaceHostHeader?: boolean;
+
+  /**
+   * Requires IAM auth on the API Gateway origin if not set to 'none'.
+   *
+   * 'sign' - Uses request headers for auth.
+   * 'presign' - Uses query string for auth.
+   *
+   * If enabled,
+   *
+   * Note: if 'sign' or 'presign', creates OriginRequest Lambda @ Edge function for API Gateway Origin
+   * @default 'sign'
+   */
+  readonly signingMode?: 'sign' | 'presign' | 'none';
+
+  /**
+   * Origin region that API Gateway will be deployed to, used
+   * for the config.yml on the Edge function to sign requests for
+   * the correct region
+   *
+   * @default undefined
+   */
+  readonly originRegion?: string;
+}
+
+export interface GenerateEdgeToOriginConfigOptions {
+  readonly originRegion: string;
+  readonly signingMode: 'sign' | 'presign' | '';
+  readonly addXForwardedHostHeader: boolean;
+  readonly replaceHostHeader: boolean;
+}
+
+/**
+ * Create a new MicroApps Edge to Origin Function w/ `config.yml`
+ */
+export class MicroAppsEdgeToOrigin extends Construct implements IMicroAppsEdgeToOrigin {
+  /**
+   * Generate the yaml config for the edge lambda
+   * @param props
+   * @returns
+   */
+  public static generateEdgeToOriginConfig(props: GenerateEdgeToOriginConfigOptions) {
+    return `originRegion: ${props.originRegion}
+${props.signingMode === '' ? '' : `signingMode: ${props.signingMode}`}
+addXForwardedHostHeader: ${props.addXForwardedHostHeader}
+replaceHostHeader: ${props.replaceHostHeader}`;
+  }
+
+  private _edgeToOriginFunction: lambda.Function | cf.experimental.EdgeFunction;
+  public get edgeToOriginFunction(): lambda.Function | cf.experimental.EdgeFunction {
+    return this._edgeToOriginFunction;
+  }
+
+  private _edgeToOriginLambdas: cf.EdgeLambda[];
+  public get edgeToOriginLambdas(): cf.EdgeLambda[] {
+    return this._edgeToOriginLambdas;
+  }
+
+  constructor(scope: Construct, id: string, props: MicroAppsEdgeToOriginProps) {
+    super(scope, id);
+
+    if (props === undefined) {
+      throw new Error('props must be set');
+    }
+
+    const {
+      removalPolicy,
+      assetNameRoot,
+      assetNameSuffix,
+      signingMode = 'sign',
+      addXForwardedHostHeader = true,
+      replaceHostHeader = true,
+      originRegion,
+    } = props;
+
+    // Create the edge function config file from the construct options
+    const edgeToOriginConfigYaml = MicroAppsEdgeToOrigin.generateEdgeToOriginConfig({
+      originRegion: originRegion || Aws.REGION,
+      addXForwardedHostHeader,
+      replaceHostHeader,
+      signingMode: signingMode === 'none' ? '' : signingMode,
+    });
+
+    //
+    // Create the Edge to Origin Function
+    //
+    const edgeToOriginFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
+      functionName: assetNameRoot ? `${assetNameRoot}-edge-to-origin${assetNameSuffix}` : undefined,
+      memorySize: 1769,
+      logRetention: logs.RetentionDays.ONE_MONTH,
+      runtime: lambda.Runtime.NODEJS_14_X,
+      timeout: Duration.seconds(5),
+      initialPolicy: [
+        // This can't have a reference to the httpApi because it would mean
+        // the parent stack (this stack) has to be created before the us-east-1
+        // child stack for the Edge Lambda Function.
+        // That's why we use a tag-based policy to allow the Edge Function
+        // to invoke any API Gateway API that we apply a tag to
+        // We allow the edge function to sign for all regions since
+        // we may use custom closest region in the future.
+        new iam.PolicyStatement({
+          actions: ['execute-api:Invoke'],
+          resources: [`arn:aws:execute-api:*:${Aws.ACCOUNT_ID}:*/*/*/*`],
+          // Unfortunately, API Gateway access cannot be restricted using
+          // tags on the target resource
+          // https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html
+          // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html#networking_svcs
+          // conditions: {
+          //   // TODO: Set this to a string unique to each stack
+          //   StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
+          // },
+        }),
+      ],
+    };
+    if (
+      process.env.NODE_ENV === 'test' ||
+      existsSync(path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'index.js'))
+    ) {
+      // Emit the config file from the construct options
+      writeFileSync(
+        path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'config.yml'),
+        edgeToOriginConfigYaml,
+      );
+      // copyFileSync(
+      //   path.join(__dirname, '..', '..', '..', 'configs', 'microapps-edge-to-origin', 'config.yml'),
+      //   path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist', 'config.yml'),
+      // );
+      // This is for tests run under jest
+      // This is also for anytime when the edge function has already been bundled
+      this._edgeToOriginFunction = new cf.experimental.EdgeFunction(this, 'edge-to-apigwy-func', {
+        code: lambda.Code.fromAsset(
+          path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist'),
+        ),
+        handler: 'index.handler',
+        ...edgeToOriginFuncProps,
+      });
+    } else if (existsSync(path.join(__dirname, 'microapps-edge-to-origin', 'index.js'))) {
+      // Emit the config file from the construct options
+      writeFileSync(
+        path.join(__dirname, 'microapps-edge-to-origin', 'config.yml'),
+        edgeToOriginConfigYaml,
+      );
+
+      // This is for built apps packaged with the CDK construct
+      this._edgeToOriginFunction = new cf.experimental.EdgeFunction(this, 'edge-to-apigwy-func', {
+        code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-edge-to-origin')),
+        handler: 'index.handler',
+        ...edgeToOriginFuncProps,
+      });
+    } else {
+      // Emit the config file from the construct options
+      writeFileSync(
+        path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'config.yml'),
+        edgeToOriginConfigYaml,
+      );
+
+      // 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.
+      // This will fail to deploy in any region other than us-east-1
+      // We cannot use NodejsFunction because it will not create in us-east-1
+      this._edgeToOriginFunction = new lambdaNodejs.NodejsFunction(this, 'edge-to-apigwy-func', {
+        entry: path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'src', 'index.ts'),
+        handler: 'handler',
+        bundling: {
+          minify: true,
+          sourceMap: true,
+          commandHooks: {
+            beforeInstall: () => [],
+            beforeBundling: () => [],
+            afterBundling: (_inputDir: string, outputDir: string) => {
+              return [
+                `${os.platform() === 'win32' ? 'copy' : 'cp'} ${path.join(
+                  __dirname,
+                  '..',
+                  '..',
+                  '..',
+                  'configs',
+                  'microapps-edge-to-origin',
+                  'config.yml',
+                )} ${outputDir}`,
+              ];
+            },
+          },
+        },
+        ...edgeToOriginFuncProps,
+      });
+
+      if (removalPolicy) {
+        this._edgeToOriginFunction.applyRemovalPolicy(removalPolicy);
+      }
+    }
+
+    this._edgeToOriginLambdas = [
+      {
+        eventType: cf.LambdaEdgeEventType.ORIGIN_REQUEST,
+        functionVersion: this._edgeToOriginFunction.currentVersion,
+        includeBody: true,
+      },
+    ];
+  }
+}
diff --git a/packages/microapps-cdk/src/MicroAppsS3.ts b/packages/microapps-cdk/src/MicroAppsS3.ts
index df26f6d1..3982f16b 100644
--- a/packages/microapps-cdk/src/MicroAppsS3.ts
+++ b/packages/microapps-cdk/src/MicroAppsS3.ts
@@ -86,7 +86,10 @@ export interface MicroAppsS3Props {
 }
 
 /**
- * Create a new MicroApps S3 Bucket.
+ * Create the durable MicroApps S3 Buckets
+ *
+ * These should be created in a stack that will not be deleted if
+ * there are breaking changes to MicroApps in the future.
  */
 export class MicroAppsS3 extends Construct implements IMicroAppsS3 {
   private _bucketApps: s3.IBucket;
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index df513832..b4918f17 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -153,6 +153,22 @@ export interface MicroAppsSvcsProps {
    * @default true
    */
   readonly requireIAMAuthorization?: boolean;
+
+  /**
+   * Existing table for apps/versions/rules
+   *
+   * @warning - It is *strongly* suggested that production stacks create
+   * their own DynamoDB Table and pass it into this construct, for protection
+   * against data loss due to logical ID changes, the ability to configure
+   * Provisioned capacity with Auto Scaling, the ability to add additional indices, etc.
+   *
+   * Requirements:
+   * - Hash Key: `PK`
+   * - Sort Key: `SK`
+   *
+   * @default created by construct
+   */
+  readonly table?: dynamodb.ITable;
 }
 
 /**
@@ -180,7 +196,8 @@ export interface IMicroAppsSvcs {
  * and Router Lambda Functions, and the DynamoDB Table used by both.
  */
 export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
-  private _table: dynamodb.Table;
+  private _ownedTable?: dynamodb.Table;
+  private _table: dynamodb.ITable;
   public get table(): dynamodb.ITable {
     return this._table;
   }
@@ -229,19 +246,25 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
     //
     // DynamoDB Table
     //
-    this._table = new dynamodb.Table(this, 'table', {
-      tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
-      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
-      partitionKey: {
-        name: 'PK',
-        type: dynamodb.AttributeType.STRING,
-      },
-      sortKey: {
-        name: 'SK',
-        type: dynamodb.AttributeType.STRING,
-      },
-      removalPolicy,
-    });
+    if (props.table === undefined) {
+      // Create able if none passed
+      this._ownedTable = new dynamodb.Table(this, 'table', {
+        tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
+        billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
+        partitionKey: {
+          name: 'PK',
+          type: dynamodb.AttributeType.STRING,
+        },
+        sortKey: {
+          name: 'SK',
+          type: dynamodb.AttributeType.STRING,
+        },
+        removalPolicy,
+      });
+      this._table = this._ownedTable;
+    } else {
+      this._table = props.table;
+    }
 
     //
     // Router Lambda Function
diff --git a/packages/microapps-cdk/src/MicroAppsTable.ts b/packages/microapps-cdk/src/MicroAppsTable.ts
new file mode 100644
index 00000000..f2611152
--- /dev/null
+++ b/packages/microapps-cdk/src/MicroAppsTable.ts
@@ -0,0 +1,94 @@
+import { RemovalPolicy } from 'aws-cdk-lib';
+import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
+import { Construct } from 'constructs';
+
+/**
+ * Properties to initialize an instance of `MicroAppsTable`.
+ */
+export interface MicroAppsTableProps {
+  /**
+   * RemovalPolicy override for child resources
+   *
+   * Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
+   *
+   * @default - per resource default
+   */
+  readonly removalPolicy?: RemovalPolicy;
+
+  /**
+   * Application environment, passed as `NODE_ENV`
+   * to the Router and Deployer Lambda functions
+   */
+  readonly appEnv: string;
+
+  /**
+   * Optional asset name root
+   *
+   * @example microapps
+   * @default - resource names auto assigned
+   */
+  readonly assetNameRoot?: string;
+
+  /**
+   * Optional asset name suffix
+   *
+   * @example -dev-pr-12
+   * @default none
+   */
+  readonly assetNameSuffix?: string;
+
+  /**
+   * Path prefix on the root of the deployment
+   *
+   * @example dev/
+   * @default none
+   */
+  readonly rootPathPrefix?: string;
+}
+
+/**
+ * Represents a MicroAppsTable
+ */
+export interface IMicroAppsTable {
+  /**
+   * DynamoDB table used by Router, Deployer, and Release console app
+   */
+  readonly table: dynamodb.Table;
+}
+
+/**
+ * Create a new MicroApps Table for apps / versions / rules
+ */
+export class MicroAppsTable extends Construct implements IMicroAppsTable {
+  private _table: dynamodb.Table;
+  public get table(): dynamodb.Table {
+    return this._table;
+  }
+
+  constructor(scope: Construct, id: string, props?: MicroAppsTableProps) {
+    super(scope, id);
+
+    if (props === undefined) {
+      throw new Error('props cannot be undefined');
+    }
+
+    const { removalPolicy, assetNameRoot, assetNameSuffix } = props;
+
+    //
+    // DynamoDB Table
+    //
+    this._table = new dynamodb.Table(this, 'table', {
+      tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
+      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
+      partitionKey: {
+        name: 'PK',
+        type: dynamodb.AttributeType.STRING,
+      },
+      sortKey: {
+        name: 'SK',
+        type: dynamodb.AttributeType.STRING,
+      },
+      removalPolicy,
+    });
+  }
+}
diff --git a/packages/microapps-cdk/src/index.ts b/packages/microapps-cdk/src/index.ts
index 305cb7e7..2d501777 100644
--- a/packages/microapps-cdk/src/index.ts
+++ b/packages/microapps-cdk/src/index.ts
@@ -1,5 +1,7 @@
 export * from './MicroApps';
 export * from './MicroAppsAPIGwy';
 export * from './MicroAppsCF';
+export * from './MicroAppsEdgeToOrigin';
 export * from './MicroAppsS3';
 export * from './MicroAppsSvcs';
+export * from './MicroAppsTable';


Test patch:
diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts
index c3aca73e..c37bbaf5 100644
--- a/packages/microapps-cdk/test/MicroApps.test.ts
+++ b/packages/microapps-cdk/test/MicroApps.test.ts
@@ -10,7 +10,7 @@ describe('MicroApps', () => {
     const app = new App({});
     const stack = new Stack(app, 'stack', {
       env: {
-        region: 'us-east-1',
+        region: 'us-east-2',
       },
     });
     const construct = new MicroApps(stack, 'construct', {
@@ -19,15 +19,184 @@ describe('MicroApps', () => {
 
     expect(construct).toBeDefined();
     expect(construct.apigwy).toBeDefined();
+    expect(construct.edgeToOrigin).toBeDefined();
     expect(construct.cf).toBeDefined();
     expect(construct.s3).toBeDefined();
     expect(construct.svcs).toBeDefined();
 
     expect(construct.node).toBeDefined();
-    // expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
+    Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+
+    // Confirm that logical IDs have not changed accidentally (causes delete/create)
+    Template.fromStack(stack).templateMatches({
+      Resources: {
+        constructapigwy894904EC: {
+          Type: 'AWS::ApiGatewayV2::Api',
+        },
+        constructcft0A8410EA: {
+          Type: 'AWS::CloudFront::Distribution',
+        },
+        constructsvcstable0311CF05: {
+          Type: 'AWS::DynamoDB::Table',
+        },
+        // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
+        constructsvcsrouterfunc73102284: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructs3apps91016270: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3logs6D37B88F: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3staging7AA4BA4F: {
+          Type: 'AWS::S3::Bucket',
+        },
+      },
+    });
   });
 
   it('works with only edge params', () => {
+    const app = new App({});
+    const stack = new Stack(app, 'stack', {
+      env: {
+        region: 'us-east-2',
+      },
+    });
+    const certEdge = new acm.Certificate(stack, 'cert', {
+      domainName: '*.test.pwrdrvr.com',
+    });
+    const r53Zone = new r53.HostedZone(stack, 'zone', {
+      zoneName: 'test.pwrdrvr.com',
+    });
+    const construct = new MicroApps(stack, 'construct', {
+      appEnv: 'dev',
+      domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      certEdge,
+      certOrigin: certEdge,
+      r53Zone,
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.apigwy).toBeDefined();
+    expect(construct.edgeToOrigin).toBeDefined();
+    expect(construct.cf).toBeDefined();
+    expect(construct.s3).toBeDefined();
+    expect(construct.svcs).toBeDefined();
+
+    expect(construct.node).toBeDefined();
+    Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+
+    // Confirm that logical IDs have not changed accidentally (causes delete/create)
+    Template.fromStack(stack).templateMatches({
+      Resources: {
+        constructapigwy894904EC: {
+          Type: 'AWS::ApiGatewayV2::Api',
+        },
+        constructcft0A8410EA: {
+          Type: 'AWS::CloudFront::Distribution',
+        },
+        constructsvcstable0311CF05: {
+          Type: 'AWS::DynamoDB::Table',
+        },
+        // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
+        constructsvcsrouterfunc73102284: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructs3apps91016270: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3logs6D37B88F: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3staging7AA4BA4F: {
+          Type: 'AWS::S3::Bucket',
+        },
+      },
+    });
+  });
+
+  it('works with asset names', () => {
+    const app = new App({});
+    const stack = new Stack(app, 'stack', {
+      env: {
+        region: 'us-east-2',
+      },
+    });
+    const certEdge = new acm.Certificate(stack, 'cert', {
+      domainName: '*.test.pwrdrvr.com',
+    });
+    const r53Zone = new r53.HostedZone(stack, 'zone', {
+      zoneName: 'test.pwrdrvr.com',
+    });
+    const construct = new MicroApps(stack, 'construct', {
+      appEnv: 'dev',
+      domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      certEdge,
+      certOrigin: certEdge,
+      r53Zone,
+      assetNameRoot: 'my-asset-root-name',
+      assetNameSuffix: '-some-suffix',
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.apigwy).toBeDefined();
+    expect(construct.edgeToOrigin).toBeDefined();
+    expect(construct.cf).toBeDefined();
+    expect(construct.s3).toBeDefined();
+    expect(construct.svcs).toBeDefined();
+
+    expect(construct.node).toBeDefined();
+    Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+      FunctionName: 'my-asset-root-name-router-some-suffix',
+    });
+    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+      FunctionName: 'my-asset-root-name-deployer-some-suffix',
+    });
+
+    // Confirm that logical IDs have not changed accidentally (causes delete/create)
+    Template.fromStack(stack).templateMatches({
+      Resources: {
+        constructapigwy894904EC: {
+          Type: 'AWS::ApiGatewayV2::Api',
+        },
+        constructcft0A8410EA: {
+          Type: 'AWS::CloudFront::Distribution',
+        },
+        constructsvcstable0311CF05: {
+          Type: 'AWS::DynamoDB::Table',
+        },
+        // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
+        constructsvcsrouterfunc73102284: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructs3apps91016270: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3logs6D37B88F: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3staging7AA4BA4F: {
+          Type: 'AWS::S3::Bucket',
+        },
+      },
+    });
+  });
+
+  it('works with asset names and edge to origin functions', () => {
     const app = new App({});
     const stack = new Stack(app, 'stack', {
       env: {
@@ -46,15 +215,139 @@ describe('MicroApps', () => {
       certEdge,
       certOrigin: certEdge,
       r53Zone,
+      assetNameRoot: 'my-asset-root-name',
+      assetNameSuffix: '-some-suffix',
+      signingMode: 'sign',
     });
 
     expect(construct).toBeDefined();
     expect(construct.apigwy).toBeDefined();
+    expect(construct.edgeToOrigin).toBeDefined();
     expect(construct.cf).toBeDefined();
     expect(construct.s3).toBeDefined();
     expect(construct.svcs).toBeDefined();
 
     expect(construct.node).toBeDefined();
-    // expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
+    Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+      FunctionName: 'my-asset-root-name-router-some-suffix',
+    });
+    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+      FunctionName: 'my-asset-root-name-deployer-some-suffix',
+    });
+
+    //
+    // Capture logical IDs by uncommenting this, running under a debugger,
+    // then dumping the IDs in the DebugConsole (VS Code does not allow copy-paste of the IDs
+    // in the tooltip object explorer).
+    // const functions = Template.fromStack(stack).findResources('AWS::Lambda::Function', {});
+    // const tables = Template.fromStack(stack).findResources('AWS::DynamoDB::Table', {});
+    // const distros = Template.fromStack(stack).findResources('AWS::CloudFront::Distribution', {});
+    // const apis = Template.fromStack(stack).findResources('AWS::ApiGatewayV2::Api', {});
+    // const s3Buckets = Template.fromStack(stack).findResources('AWS::S3::Bucket', {});
+    //
+
+    // Confirm that logical IDs have not changed accidentally (causes delete/create)
+    Template.fromStack(stack).templateMatches({
+      Resources: {
+        constructapigwy894904EC: {
+          Type: 'AWS::ApiGatewayV2::Api',
+        },
+        constructcft0A8410EA: {
+          Type: 'AWS::CloudFront::Distribution',
+        },
+        constructsvcstable0311CF05: {
+          Type: 'AWS::DynamoDB::Table',
+        },
+        constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructsvcsdeployerfunc88CC1526: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructsvcsrouterfunc73102284: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructs3apps91016270: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3logs6D37B88F: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3staging7AA4BA4F: {
+          Type: 'AWS::S3::Bucket',
+        },
+      },
+    });
+
+    // This has to be built in us-east-1 or the asset is in a different child stack
+    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+      FunctionName: 'my-asset-root-name-edge-to-origin-some-suffix',
+    });
+  });
+
+  it('allows skipping edge to origin functions', () => {
+    const app = new App({});
+    const stack = new Stack(app, 'stack', {
+      env: {
+        region: 'us-east-2',
+      },
+    });
+    const certEdge = new acm.Certificate(stack, 'cert', {
+      domainName: '*.test.pwrdrvr.com',
+    });
+    const r53Zone = new r53.HostedZone(stack, 'zone', {
+      zoneName: 'test.pwrdrvr.com',
+    });
+    const construct = new MicroApps(stack, 'construct', {
+      appEnv: 'dev',
+      domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      certEdge,
+      certOrigin: certEdge,
+      r53Zone,
+      addXForwardedHostHeader: false,
+      replaceHostHeader: false,
+      signingMode: 'none',
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.apigwy).toBeDefined();
+    expect(construct.cf).toBeDefined();
+    expect(construct.s3).toBeDefined();
+    expect(construct.svcs).toBeDefined();
+
+    expect(construct.node).toBeDefined();
+    Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
+
+    // Confirm that logical IDs have not changed accidentally (causes delete/create)
+    Template.fromStack(stack).templateMatches({
+      Resources: {
+        constructapigwy894904EC: {
+          Type: 'AWS::ApiGatewayV2::Api',
+        },
+        constructcft0A8410EA: {
+          Type: 'AWS::CloudFront::Distribution',
+        },
+        constructsvcstable0311CF05: {
+          Type: 'AWS::DynamoDB::Table',
+        },
+        constructsvcsrouterfunc73102284: {
+          Type: 'AWS::Lambda::Function',
+        },
+        constructs3apps91016270: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3logs6D37B88F: {
+          Type: 'AWS::S3::Bucket',
+        },
+        constructs3staging7AA4BA4F: {
+          Type: 'AWS::S3::Bucket',
+        },
+      },
+    });
   });
 });
diff --git a/packages/microapps-cdk/test/MicroAppsCF.test.ts b/packages/microapps-cdk/test/MicroAppsCF.test.ts
index b84df801..36ef7021 100644
--- a/packages/microapps-cdk/test/MicroAppsCF.test.ts
+++ b/packages/microapps-cdk/test/MicroAppsCF.test.ts
@@ -11,42 +11,6 @@ import * as s3 from 'aws-cdk-lib/aws-s3';
 import { MicroAppsCF } from '../src/MicroAppsCF';
 
 describe('MicroAppsCF', () => {
-  // beforeAll(() => {
-  //   process.env.AWS_REGION = 'us-east-2';
-  // });
-
-  describe('generateEdgeToOriginConfig', () => {
-    it('skips signing param', () => {
-      const config = MicroAppsCF.generateEdgeToOriginConfig({
-        signingMode: '',
-        addXForwardedHostHeader: true,
-        replaceHostHeader: true,
-        originRegion: 'us-west-1',
-      });
-
-      expect(config).toBeDefined();
-      expect(config).not.toContain('signingMode:');
-      expect(config).toContain('addXForwardedHostHeader: true');
-      expect(config).toContain('replaceHostHeader: true');
-      expect(config).toContain('originRegion: us-west-1');
-    });
-
-    it('skips signing param', () => {
-      const config = MicroAppsCF.generateEdgeToOriginConfig({
-        signingMode: 'sign',
-        addXForwardedHostHeader: false,
-        replaceHostHeader: false,
-        originRegion: 'us-west-1',
-      });
-
-      expect(config).toBeDefined();
-      expect(config).toContain('signingMode: sign');
-      expect(config).toContain('addXForwardedHostHeader: false');
-      expect(config).toContain('replaceHostHeader: false');
-      expect(config).toContain('originRegion: us-west-1');
-    });
-  });
-
   it('works with no params', () => {
     const app = new App({});
     const stack = new Stack(app, 'stack', {
@@ -80,7 +44,7 @@ describe('MicroAppsCF', () => {
     // expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
 
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginRequestPolicy', 1);
+    // Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginRequestPolicy', 1);
   });
 
   it('works with params', () => {
diff --git a/packages/microapps-cdk/test/MicroAppsEdgeToOrigin.test.ts b/packages/microapps-cdk/test/MicroAppsEdgeToOrigin.test.ts
new file mode 100644
index 00000000..71c4f419
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroAppsEdgeToOrigin.test.ts
@@ -0,0 +1,81 @@
+/// <reference types="jest" />
+import { App, Stack } from 'aws-cdk-lib';
+import { Template } from 'aws-cdk-lib/assertions';
+import { MicroAppsEdgeToOrigin } from '../src/MicroAppsEdgeToOrigin';
+
+describe('MicroAppsEdgeToOrigin', () => {
+  describe('generateEdgeToOriginConfig', () => {
+    it('skips signing param', () => {
+      const config = MicroAppsEdgeToOrigin.generateEdgeToOriginConfig({
+        signingMode: '',
+        addXForwardedHostHeader: true,
+        replaceHostHeader: true,
+        originRegion: 'us-west-1',
+      });
+
+      expect(config).toBeDefined();
+      expect(config).not.toContain('signingMode:');
+      expect(config).toContain('addXForwardedHostHeader: true');
+      expect(config).toContain('replaceHostHeader: true');
+      expect(config).toContain('originRegion: us-west-1');
+    });
+
+    it('skips signing param', () => {
+      const config = MicroAppsEdgeToOrigin.generateEdgeToOriginConfig({
+        signingMode: 'sign',
+        addXForwardedHostHeader: false,
+        replaceHostHeader: false,
+        originRegion: 'us-west-1',
+      });
+
+      expect(config).toBeDefined();
+      expect(config).toContain('signingMode: sign');
+      expect(config).toContain('addXForwardedHostHeader: false');
+      expect(config).toContain('replaceHostHeader: false');
+      expect(config).toContain('originRegion: us-west-1');
+    });
+  });
+
+  describe('MicroAppsEdgeToOrigin', () => {
+    it('works with no params', () => {
+      const app = new App({});
+      const stack = new Stack(app, 'stack', {
+        env: {
+          region: 'us-east-1',
+        },
+      });
+      const construct = new MicroAppsEdgeToOrigin(stack, 'construct', {});
+
+      expect(construct).toBeDefined();
+      expect(construct.edgeToOriginFunction).toBeDefined();
+      expect(construct.edgeToOriginLambdas).toBeDefined();
+      expect(construct.node).toBeDefined();
+      // I guess this is 2 for EdgeFunction even though only 1 is created?
+      Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2);
+    });
+
+    it('works with params', () => {
+      const app = new App({});
+      const stack = new Stack(app, 'stack', {
+        env: {
+          region: 'us-east-1',
+        },
+      });
+      const construct = new MicroAppsEdgeToOrigin(stack, 'construct', {
+        assetNameRoot: 'my-asset-name-root',
+        assetNameSuffix: '-some-suffix',
+      });
+
+      expect(construct).toBeDefined();
+      expect(construct.edgeToOriginFunction).toBeDefined();
+      expect(construct.edgeToOriginLambdas).toBeDefined();
+      expect(construct.node).toBeDefined();
+      // I guess this is 2 for EdgeFunction even though only 1 is created?
+      // (probably log retention lambda)
+      Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2);
+      Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+        FunctionName: 'my-asset-name-root-edge-to-origin-some-suffix',
+      });
+    });
+  });
+});