Functional Web Apps

The revenge of dynamic web apps

Simon MacDonald

@macdonst

What is a dynamic web app?

  • Not static! HTML is rendered on-demand

  • Completely required for building an API

  • Database backed (full stack!)

Three-Tiered Architecture

Model

View 

Controller

Traditional dynamic web app ecosystem

Rails Logical Architecture

Model

View 

Controller

Rails Physical Architecture

Rails Physical Architecture

Traditional dynamic app problems

  • 3 tier logical architecture is not the same as the physical infra

    • Difficult to deploy when infra is not versioned

    • Slow rolling deployment

    • Servers are difficult to scale horizontally

  • Slow to render HTML because traditional databases are slow

  • Maintenance cost is huge and not fun (patching, updating, debugging, etc)

  • Background jobs are required for performance but a clunky bolt on

    • pub/sub, queues, cron

📘Book Rec

⭐⭐⭐⭐⭐

Enter the

Pre-rendering HTML and served via CDN. Dynamic functionality initiated by JavaScript at runtime.

1

Totally static

Authortime build of all presentation/business logic delivered as static assets

2

Immutable deployment

Empowers the frontend web developers

3

Outsource backend

JAMstack logical architecture

JAMstack physical architecture

JAMstack

immutable deployment

JAMstack tradeoffs

  • Slow build

  • Dynamic is a second class citizen

  • Complex ecosystem of libraries, tooling and services need glue

The Rebound Effect

In conservation and energy economics, the rebound effect (or take-back effect) is the reduction in expected gains from new technologies that increase the efficiency of resource use, because of behavioral or other systemic responses. These responses diminish the beneficial effects of the new technology or other measures taken.

Rebound Effect

The expected gains from a new JavaScript framework is always less because of the amount of JavaScript being deployed on the client. This additional JavaScript diminishes the beneficial effects of the new technology and in some cases makes things worse.

The

  • HTML first (dynamic personalization and a11y is the priority)

  • Cloud function centric mode

  • On-demand database

  • Declarative deployment (explicitly defined Infra as Code)

FWA: A Different Approach

Functional Web App logical architecture

Functional Web App physical architecture

Functional Web App advantages

  • Power: build with a full-stack

  • Way more fun! Less maintaining, more shipping

  • Inclusive, fast, and accessible web consumer experience

But what about…

  • Coldstart *

  • Database story still emergent (DynamoDB, Cosmos, Planetscale, FaunaDB)

  • Declarative Infra-as-Code solutions are oft complex

  • All-in with managed services means all-in with a cloud vendor

*  there are two solutions to coldstart for AWS Lambda: write small functions or pre-provision capacity

A growing ecosystem

Functional Web Apps

in the wild

Static App → Functional Web App

  • A modern architectural pattern for dynamic apps:

    • HTML-first progressive enhancement

    • Cloud functions centered development model

    • Managed database built-in

    • Explicit declarative deployment (aka Infra-as-Code or IaC)

Architect

Cloudformation

@app
arc-fnh

@static
fingerprint true
folder public

@http
get /
get /components/*

get /players
get /players/add
post /players
post /players/:id/delete
post /players/delete

get /games
get /games/add
get /games/:id
post /games
post /games/:id
post /games/:id/delete
post /games/delete

get /import
post /import

get /invite/:id

get /seasons
get /seasons/add
post /seasons
post /seasons/delete
post /seasons/:id/delete

@events
find-spares
send-email

@scheduled
weekly-reminder cron(0 8 ? * WED *)
weekly-roster cron(0 10 ? * FRI *)

@plugins
arc-plugin-oauth
architect/plugin-lambda-invoker

@oauth
use-mock true
allow-list allow.mjs

@tables
players
  email *String

games
  gamedate *String

seasons
  seasonID *String

invites
  inviteID *String
  expiresAt TTL

@tables-indexes
players
  fulltime *String
  name playersByFulltime

games
  gamedate *String
  name gamesByDate

seasons
  seasonID *String
  name seasonsByID

# invites
#  email *String
#  name invitesByEmail

@aws
# profile default
region us-west-2
architecture arm64
runtime nodejs16.x
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "Exported by architect/package@8.0.3 on 2022-04-01T20:25:58.390Z",
  "Resources": {
    "Role": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "ArcGlobalPolicy",
            "PolicyDocument": {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:DescribeLogStreams"
                  ],
                  "Resource": "arn:aws:logs:*:*:*"
                }
              ]
            }
          },
          {
            "PolicyName": "ArcStaticBucketPolicy",
            "PolicyDocument": {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "s3:GetObject",
                    "s3:PutObject",
                    "s3:PutObjectAcl",
                    "s3:DeleteObject",
                    "s3:ListBucket"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": [
                        "arn:aws:s3:::${bukkit}",
                        {
                          "bukkit": {
                            "Ref": "StaticBucket"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:s3:::${bukkit}/*",
                        {
                          "bukkit": {
                            "Ref": "StaticBucket"
                          }
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          },
          {
            "PolicyName": "ArcDynamoPolicy",
            "PolicyDocument": {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "dynamodb:BatchGetItem",
                    "dynamodb:BatchWriteItem",
                    "dynamodb:PutItem",
                    "dynamodb:DeleteItem",
                    "dynamodb:GetItem",
                    "dynamodb:Query",
                    "dynamodb:Scan",
                    "dynamodb:UpdateItem",
                    "dynamodb:GetRecords",
                    "dynamodb:GetShardIterator",
                    "dynamodb:DescribeStream",
                    "dynamodb:ListStreams"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
                        {
                          "tablename": {
                            "Ref": "PlayersTable"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
                        {
                          "tablename": {
                            "Ref": "PlayersTable"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/stream/*",
                        {
                          "tablename": {
                            "Ref": "PlayersTable"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
                        {
                          "tablename": {
                            "Ref": "GamesTable"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
                        {
                          "tablename": {
                            "Ref": "GamesTable"
                          }
                        }
                      ]
                    },
                    {
                      "Fn::Sub": [
                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/stream/*",
                        {
                          "tablename": {
                            "Ref": "GamesTable"
                          }
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    "PlayersParam": {
      "Type": "AWS::SSM::Parameter",
      "Properties": {
        "Type": "String",
        "Name": {
          "Fn::Sub": [
            "/${AWS::StackName}/tables/${tablename}",
            {
              "tablename": "players"
            }
          ]
        },
        "Value": {
          "Ref": "PlayersTable"
        }
      }
    },
    "GamesParam": {
      "Type": "AWS::SSM::Parameter",
      "Properties": {
        "Type": "String",
        "Name": {
          "Fn::Sub": [
            "/${AWS::StackName}/tables/${tablename}",
            {
              "tablename": "games"
            }
          ]
        },
        "Value": {
          "Ref": "GamesTable"
        }
      }
    },
    "StaticBucketParam": {
      "Type": "AWS::SSM::Parameter",
      "Properties": {
        "Type": "String",
        "Name": {
          "Fn::Sub": [
            "/${AWS::StackName}/static/${key}",
            {
              "key": "bucket"
            }
          ]
        },
        "Value": {
          "Ref": "StaticBucket"
        }
      }
    },
    "StaticFingerprintParam": {
      "Type": "AWS::SSM::Parameter",
      "Properties": {
        "Type": "String",
        "Name": {
          "Fn::Sub": [
            "/${AWS::StackName}/static/${key}",
            {
              "key": "fingerprint"
            }
          ]
        },
        "Value": "true"
      }
    },
    "ParameterStorePolicy": {
      "Type": "AWS::IAM::Policy",
      "DependsOn": "Role",
      "Properties": {
        "PolicyName": "ArcParameterStorePolicy",
        "PolicyDocument": {
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "ssm:GetParametersByPath",
                "ssm:GetParameter"
              ],
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}",
                  {}
                ]
              }
            },
            {
              "Effect": "Allow",
              "Action": [
                "ssm:GetParametersByPath",
                "ssm:GetParameter"
              ],
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*",
                  {}
                ]
              }
            },
            {
              "Effect": "Allow",
              "Action": [
                "ssm:GetParametersByPath",
                "ssm:GetParameter"
              ],
              "Resource": {
                "Fn::Sub": [
                  "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*/*",
                  {}
                ]
              }
            }
          ]
        },
        "Roles": [
          {
            "Ref": "Role"
          }
        ]
      }
    },
    "HTTP": {
      "Type": "AWS::Serverless::HttpApi",
      "Properties": {
        "StageName": "$default",
        "DefinitionBody": {
          "openapi": "3.0.1",
          "info": {
            "title": {
              "Ref": "AWS::StackName"
            }
          },
          "paths": {
            "/games/add": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesAddHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/players/add": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPlayersAddHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/components/{proxy+}": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetComponentsCatchallHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/games/{id}": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesIdHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              },
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesIdHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/auth": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetAuthHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/games": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              },
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/login": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetLoginHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/players": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPlayersHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              },
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetIndexHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/players/{id}/delete": {
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersIdDeleteHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/games/{id}/delete": {
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesIdDeleteHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/players/{id}": {
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersIdHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/logout": {
              "post": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostLogoutHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/mock/auth/{part}": {
              "x-amazon-apigateway-any-method": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "2.0",
                  "type": "aws_proxy",
                  "httpMethod": "POST",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AnyMockAuthPartHTTPLambda.Arn}/invocations"
                  },
                  "connectionType": "INTERNET"
                }
              }
            },
            "/_static/{proxy+}": {
              "get": {
                "x-amazon-apigateway-integration": {
                  "payloadFormatVersion": "1.0",
                  "type": "http_proxy",
                  "httpMethod": "GET",
                  "uri": {
                    "Fn::Sub": [
                      "http://${bukkit}.s3.${AWS::Region}.amazonaws.com/{proxy}",
                      {
                        "bukkit": {
                          "Ref": "StaticBucket"
                        }
                      }
                    ]
                  },
                  "connectionType": "INTERNET",
                  "timeoutInMillis": 30000
                }
              }
            }
          }
        }
      }
    },
    "GetGamesAddHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games-add",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetGamesAddHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games/add",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetPlayersAddHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-players-add",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetPlayersAddHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/players/add",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetComponentsCatchallHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-components-catchall",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetComponentsCatchallHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/components/{proxy+}",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetGamesIdHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games-000id",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetGamesIdHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games/{id}",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetAuthHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-auth",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetAuthHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/auth",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetGamesHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetGamesHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetLoginHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-login",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetLoginHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/login",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetPlayersHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-players",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetPlayersHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/players",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "GetIndexHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-index",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_STATIC_SPA": false,
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "GetIndexHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/",
              "Method": "GET",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostPlayersIdDeleteHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players-000id-delete",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostPlayersIdDeleteHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/players/{id}/delete",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostGamesIdDeleteHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games-000id-delete",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostGamesIdDeleteHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games/{id}/delete",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostPlayersIdHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players-000id",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostPlayersIdHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/players/{id}",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostGamesIdHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games-000id",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostGamesIdHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games/{id}",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostGamesHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostGamesHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/games",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostLogoutHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/post-logout",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostLogoutHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/logout",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PostPlayersHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "PostPlayersHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/players",
              "Method": "POST",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "AnyMockAuthPartHTTPLambda": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "index.handler",
        "CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-mock-auth-000part",
        "Runtime": "nodejs14.x",
        "Architectures": [
          "arm64"
        ],
        "MemorySize": 1152,
        "Timeout": 5,
        "Environment": {
          "Variables": {
            "ARC_APP_NAME": "arc-fnh",
            "ARC_ENV": "staging",
            "ARC_ROLE": {
              "Ref": "Role"
            },
            "ARC_SESSION_TABLE_NAME": "jwe",
            "ARC_STACK_NAME": {
              "Ref": "AWS::StackName"
            },
            "ARC_STATIC_BUCKET": {
              "Ref": "StaticBucket"
            },
            "ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
            "ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
            "ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
            "ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
            "ARC_OAUTH_CUSTOM_AUTHORIZE": "",
            "ARC_OAUTH_MATCH_PROPERTY": "login",
            "ARC_OAUTH_AFTER_AUTH": "/",
            "ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
            "ARC_OAUTH_USE_ALLOW_LIST": "true",
            "ARC_OAUTH_ALLOW_LIST": "allow.mjs",
            "ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
            "ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
          }
        },
        "Role": {
          "Fn::Sub": [
            "arn:aws:iam::${AWS::AccountId}:role/${roleName}",
            {
              "roleName": {
                "Ref": "Role"
              }
            }
          ]
        },
        "Events": {
          "AnyMockAuthPartHTTPEvent": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/mock/auth/{part}",
              "Method": "ANY",
              "ApiId": {
                "Ref": "HTTP"
              }
            }
          }
        }
      }
    },
    "PlayersTable": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "KeySchema": [
          {
            "AttributeName": "email",
            "KeyType": "HASH"
          }
        ],
        "AttributeDefinitions": [
          {
            "AttributeName": "email",
            "AttributeType": "S"
          },
          {
            "AttributeName": "fulltime",
            "AttributeType": "S"
          }
        ],
        "BillingMode": "PAY_PER_REQUEST",
        "GlobalSecondaryIndexes": [
          {
            "IndexName": "playersByFulltime",
            "KeySchema": [
              {
                "AttributeName": "fulltime",
                "KeyType": "HASH"
              }
            ],
            "Projection": {
              "ProjectionType": "ALL"
            }
          }
        ]
      }
    },
    "GamesTable": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "KeySchema": [
          {
            "AttributeName": "gamedate",
            "KeyType": "HASH"
          }
        ],
        "AttributeDefinitions": [
          {
            "AttributeName": "gamedate",
            "AttributeType": "S"
          }
        ],
        "BillingMode": "PAY_PER_REQUEST",
        "GlobalSecondaryIndexes": [
          {
            "IndexName": "gamesByDate",
            "KeySchema": [
              {
                "AttributeName": "gamedate",
                "KeyType": "HASH"
              }
            ],
            "Projection": {
              "ProjectionType": "ALL"
            }
          }
        ]
      }
    },
    "StaticBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "OwnershipControls": {
          "Rules": [
            {
              "ObjectOwnership": "BucketOwnerEnforced"
            }
          ]
        },
        "WebsiteConfiguration": {
          "IndexDocument": "index.html",
          "ErrorDocument": "404.html"
        }
      }
    },
    "StaticBucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": {
          "Ref": "StaticBucket"
        },
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": [
                "s3:GetObject"
              ],
              "Effect": "Allow",
              "Principal": "*",
              "Resource": [
                {
                  "Fn::Sub": [
                    "arn:aws:s3:::${bukkit}/*",
                    {
                      "bukkit": {
                        "Ref": "StaticBucket"
                      }
                    }
                  ]
                }
              ],
              "Sid": "PublicReadGetObject"
            }
          ]
        }
      }
    }
  },
  "Outputs": {
    "API": {
      "Description": "API Gateway (HTTP)",
      "Value": {
        "Fn::Sub": [
          "https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com",
          {
            "ApiId": {
              "Ref": "HTTP"
            }
          }
        ]
      }
    },
    "ApiId": {
      "Description": "API ID (ApiId)",
      "Value": {
        "Ref": "HTTP"
      }
    },
    "BucketURL": {
      "Description": "Bucket URL",
      "Value": {
        "Fn::Sub": [
          "http://${bukkit}.s3-website-${AWS::Region}.amazonaws.com",
          {
            "bukkit": {
              "Ref": "StaticBucket"
            }
          }
        ]
      }
    }
  }
}
GET /players
import arc from '@architect/functions'
import render from '@architect/views/render.mjs'
import { getFulltimePlayers, getSpares } from '@architect/shared/db/players.mjs'
import arcOauth from 'arc-plugin-oauth'
const auth = arcOauth.auth

export const handler = arc.http.async(auth, players)

async function players(req) {
  const { type = 'fulltime' } = req.query
  const players =
    type === 'fulltime' ? await getFulltimePlayers() : await getSpares()
  const initialState = { account: req.session?.account }

  return {
    html: render(
      `
    <form method="POST" action="/players/delete">
      <hockey-page>
        <hockey-action-buttons direction="row-reverse">
          <hockey-button icon="delete">Delete</hockey-button>
          <hockey-action-button action="/players/add" icon="plus" label="Add" type="link" variant="default"></hockey-action-button>
        </hockey-action-buttons>

        <enhance-table>
            <enhance-thead>
              <enhance-tr><enhance-th width="1rem">&nbsp;</enhance-th><enhance-th>Name</enhance-th><enhance-th class="unseen">Email</enhance-th><enhance-th class="unseen">Phone</enhance-th><enhance-th>Position</enhance-th></enhance-tr>
            </enhance-thead>
            <enhance-tbody>
              ${players
                .map(
                  (player) =>
                    `<enhance-tr>
                    <enhance-td>
                      <input type="checkbox" name="todelete" class="leading5-l pt-3 pb-3 pl-1 pr-1 radius2 shadow-2" value="${
                        player.email
                      }"/>
                    </enhance-td>
                    <enhance-td><a class="color-blue" href="/players/add?id=${
                      player.email
                    }">${player.name} ${
                      player.preferred === 'true'
                        ? `<hockey-icon icon="star" style="width: 1rem; height: 1rem; display: inline;"></hockey-icon>`
                        : ''
                    }</a></enhance-td>
                    <enhance-td class="unseen">${player.email}</enhance-td>
                    <enhance-td class="unseen">${player.phone}</enhance-td>
                    <enhance-td class="capitalize">${
                      player.position
                    }</enhance-td>
                  </enhance-tr>`
                )
                .join('')}
            </enhance-tbody>
        </enhance-table>
      </hockey-page>
    </form>
`,
      initialState
    )
  }
}
POST /players
import arc from '@architect/functions'
import { upsertPlayer } from '@architect/shared/db/players.mjs'
import arcOauth from 'arc-plugin-oauth'
const auth = arcOauth.auth

export const handler = arc.http.async(auth, http)

async function http(req) {
  await upsertPlayer(req.body)
  const location =
    req.body.fulltime === 'true' ? '/players' : '/players?type=spares'

  return {
    location
  }
}

What is Enhance?

Author

Enhance allows developers to write components as pure functions that return HTML. Then render them on the server to deliver complete HTML immediately available to the end user.

Standards

Enhance takes care of the tedious parts, allowing you to use today’s Web Platform standards more efficiently. As new standards and platform features become generally available, Enhance will make way for them.

Progressive

Enhance allows for easy progressive enhancement so that working HTML can be further developed to add additional functionality with JavaScript.

Enhance is a web standards-based HTML framework. It’s designed to provide a dependable foundation for building lightweight, flexible, and future-proof web applications.

Enhance Key Concepts

File based routing with plain HTML

Reuse markup with custom elements

Built-in utility CSS based on scales rather than absolute values

API routes without manually wiring props

Progressively enhance with standard JS; no special syntax

Fullstack FWA under the hood

Demo

Learn More

Server side render web components with https://enhance.dev

Tell me how wrong I am

@macdonst

Thanks!

Conntect.tech: Functional Web Apps

By Simon MacDonald

Conntect.tech: Functional Web Apps

  • 653