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

Issue:
Demo - Replace autoDeleteEverything with RemovalPolicy
- In published MicroappsCDK Construct
- Replace the `autoDeleteEverything` property with `RemovalPolicy`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6f3289a0..ce3855e8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -120,22 +120,118 @@ jobs:
         run: |
           npx microapps-publish publish -a ${DEMO_APP_NAME} -n ${PACKAGE_VERSION} -d ${DEPLOYER_LAMBDA_NAME} -l ${DEMO_APP_LAMBDA_NAME} -s packages/demo-app/static_files -i index.html --overwrite --noCache
 
-      - name: Test Deployed App
+      - name: Test Demo App
         run: |
           echo Testing App Frame Loading
-          curl https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/
           echo Testing App HTML Loading
-          curl https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
           echo Testing App Method Invocation
-          curl https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/${PACKAGE_VERSION}/someMethod
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${DEMO_APP_NAME}/${PACKAGE_VERSION}/someMethod
 
       - name: Publish Release App to MicroApps
         run: |
           npx microapps-publish publish -a ${RELEASE_APP_NAME} -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
 
+      - name: Test Release App
+        run: |
+          echo Testing App Frame Loading
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${RELEASE_APP_NAME}/
+          echo Testing App HTML Loading
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/
+          echo Testing App Method Invocation
+          curl --fail https://${APP_HOST_NAME}.ghpublic.pwrdrvr.com/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/api/refresh/${DEMO_APP_NAME}
 
-  delete-apps:
+
+  deploy-basic:
     if: github.event_name == 'pull_request'
+    concurrency:
+      group: deploy-basic-${{ github.workflow }}-${{ github.event.pull_request.number }}
+      cancel-in-progress: false
+    needs:
+      - build
+    environment:
+      name: ghpublic
+      url: https://${{ steps.getCDKExports.outputs.edgeDomain }}/release/
+    permissions:
+      contents: read
+      id-token: write
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Extract Current NPM Version
+        run: |
+          echo 'PACKAGE_VERSION_ORIG='$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
+
+      - name: Compute NPM Version and App Lambda Name
+        run: |
+          echo 'PACKAGE_VERSION='${PACKAGE_VERSION_ORIG}${{ needs.build.outputs.prSuffixPackageVersion }} >> $GITHUB_ENV
+
+      - name: Use Node.js 16
+        uses: actions/setup-node@v2
+        with:
+          node-version: 16
+          cache: 'npm'
+          cache-dependency-path: '**/package-lock.json'
+
+      - name: Install Node Modules
+        run: npm ci
+
+      - name: Extract Release App NPM Version
+        run: |
+          echo 'RELEASE_APP_PACKAGE_VERSION='$(node -p -e "require('./node_modules/@pwrdrvr/microapps-app-release-cdk/package.json').version") >> $GITHUB_ENV
+
+      - name: Build All TypeScript
+        run: npm run build --if-present
+    
+      - name: Configure AWS Credentials
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          role-session-name: microapps-ghpublic-build
+          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/builder-writeRole
+          aws-region: ${{ env.AWS_REGION }}
+
+      - name: Deploy CDK Stack
+        run: npx cdk deploy --context @pwrdrvr/microapps:stackName=microapps-basic-ghpublic --context @pwrdrvr/microapps:deployDemoApp=true --context @pwrdrvr/microapps:deployReleaseApp=true --hotswap --require-approval never microapps-basic
+
+      - name: Get CDK Exports
+        id: getCDKExports
+        run: |
+          echo "::set-output name=edgeDomain::"$(aws cloudformation list-exports --query "Exports[?Name==\`microapps-basic-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-edge-domain-name\`].Value" --no-paginate --output text)
+          echo "EDGE_DOMAIN="$(aws cloudformation list-exports --query "Exports[?Name==\`microapps-basic-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-edge-domain-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "DEPLOYER_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`microapps-basic-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`microapps-basic-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "RELEASE_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`microapps-basic-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+
+      - name: Publish Demo App to MicroApps
+        run: |
+          npx microapps-publish publish -a ${DEMO_APP_NAME} -n ${PACKAGE_VERSION} -d ${DEPLOYER_LAMBDA_NAME} -l ${DEMO_APP_LAMBDA_NAME} -s packages/demo-app/static_files -i index.html --overwrite --noCache
+
+      - name: Test Demo App
+        run: |
+          echo Testing App Frame Loading
+          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/
+          echo Testing App HTML Loading
+          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/index.html
+          echo Testing App Method Invocation
+          curl --fail https://${EDGE_DOMAIN}/${DEMO_APP_NAME}/${PACKAGE_VERSION}/someMethod
+
+      - name: Publish Release App to MicroApps
+        run: |
+          npx microapps-publish publish -a ${RELEASE_APP_NAME} -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
+
+      - name: Test Release App
+        run: |
+          echo Testing App Frame Loading
+          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/
+          echo Testing App HTML Loading
+          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/
+          echo Testing App Method Invocation
+          curl --fail https://${EDGE_DOMAIN}/${RELEASE_APP_NAME}/${PACKAGE_VERSION}/api/refresh/${DEMO_APP_NAME}
+
+  delete-apps:
+    if: github.event_name == 'pull_request' && false
     concurrency:
       group: delete-apps-${{ github.workflow }}-${{ github.event.pull_request.number }}
       cancel-in-progress: false
@@ -209,13 +305,11 @@ jobs:
       - name: Install Node Modules
         run: npm ci
 
-      # Projen keeps creating `packages/microapps-cdk/test/hello.test.ts`
       - name: Generate Projen Files
         working-directory: packages/microapps-cdk/
         run: |
           npx projen --version
           npm run projen
-          rm test/hello.test.ts
 
       - name: Modify microapps-cdk tsconfig.json
         run: |
diff --git a/packages/cdk/bin/cdk.ts b/packages/cdk/bin/cdk.ts
index 53f360e6..6dd83ced 100644
--- a/packages/cdk/bin/cdk.ts
+++ b/packages/cdk/bin/cdk.ts
@@ -25,7 +25,26 @@ new MicroAppsStack(app, 'microapps-core', {
   autoDeleteEverything: true,
   domainNameEdge: `apps${shared.envDomainSuffix}${shared.prSuffix}.${shared.domainName}`,
   domainNameOrigin: `apps-origin${shared.envDomainSuffix}${shared.prSuffix}.${shared.domainName}`,
-  shared,
+  r53ZoneID: shared.r53ZoneID,
+  r53ZoneName: shared.r53ZoneName,
+  certARNEdge: shared.certARNEdge,
+  certARNOrigin: shared.certARNOrigin,
+  s3PolicyBypassAROAs: shared.s3PolicyBypassAROAs,
+  s3PolicyBypassPrincipalARNs: shared.s3PolicyBypassPrincipalARNs,
+  s3StrictBucketPolicy: shared.s3StrictBucketPolicy,
+  assetNameRoot: `${shared.stackName}`,
+  assetNameSuffix: `${shared.envSuffix}${shared.prSuffix}`,
+  deployDemoApp: shared.deployDemoApp,
+  deployReleaseApp: shared.deployReleaseApp,
+});
+
+new MicroAppsStack(app, 'microapps-basic', {
+  env,
+  stackName: `${shared.stackName}${shared.envSuffix}${shared.prSuffix}`,
+  ttl: shared.isPR ? shared.ttlBase : undefined,
+  autoDeleteEverything: true,
+  deployDemoApp: shared.deployDemoApp,
+  deployReleaseApp: shared.deployReleaseApp,
 });
 
 // Note: This is only run manually once per env to create build user
diff --git a/packages/cdk/lib/DemoApp.ts b/packages/cdk/lib/DemoApp.ts
index 95189aa4..620a7dd2 100644
--- a/packages/cdk/lib/DemoApp.ts
+++ b/packages/cdk/lib/DemoApp.ts
@@ -2,33 +2,62 @@ import * as cdk from '@aws-cdk/core';
 import * as lambda from '@aws-cdk/aws-lambda';
 import * as lambdaNodejs from '@aws-cdk/aws-lambda-nodejs';
 import * as logs from '@aws-cdk/aws-logs';
-import { SharedProps } from './SharedProps';
 
-export interface IDemoApp {
-  shared: SharedProps;
+export interface DemoAppProps {
+  /**
+   * Removal policy
+   *
+   * @default - per resource defaults
+   */
+  readonly removalPolicy?: cdk.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;
 
   /**
    * Name of the application in microapps
    *
    * @default 'demo-app'
    */
-  appName?: string;
+  readonly appName?: string;
 }
 
-export class DemoApp extends cdk.Construct {
-  constructor(scope: cdk.Construct, id: string, props: IDemoApp) {
+export interface IDemoApp {
+  lambdaFunction: lambda.IFunction;
+}
+
+export class DemoApp extends cdk.Construct implements IDemoApp {
+  private _lambdaFunction: lambda.Function;
+  public get lambdaFunction(): lambda.IFunction {
+    return this._lambdaFunction;
+  }
+
+  constructor(scope: cdk.Construct, id: string, props: DemoAppProps) {
     super(scope, id);
 
-    const { appName = 'demo-app', shared } = props;
+    const { appName = 'demo-app', assetNameRoot, assetNameSuffix, removalPolicy } = props;
 
     //
     // Lambda Function
     //
-    const svc = new lambdaNodejs.NodejsFunction(this, 'app-lambda', {
+    this._lambdaFunction = new lambdaNodejs.NodejsFunction(this, 'app-lambda', {
       entry: './packages/demo-app/src/index.ts',
       runtime: lambda.Runtime.NODEJS_14_X,
       handler: 'handler',
-      functionName: `${shared.stackName}-app-${appName}${shared.envSuffix}${shared.prSuffix}`,
+      functionName: assetNameRoot ? `${assetNameRoot}-app-${appName}${assetNameSuffix}` : undefined,
       logRetention: logs.RetentionDays.ONE_WEEK,
       memorySize: 512,
       timeout: cdk.Duration.seconds(3),
@@ -37,9 +66,8 @@ export class DemoApp extends cdk.Construct {
         sourceMap: true,
       },
     });
-
-    if (shared.isPR) {
-      svc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (removalPolicy !== undefined) {
+      this._lambdaFunction.applyRemovalPolicy(removalPolicy);
     }
   }
 }
diff --git a/packages/cdk/lib/Imports.ts b/packages/cdk/lib/Imports.ts
deleted file mode 100644
index 3fedc48d..00000000
--- a/packages/cdk/lib/Imports.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as acm from '@aws-cdk/aws-certificatemanager';
-import * as cdk from '@aws-cdk/core';
-import { SharedProps } from './SharedProps';
-
-interface ImportsProps {
-  readonly shared: SharedProps;
-}
-export interface IImportsExports {
-  readonly certEdge: acm.ICertificate;
-  readonly certOrigin: acm.ICertificate;
-}
-
-export class Imports extends cdk.Construct implements IImportsExports {
-  private _certEdge: acm.ICertificate;
-  public get certEdge(): acm.ICertificate {
-    return this._certEdge;
-  }
-  private _certOrigin: acm.ICertificate;
-  public get certOrigin(): acm.ICertificate {
-    return this._certOrigin;
-  }
-
-  constructor(scope: cdk.Construct, id: string, props?: ImportsProps) {
-    super(scope, id);
-
-    if (props === undefined) {
-      throw new Error('props must be set');
-    }
-
-    const { certARNEdge, certARNOrigin } = props.shared;
-
-    // CloudFront certificate
-    // Note: Must be in US East 1
-    this._certEdge = acm.Certificate.fromCertificateArn(this, 'microapps-cert-edge', certARNEdge);
-
-    // Specific cert for API Gateway
-    // Note: Must be in region where CDK stack is deployed
-    this._certOrigin = acm.Certificate.fromCertificateArn(
-      this,
-      'microapps-cert-origin',
-      certARNOrigin,
-    );
-  }
-}
diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts
index 151ef748..8e7aeb44 100644
--- a/packages/cdk/lib/MicroApps.ts
+++ b/packages/cdk/lib/MicroApps.ts
@@ -1,10 +1,13 @@
 import * as cdk from '@aws-cdk/core';
+import * as acm from '@aws-cdk/aws-certificatemanager';
 import * as lambda from '@aws-cdk/aws-lambda';
+import * as r53 from '@aws-cdk/aws-route53';
 import { TimeToLive } from '@cloudcomponents/cdk-temp-stack';
-import { MicroApps as MicroAppsCDK } from '@pwrdrvr/microapps-cdk';
+import {
+  MicroApps as MicroAppsCDK,
+  MicroAppsProps as MicroAppsCDKProps,
+} from '@pwrdrvr/microapps-cdk';
 import { DemoApp } from './DemoApp';
-import { Imports } from './Imports';
-import { SharedProps } from './SharedProps';
 import { MicroAppsAppRelease } from '@pwrdrvr/microapps-app-release-cdk';
 import { Env } from './Types';
 
@@ -23,13 +26,105 @@ export interface MicroAppsStackProps extends cdk.StackProps {
    */
   readonly autoDeleteEverything?: boolean;
 
-  readonly domainNameEdge: string;
-  readonly domainNameOrigin: string;
+  /**
+   * Optional domain name for CloudFront distribution
+   *
+   * @default auto-assigned
+   */
+  readonly domainNameEdge?: string;
+
+  /**
+   * Optional domain name for API Gateway
+   *
+   * @default auto-assigned
+   */
+  readonly domainNameOrigin?: string;
+
+  /**
+   * Optional Route53 zone name for custom names
+   */
+  readonly r53ZoneName?: string;
 
-  readonly shared: SharedProps;
+  /**
+   * Optional Route53 zone id for custom names
+   */
+  readonly r53ZoneID?: string;
+
+  /**
+   * Optional US East 1 ACM cert for custom names
+   */
+  readonly certARNEdge?: string;
+
+  /**
+   * Optional local region cert for custom names
+   */
+  readonly certARNOrigin?: string;
+
+  /**
+   * Principal ARNs (e.g. Users not Assumed Roles) that should
+   * bypass the strict S3 Bucket Policy
+   */
+  readonly s3PolicyBypassPrincipalARNs?: string[];
+
+  /**
+   * AROAs (e.g. Role AROAs) that should bypass the strict
+   * S3 Bucket Policy
+   */
+  readonly s3PolicyBypassAROAs?: string[];
+
+  /**
+   * Use a strict S3 bucket policy
+   *
+   * @default false
+   */
+  readonly s3StrictBucketPolicy?: boolean;
+
+  /**
+   * 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;
+
+  /**
+   * Deploy the Release app
+   *
+   * @default false
+   */
+  readonly deployReleaseApp?: boolean;
+
+  /**
+   * Deploy the Demo app
+   *
+   * @default false
+   */
+  readonly deployDemoApp?: boolean;
+
+  /**
+   * NODE_ENV to pass to apps
+   *
+   * @default dev
+   */
+  readonly nodeEnv?: Env;
 }
 
 export class MicroAppsStack extends cdk.Stack {
+  /**
+   * Create the MicroApps Construct
+   *
+   * @param scope
+   * @param id
+   * @param props
+   */
   constructor(scope: cdk.Construct, id: string, props?: MicroAppsStackProps) {
     super(scope, id, props);
 
@@ -37,63 +132,143 @@ export class MicroAppsStack extends cdk.Stack {
       throw new Error('props must be set');
     }
 
-    const { ttl, autoDeleteEverything = false, shared } = props;
+    const {
+      ttl,
+      autoDeleteEverything = false,
+      domainNameEdge,
+      domainNameOrigin,
+      s3PolicyBypassPrincipalARNs,
+      s3PolicyBypassAROAs,
+      s3StrictBucketPolicy = false,
+      assetNameRoot,
+      assetNameSuffix,
+      deployReleaseApp = false,
+      deployDemoApp = false,
+      nodeEnv = 'dev',
+      r53ZoneID,
+      r53ZoneName,
+      certARNEdge,
+      certARNOrigin,
+    } = props;
 
+    let removalPolicy: cdk.RemovalPolicy | undefined = undefined;
     // Set stack to delete if this is a PR build
     if (ttl !== undefined) {
       if (autoDeleteEverything === false) {
         throw new Error('autoDeleteEverything must be true when ttl is set');
       }
+      removalPolicy = cdk.RemovalPolicy.DESTROY;
       new TimeToLive(this, 'TimeToLive', {
         ttl,
       });
     }
 
-    const imports = new Imports(this, 'microapps-imports', {
-      shared,
-    });
+    // Validate custom domain options
+    if (domainNameEdge !== undefined) {
+      if (
+        domainNameOrigin === undefined ||
+        certARNEdge === undefined ||
+        certARNOrigin === undefined ||
+        r53ZoneName === undefined ||
+        r53ZoneID === undefined
+      ) {
+        throw new Error(
+          'Either all of `domainNameEdge, domainNameOrigin, certARNEdge, certARNOrigin, r53ZoneName, and r53ZoneID`, must be defined or they must all be undefined',
+        );
+      }
+    }
+
+    const optionalCustomDomainOpts: Partial<MicroAppsCDKProps> =
+      domainNameEdge !== undefined
+        ? {
+            domainNameEdge,
+            domainNameOrigin,
+            // CloudFront certificate
+            // Note: Must be in US East 1
+            certEdge: acm.Certificate.fromCertificateArn(this, 'cert-edge', certARNEdge as string),
+
+            // Specific cert for API Gateway
+            // Note: Must be in region where CDK stack is deployed
+            certOrigin: acm.Certificate.fromCertificateArn(
+              this,
+              'cert-origin',
+              certARNOrigin as string,
+            ),
+            r53Zone: r53.HostedZone.fromHostedZoneAttributes(this, 'microapps-zone', {
+              zoneName: r53ZoneName as string,
+              hostedZoneId: r53ZoneID as string,
+            }),
+          }
+        : {};
+
+    const optionals3PolicyOpts: Partial<MicroAppsCDKProps> = {
+      // Note: these can be undefined, which is ok if s3Strict is false
+      s3PolicyBypassAROAs,
+      s3PolicyBypassPrincipalARNs,
+      s3StrictBucketPolicy,
+    };
+
+    const optionalAssetNameOpts: Partial<MicroAppsCDKProps> = {
+      assetNameRoot,
+      assetNameSuffix,
+    };
 
     const microapps = new MicroAppsCDK(this, 'microapps', {
-      account: shared.account,
-      region: shared.region,
-      appEnv: shared.env,
-      assetNameRoot: `${shared.stackName}`,
-      assetNameSuffix: `${shared.envSuffix}${shared.prSuffix}`,
-      domainNameEdge: props.domainNameEdge,
-      domainNameOrigin: props.domainNameOrigin,
-      certEdge: imports.certEdge,
-      certOrigin: imports.certOrigin,
-      domainName: shared.domainName,
-      r53ZoneID: shared.r53ZoneID,
-      r53ZoneName: shared.r53ZoneName,
-      s3PolicyBypassAROAs: shared.s3PolicyBypassAROAs,
-      s3PolicyBypassPrincipalARNs: shared.s3PolicyBypassPrincipalARNs,
-      s3StrictBucketPolicy: shared.s3StrictBucketPolicy,
-      autoDeleteEverything,
+      removalPolicy,
+      appEnv: nodeEnv,
+      ...optionalAssetNameOpts,
+      ...optionals3PolicyOpts,
+      ...optionalCustomDomainOpts,
     });
 
-    if (shared.deployDemoApp) {
-      new DemoApp(this, 'demo-app', {
-        shared,
+    if (deployDemoApp) {
+      const demoApp = new DemoApp(this, 'demo-app', {
         appName: 'demo-app',
+        assetNameRoot,
+        assetNameSuffix,
+        removalPolicy,
+      });
+
+      new cdk.CfnOutput(this, 'demo-app-func-name', {
+        value: `${demoApp.lambdaFunction.functionName}`,
+        exportName: `${this.stackName}-demo-app-func-name`,
       });
     }
 
-    if (shared.deployReleaseApp) {
+    if (deployReleaseApp) {
       const sharpLayer = lambda.LayerVersion.fromLayerVersionArn(
         this,
         'sharp-lambda-layer',
-        `arn:aws:lambda:${shared.region}:${shared.account}:layer:sharp-heic:1`,
+        `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:layer:sharp-heic:1`,
       );
 
-      new MicroAppsAppRelease(this, 'release-app', {
-        functionName: `${shared.stackName}-app-release${shared.envSuffix}${shared.prSuffix}`,
+      const releaseApp = new MicroAppsAppRelease(this, 'release-app', {
+        functionName: assetNameRoot ? `${assetNameRoot}-app-release${assetNameSuffix}` : undefined,
         table: microapps.svcs.table,
         staticAssetsS3Bucket: microapps.s3.bucketApps,
-        nodeEnv: shared.env as Env,
-        removalPolicy: shared.isPR ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN,
+        nodeEnv,
+        removalPolicy,
         sharpLayer,
       });
+
+      new cdk.CfnOutput(this, 'release-app-func-name', {
+        value: `${releaseApp.lambdaFunction.functionName}`,
+        exportName: `${this.stackName}-release-app-func-name`,
+      });
     }
+
+    // Exports
+    new cdk.CfnOutput(this, 'edge-domain-name', {
+      value: domainNameEdge ? domainNameEdge : microapps.cf.cloudFrontDistro.domainName,
+      exportName: `${this.stackName}-edge-domain-name`,
+    });
+    new cdk.CfnOutput(this, 'dynamodb-table-name', {
+      value: `${microapps.svcs.table.tableName}`,
+      exportName: `${this.stackName}-dynamodb-table-name`,
+    });
+    new cdk.CfnOutput(this, 'deployer-func-name', {
+      value: `${microapps.svcs.deployerFunc.functionName}`,
+      exportName: `${this.stackName}-deployer-func-name`,
+    });
   }
 }
diff --git a/packages/cdk/lib/SharedProps.ts b/packages/cdk/lib/SharedProps.ts
index 94c80d3e..68e583ad 100644
--- a/packages/cdk/lib/SharedProps.ts
+++ b/packages/cdk/lib/SharedProps.ts
@@ -11,6 +11,13 @@ export class SharedProps {
     return parts.join('.');
   }
 
+  private static stripTrailingDomainDot(domain: string): string {
+    if (domain !== undefined && domain.endsWith('.')) {
+      return domain.slice(0, domain.length - 1);
+    }
+    return domain;
+  }
+
   private _ttlBase = cdk.Duration.hours(6);
   public get ttlBase(): cdk.Duration {
     return this._ttlBase;
@@ -48,11 +55,6 @@ export class SharedProps {
     return this._stackName;
   }
 
-  private _reverseDomainName: string;
-  public get reverseDomainName(): string {
-    return this._reverseDomainName;
-  }
-
   private _domainName: string;
   public get domainName(): string {
     return this._domainName;
@@ -122,8 +124,9 @@ export class SharedProps {
 
   constructor(scope: cdk.Construct) {
     this._r53ZoneName = scope.node.tryGetContext('@pwrdrvr/microapps:r53ZoneName');
-    this._reverseDomainName = SharedProps.reverseDomain(this._r53ZoneName);
-    this._domainName = SharedProps.reverseDomain(this._reverseDomainName);
+    this._domainName = SharedProps.stripTrailingDomainDot(
+      scope.node.tryGetContext('@pwrdrvr/microapps:r53ZoneName'),
+    );
     this._r53ZoneID = scope.node.tryGetContext('@pwrdrvr/microapps:r53ZoneID');
     this._certIDEdge = scope.node.tryGetContext('@pwrdrvr/microapps:certIDEdge');
     this._certIDOrigin = scope.node.tryGetContext('@pwrdrvr/microapps:certIDOrigin');
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index 71740952..6ed2ce46 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -38,6 +38,12 @@ new MicroApps(scope: Construct, id: string, props?: MicroAppsProps)
 
 #### Properties <a name="Properties"></a>
 
+##### `apigwy`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroApps.apigwy"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy)
+
+---
+
 ##### `cf`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroApps.cf"></a>
 
 - *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsCF`](#@pwrdrvr/microapps-cdk.IMicroAppsCF)
@@ -57,6 +63,57 @@ new MicroApps(scope: Construct, id: string, props?: MicroAppsProps)
 ---
 
 
+### MicroAppsAPIGwy <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy"></a>
+
+- *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy)
+
+#### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.Initializer"></a>
+
+```typescript
+import { MicroAppsAPIGwy } from '@pwrdrvr/microapps-cdk'
+
+new MicroAppsAPIGwy(scope: Construct, id: string, props?: MicroAppsAPIGwyProps)
+```
+
+##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.scope"></a>
+
+- *Type:* [`@aws-cdk/core.Construct`](#@aws-cdk/core.Construct)
+
+---
+
+##### `id`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.id"></a>
+
+- *Type:* `string`
+
+---
+
+##### `props`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps`](#@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps)
+
+---
+
+
+
+#### Properties <a name="Properties"></a>
+
+##### `httpApi`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.httpApi"></a>
+
+- *Type:* [`@aws-cdk/aws-apigatewayv2.HttpApi`](#@aws-cdk/aws-apigatewayv2.HttpApi)
+
+API Gateway.
+
+---
+
+##### `dnAppsOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwy.dnAppsOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-apigatewayv2.IDomainName`](#@aws-cdk/aws-apigatewayv2.IDomainName)
+
+Domain Name applied to API Gateway origin.
+
+---
+
+
 ### MicroAppsCF <a name="@pwrdrvr/microapps-cdk.MicroAppsCF"></a>
 
 - *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsCF`](#@pwrdrvr/microapps-cdk.IMicroAppsCF)
@@ -136,11 +193,7 @@ new MicroAppsS3(scope: Construct, id: string, props?: MicroAppsS3Props)
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
----
-
-##### `bucketAppsName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsName"></a>
-
-- *Type:* `string`
+S3 bucket for deployed applications.
 
 ---
 
@@ -148,23 +201,23 @@ new MicroAppsS3(scope: Construct, id: string, props?: MicroAppsS3Props)
 
 - *Type:* [`@aws-cdk/aws-cloudfront.OriginAccessIdentity`](#@aws-cdk/aws-cloudfront.OriginAccessIdentity)
 
+CloudFront Origin Access Identity for the deployed applications bucket.
+
 ---
 
 ##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsOrigin"></a>
 
 - *Type:* [`@aws-cdk/aws-cloudfront-origins.S3Origin`](#@aws-cdk/aws-cloudfront-origins.S3Origin)
 
+CloudFront Origin for the deployed applications bucket.
+
 ---
 
 ##### `bucketAppsStaging`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsStaging"></a>
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
----
-
-##### `bucketAppsStagingName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3.bucketAppsStagingName"></a>
-
-- *Type:* `string`
+S3 bucket for staged applications (prior to deploy).
 
 ---
 
@@ -172,6 +225,8 @@ new MicroAppsS3(scope: Construct, id: string, props?: MicroAppsS3Props)
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
+S3 bucket for CloudFront logs.
+
 ---
 
 
@@ -184,7 +239,7 @@ new MicroAppsS3(scope: Construct, id: string, props?: MicroAppsS3Props)
 ```typescript
 import { MicroAppsSvcs } from '@pwrdrvr/microapps-cdk'
 
-new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsStackProps)
+new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsProps)
 ```
 
 ##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.scope"></a>
@@ -201,7 +256,7 @@ new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsStackProps)
 
 ##### `props`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.props"></a>
 
-- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps`](#@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps)
+- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsSvcsProps`](#@pwrdrvr/microapps-cdk.MicroAppsSvcsProps)
 
 ---
 
@@ -209,9 +264,11 @@ new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsStackProps)
 
 #### Properties <a name="Properties"></a>
 
-##### `dnAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.dnAppsOrigin"></a>
+##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.deployerFunc"></a>
+
+- *Type:* [`@aws-cdk/aws-lambda.IFunction`](#@aws-cdk/aws-lambda.IFunction)
 
-- *Type:* [`@aws-cdk/aws-apigatewayv2.DomainName`](#@aws-cdk/aws-apigatewayv2.DomainName)
+Lambda function for the Deployer.
 
 ---
 
@@ -219,206 +276,273 @@ new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsStackProps)
 
 - *Type:* [`@aws-cdk/aws-dynamodb.ITable`](#@aws-cdk/aws-dynamodb.ITable)
 
+DynamoDB table used by Router, Deployer, and Release console app.
+
 ---
 
 
 ## Structs <a name="Structs"></a>
 
-### MicroAppsCFProps <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps"></a>
+### MicroAppsAPIGwyProps <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps"></a>
 
 #### Initializer <a name="[object Object].Initializer"></a>
 
 ```typescript
-import { MicroAppsCFProps } from '@pwrdrvr/microapps-cdk'
+import { MicroAppsAPIGwyProps } from '@pwrdrvr/microapps-cdk'
 
-const microAppsCFProps: MicroAppsCFProps = { ... }
+const microAppsAPIGwyProps: MicroAppsAPIGwyProps = { ... }
 ```
 
-##### `assetNameRoot`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.assetNameRoot"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
 
 ---
 
-##### `assetNameSuffix`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.assetNameSuffix"></a>
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.assetNameSuffix"></a>
 
 - *Type:* `string`
+- *Default:* none
+
+Optional asset name suffix.
 
 ---
 
-##### `autoDeleteEverything`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.autoDeleteEverything"></a>
+##### `certOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.certOrigin"></a>
 
-- *Type:* `boolean`
+- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+- *Default:* none
+
+Optional local region ACM certificate to use for API Gateway Note: required when using a custom domain.
 
 ---
 
-##### `certEdge`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.certEdge"></a>
+##### `domainNameEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.domainNameEdge"></a>
 
-- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+- *Type:* `string`
+- *Default:* auto-assigned
+
+CloudFront edge domain name.
 
 ---
 
-##### `domainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainName"></a>
+##### `domainNameOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.domainNameOrigin"></a>
 
 - *Type:* `string`
+- *Default:* auto-assigned
+
+API Gateway origin domain name.
 
 ---
 
-##### `domainNameEdge`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainNameEdge"></a>
+##### `r53Zone`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.r53Zone"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/aws-route53.IHostedZone`](#@aws-cdk/aws-route53.IHostedZone)
+
+Route53 zone in which to create optional `domainNameEdge` record.
 
 ---
 
-##### `domainNameOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainNameOrigin"></a>
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsAPIGwyProps.removalPolicy"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/core.RemovalPolicy`](#@aws-cdk/core.RemovalPolicy)
+- *Default:* per resource default
+
+RemovalPolicy override for child resources.
+
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
-##### `r53ZoneID`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.r53ZoneID"></a>
+### MicroAppsCFProps <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps"></a>
 
-- *Type:* `string`
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+import { MicroAppsCFProps } from '@pwrdrvr/microapps-cdk'
+
+const microAppsCFProps: MicroAppsCFProps = { ... }
+```
+
+##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.bucketAppsOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-cloudfront-origins.S3Origin`](#@aws-cdk/aws-cloudfront-origins.S3Origin)
+
+S3 bucket origin for deployed applications.
 
 ---
 
-##### `r53ZoneName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.r53ZoneName"></a>
+##### `httpApi`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.httpApi"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/aws-apigatewayv2.HttpApi`](#@aws-cdk/aws-apigatewayv2.HttpApi)
+
+API Gateway v2 HTTP API for apps.
 
 ---
 
-##### `reverseDomainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.reverseDomainName"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
 
 ---
 
-##### `s3Exports`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.s3Exports"></a>
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.assetNameSuffix"></a>
 
-- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsS3`](#@pwrdrvr/microapps-cdk.IMicroAppsS3)
+- *Type:* `string`
+- *Default:* none
 
----
+Optional asset name suffix.
 
-### MicroAppsProps <a name="@pwrdrvr/microapps-cdk.MicroAppsProps"></a>
+---
 
-Props for MicroApps.
+##### `bucketLogs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.bucketLogs"></a>
 
-#### Initializer <a name="[object Object].Initializer"></a>
+- *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
-```typescript
-import { MicroAppsProps } from '@pwrdrvr/microapps-cdk'
+S3 bucket for CloudFront logs.
 
-const microAppsProps: MicroAppsProps = { ... }
-```
+---
 
-##### `account`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.account"></a>
+##### `certEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.certEdge"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
 
-AWS Account ID that the stack is being deployed to, this is required for importing the R53 Zone.
+ACM Certificate that covers `domainNameEdge` name.
 
 ---
 
-##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.appEnv"></a>
+##### `domainNameEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainNameEdge"></a>
 
 - *Type:* `string`
-- *Default:* dev
+- *Default:* auto-assigned
 
-Passed to NODE_ENV of Router and Deployer Lambda functions.
+CloudFront Distribution domain name.
 
 ---
 
-##### `assetNameRoot`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.assetNameRoot"></a>
+##### `domainNameOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.domainNameOrigin"></a>
 
 - *Type:* `string`
-- *Default:* microapps
+- *Default:* retrieved from httpApi, if possible
 
-Start of asset names.
+API Gateway custom origin domain name.
 
 ---
 
-##### `certEdge`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.certEdge"></a>
+##### `r53Zone`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.r53Zone"></a>
 
-- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+- *Type:* [`@aws-cdk/aws-route53.IHostedZone`](#@aws-cdk/aws-route53.IHostedZone)
 
-Certificate in US-East-1 for the CloudFront distribution.
+Route53 zone in which to create optional `domainNameEdge` record.
 
 ---
 
-##### `certOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.certOrigin"></a>
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsCFProps.removalPolicy"></a>
 
-- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+- *Type:* [`@aws-cdk/core.RemovalPolicy`](#@aws-cdk/core.RemovalPolicy)
+- *Default:* per resource default
 
-Certificate in deployed region for the API Gateway.
+RemovalPolicy override for child resources.
+
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
-##### `domainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.domainName"></a>
+### MicroAppsProps <a name="@pwrdrvr/microapps-cdk.MicroAppsProps"></a>
 
-- *Type:* `string`
+Props for MicroApps.
 
-Domain name of the zone for the edge host.
+#### Initializer <a name="[object Object].Initializer"></a>
 
-Example: 'pwrdrvr.com' for apps.pwrdrvr.com
+```typescript
+import { MicroAppsProps } from '@pwrdrvr/microapps-cdk'
 
----
+const microAppsProps: MicroAppsProps = { ... }
+```
 
-##### `domainNameEdge`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.domainNameEdge"></a>
+##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.appEnv"></a>
 
 - *Type:* `string`
+- *Default:* dev
 
-CNAME for the CloudFront distribution.
+Passed to NODE_ENV of Router and Deployer Lambda functions.
 
 ---
 
-##### `domainNameOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.domainNameOrigin"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
 
-CNAME for the API Gateway HTTPv2 API.
+Optional asset name root.
 
 ---
 
-##### `r53ZoneID`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.r53ZoneID"></a>
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.assetNameSuffix"></a>
 
 - *Type:* `string`
+- *Default:* none
 
-ID of the zone in R53 to add records to.
+Optional asset name suffix.
 
 ---
 
-##### `r53ZoneName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.r53ZoneName"></a>
+##### `certEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.certEdge"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
 
-Name of the zone in R53 to add records to.
+Certificate in US-East-1 for the CloudFront distribution.
 
 ---
 
-##### `region`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.region"></a>
+##### `certOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.certOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+
+Certificate in deployed region for the API Gateway.
+
+---
+
+##### `domainNameEdge`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.domainNameEdge"></a>
 
 - *Type:* `string`
+- *Default:* auto-assigned
 
-AWS Region that the stack is being deployed to, this is required for importing the R53 Zone.
+Optional custom domain name for the CloudFront distribution.
 
 ---
 
-##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.assetNameSuffix"></a>
+##### `domainNameOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.domainNameOrigin"></a>
 
 - *Type:* `string`
-- *Default:* none
+- *Default:* auto-assigned
 
-Suffix to add to asset names, such as -[env]-pr-[prNum].
+Optional custom domain name for the API Gateway HTTPv2 API.
 
 ---
 
-##### `autoDeleteEverything`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.autoDeleteEverything"></a>
+##### `r53Zone`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.r53Zone"></a>
 
-- *Type:* `boolean`
-- *Default:* false
+- *Type:* [`@aws-cdk/aws-route53.IHostedZone`](#@aws-cdk/aws-route53.IHostedZone)
 
-Automatically destroy all assets when stack is deleted.
+Route53 zone in which to create optional `domainNameEdge` record.
+
+---
+
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.removalPolicy"></a>
+
+- *Type:* [`@aws-cdk/core.RemovalPolicy`](#@aws-cdk/core.RemovalPolicy)
+- *Default:* per resource default
+
+RemovalPolicy override for child resources.
+
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
@@ -505,148 +629,152 @@ import { MicroAppsS3Props } from '@pwrdrvr/microapps-cdk'
 const microAppsS3Props: MicroAppsS3Props = { ... }
 ```
 
-##### `assetNameRoot`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.assetNameRoot"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
 
 ---
 
-##### `assetNameSuffix`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.assetNameSuffix"></a>
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.assetNameSuffix"></a>
 
 - *Type:* `string`
+- *Default:* none
+
+Optional asset name suffix.
 
 ---
 
-##### `reverseDomainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.reverseDomainName"></a>
+##### `bucketAppsName`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.bucketAppsName"></a>
 
 - *Type:* `string`
+- *Default:* auto-assigned
 
----
+S3 deployed apps bucket name.
 
-##### `autoDeleteEverything`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.autoDeleteEverything"></a>
+---
 
-- *Type:* `boolean`
-- *Default:* false
+##### `bucketAppsStagingName`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.bucketAppsStagingName"></a>
 
-Duration before stack is automatically deleted.
+- *Type:* `string`
+- *Default:* auto-assigned
 
-Requires that autoDeleteEverything be set to true.
+S3 staging apps bucket name.
 
 ---
 
-### MicroAppsSvcsStackProps <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps"></a>
+##### `bucketLogsName`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.bucketLogsName"></a>
 
-#### Initializer <a name="[object Object].Initializer"></a>
-
-```typescript
-import { MicroAppsSvcsStackProps } from '@pwrdrvr/microapps-cdk'
+- *Type:* `string`
+- *Default:* auto-assigned
 
-const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
-```
+S3 logs bucket name.
 
-##### `account`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.account"></a>
+---
 
-- *Type:* `string`
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsS3Props.removalPolicy"></a>
 
----
+- *Type:* [`@aws-cdk/core.RemovalPolicy`](#@aws-cdk/core.RemovalPolicy)
+- *Default:* per resource default
 
-##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.appEnv"></a>
+RemovalPolicy override for child resources.
 
-- *Type:* `string`
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
-##### `assetNameRoot`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.assetNameRoot"></a>
+### MicroAppsSvcsProps <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps"></a>
 
-- *Type:* `string`
+#### Initializer <a name="[object Object].Initializer"></a>
 
----
+```typescript
+import { MicroAppsSvcsProps } from '@pwrdrvr/microapps-cdk'
 
-##### `assetNameSuffix`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.assetNameSuffix"></a>
+const microAppsSvcsProps: MicroAppsSvcsProps = { ... }
+```
+
+##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.appEnv"></a>
 
 - *Type:* `string`
 
 ---
 
-##### `autoDeleteEverything`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.autoDeleteEverything"></a>
-
-- *Type:* `boolean`
-
----
+##### `bucketApps`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.bucketApps"></a>
 
-##### `certOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.certOrigin"></a>
+- *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
-- *Type:* [`@aws-cdk/aws-certificatemanager.ICertificate`](#@aws-cdk/aws-certificatemanager.ICertificate)
+S3 bucket for deployed applications.
 
 ---
 
-##### `cfStackExports`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.cfStackExports"></a>
-
-- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsCF`](#@pwrdrvr/microapps-cdk.IMicroAppsCF)
-
----
+##### `bucketAppsOAI`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.bucketAppsOAI"></a>
 
-##### `domainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.domainName"></a>
+- *Type:* [`@aws-cdk/aws-cloudfront.OriginAccessIdentity`](#@aws-cdk/aws-cloudfront.OriginAccessIdentity)
 
-- *Type:* `string`
+CloudFront Origin Access Identity for the deployed applications bucket.
 
 ---
 
-##### `domainNameEdge`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.domainNameEdge"></a>
-
-- *Type:* `string`
+##### `bucketAppsStaging`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.bucketAppsStaging"></a>
 
----
+- *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
-##### `domainNameOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.domainNameOrigin"></a>
-
-- *Type:* `string`
+S3 bucket for staged applications (prior to deploy).
 
 ---
 
-##### `r53ZoneID`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.r53ZoneID"></a>
+##### `httpApi`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.httpApi"></a>
 
-- *Type:* `string`
+- *Type:* [`@aws-cdk/aws-apigatewayv2.HttpApi`](#@aws-cdk/aws-apigatewayv2.HttpApi)
+
+API Gateway v2 HTTP for Router and app.
 
 ---
 
-##### `r53ZoneName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.r53ZoneName"></a>
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.assetNameRoot"></a>
 
 - *Type:* `string`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
 
 ---
 
-##### `region`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.region"></a>
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.assetNameSuffix"></a>
 
 - *Type:* `string`
+- *Default:* none
 
----
+Optional asset name suffix.
 
-##### `reverseDomainName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.reverseDomainName"></a>
+---
 
-- *Type:* `string`
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.removalPolicy"></a>
 
----
+- *Type:* [`@aws-cdk/core.RemovalPolicy`](#@aws-cdk/core.RemovalPolicy)
+- *Default:* per resource default
 
-##### `s3Exports`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.s3Exports"></a>
+RemovalPolicy override for child resources.
 
-- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsS3`](#@pwrdrvr/microapps-cdk.IMicroAppsS3)
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
 
 ---
 
-##### `s3PolicyBypassAROAs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.s3PolicyBypassAROAs"></a>
+##### `s3PolicyBypassAROAs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.s3PolicyBypassAROAs"></a>
 
 - *Type:* `string`[]
 
 ---
 
-##### `s3PolicyBypassPrincipalARNs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.s3PolicyBypassPrincipalARNs"></a>
+##### `s3PolicyBypassPrincipalARNs`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.s3PolicyBypassPrincipalARNs"></a>
 
 - *Type:* `string`[]
 
 ---
 
-##### `s3StrictBucketPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsStackProps.s3StrictBucketPolicy"></a>
+##### `s3StrictBucketPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcsProps.s3StrictBucketPolicy"></a>
 
 - *Type:* `boolean`
 
@@ -662,6 +790,12 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 #### Properties <a name="Properties"></a>
 
+##### `apigwy`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroApps.apigwy"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy`](#@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy)
+
+---
+
 ##### `cf`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroApps.cf"></a>
 
 - *Type:* [`@pwrdrvr/microapps-cdk.IMicroAppsCF`](#@pwrdrvr/microapps-cdk.IMicroAppsCF)
@@ -680,6 +814,29 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 ---
 
+### 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)
+
+
+#### Properties <a name="Properties"></a>
+
+##### `httpApi`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy.httpApi"></a>
+
+- *Type:* [`@aws-cdk/aws-apigatewayv2.HttpApi`](#@aws-cdk/aws-apigatewayv2.HttpApi)
+
+API Gateway.
+
+---
+
+##### `dnAppsOrigin`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsAPIGwy.dnAppsOrigin"></a>
+
+- *Type:* [`@aws-cdk/aws-apigatewayv2.IDomainName`](#@aws-cdk/aws-apigatewayv2.IDomainName)
+
+Domain Name applied to API Gateway origin.
+
+---
+
 ### IMicroAppsCF <a name="@pwrdrvr/microapps-cdk.IMicroAppsCF"></a>
 
 - *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsCF`](#@pwrdrvr/microapps-cdk.MicroAppsCF), [`@pwrdrvr/microapps-cdk.IMicroAppsCF`](#@pwrdrvr/microapps-cdk.IMicroAppsCF)
@@ -704,11 +861,7 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
----
-
-##### `bucketAppsName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsName"></a>
-
-- *Type:* `string`
+S3 bucket for deployed applications.
 
 ---
 
@@ -716,23 +869,23 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 - *Type:* [`@aws-cdk/aws-cloudfront.OriginAccessIdentity`](#@aws-cdk/aws-cloudfront.OriginAccessIdentity)
 
+CloudFront Origin Access Identity for the deployed applications bucket.
+
 ---
 
 ##### `bucketAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsOrigin"></a>
 
 - *Type:* [`@aws-cdk/aws-cloudfront-origins.S3Origin`](#@aws-cdk/aws-cloudfront-origins.S3Origin)
 
+CloudFront Origin for the deployed applications bucket.
+
 ---
 
 ##### `bucketAppsStaging`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsStaging"></a>
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
----
-
-##### `bucketAppsStagingName`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsS3.bucketAppsStagingName"></a>
-
-- *Type:* `string`
+S3 bucket for staged applications (prior to deploy).
 
 ---
 
@@ -740,6 +893,8 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 - *Type:* [`@aws-cdk/aws-s3.IBucket`](#@aws-cdk/aws-s3.IBucket)
 
+S3 bucket for CloudFront logs.
+
 ---
 
 ### IMicroAppsSvcs <a name="@pwrdrvr/microapps-cdk.IMicroAppsSvcs"></a>
@@ -749,9 +904,11 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 #### Properties <a name="Properties"></a>
 
-##### `dnAppsOrigin`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsSvcs.dnAppsOrigin"></a>
+##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsSvcs.deployerFunc"></a>
+
+- *Type:* [`@aws-cdk/aws-lambda.IFunction`](#@aws-cdk/aws-lambda.IFunction)
 
-- *Type:* [`@aws-cdk/aws-apigatewayv2.DomainName`](#@aws-cdk/aws-apigatewayv2.DomainName)
+Lambda function for the Deployer.
 
 ---
 
@@ -759,5 +916,7 @@ const microAppsSvcsStackProps: MicroAppsSvcsStackProps = { ... }
 
 - *Type:* [`@aws-cdk/aws-dynamodb.ITable`](#@aws-cdk/aws-dynamodb.ITable)
 
+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 ab0c1d03..86be83d3 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -1,8 +1,11 @@
 import * as acm from '@aws-cdk/aws-certificatemanager';
+import * as r53 from '@aws-cdk/aws-route53';
 import * as cdk from '@aws-cdk/core';
+import { IMicroAppsAPIGwy, MicroAppsAPIGwy } from './MicroAppsAPIGwy';
 import { IMicroAppsCF, MicroAppsCF } from './MicroAppsCF';
 import { IMicroAppsS3, MicroAppsS3 } from './MicroAppsS3';
 import { IMicroAppsSvcs, MicroAppsSvcs } from './MicroAppsSvcs';
+import { reverseDomain } from './utils/ReverseDomain';
 
 /**
  * Props for MicroApps
@@ -10,11 +13,13 @@ import { IMicroAppsSvcs, MicroAppsSvcs } from './MicroAppsSvcs';
  */
 export interface MicroAppsProps {
   /**
-   * Automatically destroy all assets when stack is deleted
+   * RemovalPolicy override for child resources
    *
-   * @default false
+   * Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
+   *
+   * @default - per resource default
    */
-  readonly autoDeleteEverything?: boolean;
+  readonly removalPolicy?: cdk.RemovalPolicy;
 
   /**
    * Passed to NODE_ENV of Router and Deployer Lambda functions.
@@ -24,51 +29,37 @@ export interface MicroAppsProps {
   readonly appEnv: string;
 
   /**
-   * Start of asset names.
+   * Optional asset name root
    *
-   * @default microapps
+   * @example microapps
+   * @default - resource names auto assigned
    */
-  readonly assetNameRoot: string;
+  readonly assetNameRoot?: string;
 
   /**
-   * Suffix to add to asset names, such as -[env]-pr-[prNum]
+   * Optional asset name suffix
    *
-   * @default - none
+   * @example -dev-pr-12
+   * @default none
    */
   readonly assetNameSuffix?: string;
 
   /**
-   * Domain name of the zone for the edge host.
-   * Example: 'pwrdrvr.com' for apps.pwrdrvr.com
-   *
+   * Route53 zone in which to create optional `domainNameEdge` record
    */
-  readonly domainName: string;
-
-  /**
-   * Name of the zone in R53 to add records to.
-   *
-   * @example pwrdrvr.com.
-   *
-   */
-  readonly r53ZoneName: string;
-
-  /**
-   * ID of the zone in R53 to add records to.
-   *
-   */
-  readonly r53ZoneID: string;
+  readonly r53Zone?: r53.IHostedZone;
 
   /**
    * Certificate in US-East-1 for the CloudFront distribution.
    *
    */
-  readonly certEdge: acm.ICertificate;
+  readonly certEdge?: acm.ICertificate;
 
   /**
    * Certificate in deployed region for the API Gateway.
    *
    */
-  readonly certOrigin: acm.ICertificate;
+  readonly certOrigin?: acm.ICertificate;
 
   /**
    * Use a strict S3 Bucket Policy that prevents applications
@@ -142,55 +133,33 @@ export interface MicroAppsProps {
   readonly s3PolicyBypassAROAs?: string[];
 
   /**
-   * AWS Account ID that the stack is being deployed to, this is
-   * required for importing the R53 Zone.
-   *
-   * @example 012345678901234
-   */
-  readonly account: string;
-
-  /**
-   * AWS Region that the stack is being deployed to, this is
-   * required for importing the R53 Zone.
-   *
-   * @example us-east-2
-   */
-  readonly region: string;
-
-  /**
-   * CNAME for the CloudFront distribution.
+   * Optional custom domain name for the CloudFront distribution.
    *
    * @example apps.pwrdrvr.com
+   * @default auto-assigned
    */
-  readonly domainNameEdge: string;
+  readonly domainNameEdge?: string;
 
   /**
-   * CNAME for the API Gateway HTTPv2 API.
+   * Optional custom domain name for the API Gateway HTTPv2 API.
    *
    * @example apps-origin.pwrdrvr.com
+   * @default auto-assigned
    */
-  readonly domainNameOrigin: string;
+  readonly domainNameOrigin?: string;
 }
 
 export interface IMicroApps {
   readonly cf: IMicroAppsCF;
   readonly s3: IMicroAppsS3;
   readonly svcs: IMicroAppsSvcs;
+  readonly apigwy: IMicroAppsAPIGwy;
 }
 
 /**
  * Application deployment and runtime environment.
  */
 export class MicroApps extends cdk.Construct implements IMicroApps {
-  // input like 'example.com.' will return as 'com.example'
-  private static reverseDomain(domain: string): string {
-    let parts = domain.split('.').reverse();
-    if (parts[0] === '') {
-      parts = parts.slice(1);
-    }
-    return parts.join('.');
-  }
-
   private _cf: MicroAppsCF;
   public get cf(): IMicroAppsCF {
     return this._cf;
@@ -201,6 +170,11 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
     return this._s3;
   }
 
+  private _apigwy: MicroAppsAPIGwy;
+  public get apigwy(): IMicroAppsAPIGwy {
+    return this._apigwy;
+  }
+
   private _svcs: MicroAppsSvcs;
   public get svcs(): IMicroAppsSvcs {
     return this._svcs;
@@ -208,6 +182,7 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
 
   /**
    * MicroApps - Create entire stack of CloudFront, S3, API Gateway, and Lambda Functions.
+   * This is the "Easy Button" construct to get started as quickly as possible.
    *
    * @param scope
    * @param id
@@ -221,60 +196,60 @@ export class MicroApps extends cdk.Construct implements IMicroApps {
     }
 
     const {
-      domainName,
       domainNameEdge,
       domainNameOrigin,
-      assetNameRoot = 'microapps',
-      assetNameSuffix = '',
-      autoDeleteEverything = false,
-      r53ZoneID,
-      r53ZoneName,
+      assetNameRoot,
+      assetNameSuffix,
+      r53Zone,
       certEdge,
-      account,
-      region,
       appEnv = 'dev',
       certOrigin,
+      removalPolicy,
       s3PolicyBypassAROAs,
       s3PolicyBypassPrincipalARNs,
       s3StrictBucketPolicy,
     } = props;
-    const reverseDomainName = MicroApps.reverseDomain(domainName);
 
-    this._s3 = new MicroAppsS3(this, 'microapps-s3', {
-      autoDeleteEverything,
-      reverseDomainName,
+    this._s3 = new MicroAppsS3(this, 's3', {
+      removalPolicy,
+      bucketLogsName: domainNameEdge ? `${reverseDomain(domainNameEdge)}-logs` : undefined,
+      bucketAppsName: domainNameEdge ? `${reverseDomain(domainNameEdge)}` : undefined,
+      bucketAppsStagingName: domainNameEdge
+        ? `${reverseDomain(domainNameEdge)}-staging`
+        : undefined,
       assetNameRoot,
       assetNameSuffix,
     });
-    this._cf = new MicroAppsCF(this, 'microapps-cloudfront', {
-      s3Exports: this._s3,
+    this._apigwy = new MicroAppsAPIGwy(this, 'api', {
+      removalPolicy,
       assetNameRoot,
       assetNameSuffix,
-      domainName,
-      reverseDomainName,
       domainNameEdge,
       domainNameOrigin,
-      autoDeleteEverything,
-      r53ZoneID,
-      r53ZoneName,
-      certEdge,
+      r53Zone,
+      certOrigin,
     });
-    this._svcs = new MicroAppsSvcs(this, 'microapps-svcs', {
-      cfStackExports: this._cf,
-      s3Exports: this._s3,
+    this._cf = new MicroAppsCF(this, 'cft', {
+      removalPolicy,
       assetNameRoot,
       assetNameSuffix,
-      domainName,
-      reverseDomainName,
       domainNameEdge,
       domainNameOrigin,
-      autoDeleteEverything,
-      r53ZoneID,
-      r53ZoneName,
-      account,
-      region,
+      httpApi: this._apigwy.httpApi,
+      r53Zone,
+      certEdge,
+      bucketAppsOrigin: this._s3.bucketAppsOrigin,
+      bucketLogs: this._s3.bucketLogs,
+    });
+    this._svcs = new MicroAppsSvcs(this, 'svcs', {
+      httpApi: this.apigwy.httpApi,
+      removalPolicy,
+      bucketApps: this._s3.bucketApps,
+      bucketAppsOAI: this._s3.bucketAppsOAI,
+      bucketAppsStaging: this._s3.bucketAppsStaging,
+      assetNameRoot,
+      assetNameSuffix,
       appEnv,
-      certOrigin,
       s3PolicyBypassAROAs,
       s3PolicyBypassPrincipalARNs,
       s3StrictBucketPolicy,
diff --git a/packages/microapps-cdk/src/MicroAppsAPIGwy.ts b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
new file mode 100644
index 00000000..308bb9f7
--- /dev/null
+++ b/packages/microapps-cdk/src/MicroAppsAPIGwy.ts
@@ -0,0 +1,261 @@
+import * as apigwy from '@aws-cdk/aws-apigatewayv2';
+import * as acm from '@aws-cdk/aws-certificatemanager';
+import * as iam from '@aws-cdk/aws-iam';
+import * as logs from '@aws-cdk/aws-logs';
+import * as r53 from '@aws-cdk/aws-route53';
+import * as r53targets from '@aws-cdk/aws-route53-targets';
+import * as cdk from '@aws-cdk/core';
+
+export interface MicroAppsAPIGwyProps {
+  /**
+   * 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?: cdk.RemovalPolicy;
+
+  /**
+   * CloudFront edge domain name
+   *
+   * @example apps.pwrdrvr.com
+   * @default auto-assigned
+   */
+  readonly domainNameEdge?: string;
+
+  /**
+   * API Gateway origin domain name
+   *
+   * @example apps-origin.pwrdrvr.com
+   * @default auto-assigned
+   */
+  readonly domainNameOrigin?: 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;
+
+  /**
+   * Optional local region ACM certificate to use for API Gateway
+   * Note: required when using a custom domain
+   *
+   * @default none
+   */
+  readonly certOrigin?: acm.ICertificate;
+
+  /**
+   * Route53 zone in which to create optional `domainNameEdge` record
+   */
+  readonly r53Zone?: r53.IHostedZone;
+}
+
+export interface IMicroAppsAPIGwy {
+  /**
+   * Domain Name applied to API Gateway origin
+   */
+  readonly dnAppsOrigin?: apigwy.IDomainName;
+
+  /**
+   * API Gateway
+   */
+  readonly httpApi: apigwy.HttpApi;
+}
+
+export class MicroAppsAPIGwy extends cdk.Construct implements IMicroAppsAPIGwy {
+  private _dnAppsOrigin: apigwy.DomainName | undefined;
+  public get dnAppsOrigin(): apigwy.IDomainName | undefined {
+    return this._dnAppsOrigin;
+  }
+
+  private _httpApi: apigwy.HttpApi;
+  public get httpApi(): apigwy.HttpApi {
+    return this._httpApi;
+  }
+
+  /**
+   * MicroApps - Create just API Gateway
+   * @param scope
+   * @param id
+   * @param props
+   */
+  constructor(scope: cdk.Construct, id: string, props?: MicroAppsAPIGwyProps) {
+    super(scope, id);
+
+    if (props === undefined) {
+      throw new Error('props cannot be undefined');
+    }
+
+    if (
+      (props.r53Zone === undefined && props.domainNameEdge !== undefined) ||
+      (props.r53Zone !== undefined && props.domainNameEdge === undefined)
+    ) {
+      throw new Error('If either of r53Zone or domainNameEdge are set then the other must be set');
+    }
+
+    if (
+      (props.domainNameOrigin === undefined && props.certOrigin !== undefined) ||
+      (props.domainNameOrigin !== undefined && props.certOrigin === undefined)
+    ) {
+      throw new Error(
+        'If either of domainNameOrigin or certOrigin are set then the other must be set',
+      );
+    }
+
+    if (
+      (props.domainNameEdge === undefined && props.certOrigin !== undefined) ||
+      (props.domainNameEdge !== undefined && props.certOrigin === undefined)
+    ) {
+      throw new Error(
+        'If either of domainNameOrigin or certOrigin are set then the other must be set',
+      );
+    }
+
+    const {
+      r53Zone,
+      domainNameEdge,
+      domainNameOrigin,
+      certOrigin,
+      removalPolicy,
+      assetNameRoot,
+      assetNameSuffix,
+    } = props;
+
+    // API Gateway uses the `id` string as the gateway name without
+    // any randomization... we have to make sure the name is unique-ish
+    const apigatewayName = assetNameRoot
+      ? `${assetNameRoot}${assetNameSuffix}`
+      : `${cdk.Stack.of(this).stackName}-microapps`;
+
+    //
+    // APIGateway domain names for CloudFront and origin
+    //
+    if (domainNameEdge !== undefined && certOrigin !== undefined) {
+      // Create Custom Domains for API Gateway
+      const dnAppsEdge = new apigwy.DomainName(this, 'microapps-apps-edge-dn', {
+        domainName: domainNameEdge,
+        certificate: certOrigin,
+      });
+      if (removalPolicy !== undefined) {
+        dnAppsEdge.applyRemovalPolicy(removalPolicy);
+      }
+
+      // Create APIGateway for the Edge name
+      const httpApiDomainMapping: apigwy.DomainMappingOptions = {
+        domainName: dnAppsEdge,
+      };
+      this._httpApi = new apigwy.HttpApi(this, 'gwy', {
+        defaultDomainMapping: httpApiDomainMapping,
+        apiName: apigatewayName,
+      });
+    } else {
+      // Create HttpApi with default names
+      this._httpApi = new apigwy.HttpApi(this, 'gwy', {
+        apiName: apigatewayName,
+      });
+    }
+
+    if (removalPolicy !== undefined) {
+      this._httpApi.applyRemovalPolicy(removalPolicy);
+    }
+
+    if (domainNameOrigin !== undefined && certOrigin !== undefined) {
+      this._dnAppsOrigin = new apigwy.DomainName(this, 'origin-dn', {
+        domainName: domainNameOrigin,
+        certificate: certOrigin,
+      });
+      if (removalPolicy !== undefined) {
+        this._dnAppsOrigin.applyRemovalPolicy(removalPolicy);
+      }
+    }
+
+    // Enable access logs on API Gateway
+    const apiAccessLogs = new logs.LogGroup(this, 'logs', {
+      logGroupName: apigatewayName
+        ? `/aws/apigwy/${apigatewayName}`
+        : `/aws/apigwy/${this.httpApi.httpApiName}`,
+      retention: logs.RetentionDays.TWO_WEEKS,
+    });
+    if (removalPolicy !== undefined) {
+      apiAccessLogs.applyRemovalPolicy(removalPolicy);
+    }
+    const stage = this._httpApi.defaultStage?.node.defaultChild as apigwy.CfnStage;
+    stage.accessLogSettings = {
+      destinationArn: apiAccessLogs.logGroupArn,
+      format: JSON.stringify({
+        requestId: '$context.requestId',
+        userAgent: '$context.identity.userAgent',
+        sourceIp: '$context.identity.sourceIp',
+        requestTime: '$context.requestTime',
+        requestTimeEpoch: '$context.requestTimeEpoch',
+        httpMethod: '$context.httpMethod',
+        path: '$context.path',
+        status: '$context.status',
+        protocol: '$context.protocol',
+        responseLength: '$context.responseLength',
+        domainName: '$context.domainName',
+      }),
+    };
+
+    // Create a logging role
+    // Tips: https://github.com/aws/aws-cdk/issues/11100
+    const apiGwyLogRole = new iam.Role(this, 'logs-role', {
+      managedPolicies: [
+        iam.ManagedPolicy.fromAwsManagedPolicyName(
+          'service-role/AmazonAPIGatewayPushToCloudWatchLogs',
+        ),
+      ],
+      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
+    });
+    apiAccessLogs.grantWrite(apiGwyLogRole);
+
+    //
+    // Let API Gateway accept requests using domainNameOrigin
+    // That is the origin URI that CloudFront uses for this gateway.
+    // The gateway will refuse the traffic if it doesn't have the
+    // domain name registered.
+    //
+    if (this._dnAppsOrigin !== undefined) {
+      const mappingAppsApis = new apigwy.ApiMapping(this, 'api-map-origin', {
+        api: this._httpApi,
+        domainName: this._dnAppsOrigin,
+      });
+      // 2021-12-12 - This should not be needed
+      mappingAppsApis.node.addDependency(this._dnAppsOrigin);
+      if (removalPolicy !== undefined) {
+        mappingAppsApis.applyRemovalPolicy(removalPolicy);
+      }
+    }
+
+    //
+    // Create the origin name for API Gateway
+    //
+    if (r53Zone !== undefined && this._dnAppsOrigin) {
+      const rrAppsOrigin = new r53.ARecord(this, 'origin-arecord', {
+        zone: r53Zone,
+        recordName: domainNameOrigin,
+        target: r53.RecordTarget.fromAlias(
+          new r53targets.ApiGatewayv2DomainProperties(
+            this._dnAppsOrigin.regionalDomainName,
+            this._dnAppsOrigin.regionalHostedZoneId,
+          ),
+        ),
+      });
+      if (removalPolicy !== undefined) {
+        rrAppsOrigin.applyRemovalPolicy(removalPolicy);
+      }
+    }
+  }
+}
diff --git a/packages/microapps-cdk/src/MicroAppsCF.ts b/packages/microapps-cdk/src/MicroAppsCF.ts
index d224a78b..d1f2e609 100644
--- a/packages/microapps-cdk/src/MicroAppsCF.ts
+++ b/packages/microapps-cdk/src/MicroAppsCF.ts
@@ -1,30 +1,83 @@
+import * as apigwy from '@aws-cdk/aws-apigatewayv2';
 import * as acm from '@aws-cdk/aws-certificatemanager';
 import * as cf from '@aws-cdk/aws-cloudfront';
 import * as cforigins from '@aws-cdk/aws-cloudfront-origins';
 import * as r53 from '@aws-cdk/aws-route53';
 import * as r53targets from '@aws-cdk/aws-route53-targets';
+import * as s3 from '@aws-cdk/aws-s3';
 import * as cdk from '@aws-cdk/core';
-import { IMicroAppsS3 } from './MicroAppsS3';
+import { reverseDomain } from './utils/ReverseDomain';
 
 export interface IMicroAppsCF {
   readonly cloudFrontDistro: cf.Distribution;
 }
 
 export interface MicroAppsCFProps {
-  readonly autoDeleteEverything: boolean;
-  readonly s3Exports: IMicroAppsS3;
-  readonly reverseDomainName: string;
-  readonly domainName: string;
-  readonly domainNameEdge: string;
-  readonly domainNameOrigin: string;
+  /**
+   * 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?: cdk.RemovalPolicy;
+
+  /**
+   * S3 bucket origin for deployed applications
+   */
+  readonly bucketAppsOrigin: cforigins.S3Origin;
+
+  /**
+   * S3 bucket for CloudFront logs
+   */
+  readonly bucketLogs?: s3.IBucket;
+
+  /**
+   * CloudFront Distribution domain name
+   *
+   * @example apps.pwrdrvr.com
+   * @default auto-assigned
+   */
+  readonly domainNameEdge?: string;
+
+  /**
+   * API Gateway custom origin domain name
+   *
+   * @example apps.pwrdrvr.com
+   * @default - retrieved from httpApi, if possible
+   */
+  readonly domainNameOrigin?: string;
+
+  /**
+   * API Gateway v2 HTTP API for apps
+   */
+  readonly httpApi: apigwy.HttpApi;
 
-  readonly assetNameRoot: string;
-  readonly assetNameSuffix: string;
+  /**
+   * Optional asset name root
+   *
+   * @example microapps
+   * @default - resource names auto assigned
+   */
+  readonly assetNameRoot?: string;
 
-  readonly certEdge: acm.ICertificate;
+  /**
+   * Optional asset name suffix
+   *
+   * @example -dev-pr-12
+   * @default none
+   */
+  readonly assetNameSuffix?: string;
 
-  readonly r53ZoneName: string;
-  readonly r53ZoneID: string;
+  /**
+   * ACM Certificate that covers `domainNameEdge` name
+   */
+  readonly certEdge?: acm.ICertificate;
+
+  /**
+   * Route53 zone in which to create optional `domainNameEdge` record
+   */
+  readonly r53Zone?: r53.IHostedZone;
 }
 
 export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
@@ -46,47 +99,74 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
       throw new Error('props must be set');
     }
 
+    if (
+      (props.r53Zone === undefined && props.domainNameEdge !== undefined) ||
+      (props.r53Zone !== undefined && props.domainNameEdge === undefined)
+    ) {
+      throw new Error('If either of r53Zone or domainNameEdge are set then the other must be set');
+    }
+
     const {
-      s3Exports,
       domainNameEdge,
       domainNameOrigin,
-      reverseDomainName,
-      autoDeleteEverything,
+      httpApi,
+      removalPolicy,
       certEdge,
       assetNameRoot,
       assetNameSuffix,
-      r53ZoneName,
-      r53ZoneID,
+      r53Zone,
+      bucketLogs,
+      bucketAppsOrigin,
     } = props;
 
     //
     // CloudFront Distro
     //
-    const apiGwyOrigin = new cforigins.HttpOrigin(domainNameOrigin, {
+    let httpOriginFQDN: string = 'invalid.pwrdrvr.com';
+    let apigwyOriginPolicy: cf.IOriginRequestPolicy = cf.OriginRequestPolicy.ALL_VIEWER;
+    if (domainNameOrigin !== undefined) {
+      // When using a custom domain we can use the default apigwy origin policy of
+      // ALL_VIEWER (all header, all cookies, all query strings)
+      httpOriginFQDN = domainNameOrigin;
+    } else {
+      // When not using a custom domain name we must limit down the origin policy to
+      // prevent it from passing the Host header (distribution_id.cloudfront.net) to
+      // apigwy which will then reject it with a 403 because it does not match the
+      // execute-api name that apigwhy is expecting.
+      apigwyOriginPolicy = new cf.OriginRequestPolicy(this, 'apigwy-origin-policy', {
+        comment: assetNameRoot ? `${assetNameRoot}-apigwy${assetNameSuffix}` : undefined,
+        cookieBehavior: cf.OriginRequestCookieBehavior.all(),
+        queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(),
+        headerBehavior: cf.OriginRequestHeaderBehavior.allowList('user-agent', 'referer'),
+      });
+      httpOriginFQDN = `${httpApi.apiId}.execute-api.${cdk.Aws.REGION}.amazonaws.com`;
+    }
+    const apiGwyOrigin = new cforigins.HttpOrigin(httpOriginFQDN, {
       protocolPolicy: cf.OriginProtocolPolicy.HTTPS_ONLY,
       originSslProtocols: [cf.OriginSslPolicy.TLS_V1_2],
     });
-    this._cloudFrontDistro = new cf.Distribution(this, 'microapps-cloudfront', {
-      comment: `${assetNameRoot}${assetNameSuffix}`,
-      domainNames: [domainNameEdge],
+    this._cloudFrontDistro = new cf.Distribution(this, 'cft', {
+      comment: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : domainNameEdge,
+      domainNames: domainNameEdge !== undefined ? [domainNameEdge] : undefined,
       certificate: certEdge,
       httpVersion: cf.HttpVersion.HTTP2,
       defaultBehavior: {
         allowedMethods: cf.AllowedMethods.ALLOW_ALL,
         cachePolicy: cf.CachePolicy.CACHING_DISABLED,
         compress: true,
-        originRequestPolicy: cf.OriginRequestPolicy.ALL_VIEWER,
+        originRequestPolicy: apigwyOriginPolicy,
         origin: apiGwyOrigin,
         viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
       },
       enableIpv6: true,
       priceClass: cf.PriceClass.PRICE_CLASS_100,
-      enableLogging: true,
-      logBucket: props.s3Exports.bucketLogs,
-      logFilePrefix: `${reverseDomainName}/cloudfront-raw/`,
+      logBucket: bucketLogs,
+      logFilePrefix: props.domainNameEdge
+        ? `${reverseDomain(props.domainNameEdge)}/cloudfront-raw/`
+        : undefined,
     });
-    if (autoDeleteEverything) {
-      this._cloudFrontDistro.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (removalPolicy !== undefined) {
+      this._cloudFrontDistro.applyRemovalPolicy(removalPolicy);
     }
 
     //
@@ -96,7 +176,7 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
       allowedMethods: cf.AllowedMethods.ALLOW_ALL,
       cachePolicy: cf.CachePolicy.CACHING_DISABLED,
       compress: true,
-      originRequestPolicy: cf.OriginRequestPolicy.ALL_VIEWER,
+      originRequestPolicy: apigwyOriginPolicy,
       viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
     };
     const s3Behavior: cf.AddBehaviorOptions = {
@@ -108,8 +188,9 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
     };
     const apiGwyVersionRootBehavior: cf.AddBehaviorOptions = {
       allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
-      cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
+      cachePolicy: cf.CachePolicy.CACHING_DISABLED,
       compress: true,
+      originRequestPolicy: apigwyOriginPolicy,
       viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
     };
 
@@ -120,27 +201,24 @@ export class MicroAppsCF extends cdk.Construct implements IMicroAppsCF {
     // Let everything else fall through to the API Gateway
     //
     this._cloudFrontDistro.addBehavior('/*/*/api/*', apiGwyOrigin, apiGwyBehavior);
-    this._cloudFrontDistro.addBehavior('/*/*/static/*', s3Exports.bucketAppsOrigin, s3Behavior);
-    this._cloudFrontDistro.addBehavior('/*/*/*.*', s3Exports.bucketAppsOrigin, s3Behavior);
+    this._cloudFrontDistro.addBehavior('/*/*/static/*', bucketAppsOrigin, s3Behavior);
+    this._cloudFrontDistro.addBehavior('/*/*/*.*', bucketAppsOrigin, s3Behavior);
     this._cloudFrontDistro.addBehavior('/*/*/', apiGwyOrigin, apiGwyVersionRootBehavior);
 
     //
     // Create the edge name for the CloudFront distro
     //
 
-    const zone = r53.HostedZone.fromHostedZoneAttributes(this, 'microapps-zone', {
-      zoneName: r53ZoneName,
-      hostedZoneId: r53ZoneID,
-    });
-
-    const rrAppsEdge = new r53.RecordSet(this, 'microapps-edge-arecord', {
-      recordName: domainNameEdge,
-      recordType: r53.RecordType.A,
-      target: r53.RecordTarget.fromAlias(new r53targets.CloudFrontTarget(this._cloudFrontDistro)),
-      zone,
-    });
-    if (autoDeleteEverything) {
-      rrAppsEdge.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (r53Zone !== undefined) {
+      const rrAppsEdge = new r53.RecordSet(this, 'edge-arecord', {
+        recordName: domainNameEdge,
+        recordType: r53.RecordType.A,
+        target: r53.RecordTarget.fromAlias(new r53targets.CloudFrontTarget(this._cloudFrontDistro)),
+        zone: r53Zone,
+      });
+      if (removalPolicy !== undefined) {
+        rrAppsEdge.applyRemovalPolicy(removalPolicy);
+      }
     }
   }
 }
diff --git a/packages/microapps-cdk/src/MicroAppsS3.ts b/packages/microapps-cdk/src/MicroAppsS3.ts
index 10111225..c3c82b5f 100644
--- a/packages/microapps-cdk/src/MicroAppsS3.ts
+++ b/packages/microapps-cdk/src/MicroAppsS3.ts
@@ -4,28 +4,78 @@ import * as s3 from '@aws-cdk/aws-s3';
 import * as cdk from '@aws-cdk/core';
 
 export interface IMicroAppsS3 {
+  /**
+   * S3 bucket for deployed applications
+   */
   readonly bucketApps: s3.IBucket;
-  readonly bucketAppsName: string;
+
+  /**
+   * CloudFront Origin Access Identity for the deployed applications bucket
+   */
   readonly bucketAppsOAI: cf.OriginAccessIdentity;
+
+  /**
+   * CloudFront Origin for the deployed applications bucket
+   */
   readonly bucketAppsOrigin: cforigins.S3Origin;
+
+  /**
+   * S3 bucket for staged applications (prior to deploy)
+   */
   readonly bucketAppsStaging: s3.IBucket;
-  readonly bucketAppsStagingName: string;
+
+  /**
+   * S3 bucket for CloudFront logs
+   */
   readonly bucketLogs: s3.IBucket;
 }
 
 export interface MicroAppsS3Props {
   /**
-   * Duration before stack is automatically deleted.
-   * Requires that autoDeleteEverything be set to true.
+   * 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?: cdk.RemovalPolicy;
+
+  /**
+   * S3 deployed apps bucket name
+   *
+   * @default auto-assigned
+   */
+  readonly bucketAppsName?: string;
+
+  /**
+   * S3 staging apps bucket name
+   *
+   * @default auto-assigned
+   */
+  readonly bucketAppsStagingName?: string;
+
+  /**
+   * S3 logs bucket name
    *
-   * @default false
+   * @default auto-assigned
    */
-  readonly autoDeleteEverything?: boolean;
+  readonly bucketLogsName?: string;
 
-  readonly reverseDomainName: string;
+  /**
+   * Optional asset name root
+   *
+   * @example microapps
+   * @default - resource names auto assigned
+   */
+  readonly assetNameRoot?: string;
 
-  readonly assetNameRoot: string;
-  readonly assetNameSuffix: string;
+  /**
+   * Optional asset name suffix
+   *
+   * @example -dev-pr-12
+   * @default none
+   */
+  readonly assetNameSuffix?: string;
 }
 
 export class MicroAppsS3 extends cdk.Construct implements IMicroAppsS3 {
@@ -34,11 +84,6 @@ export class MicroAppsS3 extends cdk.Construct implements IMicroAppsS3 {
     return this._bucketApps;
   }
 
-  private _bucketAppsName: string;
-  public get bucketAppsName(): string {
-    return this._bucketAppsName;
-  }
-
   private _bucketAppsOAI: cf.OriginAccessIdentity;
   public get bucketAppsOAI(): cf.OriginAccessIdentity {
     return this._bucketAppsOAI;
@@ -54,11 +99,6 @@ export class MicroAppsS3 extends cdk.Construct implements IMicroAppsS3 {
     return this._bucketAppsStaging;
   }
 
-  private _bucketAppsStagingName: string;
-  public get bucketAppsStagingName(): string {
-    return this._bucketAppsStagingName;
-  }
-
   private _bucketLogs: s3.IBucket;
   public get bucketLogs(): s3.IBucket {
     return this._bucketLogs;
@@ -77,56 +117,43 @@ export class MicroAppsS3 extends cdk.Construct implements IMicroAppsS3 {
       throw new Error('props must be set');
     }
 
-    const {
-      autoDeleteEverything = false,
-      reverseDomainName,
-      assetNameRoot,
-      assetNameSuffix,
-    } = props;
-
-    // Use Auto-Delete S3Bucket for PRs
-    this._bucketAppsName = `${reverseDomainName}-${assetNameRoot}${assetNameSuffix}`;
-    this._bucketAppsStagingName = `${reverseDomainName}-${assetNameRoot}-staging${assetNameSuffix}`;
+    const { removalPolicy, assetNameRoot, assetNameSuffix } = props;
 
-    const s3RemovalPolicy = autoDeleteEverything
-      ? cdk.RemovalPolicy.DESTROY
-      : cdk.RemovalPolicy.RETAIN;
-    const s3AutoDeleteItems = autoDeleteEverything;
+    // Use Auto-Delete S3Bucket if removal policy is DESTROY
+    const s3AutoDeleteItems = removalPolicy === cdk.RemovalPolicy.DESTROY;
 
     //
     // S3 Bucket for Logging - Usable by many stacks
     //
-    this._bucketLogs = new s3.Bucket(this, 'microapps-logs', {
-      bucketName: `${reverseDomainName}-${assetNameRoot}-logs${assetNameSuffix}`,
+    this._bucketLogs = new s3.Bucket(this, 'logs', {
+      bucketName: props.bucketLogsName,
       autoDeleteObjects: s3AutoDeleteItems,
-      removalPolicy: s3RemovalPolicy,
+      removalPolicy,
     });
 
     //
     // S3 Buckets for Apps
     //
-    this._bucketApps = new s3.Bucket(this, 'microapps-apps', {
-      bucketName: this._bucketAppsName,
+    this._bucketApps = new s3.Bucket(this, 'apps', {
+      bucketName: props.bucketAppsName,
       autoDeleteObjects: s3AutoDeleteItems,
-      removalPolicy: s3RemovalPolicy,
+      removalPolicy,
     });
-    this._bucketAppsStaging = new s3.Bucket(this, 'microapps-apps-staging', {
-      bucketName: this._bucketAppsStagingName,
+    this._bucketAppsStaging = new s3.Bucket(this, 'staging', {
+      bucketName: props.bucketAppsStagingName,
       autoDeleteObjects: s3AutoDeleteItems,
-      removalPolicy: s3RemovalPolicy,
+      removalPolicy,
     });
 
     // Create S3 Origin Identity
-    this._bucketAppsOAI = new cf.OriginAccessIdentity(this, 'microapps-oai', {
-      comment: `${assetNameRoot}${assetNameSuffix}`,
+    this._bucketAppsOAI = new cf.OriginAccessIdentity(this, 'oai', {
+      comment: assetNameRoot !== undefined ? `${assetNameRoot}${assetNameSuffix}` : undefined,
     });
-    if (autoDeleteEverything) {
-      this._bucketAppsOAI.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (removalPolicy !== undefined) {
+      this._bucketAppsOAI.applyRemovalPolicy(removalPolicy);
     }
 
-    //
-    // Add Origins
-    //
+    // Add Origin for CloudFront
     this._bucketAppsOrigin = new cforigins.S3Origin(this._bucketApps, {
       originAccessIdentity: this.bucketAppsOAI,
     });
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index fddf7f3e..d5a6a5cd 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -2,96 +2,118 @@ import { existsSync } from 'fs';
 import * as path from 'path';
 import * as apigwy from '@aws-cdk/aws-apigatewayv2';
 import * as apigwyint from '@aws-cdk/aws-apigatewayv2-integrations';
-import * as acm from '@aws-cdk/aws-certificatemanager';
+import * as cf from '@aws-cdk/aws-cloudfront';
 import * as dynamodb from '@aws-cdk/aws-dynamodb';
 import * as iam from '@aws-cdk/aws-iam';
 import * as lambda from '@aws-cdk/aws-lambda';
 import * as lambdaNodejs from '@aws-cdk/aws-lambda-nodejs';
 import * as logs from '@aws-cdk/aws-logs';
-import * as r53 from '@aws-cdk/aws-route53';
-import * as r53targets from '@aws-cdk/aws-route53-targets';
 import * as s3 from '@aws-cdk/aws-s3';
 import * as cdk from '@aws-cdk/core';
-import { IMicroAppsCF } from './MicroAppsCF';
-import { IMicroAppsS3 } from './MicroAppsS3';
 
-export interface MicroAppsSvcsStackProps {
-  readonly cfStackExports: IMicroAppsCF;
-  readonly s3Exports: IMicroAppsS3;
+export interface MicroAppsSvcsProps {
+  /**
+   * 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?: cdk.RemovalPolicy;
 
-  readonly appEnv: string;
-  readonly autoDeleteEverything: boolean;
-  readonly reverseDomainName: string;
-  readonly domainName: string;
-  readonly domainNameEdge: string;
-  readonly domainNameOrigin: string;
+  /**
+   * S3 bucket for deployed applications
+   */
+  readonly bucketApps: s3.IBucket;
 
-  readonly assetNameRoot: string;
-  readonly assetNameSuffix: string;
+  /**
+   * CloudFront Origin Access Identity for the deployed applications bucket
+   */
+  readonly bucketAppsOAI: cf.OriginAccessIdentity;
 
-  readonly certOrigin: acm.ICertificate;
+  /**
+   * S3 bucket for staged applications (prior to deploy)
+   */
+  readonly bucketAppsStaging: s3.IBucket;
 
-  readonly r53ZoneName: string;
-  readonly r53ZoneID: string;
+  /**
+   * API Gateway v2 HTTP for Router and app
+   */
+  readonly httpApi: apigwy.HttpApi;
+
+  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;
 
   readonly s3StrictBucketPolicy?: boolean;
   readonly s3PolicyBypassAROAs?: string[];
   readonly s3PolicyBypassPrincipalARNs?: string[];
-
-  readonly account: string;
-  readonly region: string;
 }
 
 export interface IMicroAppsSvcs {
-  readonly dnAppsOrigin: apigwy.DomainName;
+  /**
+   * DynamoDB table used by Router, Deployer, and Release console app
+   */
   readonly table: dynamodb.ITable;
+
+  /**
+   * Lambda function for the Deployer
+   */
+  readonly deployerFunc: lambda.IFunction;
 }
 
 export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
-  private _dnAppsOrigin: apigwy.DomainName;
-  public get dnAppsOrigin(): apigwy.DomainName {
-    return this._dnAppsOrigin;
-  }
-
   private _table: dynamodb.Table;
   public get table(): dynamodb.ITable {
     return this._table;
   }
 
+  private _deployerFunc: lambda.Function;
+  public get deployerFunc(): lambda.IFunction {
+    return this._deployerFunc;
+  }
+
   /**
-   * MicroApps - Create just API Gateway and Lambda resources.
+   * MicroApps - Create Lambda resources, DynamoDB, and grant S3 privs
    * @param scope
    * @param id
    * @param props
    */
-  constructor(scope: cdk.Construct, id: string, props?: MicroAppsSvcsStackProps) {
+  constructor(scope: cdk.Construct, id: string, props?: MicroAppsSvcsProps) {
     super(scope, id);
 
     if (props === undefined) {
       throw new Error('props cannot be undefined');
     }
 
-    const { bucketApps, bucketAppsName, bucketAppsOAI, bucketAppsStaging, bucketAppsStagingName } =
-      props.s3Exports;
     const {
-      r53ZoneID,
-      r53ZoneName,
+      bucketApps,
+      bucketAppsOAI,
+      bucketAppsStaging,
       s3PolicyBypassAROAs = [],
       s3PolicyBypassPrincipalARNs = [],
       s3StrictBucketPolicy = false,
-      autoDeleteEverything,
       appEnv,
-      domainNameEdge,
-      domainNameOrigin,
-      certOrigin,
-      account,
-      region,
+      httpApi,
+      removalPolicy,
       assetNameRoot,
       assetNameSuffix,
     } = props;
 
-    const apigatewayName = `${assetNameRoot}${assetNameSuffix}`;
-
     if (s3StrictBucketPolicy === true) {
       if (s3PolicyBypassAROAs.length === 0 && s3PolicyBypassPrincipalARNs.length === 0) {
         throw new Error(
@@ -103,8 +125,8 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     //
     // DynamoDB Table
     //
-    this._table = new dynamodb.Table(this, 'microapps-router-table', {
-      tableName: `${assetNameRoot}${assetNameSuffix}`,
+    this._table = new dynamodb.Table(this, 'table', {
+      tableName: assetNameRoot ? `${assetNameRoot}${assetNameSuffix}` : undefined,
       billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
       partitionKey: {
         name: 'PK',
@@ -114,10 +136,8 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         name: 'SK',
         type: dynamodb.AttributeType.STRING,
       },
+      removalPolicy,
     });
-    if (autoDeleteEverything) {
-      this._table.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
 
     //
     // Router Lambda Function
@@ -126,7 +146,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     // Create Router Lambda Function
     let routerFunc: lambda.Function;
     const routerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
-      functionName: `${assetNameRoot}-router${assetNameSuffix}`,
+      functionName: assetNameRoot ? `${assetNameRoot}-router${assetNameSuffix}` : undefined,
       memorySize: 1769,
       logRetention: logs.RetentionDays.ONE_MONTH,
       runtime: lambda.Runtime.NODEJS_14_X,
@@ -137,31 +157,34 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
       },
     };
-    if (existsSync(`${path.resolve(__dirname)}/../dist/microapps-router/index.js`)) {
+    if (
+      process.env.NODE_ENV === 'test' &&
+      existsSync(path.join(__dirname, '..', '..', 'microapps-router', 'dist', 'index.js'))
+    ) {
       // This is for local dev
-      routerFunc = new lambda.Function(this, 'microapps-router-func', {
-        code: lambda.Code.fromAsset(`${path.resolve(__dirname)}/../dist/microapps-router/`),
+      routerFunc = new lambda.Function(this, 'router-func', {
+        code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-router', 'dist')),
         handler: 'index.handler',
         ...routerFuncProps,
       });
-    } else if (existsSync(`${path.resolve(__dirname)}/microapps-router/index.js`)) {
+    } else if (existsSync(path.join(__dirname, 'microapps-router', 'index.js'))) {
       // This is for built apps packaged with the CDK construct
-      routerFunc = new lambda.Function(this, 'microapps-router-func', {
-        code: lambda.Code.fromAsset(`${path.resolve(__dirname)}/microapps-router/`),
+      routerFunc = new lambda.Function(this, 'router-func', {
+        code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-router')),
         handler: 'index.handler',
         ...routerFuncProps,
       });
     } else {
       // Create Router Lambda Layer
-      const routerDataFiles = new lambda.LayerVersion(this, 'microapps-router-layer', {
-        code: lambda.Code.fromAsset('./packages/microapps-router/templates/'),
+      const routerDataFiles = new lambda.LayerVersion(this, 'router-templates', {
+        code: lambda.Code.fromAsset(
+          path.join(__dirname, '..', '..', 'microapps-router', 'templates'),
+        ),
+        removalPolicy,
       });
-      if (autoDeleteEverything) {
-        routerDataFiles.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-      }
 
-      routerFunc = new lambdaNodejs.NodejsFunction(this, 'microapps-router-func', {
-        entry: './packages/microapps-router/src/index.ts',
+      routerFunc = new lambdaNodejs.NodejsFunction(this, 'router-func', {
+        entry: path.join(__dirname, '..', '..', 'microapps-router', 'src', 'index.ts'),
         handler: 'handler',
         bundling: {
           minify: true,
@@ -171,8 +194,8 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         ...routerFuncProps,
       });
     }
-    if (autoDeleteEverything) {
-      routerFunc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (removalPolicy !== undefined) {
+      routerFunc.applyRemovalPolicy(removalPolicy);
     }
     const policyReadTarget = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
@@ -186,117 +209,17 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       this._table.grant(router, 'dynamodb:DescribeTable');
     }
 
-    // TODO: Add Last Route for /*/{proxy+}
-    // Note: That might not work, may need a Behavior in CloudFront
-    //       or a Lambda @ Edge function that detects these and routes
-    //       to origin Lambda Router function.
-
-    //
-    // APIGateway domain names for CloudFront and origin
-    //
-
-    // Create Custom Domains for API Gateway
-    const dnAppsEdge = new apigwy.DomainName(this, 'microapps-apps-edge-dn', {
-      domainName: domainNameEdge,
-      certificate: certOrigin,
-    });
-    if (autoDeleteEverything) {
-      dnAppsEdge.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
-    this._dnAppsOrigin = new apigwy.DomainName(this, 'microapps-apps-origin-dn', {
-      domainName: domainNameOrigin,
-      certificate: certOrigin,
-    });
-    if (autoDeleteEverything) {
-      this._dnAppsOrigin.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
-
-    // Create an integration for the Router
-    // Do this here since it's the default route
-    const intRouter = new apigwyint.LambdaProxyIntegration({
-      handler: routerFunc,
-    });
-
-    // Create APIGateway for the Edge name
-    const httpApiDomainMapping: apigwy.DomainMappingOptions = {
-      domainName: dnAppsEdge,
-    };
-    const httpApi = new apigwy.HttpApi(this, 'microapps-api', {
-      defaultDomainMapping: httpApiDomainMapping,
-      defaultIntegration: intRouter,
-      apiName: apigatewayName,
-    });
-    if (autoDeleteEverything) {
-      httpApi.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
-
-    // Enable access logs on API Gateway
-    const apiAccessLogs = new logs.LogGroup(this, 'microapps-api-logs', {
-      logGroupName: `/aws/apigwy/${apigatewayName}`,
-      retention: logs.RetentionDays.TWO_WEEKS,
-    });
-    if (autoDeleteEverything) {
-      apiAccessLogs.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
-    const stage = httpApi.defaultStage?.node.defaultChild as apigwy.CfnStage;
-    stage.accessLogSettings = {
-      destinationArn: apiAccessLogs.logGroupArn,
-      format: JSON.stringify({
-        requestId: '$context.requestId',
-        userAgent: '$context.identity.userAgent',
-        sourceIp: '$context.identity.sourceIp',
-        requestTime: '$context.requestTime',
-        requestTimeEpoch: '$context.requestTimeEpoch',
-        httpMethod: '$context.httpMethod',
-        path: '$context.path',
-        status: '$context.status',
-        protocol: '$context.protocol',
-        responseLength: '$context.responseLength',
-        domainName: '$context.domainName',
-      }),
-    };
-
-    // Create a logging role
-    // Tips: https://github.com/aws/aws-cdk/issues/11100
-    const apiGwyLogRole = new iam.Role(this, 'microapps-api-logs-role', {
-      managedPolicies: [
-        iam.ManagedPolicy.fromAwsManagedPolicyName(
-          'service-role/AmazonAPIGatewayPushToCloudWatchLogs',
-        ),
-      ],
-      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
-    });
-    apiAccessLogs.grantWrite(apiGwyLogRole);
-
-    // Add default route on API Gateway to point to the router
-    // httpApi.addRoutes({
-    //   path: '$default',
-    //   integration: intRouter,
-    // });
-
-    //
-    // Let API Gateway accept requests using domainNameOrigin
-    // That is the origin URI that CloudFront uses for this gateway.
-    // The gateway will refuse the traffic if it doesn't have the
-    // domain name registered.
-    //
-    const mappingAppsApis = new apigwy.ApiMapping(this, 'microapps-api-mapping-origin', {
-      api: httpApi,
-      domainName: this.dnAppsOrigin,
-    });
-    mappingAppsApis.node.addDependency(this.dnAppsOrigin);
-    if (autoDeleteEverything) {
-      mappingAppsApis.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
-
     //
     // Deployer Lambda Function
     //
 
     // Create Deployer Lambda Function
-    const iamRoleUploadName = `${assetNameRoot}-deployer-upload${assetNameSuffix}`;
-    const deployerFuncName = `${assetNameRoot}-deployer${assetNameSuffix}`;
-    let deployerFunc: lambda.Function;
+    const iamRoleUploadName = assetNameRoot
+      ? `${assetNameRoot}-deployer-upload${assetNameSuffix}`
+      : undefined;
+    const deployerFuncName = assetNameRoot
+      ? `${assetNameRoot}-deployer${assetNameSuffix}`
+      : undefined;
     const deployerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
       functionName: deployerFuncName,
       memorySize: 1769,
@@ -307,29 +230,31 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         NODE_ENV: appEnv,
         APIGWY_ID: httpApi.httpApiId,
         DATABASE_TABLE_NAME: this._table.tableName,
-        FILESTORE_STAGING_BUCKET: bucketAppsStagingName,
-        FILESTORE_DEST_BUCKET: bucketAppsName,
-        UPLOAD_ROLE_NAME: iamRoleUploadName,
+        FILESTORE_STAGING_BUCKET: bucketAppsStaging.bucketName,
+        FILESTORE_DEST_BUCKET: bucketApps.bucketName,
         AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
       },
     };
-    if (existsSync(`${path.resolve(__dirname)}/../dist/microapps-deployer/index.js`)) {
+    if (
+      process.env.NODE_ENV === 'test' &&
+      existsSync(path.join(__dirname, '..', '..', 'microapps-deployer', 'dist', 'index.js'))
+    ) {
       // This is for local dev
-      deployerFunc = new lambda.Function(this, 'microapps-deployer-func', {
-        code: lambda.Code.fromAsset(`${path.resolve(__dirname)}/../dist/microapps-deployer/`),
+      this._deployerFunc = new lambda.Function(this, 'deployer-func', {
+        code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-deployer', 'dist')),
         handler: 'index.handler',
         ...deployerFuncProps,
       });
-    } else if (existsSync(`${path.resolve(__dirname)}/microapps-deployer/index.js`)) {
+    } else if (existsSync(path.join(__dirname, 'microapps-deployer', 'index.js'))) {
       // This is for built apps packaged with the CDK construct
-      deployerFunc = new lambda.Function(this, 'microapps-deployer-func', {
-        code: lambda.Code.fromAsset(`${path.resolve(__dirname)}/microapps-deployer/`),
+      this._deployerFunc = new lambda.Function(this, 'deployer-func', {
+        code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-deployer')),
         handler: 'index.handler',
         ...deployerFuncProps,
       });
     } else {
-      deployerFunc = new lambdaNodejs.NodejsFunction(this, 'microapps-deployer-func', {
-        entry: './packages/microapps-deployer/src/index.ts',
+      this._deployerFunc = new lambdaNodejs.NodejsFunction(this, 'deployer-func', {
+        entry: path.join(__dirname, '..', '..', 'microapps-deployer', 'src', 'index.ts'),
         handler: 'handler',
         bundling: {
           minify: true,
@@ -338,19 +263,19 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         ...deployerFuncProps,
       });
     }
-    if (autoDeleteEverything) {
-      deployerFunc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
+    if (removalPolicy !== undefined) {
+      this._deployerFunc.applyRemovalPolicy(removalPolicy);
     }
     // Give the Deployer access to DynamoDB table
-    this._table.grantReadWriteData(deployerFunc);
-    this._table.grant(deployerFunc, 'dynamodb:DescribeTable');
+    this._table.grantReadWriteData(this._deployerFunc);
+    this._table.grant(this._deployerFunc, 'dynamodb:DescribeTable');
 
     //
     // Deloyer upload temp role
     // Deployer assumes this role with a limited policy to generate
     // an STS temp token to return to microapps-publish for the upload.
     //
-    const iamRoleUpload = new iam.Role(this, 'microapps-deployer-upload-role', {
+    const iamRoleUpload = new iam.Role(this, 'deployer-upload-role', {
       roleName: iamRoleUploadName,
       inlinePolicies: {
         uploadPolicy: new iam.PolicyDocument({
@@ -366,8 +291,9 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
           ],
         }),
       },
-      assumedBy: deployerFunc.grantPrincipal,
+      assumedBy: this._deployerFunc.grantPrincipal,
     });
+    this._deployerFunc.addEnvironment('UPLOAD_ROLE_NAME', iamRoleUpload.roleName);
 
     //
     // Update S3 permissions
@@ -395,7 +321,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         ),
         new iam.AccountRootPrincipal(),
         ...s3PolicyBypassArnPrincipals,
-        deployerFunc.grantPrincipal,
+        this._deployerFunc.grantPrincipal,
       ],
       notResources: [
         `${bucketApps.bucketArn}/\${aws:PrincipalTag/microapp-name}/*`,
@@ -406,11 +332,11 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         // StringNotLike: {'aws:'}
       },
     });
-    if (autoDeleteEverything) {
+    if (removalPolicy !== undefined) {
       policyDenyPrefixOutsideTag.addCondition(
         // Allows the DeletableBucket Lambda to delete items in the buckets
         'StringNotLike',
-        { 'aws:PrincipalTag/application': `${assetNameRoot}-core*` },
+        { 'aws:PrincipalTag/application': `${cdk.Stack.of(this).stackName}-core*` },
       );
     }
     const policyDenyMissingTag = new iam.PolicyStatement({
@@ -423,11 +349,11 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         ),
         new iam.AccountRootPrincipal(),
         // Exclude the Deployer Function directly
-        deployerFunc.grantPrincipal,
+        this._deployerFunc.grantPrincipal,
         // 2021-12-04 - Not 100% sure that this is actually needed...
         // Let's test this and remove if actually not necessary
         new iam.ArnPrincipal(
-          `arn:aws:sts::${account}:assumed-role/${deployerFunc.role?.roleName}/${deployerFunc.functionName}`,
+          `arn:aws:sts::${cdk.Aws.ACCOUNT_ID}:assumed-role/${this._deployerFunc.role?.roleName}/${this._deployerFunc.functionName}`,
         ),
         ...s3PolicyBypassArnPrincipals,
       ],
@@ -453,14 +379,14 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
         // To get the AROA with the AWS CLI:
         //   aws iam get-role --role-name ROLE-NAME
         //   aws iam get-user -–user-name USER-NAME
-        StringNotLike: { 'aws:userid': [account, ...s3PolicyBypassAROAMatches] },
+        StringNotLike: { 'aws:userid': [cdk.Aws.ACCOUNT_ID, ...s3PolicyBypassAROAMatches] },
       },
     });
-    if (autoDeleteEverything) {
+    if (removalPolicy !== undefined) {
       policyDenyMissingTag.addCondition(
         // Allows the DeletableBucket Lambda to delete items in the buckets
         'StringNotLike',
-        { 'aws:PrincipalTag/application': `${assetNameRoot}-core*` },
+        { 'aws:PrincipalTag/application': `${cdk.Stack.of(this).stackName}-core*` },
       );
     }
     const policyCloudFrontAccess = new iam.PolicyStatement({
@@ -476,7 +402,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     });
 
     if (bucketApps.policy === undefined) {
-      const document = new s3.BucketPolicy(this, 'CFPolicy', {
+      const document = new s3.BucketPolicy(this, 's3-policy', {
         bucket: bucketApps,
       }).document;
       document.addStatements(policyCloudFrontAccess);
@@ -501,7 +427,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       actions: ['s3:DeleteObject', 's3:GetObject', 's3:ListBucket'],
       resources: [`${bucketAppsStaging.bucketArn}/*`, bucketAppsStaging.bucketArn],
     });
-    deployerFunc.addToRolePolicy(policyReadListStaging);
+    this._deployerFunc.addToRolePolicy(policyReadListStaging);
 
     // Allow the Lambda to write to the target bucket and delete
     const policyReadWriteListTarget = new iam.PolicyStatement({
@@ -509,7 +435,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       actions: ['s3:GetObject', 's3:PutObject', 's3:ListBucket', 's3:DeleteObject'],
       resources: [`${bucketApps.bucketArn}/*`, bucketApps.bucketArn],
     });
-    deployerFunc.addToRolePolicy(policyReadWriteListTarget);
+    this._deployerFunc.addToRolePolicy(policyReadWriteListTarget);
 
     // Allow the deployer to get a temporary STS token
     const policyGetSTSToken = new iam.PolicyStatement({
@@ -517,7 +443,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       actions: ['sts:GetFederationToken'],
       resources: ['*'],
     });
-    deployerFunc.addToRolePolicy(policyGetSTSToken);
+    this._deployerFunc.addToRolePolicy(policyGetSTSToken);
 
     // Allow the deployer to assume the upload role
     const policyAssumeUpload = new iam.PolicyStatement({
@@ -525,7 +451,7 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
       actions: ['sts:AssumeRole'],
       resources: [iamRoleUpload.roleArn],
     });
-    deployerFunc.addToRolePolicy(policyAssumeUpload);
+    this._deployerFunc.addToRolePolicy(policyAssumeUpload);
 
     //
     // Give Deployer permissions to create routes and integrations
@@ -536,57 +462,45 @@ export class MicroAppsSvcs extends cdk.Construct implements IMicroAppsSvcs {
     const policyAPIList = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
       actions: ['apigateway:GET'],
-      resources: [`arn:aws:apigateway:${region}::/apis`],
+      resources: [`arn:aws:apigateway:${cdk.Aws.REGION}::/apis`],
     });
-    deployerFunc.addToRolePolicy(policyAPIList);
+    this._deployerFunc.addToRolePolicy(policyAPIList);
     // Grant full control over the API we created
     const policyAPIManage = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
       actions: ['apigateway:*'],
       resources: [
-        `arn:aws:apigateway:${region}:${account}:${httpApi.httpApiId}/*`,
-        `arn:aws:apigateway:${region}::/apis/${httpApi.httpApiId}/integrations/*`,
-        `arn:aws:apigateway:${region}::/apis/${httpApi.httpApiId}/integrations`,
-        `arn:aws:apigateway:${region}::/apis/${httpApi.httpApiId}/routes`,
-        `arn:aws:apigateway:${region}::/apis/${httpApi.httpApiId}/routes/*`,
+        `arn:aws:apigateway:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:${httpApi.httpApiId}/*`,
+        `arn:aws:apigateway:${cdk.Aws.REGION}::/apis/${httpApi.httpApiId}/integrations/*`,
+        `arn:aws:apigateway:${cdk.Aws.REGION}::/apis/${httpApi.httpApiId}/integrations`,
+        `arn:aws:apigateway:${cdk.Aws.REGION}::/apis/${httpApi.httpApiId}/routes`,
+        `arn:aws:apigateway:${cdk.Aws.REGION}::/apis/${httpApi.httpApiId}/routes/*`,
       ],
     });
-    deployerFunc.addToRolePolicy(policyAPIManage);
+    this._deployerFunc.addToRolePolicy(policyAPIManage);
     // Grant full control over lambdas that indicate they are microapps
     const policyAPIManageLambdas = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
       actions: ['lambda:*'],
       resources: [
-        `arn:aws:lambda:${region}:${account}:function:*`,
-        `arn:aws:lambda:${region}:${account}:function:*:*`,
+        `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:function:*`,
+        `arn:aws:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:function:*:*`,
       ],
       conditions: {
         StringEqualsIfExists: { 'aws:ResourceTag/microapp-managed': 'true' },
       },
     });
-    deployerFunc.addToRolePolicy(policyAPIManageLambdas);
+    this._deployerFunc.addToRolePolicy(policyAPIManageLambdas);
 
-    //
-    // Create the origin name for API Gateway
-    //
-
-    const zone = r53.HostedZone.fromHostedZoneAttributes(this, 'microapps-zone', {
-      zoneName: r53ZoneName,
-      hostedZoneId: r53ZoneID,
+    // Create an integration for the Router
+    // All traffic without another route goes to the Router
+    const intRouter = new apigwyint.LambdaProxyIntegration({
+      handler: routerFunc,
     });
-
-    const rrAppsOrigin = new r53.ARecord(this, 'microapps-origin-arecord', {
-      zone: zone,
-      recordName: domainNameOrigin,
-      target: r53.RecordTarget.fromAlias(
-        new r53targets.ApiGatewayv2DomainProperties(
-          this._dnAppsOrigin.regionalDomainName,
-          this._dnAppsOrigin.regionalHostedZoneId,
-        ),
-      ),
+    new apigwy.HttpRoute(this, 'route-default', {
+      httpApi,
+      routeKey: apigwy.HttpRouteKey.DEFAULT,
+      integration: intRouter,
     });
-    if (autoDeleteEverything) {
-      rrAppsOrigin.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
-    }
   }
 }
diff --git a/packages/microapps-cdk/src/index.ts b/packages/microapps-cdk/src/index.ts
index 9ed9b214..305cb7e7 100644
--- a/packages/microapps-cdk/src/index.ts
+++ b/packages/microapps-cdk/src/index.ts
@@ -1,4 +1,5 @@
 export * from './MicroApps';
+export * from './MicroAppsAPIGwy';
 export * from './MicroAppsCF';
 export * from './MicroAppsS3';
 export * from './MicroAppsSvcs';
diff --git a/packages/microapps-cdk/src/utils/ReverseDomain.ts b/packages/microapps-cdk/src/utils/ReverseDomain.ts
new file mode 100644
index 00000000..ebb86242
--- /dev/null
+++ b/packages/microapps-cdk/src/utils/ReverseDomain.ts
@@ -0,0 +1,10 @@
+/**
+ * Input like 'example.com.' will return as 'com.example'
+ */
+export function reverseDomain(domain: string): string {
+  let parts = domain.split('.').reverse();
+  if (parts[0] === '') {
+    parts = parts.slice(1);
+  }
+  return parts.join('.');
+}


Test patch:
diff --git a/packages/cdk/test/MicroApps.spec.ts b/packages/cdk/test/MicroApps.spec.ts
new file mode 100644
index 00000000..0c808021
--- /dev/null
+++ b/packages/cdk/test/MicroApps.spec.ts
@@ -0,0 +1,54 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import { App } from '@aws-cdk/core';
+import { MicroAppsStack } from '../lib/MicroApps';
+
+describe('MicroAppsStack', () => {
+  it('works with no params', () => {
+    const app = new App({});
+    const stackName = 'construct';
+    const stack = new MicroAppsStack(app, stackName, {});
+
+    expect(stack).toBeDefined();
+    const template = app.synth().getStackArtifact(stack.artifactId).template;
+    expect(template).toHaveResource('AWS::Lambda::Function');
+    expect(template).toHaveOutput({
+      exportName: `${stackName}-edge-domain-name`,
+      outputValue: { 'Fn::GetAtt': ['microappscft5FDF8AB8', 'DomainName'] },
+    });
+    // Not sure why this is giving a Value Ref
+    expect(template).toHaveOutput({
+      exportName: `${stackName}-dynamodb-table-name`,
+      outputValue: { Ref: 'microappssvcstable1C50DC7E' },
+    });
+  });
+
+  it('stable names with params', () => {
+    const app = new App({});
+    const stackName = 'microapps-stack-name';
+    const stack = new MicroAppsStack(app, 'construct', {
+      stackName,
+      domainNameEdge: 'appz.pwrdrvr.com',
+      domainNameOrigin: 'appz-origin.pwrdrvr.com',
+      certARNEdge: `arn:aws:acm:us-east-1:123456789012:certificate/26EA0E0D-C956-493C-B60D-76EB8C5BA32D`,
+      certARNOrigin: `arn:aws:acm:us-east-2:123456789012:certificate/B2ACCAE3-9023-444E-A3D0-F58C2740882C`,
+      r53ZoneID: '23523ASDG',
+      r53ZoneName: 'pwrdrvr.com',
+      assetNameRoot: 'microapps-random',
+      assetNameSuffix: '-pr-123',
+    });
+
+    expect(stack).toBeDefined();
+    const template = app.synth().getStackArtifact(stack.artifactId).template;
+    expect(template).toHaveResource('AWS::Lambda::Function');
+    expect(template).toHaveOutput({
+      exportName: `${stackName}-edge-domain-name`,
+      outputValue: 'appz.pwrdrvr.com',
+    });
+    // Not sure why this is giving a Value Ref
+    expect(template).toHaveOutput({
+      exportName: `${stackName}-dynamodb-table-name`,
+      outputValue: { Ref: 'microappssvcstable1C50DC7E' },
+    });
+  });
+});
diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts
new file mode 100644
index 00000000..9355128f
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroApps.test.ts
@@ -0,0 +1,25 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import { App, Stack } from '@aws-cdk/core';
+import { MicroApps } from '../src/MicroApps';
+
+describe('MicroApps', () => {
+  const app = new App({});
+
+  it('works with no params', () => {
+    const stack = new Stack(app, 'stack');
+    const construct = new MicroApps(stack, 'construct', {
+      appEnv: 'dev',
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.apigwy).toBeDefined();
+    expect(construct.cf).toBeDefined();
+    expect(construct.s3).toBeDefined();
+    expect(construct.svcs).toBeDefined();
+
+    expect(construct.node).toBeDefined();
+    // expect(stack).toHaveResource('AWS::S3::Bucket');
+    // expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+  });
+});
diff --git a/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts b/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts
new file mode 100644
index 00000000..c06d2a07
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroAppsAPIGwy.test.ts
@@ -0,0 +1,30 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import { App, Stack } from '@aws-cdk/core';
+import { MicroAppsAPIGwy } from '../src/MicroAppsAPIGwy';
+
+describe('MicroAppsAPIGwy', () => {
+  const app = new App({});
+
+  it('works with no params', () => {
+    const stack = new Stack(app, 'stack');
+    // const cloudFrontDistro = new cf.Distribution(stack, 'cf-distro', {
+    //   defaultBehavior: {
+    //     allowedMethods: cf.AllowedMethods.ALLOW_ALL,
+    //     cachePolicy: cf.CachePolicy.CACHING_DISABLED,
+    //     compress: true,
+    //     originRequestPolicy: cf.OriginRequestPolicy.ALL_VIEWER,
+    //     origin: bucketAppsOrigin, // Just a dummy to create this distro
+    //     viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
+    //   },
+    // });
+    const construct = new MicroAppsAPIGwy(stack, 'construct', {});
+
+    expect(construct).toBeDefined();
+    expect(construct.dnAppsOrigin).toBeUndefined();
+    expect(construct.httpApi).toBeDefined();
+    expect(construct.node).toBeDefined();
+
+    expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+  });
+});
diff --git a/packages/microapps-cdk/test/MicroAppsCF.test.ts b/packages/microapps-cdk/test/MicroAppsCF.test.ts
new file mode 100644
index 00000000..a99fa390
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroAppsCF.test.ts
@@ -0,0 +1,42 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import * as apigwy from '@aws-cdk/aws-apigatewayv2';
+import * as cf from '@aws-cdk/aws-cloudfront';
+import * as cforigins from '@aws-cdk/aws-cloudfront-origins';
+import * as s3 from '@aws-cdk/aws-s3';
+import { App, Stack } from '@aws-cdk/core';
+import { MicroAppsCF } from '../src/MicroAppsCF';
+
+describe('MicroAppsCF', () => {
+  const app = new App({});
+
+  it('works with no params', () => {
+    const stack = new Stack(app, 'stack');
+    const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
+      apiName: 'some-api',
+      // defaultDomainMapping: {
+      //   domainName: new apigwy.DomainName(stack, 'domain-name', {
+      //     domainName: 'apps.pwrdrvr.com',
+      //     certificate: new acm.Certificate(stack, 'cert', {
+      //       domainName: 'apps.pwrdrvr.com',
+      //     }),
+      //   }),
+      // },
+    });
+    const construct = new MicroAppsCF(stack, 'construct', {
+      httpApi,
+      bucketAppsOrigin: new cforigins.S3Origin(new s3.Bucket(stack, 'bucket-apps', {}), {
+        originAccessIdentity: new cf.OriginAccessIdentity(stack, 'oai', {}),
+      }),
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.cloudFrontDistro).toBeDefined();
+    expect(construct.node).toBeDefined();
+    // expect(stack).toHaveResource('AWS::S3::Bucket');
+    expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+
+    const template = app.synth().getStackArtifact(stack.artifactId).template;
+    expect(template).toHaveResource('AWS::CloudFront::Distribution');
+  });
+});
diff --git a/packages/microapps-cdk/test/MicroAppsS3.test.ts b/packages/microapps-cdk/test/MicroAppsS3.test.ts
new file mode 100644
index 00000000..5d82a924
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroAppsS3.test.ts
@@ -0,0 +1,24 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import { App, Stack } from '@aws-cdk/core';
+import { MicroAppsS3 } from '../src/MicroAppsS3';
+
+describe('MicroAppsS3', () => {
+  const app = new App({});
+
+  it('works with no params', () => {
+    const stack = new Stack(app, 'stack');
+    const construct = new MicroAppsS3(stack, 'construct', {});
+
+    expect(construct).toBeDefined();
+    expect(construct.bucketApps).toBeDefined();
+    expect(construct.bucketAppsStaging).toBeDefined();
+    expect(construct.bucketLogs).toBeDefined();
+    expect(construct.bucketAppsOAI).toBeDefined();
+    expect(construct.bucketAppsOrigin).toBeDefined();
+
+    expect(construct.node).toBeDefined();
+    // expect(stack).toHaveResource('AWS::S3::Bucket');
+    expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
+  });
+});
diff --git a/packages/microapps-cdk/test/MicroAppsSvcs.test.ts b/packages/microapps-cdk/test/MicroAppsSvcs.test.ts
new file mode 100644
index 00000000..72433fb5
--- /dev/null
+++ b/packages/microapps-cdk/test/MicroAppsSvcs.test.ts
@@ -0,0 +1,35 @@
+/// <reference types="jest" />
+import '@aws-cdk/assert/jest';
+import * as apigwy from '@aws-cdk/aws-apigatewayv2';
+import * as cf from '@aws-cdk/aws-cloudfront';
+import * as cforigins from '@aws-cdk/aws-cloudfront-origins';
+import * as s3 from '@aws-cdk/aws-s3';
+import { App, Stack } from '@aws-cdk/core';
+import { MicroAppsSvcs } from '../src/MicroAppsSvcs';
+
+describe('MicroAppsSvcs', () => {
+  const app = new App({});
+
+  it('works with no params', () => {
+    const stack = new Stack(app, 'stack');
+    const bucketAppsOAI = new cf.OriginAccessIdentity(stack, 'oai', {});
+    const bucketApps = new s3.Bucket(stack, 'bucket-apps', {});
+    const bucketAppsStaging = new s3.Bucket(stack, 'bucket-apps-staging', {});
+    const httpApi = new apigwy.HttpApi(stack, 'httpapi', {
+      apiName: 'some-api',
+    });
+    const construct = new MicroAppsSvcs(stack, 'construct', {
+      appEnv: 'dev',
+      bucketApps,
+      bucketAppsOAI,
+      bucketAppsStaging,
+      httpApi,
+    });
+
+    expect(construct).toBeDefined();
+    expect(construct.table).toBeDefined();
+
+    const template = app.synth().getStackArtifact(stack.artifactId).template;
+    expect(template).toHaveResource('AWS::Lambda::Function');
+  });
+});
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap
new file mode 100644
index 00000000..52556fc8
--- /dev/null
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsAPIGwy.test.ts.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MicroAppsAPIGwy works with no params 1`] = `
+Object {
+  "Resources": Object {
+    "constructgwyDefaultStageBB1CA110": Object {
+      "Properties": Object {
+        "AccessLogSettings": Object {
+          "DestinationArn": Object {
+            "Fn::GetAtt": Array [
+              "constructlogsBC77618C",
+              "Arn",
+            ],
+          },
+          "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"userAgent\\":\\"$context.identity.userAgent\\",\\"sourceIp\\":\\"$context.identity.sourceIp\\",\\"requestTime\\":\\"$context.requestTime\\",\\"requestTimeEpoch\\":\\"$context.requestTimeEpoch\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"path\\":\\"$context.path\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\",\\"domainName\\":\\"$context.domainName\\"}",
+        },
+        "ApiId": Object {
+          "Ref": "constructgwyE80472DA",
+        },
+        "AutoDeploy": true,
+        "StageName": "$default",
+      },
+      "Type": "AWS::ApiGatewayV2::Stage",
+    },
+    "constructgwyE80472DA": Object {
+      "Properties": Object {
+        "Name": "stack-microapps",
+        "ProtocolType": "HTTP",
+      },
+      "Type": "AWS::ApiGatewayV2::Api",
+    },
+    "constructlogsBC77618C": Object {
+      "DeletionPolicy": "Retain",
+      "Properties": Object {
+        "LogGroupName": "/aws/apigwy/stack-microapps",
+        "RetentionInDays": 14,
+      },
+      "Type": "AWS::Logs::LogGroup",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "constructlogsrole387D81E6": Object {
+      "Properties": Object {
+        "AssumeRolePolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": "sts:AssumeRole",
+              "Effect": "Allow",
+              "Principal": Object {
+                "Service": "apigateway.amazonaws.com",
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "ManagedPolicyArns": Array [
+          Object {
+            "Fn::Join": Array [
+              "",
+              Array [
+                "arn:",
+                Object {
+                  "Ref": "AWS::Partition",
+                },
+                ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+              ],
+            ],
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Role",
+    },
+    "constructlogsroleDefaultPolicyD4FA927F": Object {
+      "Properties": Object {
+        "PolicyDocument": Object {
+          "Statement": Array [
+            Object {
+              "Action": Array [
+                "logs:CreateLogStream",
+                "logs:PutLogEvents",
+              ],
+              "Effect": "Allow",
+              "Resource": Object {
+                "Fn::GetAtt": Array [
+                  "constructlogsBC77618C",
+                  "Arn",
+                ],
+              },
+            },
+          ],
+          "Version": "2012-10-17",
+        },
+        "PolicyName": "constructlogsroleDefaultPolicyD4FA927F",
+        "Roles": Array [
+          Object {
+            "Ref": "constructlogsrole387D81E6",
+          },
+        ],
+      },
+      "Type": "AWS::IAM::Policy",
+    },
+  },
+}
+`;
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap
new file mode 100644
index 00000000..2494e9e1
--- /dev/null
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsCF.test.ts.snap
@@ -0,0 +1,198 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MicroAppsCF works with no params 1`] = `
+Object {
+  "Resources": Object {
+    "bucketapps06013E35": Object {
+      "DeletionPolicy": "Retain",
+      "Type": "AWS::S3::Bucket",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "constructapigwyoriginpolicy9DEFDDA9": Object {
+      "Properties": Object {
+        "OriginRequestPolicyConfig": Object {
+          "CookiesConfig": Object {
+            "CookieBehavior": "all",
+          },
+          "HeadersConfig": Object {
+            "HeaderBehavior": "whitelist",
+            "Headers": Array [
+              "user-agent",
+              "referer",
+            ],
+          },
+          "Name": "stackconstructapigwyoriginpolicy94306769",
+          "QueryStringsConfig": Object {
+            "QueryStringBehavior": "all",
+          },
+        },
+      },
+      "Type": "AWS::CloudFront::OriginRequestPolicy",
+    },
+    "constructcftBE9A3354": Object {
+      "Properties": Object {
+        "DistributionConfig": Object {
+          "CacheBehaviors": Array [
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+                "PUT",
+                "PATCH",
+                "POST",
+                "DELETE",
+              ],
+              "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+              "Compress": true,
+              "OriginRequestPolicyId": Object {
+                "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+              },
+              "PathPattern": "/*/*/api/*",
+              "TargetOriginId": "stackconstructcftOrigin1D000029C",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
+              "Compress": true,
+              "OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
+              "PathPattern": "/*/*/static/*",
+              "TargetOriginId": "stackconstructcftOrigin2BE69FFBB",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
+              "Compress": true,
+              "OriginRequestPolicyId": "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf",
+              "PathPattern": "/*/*/*.*",
+              "TargetOriginId": "stackconstructcftOrigin2BE69FFBB",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+            Object {
+              "AllowedMethods": Array [
+                "GET",
+                "HEAD",
+                "OPTIONS",
+              ],
+              "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+              "Compress": true,
+              "OriginRequestPolicyId": Object {
+                "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+              },
+              "PathPattern": "/*/*/",
+              "TargetOriginId": "stackconstructcftOrigin1D000029C",
+              "ViewerProtocolPolicy": "redirect-to-https",
+            },
+          ],
+          "DefaultCacheBehavior": Object {
+            "AllowedMethods": Array [
+              "GET",
+              "HEAD",
+              "OPTIONS",
+              "PUT",
+              "PATCH",
+              "POST",
+              "DELETE",
+            ],
+            "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
+            "Compress": true,
+            "OriginRequestPolicyId": Object {
+              "Ref": "constructapigwyoriginpolicy9DEFDDA9",
+            },
+            "TargetOriginId": "stackconstructcftOrigin1D000029C",
+            "ViewerProtocolPolicy": "redirect-to-https",
+          },
+          "Enabled": true,
+          "HttpVersion": "http2",
+          "IPV6Enabled": true,
+          "Origins": Array [
+            Object {
+              "CustomOriginConfig": Object {
+                "OriginProtocolPolicy": "https-only",
+                "OriginSSLProtocols": Array [
+                  "TLSv1.2",
+                ],
+              },
+              "DomainName": Object {
+                "Fn::Join": Array [
+                  "",
+                  Array [
+                    Object {
+                      "Ref": "httpapi04200D5F",
+                    },
+                    ".execute-api.",
+                    Object {
+                      "Ref": "AWS::Region",
+                    },
+                    ".amazonaws.com",
+                  ],
+                ],
+              },
+              "Id": "stackconstructcftOrigin1D000029C",
+            },
+            Object {
+              "DomainName": Object {
+                "Fn::GetAtt": Array [
+                  "bucketapps06013E35",
+                  "RegionalDomainName",
+                ],
+              },
+              "Id": "stackconstructcftOrigin2BE69FFBB",
+              "S3OriginConfig": Object {
+                "OriginAccessIdentity": Object {
+                  "Fn::Join": Array [
+                    "",
+                    Array [
+                      "origin-access-identity/cloudfront/",
+                      Object {
+                        "Ref": "oai7A77E975",
+                      },
+                    ],
+                  ],
+                },
+              },
+            },
+          ],
+          "PriceClass": "PriceClass_100",
+        },
+      },
+      "Type": "AWS::CloudFront::Distribution",
+    },
+    "httpapi04200D5F": Object {
+      "Properties": Object {
+        "Name": "some-api",
+        "ProtocolType": "HTTP",
+      },
+      "Type": "AWS::ApiGatewayV2::Api",
+    },
+    "httpapiDefaultStageEEE9F4E2": Object {
+      "Properties": Object {
+        "ApiId": Object {
+          "Ref": "httpapi04200D5F",
+        },
+        "AutoDeploy": true,
+        "StageName": "$default",
+      },
+      "Type": "AWS::ApiGatewayV2::Stage",
+    },
+    "oai7A77E975": Object {
+      "Properties": Object {
+        "CloudFrontOriginAccessIdentityConfig": Object {
+          "Comment": "Allows CloudFront to reach the bucket",
+        },
+      },
+      "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
+    },
+  },
+}
+`;
diff --git a/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.test.ts.snap b/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.test.ts.snap
new file mode 100644
index 00000000..a2494ba9
--- /dev/null
+++ b/packages/microapps-cdk/test/__snapshots__/MicroAppsS3.test.ts.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MicroAppsS3 works with no params 1`] = `
+Object {
+  "Resources": Object {
+    "constructappsB8541FB8": Object {
+      "DeletionPolicy": "Retain",
+      "Type": "AWS::S3::Bucket",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "constructlogsBC77618C": Object {
+      "DeletionPolicy": "Retain",
+      "Type": "AWS::S3::Bucket",
+      "UpdateReplacePolicy": "Retain",
+    },
+    "constructoai26A46529": Object {
+      "Properties": Object {
+        "CloudFrontOriginAccessIdentityConfig": Object {
+          "Comment": "Allows CloudFront to reach the bucket",
+        },
+      },
+      "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
+    },
+    "constructstaging6988EDA5": Object {
+      "DeletionPolicy": "Retain",
+      "Type": "AWS::S3::Bucket",
+      "UpdateReplacePolicy": "Retain",
+    },
+  },
+}
+`;