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
-
AWS SAM
-
Azure Functions
-
Begin
-
Cloudflare
-
Deno Deploy
-
GCP
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"> </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
Check out https://github.com/architect/architect
Server side render web components with https://enhance.dev
Read https://fwa.dev
Tell me how wrong I am
Thanks!
Conntect.tech: Functional Web Apps
By Simon MacDonald
Conntect.tech: Functional Web Apps
- 653