diff --git a/blogs/web-dev-bootcamp/index.html b/blogs/web-dev-bootcamp/index.html index 3b2cdc5..96a4be4 100644 --- a/blogs/web-dev-bootcamp/index.html +++ b/blogs/web-dev-bootcamp/index.html @@ -588,8 +588,8 @@

Step 5 - Play around

Find some repos that interest you and play around with them. Be curious and add a couple of cool features.

Recourses

Step 6 - Build a Personal Project

@@ -598,7 +598,7 @@

Step 6 - Build a Personal Project

Recourses

Step 7 - Call Me! ☎️

diff --git a/index.json b/index.json index b74bec7..cf85708 100644 --- a/index.json +++ b/index.json @@ -1 +1 @@ -[{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eSo you are interested in Web Development? There are of 100s of different tracks that can be followed and I am not going to argue that the track below it the only or even best.\nThis blog post simply outlines;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHow I got into web development, what I found useful and what worked for me.\u003c/li\u003e\n\u003cli\u003eSome useful recourses to get you started with web development in 2024.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"bootcamp\"\u003eBootcamp\u003c/h2\u003e\n\u003ch3 id=\"step-1---javascript-fundamentals\"\u003eStep 1 - JavaScript Fundamentals\u003c/h3\u003e\n\u003cp\u003eFirst step is to get familiar with coding in general and learning some javascript.\nCoding is an infinite virtual jigsaw puzzle where you can build whatever you can imagine.\nIf the prospect of this excites you, it serves as a promising indicator to persist and delve deeper into the world of coding.\u003c/p\u003e\n\u003ch4 id=\"recourses\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.codecademy.com/courses/introduction-to-javascript/informationals/learn-javascript-welcome\"\u003eCodecademy Free Trial\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-2---web-dev-fundamentals\"\u003eStep 2 - Web Dev Fundamentals\u003c/h3\u003e\n\u003cp\u003eThe next step would be to learn web dev fundaments. Topics such as;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eFundamentally how do website work?\u003c/li\u003e\n\u003cli\u003eWhere and how does the code I write get executed?\u003c/li\u003e\n\u003cli\u003eHow do I style a site?\u003c/li\u003e\n\u003cli\u003eResponsive design\u003c/li\u003e\n\u003cli\u003eAuthentication\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"some-tips\"\u003eSome tips\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eFocus on the fun! Do not try to memories 100s of CSS properties. Focus building stuff that you think is cool.\u003c/li\u003e\n\u003cli\u003eYou do not have to spend a lot of money. YouTube series are great! And even Udemy coerces are often heavily reduced. Also if you are a student you may be able\nto get a free/cheap Udemy subscription.\u003c/li\u003e\n\u003cli\u003eReddit and Stack Overflow are probably the best places to get answers to your questions. Reddit for general questions questions (e.g: \u003ca href=\"https://www.reddit.com/r/aws/comments/qcpyiz/cheapest_way_to_integrate_aws_cognito_with/\"\u003ethis\u003c/a\u003e)\nStack Overflow for specific questions (e.g \u003ca href=\"https://stackoverflow.com/questions/69365021/typegoose-mongoose-map-custom-type-to-db\"\u003ethis\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003eI do not rate classes. If you need a class to pleasure you to keep learning. Maybe you are not motivated enough to keep doing this.\nFocus on what you enjoy!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"what-should-i-learn\"\u003eWhat should I learn?\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eLearn a frontend framework. I recommend React.\u003c/li\u003e\n\u003cli\u003eIf you learn React. Learn functional components form the start. Do not waste your time learning the old style class based components\u003c/li\u003e\n\u003cli\u003eYou may want to learn a server side React (i.e: \u003ca href=\"https://nextjs.org/\"\u003eNextJS\u003c/a\u003e) but I would say that is very complicated place to start.\nI would suggest starting with single page applications (i.e: \u003ca href=\"https://vitejs.dev/guide/\"\u003eVite\u003c/a\u003e) and be aware that server side React exists so that you can reach for it in future if you need it\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"recourses-1\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.udemy.com/course/the-web-developer-bootcamp/\"\u003eUdemy - The Web Developer Bootcamp 2024\u003c/a\u003e -\nWhile I haven\u0026rsquo;t personally taken this course, I was thoroughly impressed by my friend\u0026rsquo;s journey. Having transitioned from a school teacher to a web developer,\nthey demonstrated remarkable proficiency within just 1.5 years of embarking on their web development venture.\nThis was their first course they highly recommend it.\u003c/li\u003e\n\u003cli\u003eFor help - \u003ca href=\"https://www.reddit.com/r/aws/\"\u003er/aws\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/javascript/\"\u003er/javascript\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/nextjs/\"\u003er/nextjs\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/node/\"\u003er/node\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-3---learn-typescript\"\u003eStep 3 - Learn TypeScript\u003c/h3\u003e\n\u003cp\u003eNow it\u0026rsquo;s time to learn TypeScript. You should really do this as soon as possible. There is an initial learning curve but then immediate payoff which will accelerate\nyour productivity. Learning TypeScript also opens the door to new frameworks and libraries (such as \u003ca href=\"https://nestjs.com/\"\u003eNest\u003c/a\u003e and \u003ca href=\"https://typegraphql.com/\"\u003eTypeGraphQL\u003c/a\u003e) which often have a better developer experience than using JS alone.\nMy biggest regret in my own web dev journey is not learning TypeScript sooner!\u003c/p\u003e\n\u003ch4 id=\"recourses-2\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=jrKcJxF0lAU\"\u003eYouTube - React with TypeScript Crash Course\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=Z5iWr6Srsj8\u0026amp;t=3s\"\u003eYouTube - React Typescript Tutorial\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/@bawad\"\u003eYouTube - Anything and Everything from Ben Awad!\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-4---learn-fullstack-typescript-development\"\u003eStep 4 - Learn FullStack TypeScript Development\u003c/h3\u003e\n\u003cp\u003eYou need a lot more than React to build and deploy a modern website.\u003c/p\u003e\n\u003ch4 id=\"some-key-topis\"\u003eSome key topis\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eWorking with apis. Even if you are not a full stack developer you will inevitably have to use apis in web development. Learn how to make and consume Type Safe APIs!\u003c/li\u003e\n\u003cli\u003eLearn how to deploy websites. Deploying on AWS is a popular choice. You can do this with TypeScript too!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"recourses-3\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/blogs/typescript-journey/\"\u003eTypeScript Journey\u003c/a\u003e - A journey into Modern Web Development, CI/CD and AWS Magic!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-5---play-around-with-some-sample-repositories\"\u003eStep 5 - Play around with some sample repositories\u003c/h3\u003e\n\u003cp\u003eFind some repos that interest you and play around with them. Be curious and add a couple of cool features.\u003c/p\u003e\n\u003ch4 id=\"recourses-4\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/#samples\"\u003eCode Samples\u003c/a\u003e here are some of my sample projects. Check them out and play around.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/\"\u003eGitHub\u003c/a\u003e there are thousands of projects there to look at! Most cool new libraries (e.g: \u003ca href=\"https://tanstack.com/router/v1\"\u003eTanStack Router\u003c/a\u003e)\nhave stater projects on GitHub. Playing around with them is a great way to learn.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-6---build-a-personal-project\"\u003eStep 6 - Build a Personal Project\u003c/h3\u003e\n\u003cp\u003eBuild something fun! The best way to learn is to build something from scratch and expand your horizons. Get yourself totally stuck and learn how to unblock yourself.\nDo not try to build the next AirBnb. Build something simple and fun that you and your friends/family might use.\u003c/p\u003e\n\u003ch4 id=\"recourses-5\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/blogs/wakeboard-competition-app/\"\u003eWakeboard Competition App Project\u003c/a\u003e - My first passion project\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.google.com/document/d/1kjhAq2c5wTl7shTc2lWM8Pe_JBD1edNWy4XQdGbMNiE/edit?usp=sharing\"\u003eCode Assignment\u003c/a\u003e if you are really stuck for ideas then here\nis a code assignment which I set to evaluate candidates.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-7---call-me-\"\u003eStep 7 - Call Me! ☎️\u003c/h3\u003e\n\u003cp\u003eStill having fun!? If you made it this far than you must really enjoy this stuff. Give me a call and let me see if I can find you a real project to work on!\u003c/p\u003e\n","description":"","image":"/images/posts/web-dev-bootcamp/react.jpg","permalink":"https://ziggy6792.github.io/blogs/web-dev-bootcamp/","title":"Fullstack Web Development - Bootcamp"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eModern web development is an exciting challenge, with a myriad of languages, runtimes, and AWS services to conquer. Dive into this interactive workshop, where we\u0026rsquo;ll explore cutting-edge frontend and backend development, all while staying firmly in the TypeScript world.\u003c/p\u003e\n\u003cp\u003eHighlights:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreate modern TypeScript web apps with React, Vite, and Next.JS.\u003c/li\u003e\n\u003cli\u003eMaster TypeScript CI/CD with SST and AWS CDK.\u003c/li\u003e\n\u003cli\u003eDeploy a fullstack application to AWS complete with; Next.js frontend, Node.JS apis, cognito login, S3 upload/download and TypeScript IAC.\u003c/li\u003e\n\u003cli\u003eEnsure type safety from backend to frontend through the capabilities of tRPC.\u003c/li\u003e\n\u003cli\u003eGet a glimpse of the future with Deno and Bun.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eJoin us for an action-packed TypeScript adventure! 🚀🌟\u003c/p\u003e\n\u003ch3 id=\"target-audience\"\u003eTarget Audience\u003c/h3\u003e\n\u003cp\u003eAre you interested in web development with TypeScript? Maybe you already know some React but want to level up to deploying and running fullstack web applications to AWS. This web series is for you!\u003c/p\u003e\n\u003ch3 id=\"links\"\u003eLinks\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/playlist?list=PLz7EK8bcbCdDEcoa0cz4DNXjsFaxCVOfs\"\u003eWeb Series\u003c/a\u003e (YouTube private link, email me for access)\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.meetup.com/tech-meetup/events/296204384\"\u003eMeetup\u003c/a\u003e This web series is based off a workshop delivered in Ho Chi Minh, October 2023.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.google.com/presentation/d/1mYOBD6kO1uWB57J3P9wYvQVRdLzKPNj-/edit?usp=sharing\u0026amp;ouid=115437333129535741316\u0026amp;rtpof=true\u0026amp;sd=true\"\u003eSlides\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/ziggy6792/typeScript-journey\"\u003eCode\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"course-outline\"\u003eCourse Outline\u003c/h2\u003e\n\u003ch4 id=\"1---welcome\"\u003e1 - Welcome\u003c/h4\u003e\n\u003cp\u003eWelcome to my web series “TypeScript Journey” a journey into Modern Web Development, CI/CD and AWS Magic!\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"2---demo-intro--prerequisites\"\u003e2 - Demo Intro \u0026amp; Prerequisites\u003c/h4\u003e\n\u003cp\u003eA look at what we will be building in this web series and prerequisites to get started.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"3---intro-ts-mono-repo\"\u003e3 - Intro TS Mono-Repo\u003c/h4\u003e\n\u003cp\u003eIntroduction to a modern TypeScript Mono-Repository using; Turborepo, yarn workspaces, Eslint, CDK, SST.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"4---add-and-deploy-vite-react-app\"\u003e4 - Add and deploy Vite React App\u003c/h4\u003e\n\u003cp\u003eCreating a Vite React App in a TypeScript Mono-Repo deploying to AWS using Cloud Development Kit (SST).\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"5---add-next-js-app\"\u003e5 - Add Next JS App\u003c/h4\u003e\n\u003cp\u003eCreating a Next JS App in a TypeScript Mono-Repo using Create T3 App.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"6---add-next-auth-cognito\"\u003e6 - Add Next Auth (Cognito)\u003c/h4\u003e\n\u003cp\u003eDeploying Cognito Userpool to AWS using Cloud Development Kit (SST) and configuring sign in with Next Auth.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"7---deploy-next-app-to-aws\"\u003e7 - Deploy Next App to AWS\u003c/h4\u003e\n\u003cp\u003eDeploying Next App to AWS using Open Next. Next App runs serverless on AWS lambda which is a very cheap way to deploy a Next App. We also configure AWS Cognito to accept signin redirect requests from the deployed site. And this highlights the issue of dependency cycles that can occur in IAC.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"8---why-i-like-this-stack\"\u003e8 - Why I Like this Stack\u003c/h4\u003e\n\u003cp\u003eAn overview of some of the advantages of using this stack. Cheap run costs due to serverless architecture, bleeding edge features, fine grained access control through NextAuth and tRPC and Type Safe APIs.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"9---fix-trpc-mutations\"\u003e9 - Fix tRPC Mutations\u003c/h4\u003e\n\u003cp\u003eOpen Next does not support streaming tPRC mutations out of the box. This short video shows a fix.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"10---upload-download-demo\"\u003e10 - Upload Download Demo\u003c/h4\u003e\n\u003cp\u003eImplementing upload/download functionality to the Next app. Adding an S3 bucket to SST app, importing the bucket into Next App and creating an api to generate pre-signed urls. In this demo, bucket name is defined in a common package and imported into SST app and Next app.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"11---using-sst-bind-and-summary\"\u003e11 - Using SST Bind and Summary\u003c/h4\u003e\n\u003cp\u003eUsing SST bind to refactor the approach from the previous video. In this demo, bucket name is defined in SST app and then SST bind is run in Next app to generate types and the bucket name to use. Finally a summary of what we have built.\u003c/p\u003e\n","description":"","image":"/images/posts/typescript-journey/meetup.png","permalink":"https://ziggy6792.github.io/blogs/typescript-journey/","title":"TypeScript Journey - Web Series"},{"content":"\u003ch1 id=\"join-alpaca-consultants-network\"\u003eJoin Alpaca Consultants Network\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eEngage in tech discussions\u003c/li\u003e\n\u003cli\u003eAcquire knowledge and solutions\u003c/li\u003e\n\u003cli\u003eGet answers to tech questions\u003c/li\u003e\n\u003cli\u003eRecruiting skilled developers for paid projects\u003c/li\u003e\n\u003c/ul\u003e\n","description":"","image":"/images/posts/join/join-network.png","permalink":"https://ziggy6792.github.io/join/","title":"Join Alpaca Consultants Network"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhire is a Singaporean Tech Startup which helps companies hire great people faster through its platform and a top tier community of hiring ambassadors, saving companies hours on pre-screening and outreach efforts as well as thousands of dollars on job ads and agency fees without compromising candidate quality.\u003c/p\u003e\n\u003ch3 id=\"motivation\"\u003eMotivation\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://www.linkedin.com/in/marsilib/\"\u003eBenjamin Marsili\u003c/a\u003e (founder of Whire) hired me to help him build a recruiting network platform for companies to;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBroadcast jobs from LinkedIn (and other sources)\u003c/li\u003e\n\u003cli\u003eDirectly interview candidates who are referred members of the Whire network\u003c/li\u003e\n\u003cli\u003eMake hiring decisions and then pay for successfully hired candidates\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor more information on Whire visit \u003ca href=\"https://www.whire.net/\"\u003ewhire.net\u003c/a\u003e.\u003cbr\u003e\nTo try the Whire platform yourself, visit \u003ca href=\"https://app.whire.net/\"\u003eapp.whire.net\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"my-engagement-with-whire\"\u003eMy Engagement with Whire\u003c/h2\u003e\n\u003ch3 id=\"initial-phase\"\u003eInitial Phase\u003c/h3\u003e\n\u003cp\u003eBefore engaging with me Whire already had 1 fulltime junior developer and a React (Typescript) UI component library consisting of simple UI components and some full screen layouts. The component library was also complete wth Story Book (UI Component Development Library). Up to this point, the component library was primarily used to show potential investors what they were trying to build.\u003c/p\u003e\n\u003cp\u003eWhire had also started work on a Next.JS web-app using their component library. The app to this point was also for demo purposes only; login/data/integrations were all mocked.\u003c/p\u003e\n\u003ch3 id=\"phase-1-2-weeks\"\u003ePhase 1 (2 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eFor phase 1, Whire\u0026rsquo;s initial aims were;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDeploy existing web-app a cost effective and scalable way\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eI strongly recommended to deploy to AWS for the following reasons:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAWS can do a lot of undifferentiated heavy lifting (e.g: Fargate can be used to deploy and scale the web-app without having to do a lot of configuration)\u003c/li\u003e\n\u003cli\u003eAWS allows a pay as you use payment model\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eNote: Vercel was also considered, however as their was a future plan to integrate other Amazon services it seemed better to keep everything under one roof. Also I had read that Vercel costs are expensive for sites once traffic increases\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eI proposed the following AWS architecture to Ben\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/whire-architecture-phase-1.png\"\u003e\u003cimg src=\"/images/projects/whire/whire-architecture-phase-1.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Architecture Phase 1\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eWeb-app is containerized and pushed to ECR\u003c/li\u003e\n\u003cli\u003eWeb-app deployed managed and auto-scaled using Fargate\u003c/li\u003e\n\u003cli\u003eAWS Code pipeline to automatically build/test and deploy code\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eIn order to execute this plan I completed the following steps;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRestructured the frontend into a mono-repo so that component library and web-app are built and tested independently (using shared extended configurations)\u003c/li\u003e\n\u003cli\u003eAdded (Jest) tests for web-app and component library\u003c/li\u003e\n\u003cli\u003eWrote a docker compose file to build web-app and component library into an image that could be pushed to Amazon ECR\u003c/li\u003e\n\u003cli\u003eCreated a CDK stack to deploy the planned architecture to staging and production environments\u003c/li\u003e\n\u003cli\u003eCreated a CDK pipeline to build, test and deploy the web-app and component library\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"result\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eWhire web-app was successfully deployed on AWS\u003c/p\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1916px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-1/deployed-web-app_hu47c770cf9e83f337bb3d9007024937eb_607014_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-1/deployed-web-app.png\" data-pswp-width=\"1916\"\n data-pswp-height=\"1056\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-1/deployed-web-app_hu47c770cf9e83f337bb3d9007024937eb_607014_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-1/deployed-web-app.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-1/deployed-web-app.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-1/deployed-web-app.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-1/pipeline_hu1f27d3e5234630886d6909584039e8fb_497806_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-1/pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"3394\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-1/pipeline_hu1f27d3e5234630886d6909584039e8fb_497806_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-1/pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-1/pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-1/pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-2-4-weeks\"\u003ePhase 2 (4 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective-1\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eAfter successfully deploying their web-app to AWS. For phase 2, Whire\u0026rsquo;s aims were\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStart working on a backend API to store and serve data about jobs and candidates in the whire network\u003c/li\u003e\n\u003cli\u003eCreate an authentication solution so users could login to Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture-1\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eFor building Whire\u0026rsquo;s api I proposed creating a NodeJS GraphQL api in Typescript and deploying it to Lambda. This has the following advantages.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBuilding everything in Typescript keeps development simple as skills/knowledge/tools are reusable\u003c/li\u003e\n\u003cli\u003eDeploying to Lambda is very cheap and scalable\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eNote: AppSync was also considered for the GraphQL api. However I find it very awkward to extend beyond out of the box functionality (thought maybe now it has improved).\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor storing Data I suggested using a serverless MongoDB. A schemaless DB was preferable as data such as job posts coming from different sources can be unstructured.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eNote: The reason for choosing MongoDB and not Amazon\u0026rsquo;s own schemaless DB DynamoDB is that mongo has fantastic Typescript support. Namely, \u003ca href=\"https://typegoose.github.io/typegoose/\"\u003etypegoose\u003c/a\u003e, an object document model written and managed by MongoDB\u0026rsquo;s creators.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor authenticating users I suggested AWS Cognito for user login along with AWS Api Gateway to authenticate user api requests.\u003c/p\u003e\n\u003cp\u003eI proposed the following AWS architecture to Ben\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/whire-architecture-phase-2.png\"\u003e\u003cimg src=\"/images/projects/whire/whire-architecture-phase-2.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Architecture Phase 2\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eGraphQL api built with NodeJS/Apollo Server\u003c/li\u003e\n\u003cli\u003eServerless MongoDB\u003c/li\u003e\n\u003cli\u003eAWS Code pipeline to automatically build/test and deploy code\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution-1\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eIn order to execute this plan I completed the following steps;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreated a GraphQL api in NodeJS using Apollo Server and TypeGraphQL\u003c/li\u003e\n\u003cli\u003eConfigured Api Gateway (using CDK) to allow api requests from authenticated cognito users (use access) and allowed IAM roles (role access)\u003c/li\u003e\n\u003cli\u003eCreated a CDK stack to deploy the planned architecture to staging and production environments\u003c/li\u003e\n\u003cli\u003eCreated a CDK pipeline to build, test and deploy the api\u003c/li\u003e\n\u003cli\u003eCoached Whire\u0026rsquo;s junior developer so he could create his own GraphQL queries and mutations\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"result-1\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eWhire api was successfully built and deployed on AWS\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1921px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-2/api-gateway_hu015b6f86db0c3cc0ddb3a9ae399999e8_148013_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-2/api-gateway.png\" data-pswp-width=\"1921\"\n data-pswp-height=\"973\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-2/api-gateway_hu015b6f86db0c3cc0ddb3a9ae399999e8_148013_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-2/api-gateway.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-2/api-gateway.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-2/api-gateway.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-2/pipeline_huc93bea273eb34a29156b2ddefb9f0405_598639_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-2/pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"3516\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-2/pipeline_huc93bea273eb34a29156b2ddefb9f0405_598639_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-2/pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-2/pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-2/pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-3-8-weeks\"\u003ePhase 3 (8 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective-2\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eOne of the key features for Whire is referring candidates. For referrals, both the referrer and referee should use LinkedIn to login and share their information.\u003c/p\u003e\n\u003cp\u003eFor phase 3, Whire\u0026rsquo;s aims were;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUsers should be able to login to Whire using their LinkedIn credentials\u003c/li\u003e\n\u003cli\u003eLinkedIn authenticated users should have api access\u003c/li\u003e\n\u003cli\u003eOn logging in for the first time a user should be created in Whire\u0026rsquo;s database\u003c/li\u003e\n\u003cli\u003eUsers LinkedIn profile information (name, nationality, skills etc\u0026hellip;) should be pulled into Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture-2\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eI proposed the following process for user registration, profile setup and sign in.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/auth.png\"\u003e\u003cimg src=\"/images/projects/whire/auth.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Authentication Flow\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eUse LinkedIn as federated auth provider with Cognito\u003c/li\u003e\n\u003cli\u003eA Lambda runs on user confirmation (after email validation) to fetch the users profile information. This is non blocking, incase the data can not be retrieved we do not block the user from being onboarded to Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution-2\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eAfter starting to execute this plan we ran into a problem. Cognito does not support LinkedIn login out of the box. Cognito only supports some federated providers (Facebook/Google) as well as OpenId/SAML identity providers (LinkedIn only has an Auth2 provider).\u003c/p\u003e\n\u003cp\u003eI then proposed 3 options\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eOption\u003c/th\u003e\n\u003cth\u003eAdvantages\u003c/th\u003e\n\u003cth\u003eDisadvantages\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eUse a third party solution like Auth0/octa\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eSimple\u003c/strong\u003e to set up LinkedIn login\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eExpensive\u003c/strong\u003e (Auth0 $240/mo - octa $3/MAU)\u003cbr\u003e Outside the AWS ecosystem (bad for developers and CI/CD) \u003cbr\u003e \u003cstrong\u003eAPI Access\u003c/strong\u003e would require a custom lambda authorizer in AWS gateway \u003cbr\u003e \u003cstrong\u003eSign Up events\u003c/strong\u003e would require custom setup outside of AWS\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eMixed approach. Use cognito with Auth0 OpenID provider (use Auth0 as a middleman)\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eSimplest solution\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eBad user experience.\u003c/strong\u003e We don’t control the Auth0 login flow \u003cbr\u003e \u003cstrong\u003eMost expensive solution.\u003c/strong\u003e As we pay for our LinkedIn users twice \u003cbr\u003e \u003cstrong\u003eNo one source of truth\u003c/strong\u003e as users managed in Auth0 and cognito\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eWrite my own OpenId provider.\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eCheap\u003c/strong\u003e \u003cbr\u003e \u003cstrong\u003eEverything kept in AWS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eComplex\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eI chose to write my own OpenId provider, which adapts LinkedIn Auth2 provider to OpenId. Luckily I was able to find an existing project for the \u003ca href=\"https://github.com/TimothyJones/github-cognito-openid-wrapper\"\u003eGithub Auth2 provider\u003c/a\u003e and adapt the code to my needs (LinkedIn apis/scopes). I hosted the custom provider on AWS lambda and added the deployment to CDK along with the additional \u003ca href=\"https://github.com/ziggy6792/linkedin-cognito-openid-wrapper#3-finalise-cognito-configuration\"\u003eCogntio OpenId configuration\u003c/a\u003e. The code more my standalone OpenId Auth2 wrapper can be found \u003ca href=\"https://github.com/ziggy6792/linkedin-cognito-openid-wrapper\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003ch4 id=\"result-2\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eIn the video below you can see Whire\u0026rsquo;s sign up process for referrers and referees. Users are able to sign up to Whire using their LinkedIn credentials and profile data (e.g. full name and profile photo) is pulled into Whire.\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://player.vimeo.com/video/816871864\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"vimeo video\" webkitallowfullscreen mozallowfullscreen allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-4-4-weeks\"\u003ePhase 4 (4 weeks)\u003c/h3\u003e\n\u003cp\u003eFor phase 4 I added ATS integration using \u003ca href=\"https://merge.dev/categories/ats-recruiting-api\"\u003emerge.dev\u003c/a\u003e which allows companies to integrate Whire with their existing recruiting flows and tools. In this phase Whire was successfully integrated with \u003ca href=\"https://recruitee.com/\"\u003eRecruitee\u003c/a\u003e and \u003ca href=\"https://www.greenhouse.com/\"\u003eGreenhouse\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n\u003ch2 id=\"impact\"\u003eImpact\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eWhire Network has sofar grown to; 30 clients, 200 referrers and 100 posted jobs.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eWhether you are starting a new project or restructuring an old stack, his work ethics, drive and sound engineering mind will make the difference between what could have been an expensive failure and a software stack providing a great user as well as developer experience. Working with Simon is investing in your company\u0026rsquo;s sustainable future.\u003c/p\u003e\n— \u003ccite\u003e\u003ca href=\"https://www.linkedin.com/in/harikrishnan-nagarajan-65871a55/\"\u003eBenjamin Marsili\u003c/a\u003e\u003c/cite\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIn June 2021, I took on a role as CTO of Whire.\u003c/p\u003e\n\u003c/blockquote\u003e\n","description":"","image":"/images/projects/whire/whire.png","permalink":"https://ziggy6792.github.io/projects/whire/","title":"Whire"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://alpacacup.com/events\"\u003eAlpaca Cup\u003c/a\u003e is an event management application for hosting and scoring competitions. The app has already been used in wakeboarding and wakeskating competitions all over the world, and we hope to branch out in other extreme sports in the future (such as skateboarding).\u003c/p\u003e\n\u003cp\u003eAlpaca Cup serves 2 main purposes;\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSharing event information (who is in each round, when rounds start, who progress to the next round)\u003c/li\u003e\n\u003cli\u003eSharing results for each round in realtime (i.e: live scores as they are entered by judges)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTo try the Alpaca Cup yourself, visit \u003ca href=\"https://alpacacup.com/\"\u003ealpacacup.com\u003c/a\u003e.\u003c/p\u003e\n\u003ch3 id=\"related-articles\"\u003eRelated Articles\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/alpaca-cup-architecture\"\u003ehere\u003c/a\u003e for deep dive into the technology behind Alpaca Cup\u003c/li\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/wakeboard-competition-app\"\u003ehere\u003c/a\u003e for deep dive into the original motivations for this project\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"features\"\u003eFeatures\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup\u0026rsquo;s main features are;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRealTime Updates:\u003c/strong\u003e All event data; competitor positions, scores, rankings, heat allocations and more is updated in realtime.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive Scoring:\u003c/strong\u003e Competition judges enter scores in a collaborative realtime draft table before publishing. Competitors/spectators can see their scores live (immediately after judge confirmation).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive Broadcast Integration:\u003c/strong\u003e Providing overlay screens that can be used in event live streams (e.g: using \u003ca href=\"https://www.vmix.com/\"\u003evMix\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompetition Building:\u003c/strong\u003e Event organizers can build and edit their own competitions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEvent Timetable:\u003c/strong\u003e Event organizers can organize competition rounds into a timetable and schedule custom items (such as registration and prize ceremony).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eResponsive Mobile First Design:\u003c/strong\u003e Looks great on all devices.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSocial Integration:\u003c/strong\u003e Users can login using Facebook.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScore export and backup:\u003c/strong\u003e Competition scores are backed up and exported to google sheets.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 742px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/1-live-page_hu733f9afe48e072450eab11d781f9f70a_609132_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/1-live-page.png\" data-pswp-width=\"742\"\n data-pswp-height=\"1502\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/1-live-page_hu733f9afe48e072450eab11d781f9f70a_609132_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/1-live-page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n All event data; competitor positions, scores, rankings, heat allocations and more is updated in realtime.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eRealTime Updates\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 742px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/2-live-scores_hu158a50b560678b0f13c7464066740b45_265341_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/2-live-scores.png\" data-pswp-width=\"742\"\n data-pswp-height=\"1502\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/2-live-scores_hu158a50b560678b0f13c7464066740b45_265341_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/2-live-scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Competition judges enter scores in a collaborative realtime draft table before publishing. Competitors/spectators can see their scores live (immediately after judge confirmation).\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eLive Scoring\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/3-competition-builder_hu8b00e43bd98317dee242d66e9f7b830d_356718_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/3-competition-builder.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/3-competition-builder_hu8b00e43bd98317dee242d66e9f7b830d_356718_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/3-competition-builder.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Event organizers can build and edit their own competitions.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eCompetition Building\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2048px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration_hucaa59dfb1a956a0b2ab2ffd7b954b152_155243_300x300_fill_q75_box_smart1.jpg'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration.jpg\" data-pswp-width=\"2048\"\n data-pswp-height=\"946\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration_hucaa59dfb1a956a0b2ab2ffd7b954b152_155243_300x300_fill_q75_box_smart1.jpg\" width=\"300\" height=\"300\" alt=\"gallery-features/3a-live-broadcast-integration.jpg\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Providing overlay screens that can be used in event live streams (e.g: using [vMix](https://www.vmix.com/))\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eLive Broadcast Integration\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable_hub4925d7dd7122eb41302d9b344f1a4ef_536353_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable_hub4925d7dd7122eb41302d9b344f1a4ef_536353_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/4-admin manage timetable.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Event organizers can organize competition rounds into a timetable and schedule custom items (such as registration and prize ceremony).\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Timetable\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 3515px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/5-responsive_hua06cd6e77af1d65d214b7926870c326b_1824028_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/5-responsive.png\" data-pswp-width=\"3515\"\n data-pswp-height=\"1549\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/5-responsive_hua06cd6e77af1d65d214b7926870c326b_1824028_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/5-responsive.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Looks great on all devices.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eResponsive Mobile First Design\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 686px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/6-facebook-login_hu68f9c20fa11b725293449ec7ddfe0029_270994_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/6-facebook-login.png\" data-pswp-width=\"686\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/6-facebook-login_hu68f9c20fa11b725293449ec7ddfe0029_270994_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/6-facebook-login.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Users can login using Facebook.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eSocial Integration\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/7-export-results_hu1f36569ebf5ddc3d97334c873a75b260_594964_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/7-export-results.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/7-export-results_hu1f36569ebf5ddc3d97334c873a75b260_594964_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/7-export-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Competition scores are backed up and exported to google sheets.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eScore export and backup\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"useful-terms\"\u003eUseful Terms\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTerm\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003cth\u003eExample\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eEvent\u003c/td\u003e\n\u003ctd\u003eAn event is put on by an organizer and usually takes place over 1 or 2 days.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/event/embily\"\u003eEmbily Open 2023\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCompetition\u003c/td\u003e\n\u003ctd\u003eEvents consist of competitions/disciplines.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/competition/3f662283-b47e-48f8-915f-bfca763ebe0b\"\u003ePro Men\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eRound\u003c/td\u003e\n\u003ctd\u003eCompetitions consist of rounds. Each round will contain fewer riders than the previous. The final round winners are the competition winners.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/competition/3f662283-b47e-48f8-915f-bfca763ebe0b\"\u003ePro Men - Round 1\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eHeat\u003c/td\u003e\n\u003ctd\u003eRounds consist of heats. The winners of each heat will progress to the next round.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/heat/7d6602e7-9f48-43de-999e-09c61a0431e3\"\u003ePro Men - Round 1 - Heat 1\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSeed\u003c/td\u003e\n\u003ctd\u003eA seed is a preliminary ranking given to competitors the purposes of making a fair competition. I.e: so that the best 2 competitors do not face off in round 1\u003c/td\u003e\n\u003ctd\u003e1\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAlpaca Cup has 3 main types of users (user roles)\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCompetitors / Spectators\u003c/li\u003e\n\u003cli\u003eEvent Administrators\u003c/li\u003e\n\u003cli\u003eJudges. There are usually 3 judges for each heat.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"competitors--spectators\"\u003eCompetitors / Spectators\u003c/h3\u003e\n\u003cp\u003eCompetitors / Spectators use the app to see the overall competition structure (number of rounds, how many qualify to the next round etc..).\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/competition-results.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/competition-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/competition-results.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/competition-results.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/event%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/event page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Shows key event info, the competitions taking place registered competitors.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Page: Shows key event info...\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/live%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/live page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/live page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/live page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/timetable%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/timetable page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/timetable page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/timetable page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"event-administrators\"\u003eEvent Administrators\u003c/h3\u003e\n\u003cp\u003eEvent Administrators use the app to build and manage events.\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1917px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager_hua5a7a87ff55a2c52a93befaf5f3d418e_380001_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager.png\" data-pswp-width=\"1917\"\n data-pswp-height=\"974\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager_hua5a7a87ff55a2c52a93befaf5f3d418e_380001_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin competition manager.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin competition manager.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin competition manager.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event_hu7cb256c6267440e7494815492fc5dc7e_976063_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"976\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event_hu7cb256c6267440e7494815492fc5dc7e_976063_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin create new event.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin create new event.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin create new event.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event_hu7ab9fa2d59a957643fc0431730365a97_651913_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"975\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event_hu7ab9fa2d59a957643fc0431730365a97_651913_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin edit event.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin edit event.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin edit event.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1918px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable_hue27a151a759dce6f6f38059fa626af9a_464123_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable.png\" data-pswp-width=\"1918\"\n data-pswp-height=\"974\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable_hue27a151a759dce6f6f38059fa626af9a_464123_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin manage timetable.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin manage timetable.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin manage timetable.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores_hu7c76907e69eb6dea637ddcfa149b448d_194096_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"975\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores_hu7c76907e69eb6dea637ddcfa149b448d_194096_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin request scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin request scores.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin request scores.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores_hufeb64fab6eb65b85fcd460b3e9879488_199477_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"971\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores_hufeb64fab6eb65b85fcd460b3e9879488_199477_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin review scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin review scores.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin review scores.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"judges\"\u003eJudges\u003c/h3\u003e\n\u003cp\u003eJudges use the app score competitors. Judge scoring is a collaborative process and every judge is able to see how other judges are scoring in realtime. Once the judges agree, the scores my be checked by an administrator before becoming public.\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-1_hucdf69c751da77027bde1f9b2b3194bd3_89037_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-1.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-1_hucdf69c751da77027bde1f9b2b3194bd3_89037_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-1.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-1.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-1.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft_huedcbe82e9fdfea4c88609d54c42f831a_150824_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft_huedcbe82e9fdfea4c88609d54c42f831a_150824_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2-draft.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2-draft.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2-draft.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded_huacc18f208dd04d3a6bf5d00a6a0909f1_108578_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded_huacc18f208dd04d3a6bf5d00a6a0909f1_108578_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2-expanded.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2-expanded.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2-expanded.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2_huedcbe82e9fdfea4c88609d54c42f831a_123610_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2_huedcbe82e9fdfea4c88609d54c42f831a_123610_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup is a state of the art web application build by professional web developers with over 20 years\u0026rsquo; combined experience. For an in depth look into the technology behind Alpaca Cup \u003ca href=\"/blogs/alpaca-cup-architecture/\"\u003esee this article\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"development-team\"\u003eDevelopment Team\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.linkedin.com/in/simonverhoeven067/\"\u003eSimon\u003c/a\u003e: system design, architecture, backend, frontend\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.linkedin.com/in/lorenzoong/\"\u003eLorenzo\u003c/a\u003e: frontend, ui/styling wizard\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"impact\"\u003eImpact\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e42 live competitions completed\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAverage 1000 active users per event\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eSee Alpaca Cup in action at Brezel Wakeboard Contest 2023\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://www.youtube.com/embed/o4QmmCv05Ec\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" allowfullscreen title=\"YouTube Video\"\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\u003cp\u003eSee Alpaca Cup in action at the 2023 Embily Open\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://www.youtube.com/embed/cnMjZtYj6us\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" allowfullscreen title=\"YouTube Video\"\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n","description":"","image":"/images/projects/alpaca/alpaca.png","permalink":"https://ziggy6792.github.io/projects/alpaca-cup/","title":"Alpaca Cup"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eThis article shows an overview of the technology and architecture behind \u003ca href=\"https://alpacacup.com/events\"\u003eAlpaca Cup\u003c/a\u003e, an event management application for hosting and scoring competitions.\u003c/p\u003e\n\u003cp\u003eTo try the Alpaca Cup yourself, visit \u003ca href=\"https://alpacacup.com/\"\u003ealpacacup.com\u003c/a\u003e.\u003c/p\u003e\n\u003ch3 id=\"related-articles\"\u003eRelated Articles\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eClick \u003ca href=\"/projects/alpaca-cup\"\u003ehere\u003c/a\u003e for introduction and overview of app features\u003c/li\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/wakeboard-competition-app\"\u003ehere\u003c/a\u003e for deep dive into the original motivations for this project\u003c/li\u003e\n\u003c/ul\u003e\n\u003c!-- \u003cfigure\u003e\u003cimg src=\"judge%20scoring.png\"\n alt=\"Lighthouse Amrum\" width=\"200\" height=\"300\"/\u003e\u003cfigcaption\u003e\n \u003cp\u003eBla bla bla\u003c/p\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cfigure\u003e\u003cimg src=\"judge%20scoring.png\"\n alt=\"Lighthouse Amrum\" width=\"200\" height=\"300\"/\u003e\u003cfigcaption\u003e\n \u003cp\u003eBla bla bla\u003c/p\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n --\u003e\n\u003ch2 id=\"tools\"\u003eTools\u003c/h2\u003e\n\u003ch3 id=\"amazon-web-services\"\u003eAmazon Web Services\u003c/h3\u003e\n\u003cp\u003eAlpaca Cup makes use of several cloud services and tools provided by Amazon Web Services (AWS), which offer;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHigh scalability and availability\u003c/li\u003e\n\u003cli\u003ePay as you use payment model (when no competitions are running the cost of running the application will be negligible)\u003c/li\u003e\n\u003cli\u003eMany libraries, packages and tools available to do much of the heavy lifting involved in building modern, robust applications\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key AWS tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/cdk/\"\u003eCloud Development kit\u003c/a\u003e, to rapidly provision and configure cloud services using a CI/CD pipeline\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/cognito/\"\u003eCognito\u003c/a\u003e, to handle user sign up authorization\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/dynamodb/\"\u003eDynamoDB\u003c/a\u003e, to provide millisecond response times to API queries for competition and rider data\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/lambda/\"\u003eLambda\u003c/a\u003e, for hosting application api / executing requests serverlessly\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/api-gateway/\"\u003eAPI Gateway\u003c/a\u003e, to handle backend authentication\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.localstack.cloud/\"\u003eLocalStack\u003c/a\u003e, to run a local DynamoDB database (deployed to a docker container using CDK)\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/api-gateway_hu6674fa9b6d3e6181ab2d867db3c2c58a_476650_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/api-gateway.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"971\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/api-gateway_hu6674fa9b6d3e6181ab2d867db3c2c58a_476650_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/api-gateway.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/api-gateway.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/api-gateway.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB_hubbdf9b99a737ace09b2f6f42d076a3ed_529145_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"969\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB_hubbdf9b99a737ace09b2f6f42d076a3ed_529145_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/DynamoDB.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/DynamoDB.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/DynamoDB.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2032px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/localstack_hu660e0c719cbff297f49dc0a43670323e_485114_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/localstack.png\" data-pswp-width=\"2032\"\n data-pswp-height=\"1167\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/localstack_hu660e0c719cbff297f49dc0a43670323e_485114_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/localstack.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/localstack.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/localstack.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"typescript-everywhere\"\u003eTypescript Everywhere\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup is build on the \u003ca href=\"https://creativedesignsguru.com/typescript-everywhere/\"\u003eTypescript Everywhere\u003c/a\u003e stack this makes development much simpler. The stack consist of;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eNodeJS Backend\u003c/li\u003e\n\u003cli\u003eReactJS Frontend\u003c/li\u003e\n\u003cli\u003eInfrastructure as Code (AWS CDK)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"nodejs-backend\"\u003eNodeJS Backend\u003c/h3\u003e\n\u003cp\u003eThe backend is built using NodeJS, which offers;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVery fast execution times on AWS Lambda\u003c/li\u003e\n\u003cli\u003eGreat simplicity when working with asynchronous operations. Alpaca Cup has to interface with many AWS services asynchronously (Dynamo DB calls, fetching keys from Secrets Manager, auth requests to Cognito).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key ReactJS libraries tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/sdk-for-javascript/\"\u003eAWS SDK\u003c/a\u003e to integrate with AWS services\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://jestjs.io/\"\u003eJest\u003c/a\u003e, to run integration tests that run against a local test db\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.apollographql.com/docs/apollo-server/\"\u003eApollo Server\u003c/a\u003e to create a GraphQL api for serving requests to fetch and update event/competition data.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://typegraphql.com/\"\u003eType GraphQL\u003c/a\u003e is a library that extends Apollo Server, removing a lot of boilerplate code and providing a fantastic developer experience. The main idea of TypeGraphQL it to use Typescript classes and decorators to provide one source of truth by defining GraphQL schema.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/shiftcode/dynamo-easy\"\u003eDynamo Easy\u003c/a\u003e provides a Object Data Model as an abstraction layer for working with Dynamo DB tables\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"reactjs-frontend\"\u003eReactJS Frontend\u003c/h3\u003e\n\u003cp\u003eThe frontend is built using ReactJS, which offers;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA robust and flexible approach to handling dynamic content and rich stateful interactions (e.g. managing and rendering competition state)\u003c/li\u003e\n\u003cli\u003eA really fun development experience. Many developers, including myself, really enjoy ReactJS programming. This also ensures a rich and growing ecosystem of React libraries and tools to support present and future development\u003c/li\u003e\n\u003cli\u003ePotential to create a React Native Mobile App in the future, while reusing (and not replacing) a lot of solution code\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key ReactJS libraries tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.amplify.aws/\"\u003eAWS Amplify\u003c/a\u003e, to connect to cloud resources\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://jestjs.io/\"\u003eJest\u003c/a\u003e, to test critical app components\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.apollographql.com/docs/react/\"\u003eApollo Client\u003c/a\u003e, to store api state coming from GraphQL api\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws-amplify.github.io/\"\u003eRedux\u003c/a\u003e, to store app global state (such as user authentication state)\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://material-ui.com/\"\u003eMaterial UI\u003c/a\u003e, to make use of many out of the box UI components with a common theme and appearance\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.npmjs.com/package/axios\"\u003eAxios\u003c/a\u003e, to wrap calls to api and apply middleware (such as adding AWS authentication credentials)\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/competition-results.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/competition-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/competition-results.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/competition-results.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/event%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/event page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Shows key event info, the competitions taking place registered competitors.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Page: Shows key event info...\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/live%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/live page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/live page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/live page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/timetable page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/timetable page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/timetable page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"infrastructure-as-code-aws-cdk\"\u003eInfrastructure as Code (AWS CDK)\u003c/h3\u003e\n\u003cp\u003eAWS Cloud Development kit is used to rapidly provision and configure cloud services.\u003c/p\u003e\n\u003cp\u003eHere are the key CDK components being utilized.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.Stage.html\"\u003eCDK Stages\u003c/a\u003e to configure separate staging and production builds\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.aws.amazon.com/cdk/v2/guide/cdk_pipeline.html\"\u003eCDK Pipelines\u003c/a\u003e are used to automatically deploy app updates safely in a consistent build environment. Merging to master triggers a staging build which is deployed if all build checks pass (typechecking/lint/build/test). There is then a manual approval step to push deployment to production.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/infra/backend-pipeline_hu917c2c6d960bd9704045968688b992d4_413353_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/infra/backend-pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"2708\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/infra/backend-pipeline_hu917c2c6d960bd9704045968688b992d4_413353_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"infra/backend-pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n infra/backend-pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003einfra/backend-pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/infra/frontend-pipeline_hub4c1f49c149ebc70605b7772ada922e4_457082_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/infra/frontend-pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"2728\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/infra/frontend-pipeline_hub4c1f49c149ebc70605b7772ada922e4_457082_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"infra/frontend-pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n infra/frontend-pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003einfra/frontend-pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003c/h2\u003e\n\u003cp\u003eHere’s a map of the services and tools we are using and how they connect to each-other.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/alpaca/alpaca-cup-architecture.png\"\u003e\u003cimg src=\"/images/projects/alpaca/alpaca-cup-architecture.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eAlpaca Cup Architecture\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n","description":"","image":"/images/posts/alpaca-cup-architecture/alpaca-web-developer.png","permalink":"https://ziggy6792.github.io/blogs/alpaca-cup-architecture/","title":"Alpaca Cup Architecture"},{"content":"","description":"","image":null,"permalink":"https://ziggy6792.github.io/gallery/","title":"Image Gallery"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eAmplify offers the ability to add Lambda functions which use other configured resources including API objects backed by DynamoDb. This article will focus on creating a lambda function with the Amplify CLI that has access to the DynamoDB tables setup for an \u003ca href=\"https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js#create-the-graphql-api\"\u003eAppSync GraphQL API\u003c/a\u003e. \u003cbr\u003e\nSpecifically we will look at;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreating a Lambda function with Amplify CLI configured to use DynamoDB table resources\u003c/li\u003e\n\u003cli\u003eThe resource provisioning mechanism and potential problems with creating Lambdas created through Amplify\u003c/li\u003e\n\u003cli\u003eWriting \u0026amp; testing a sample Lambda handler to demonstrate its capabilities\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eWHY?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePerhaps you want to write some custom graphQL \u003ca href=\"https://docs.amplify.aws/cli/graphql-transformer/directives#function\"\u003e@function\u003c/a\u003e resolvers that interact with your API DynamoDB tables directly (e.g. add a batch of items to a table)\u003c/li\u003e\n\u003cli\u003eOr, set up triggers on your tables (e.g. to cascade deletes to orphaned items)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"pre-requisites\"\u003ePre-requisites\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eInstall Amplify CLI (version 4.16.1 or higher)\u003c/li\u003e\n\u003cli\u003eRun Amplify Init\u003c/li\u003e\n\u003cli\u003eCreate GraphQL API\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"creating-a-lambda-function-with-amplify-cli\"\u003eCreating a Lambda function with Amplify CLI\u003c/h2\u003e\n\u003cp\u003eUse the following commands to create a Lambda function with the Amplify CLI. After selecting the category \u003ccode\u003estorage\u003c/code\u003e, you will be prompted to select DynamoDB tables wich exist for the GraphQL API \u003ca href=\"https://docs.amplify.aws/cli/graphql-transformer/directives#model\"\u003e@model\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e: You will need Amplify CLI version 4.16.1 or higher.\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=createLambdaFunction.txt\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eNBMAC0056:SwtNinja verhoeven$ amplify add function\nUsing service: Lambda, provided by: awscloudformation\n? Provide a friendly name for your resource to be used as a label for this category in the project: doSomethingToDBTables\n? Provide the AWS Lambda function name: doSomethingToDBTables\n? Choose the function runtime that you want to use: NodeJS\n? Choose the function template that you want to use: Hello World\n? Do you want to access other resources created in this project from your Lambda function? Yes\n? Select the category storage\n? Storage has 8 resources in this project. Select the one you would like your Lambda to access Event:@model(appsync), Competition:@model(appsync), Heat:@model(appsync), SeedSlot:@model(appsync), Ride\nrAllocation:@model(appsync), User:@model(appsync)\n? Select the operations you want to permit for Event:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for Competition:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for Heat:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for SeedSlot:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for RiderAllocation:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for User:@model(appsync) create, read, update, delete\n\nYou can access the following resource attributes as environment variables from your Lambda function\n API_COMPAPI_COMPETITIONTABLE_ARN\n API_COMPAPI_COMPETITIONTABLE_NAME\n API_COMPAPI_EVENTTABLE_ARN\n API_COMPAPI_EVENTTABLE_NAME\n API_COMPAPI_GRAPHQLAPIIDOUTPUT\n API_COMPAPI_HEATTABLE_ARN\n API_COMPAPI_HEATTABLE_NAME\n API_COMPAPI_RIDERALLOCATIONTABLE_ARN\n API_COMPAPI_RIDERALLOCATIONTABLE_NAME\n API_COMPAPI_SEEDSLOTTABLE_ARN\n API_COMPAPI_SEEDSLOTTABLE_NAME\n API_COMPAPI_USERTABLE_ARN\n API_COMPAPI_USERTABLE_NAME\n ENV\n REGION\n? Do you want to invoke this function on a recurring schedule? No\n? Do you want to edit the local lambda function now? Yes\nPlease edit the file in your editor: \u0026lt;project-dir\u0026gt;/amplify/backend/function/doSomethingToDBTables/src/index.js\n? Press enter to continue\nSuccessfully added resource doSomethingToDBTables locally.\n\nNext steps:\nCheck out sample function code generated in \u0026lt;project-dir\u0026gt;/amplify/backend/function/doSomethingToDBTables/src\n\u0026#34;amplify function build\u0026#34; builds all of your functions currently in the project\n\u0026#34;amplify mock function \u0026lt;functionName\u0026gt;\u0026#34; runs your function locally\n\u0026#34;amplify push\u0026#34; builds all of your local backend resources and provisions them in the cloud\n\u0026#34;amplify publish\u0026#34; builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud\n\nNBMAC0056:SwtNinja verhoeven$ amplify push\n✔ Successfully pulled backend environment compapi from the cloud.\n\nCurrent Environment: compapi\n\n| Category | Resource name | Operation | Provider plugin |\n| -------- | --------------------- | --------- | ----------------- |\n| Function | doSomethingToDBTables | Create | awscloudformation |\n| Api | compapi | No Change | awscloudformation |\n? Are you sure you want to continue? Yes\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"potential-maximum-policy-size-issue-when-running-amplify-push\"\u003ePotential maximum policy size issue when running amplify push\u003c/h2\u003e\n\u003cp\u003eWhen running amplify push at the end of the last step you may see the following \u003ca href=\"https://github.com/aws-amplify/amplify-cli/issues/1699\"\u003eissue\u003c/a\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Maximum policy size of 10240 bytes exceeded for role \u003ccode\u003e\u0026lt;apiName\u0026gt;LambdaRole26741da9-\u0026lt;apiName\u0026gt;\u003c/code\u003e\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis issue occurs when the policy created for the lambda role (the IAM role created to run your new lambda function) exceeds the maximum size allowed for role inline policies.\u003c/p\u003e\n\u003ch3 id=\"option1-reducing-the-number-of-tables-added-to-the-role-policy\"\u003eOption1: Reducing the number of tables added to the role policy\u003c/h3\u003e\n\u003cp\u003eIf possible, you should reduce the tables and actions granted in the previous step (following the best practice that each lambda only has access to the actions and resources it expressly needs). However, this may not be possible. In my case, I wanted to write a lambda function that cascaded deletes through my table schema. Therefore, my lambda function needs to have access to all of my API tables.\u003c/p\u003e\n\u003ch3 id=\"option2-customizing-local-cloudformation-template-generated-by-amplify\"\u003eOption2: Customizing local CloudFormation template generated by Amplify\u003c/h3\u003e\n\u003cp\u003eTo understand a work around for this issue, it is important to explain in more detail what \u003ccode\u003eamplify add function\u003c/code\u003e is actually doing. This method creates a local folder for the function containing a CloudFormation template. E.g;\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eamplify\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003ebackend\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003efunction\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003edoSomethingToDBTables\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003edoSomethingToDBTables\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003ecloudformation\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003etemplate\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ejson\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003eThis template describes the lambda to be created when amplify push is next run. However, Amplify is not very clever when it comes to creating the templates which create role inline policies. The Amplify created CloudFormation template creates a separate inline policy statement for each DynamoDB table (even if the allowed actions are the same).\u003c/p\u003e\n\u003cp\u003eFor example; granting these actions;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Put*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Create*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:BatchWriteItem\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Get*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:BatchGetItem\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:List*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Describe*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Scan\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Query\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Update*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:RestoreTable*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Delete*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTo these tables;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCompetition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eUser-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eEvent-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eHeat-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eRiderAllocation-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eSeedSlot-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWill create the following access policy list of statements;\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=accessPolicyExample1.json\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e{\n \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;,\n \u0026#34;Statement\u0026#34;: [\n {\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/Competition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/Competition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n },{\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/User-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/User-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n },\n ...Repeat For Each Table\n ]\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWhich is actually equivalent to the following single statement (using the pattern resource identifier: \u003ccode\u003e*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/code\u003e matching every table for \u003cstrong\u003ethis particular GraphQL API Environment\u003c/strong\u003e);\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=accessPolicyExample2.json\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e{\n \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;,\n \u0026#34;Statement\u0026#34;: [\n {\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n }\n ]\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThe difference with using the second example however is that; generating policies this way will not cause policies to grow in size with the number of API tables and therefore will not easily hit the maximum policy size and cause the maximum policy size issue.\u003c/p\u003e\n\u003cp\u003eTherefore, a workaround for the maximum policy size issue is to modify your CloudFormation template to consolidate the assignment of actions to tables. Exactly how to modify the CloudFormation template will depend on the specific access rights you want to grant to your lambda. A common example however would be if you wanted to grant the same access to all API tables for a particular environment as shown above.\u003c/p\u003e\n\u003ch2 id=\"modify-cloudformation-template-to-give-access-to-all-amplify-api-tables\"\u003eModify CloudFormation template to give access to all Amplify API tables\u003c/h2\u003e\n\u003cp\u003eUsing \u003ca href=\"https://stedolan.github.io/jq\"\u003ejq\u003c/a\u003e we can first remove all but the first policy statement from the generated \u003ccode\u003edoSomethingToDBTables-cloudformation-template.json\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejq \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.Resources.AmplifyResourcesPolicy.Properties.PolicyDocument.Statement[0] as $stmt0 |\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e .Resources.AmplifyResourcesPolicy.Properties.PolicyDocument.Statement = [$stmt0]\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e doSomethingToDBTables-cloudformation-template.json \u0026gt;output.json\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e: \u003ccode\u003ejq\u003c/code\u003e currently does not support in-place editing of files\u003c/p\u003e\n\u003cp\u003eThis will create a new file \u003ccode\u003eoutput.json\u003c/code\u003e in the same directory, manually edit this file replacing the tableName specific resource identifier with the pattern identifier (\u003ccode\u003e*-\u0026lt;ApiName\u0026gt;-\u0026lt;EnvName\u0026gt;\u003c/code\u003e). Here\u0026rsquo;s a sample diff;\u003c/p\u003e\n\u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=editCloudFormationTemplate.diff\"\u003e\u003c/script\u003e\n\n\u003cp\u003eNext, replace original \u003ccode\u003edoSomethingToDBTables-cloudformation-template.json\u003c/code\u003e with \u003ccode\u003eoutput.json\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\u003c/span\u003e\u003cspan\u003erm doSomethingToDBTables-cloudformation-template.json\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\u003c/span\u003e\u003cspan\u003emv output.json doSomethingToDBTables-cloudformation-template.json\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003e\u003ca href=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0#file-dosomethingtodbtables-cloudformation-template-json\"\u003eHere\u003c/a\u003e is an example of my final CloudFormation template.\u003c/p\u003e\n\u003cp\u003eFinally, run \u003ccode\u003eamplify push\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eGo to your ec2 console. You should see the following created lambda function;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Created-Function.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eCreated Lambda Function\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eAlong with the following attached role and access policy.\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Created-Role.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eCreated Role\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"test-your-new-function-write-to-dynamodb-from-lambda-function\"\u003eTest your new function. Write to DynamoDB from Lambda Function\u003c/h2\u003e\n\u003cp\u003eTo test access to dynamoDB from your new Lambda function, copy the following code (from line 23) into your local lambda function index.js, replacing the hello world function that was created \u003ca href=\"/posts/amplify-lambda-dynamodb/#creating-a-lambda-function-with-amplify-cli\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=lambda-function-index.js\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/* Amplify Params - DO NOT EDIT\n\tAPI_COMPAPI_BPOSTTABLE_ARN\n\tAPI_COMPAPI_BPOSTTABLE_NAME\n\tAPI_COMPAPI_COMPETITIONTABLE_ARN\n\tAPI_COMPAPI_COMPETITIONTABLE_NAME\n\tAPI_COMPAPI_CUSTOMERTABLE_ARN\n\tAPI_COMPAPI_CUSTOMERTABLE_NAME\n\tAPI_COMPAPI_EVENTTABLE_ARN\n\tAPI_COMPAPI_EVENTTABLE_NAME\n\tAPI_COMPAPI_GRAPHQLAPIIDOUTPUT\n\tAPI_COMPAPI_HEATTABLE_ARN\n\tAPI_COMPAPI_HEATTABLE_NAME\n\tAPI_COMPAPI_RIDERALLOCATIONTABLE_ARN\n\tAPI_COMPAPI_RIDERALLOCATIONTABLE_NAME\n\tAPI_COMPAPI_SEEDSLOTTABLE_ARN\n\tAPI_COMPAPI_SEEDSLOTTABLE_NAME\n\tAPI_COMPAPI_USERTABLE_ARN\n\tAPI_COMPAPI_USERTABLE_NAME\n\tENV\n\tREGION\nAmplify Params - DO NOT EDIT */\n\nvar AWS = require(\u0026#39;aws-sdk\u0026#39;);\nvar uuid = require(\u0026#39;uuid\u0026#39;);\n\nAWS.config.update({ region: process.env.REGION });\n\nvar ddb = new AWS.DynamoDB({ apiVersion: \u0026#39;2012-08-10\u0026#39; });\n\nexports.handler = async (event, context, callback) =\u0026gt; {\n\tlet items = [{id:uuid.v4(),name:\u0026#34;Hello World\u0026#34;}];\n\ttry {\n\t\tawait write(process.env.API_COMPAPI_EVENTTABLE_NAME,items)\n\t} catch (err) {\n\t\tcallback(err)\n\t}\n\treturn callback(null,items);\n};\n\nasync function write(tableName, items) {\n\tvar itemsToWrite = items\n\tvar params = {\n\t\tRequestItems: {\n\t\t\t[tableName]:\n\t\t\t\titemsToWrite.map((item) =\u0026gt; {\n\t\t\t\t\treturn ({\n\t\t\t\t\t\tPutRequest: {\n\t\t\t\t\t\t\tItem: AWS.DynamoDB.Converter.marshall(item)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t}\n\n\t};\n\tconst data = await ddb.batchWriteItem(params).promise();\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eIMPORTANT\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDo not replace/edit the Amplify auto generated variables (lines 1 to 21), keep your own variables separate from this auto-generated code to ensure they are not overwritten.\u003c/li\u003e\n\u003cli\u003eReplace tableName (line 33) with one of your own resource attributes as environment variables that was created \u003ca href=\"/posts/amplify-lambda-dynamodb/#creating-a-lambda-function-with-amplify-cli\"\u003ehere\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eYou will also need to cd to you local lambda function directory and install npm package uuid locally\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\u003c/span\u003e\u003cspan\u003ecd \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;amplify/backend/function/doSomethingToDBTables/src/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\u003c/span\u003e\u003cspan\u003enpm i --save uuid\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003eNext run \u003ccode\u003eamplify push\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eNow you can test your function in the AWS console;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Test-Function.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eTest Function\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eAnd then verify that an item was created in the specified table;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Test-Function-Created-Item.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eTest Function Success - Created Item\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003c!-- \u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 1\u003c/span\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 2\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;doSomethingToDBTables\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 3\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;version\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;2.0.0\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 4\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;description\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Lambda function generated by Amplify\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 5\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;main\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;index.js\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 6\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;license\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Apache-2.0\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 7\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;dependencies\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 8\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;lodash\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;latest\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 9\u003c/span\u003e\u003cspan\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e10\u003c/span\u003e\u003cspan\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e --\u003e\n","description":"","image":"/images/posts/Accessing-DynamoDB-Tables/Amplify.png","permalink":"https://ziggy6792.github.io/blogs/amplify-lambda-dynamodb/","title":"Accessing Amplify GraphQL API objects from Lambda functions"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003c!-- ### About Us\n\nSimon + Vincent --\u003e\n\u003ch3 id=\"motivation\"\u003eMotivation\u003c/h3\u003e\n\u003cp\u003eCable Wakeboarding is an extreme water sport where participants (riders) are pulled (at speeds of 30+ kph) around a lake by an overhead cable (very similar to a cable car system). Much like skate parks, wake parks have obstacles (such as jumps and rails) built along the cable path. In competition, riders are judged subjectively on their ability to perform ticks (such as flips) both on and off these obstacles. There are 700+ Wakeparks in the world (60+ in Asia).\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.singaporewakepark.com/home/\"\u003eSingapore Wakepark\u003c/a\u003e (SWP) and other parks host about 4 competitions per year with an average turn out of 100 riders and 300 spectators with a SGD 60 admission fee. A typical competition consists of several disciplines (e.g. “Men’s Beginner”) containing rounds and heats (e.g. “Round 2 - Semi Final 1”). Riders are scored and ranked in each round, hoping to progress to the final, where the top 3 are awarded a price (1st place upwards of SGD 1000).\u003c/p\u003e\n\u003cp\u003eFor competition organizers there are currently 2 options when it comes to managing a competition;\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePay a federation like the IWWF to manage the competition and use their event management system. Disadvantages;\n\u003cul\u003e\n\u003cli\u003eYearly member subscription fees\u003c/li\u003e\n\u003cli\u003ePercentage taken from rider admission fees\u003c/li\u003e\n\u003cli\u003eCan only use judges affiliated with the federation (sometimes judges need to be flown in at an expense to the park)\u003c/li\u003e\n\u003cli\u003eLittle control over the structure of competition\u003c/li\u003e\n\u003cli\u003eThe applications provided by federations are not very good; not mobile optimized, results are not pushed automatically (need to refresh page), results are not real-time\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eManage the competition on your own. Receiving admissions by email, manually creating heat tables using excel, sharing screen shots of start lists on Facebook, then printing round results and pinning them on a notice board. Disadvantages;\n\u003cul\u003e\n\u003cli\u003eVery time consuming and rigid format\u003c/li\u003e\n\u003cli\u003eVery little feedback to interested parties on the state of the competition as it takes place (e.g. current round results, next round start list)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"our-goals\"\u003eOur Goals\u003c/h3\u003e\n\u003cp\u003eWe are building a web application to be used by organizers, riders, judges and spectators with the following goals;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eProvide a cheap, intuitive and flexible workflow to organizers for creating and hosting competitions\u003c/li\u003e\n\u003cli\u003eGive maximum control to organizers over their competition structure (number of disciplines, number of heats per round etc.)\u003c/li\u003e\n\u003cli\u003eGive real-time monitoring metrics to interested parties. E.g. how long will a competition take, is it running behind schedule (adverse weather can delay a schedule)\u003c/li\u003e\n\u003cli\u003eGive real-time feedback to interested parties on the state of a competition (e.g: how many points the current rider needs to proceed to the next round). This is particularly useful for competition commentators who rely on this real-time information to keep the spectators engaged and excited.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"solution\"\u003eSolution\u003c/h2\u003e\n\u003ch3 id=\"our-stakeholders\"\u003eOur Stakeholders\u003c/h3\u003e\n\u003cp\u003eAll those involved in Wakeboard Competitions;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eOrganizers e.g. Singapore Wakepark Owner\u003c/li\u003e\n\u003cli\u003eRiders\u003c/li\u003e\n\u003cli\u003eSpectators (mostly the families of riders, especially younger riders who often have a whole family to support them)\u003c/li\u003e\n\u003cli\u003eJudges\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"requirements\"\u003eRequirements\u003c/h3\u003e\n\u003cp\u003eAfter talking to our stakeholders, here are some of the key features of the app we are building;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAs an organizer I want to create competitions and heats\u003c/li\u003e\n\u003cli\u003eAs an organizer I want to assign starting \u003ca href=\"https://en.wikipedia.org/wiki/Seed_(sports)\"\u003eseeds\u003c/a\u003e to riders\u003c/li\u003e\n\u003cli\u003eAs an organizer I want to allocate registered riders into the first round \u003ca href=\"https://www.merriam-webster.com/dictionary/qualifying%20heat\"\u003eheats\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eAs a rider I want to sign up to a competition\u003c/li\u003e\n\u003cli\u003eAs a rider I want to see all the competitions and heats I am assigned to\u003c/li\u003e\n\u003cli\u003eAs a rider I want to track my overall progression in real-time (as soon as a result comes in from the judges I should receive a notification with my updated place on the scoreboard)\u003c/li\u003e\n\u003cli\u003eAs a judge I want to see the current heat I am scoring\u003c/li\u003e\n\u003cli\u003eAs a judge I want to be able to input scores for riders as they compete\u003c/li\u003e\n\u003cli\u003eAs a judge I want to be able to end a heat and have riders allocated automatically to the next round (based on the competition structure defined by the organizer)\u003c/li\u003e\n\u003cli\u003eAs a user I want to track the overall progression of all riders in real-time\u003c/li\u003e\n\u003cli\u003eAs a user I want to see real-time metrics. E.g. is a competition running behind schedule\u003c/li\u003e\n\u003c/ul\u003e\n","description":"","image":"/images/posts/wakeboard-competition-app/wakeboard-bw.jpg","permalink":"https://ziggy6792.github.io/blogs/wakeboard-competition-app/","title":"Wakeboard Competition App Project"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn this blog I aspire to detail my journey in adopting AWS Serverless Architecture;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAmplify\u003c/li\u003e\n\u003cli\u003eLambda\u003c/li\u003e\n\u003cli\u003eCognito\u003c/li\u003e\n\u003cli\u003eAppSync\u003c/li\u003e\n\u003cli\u003eDynamoDB\u003c/li\u003e\n\u003cli\u003eAPI Gateway\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eI am creating a Competition Management App for event organizers and competitors at \u003ca href=\"http://www.singaporewakepark.com/home/\"\u003eSingapore Wakepark\u003c/a\u003e. This is a full stack serverless web application (ReactJS front-end, GraphQL API (AppSync + Node JS Lambda) back-end. I chose these technologies for the job because I believe them to be a good fit to my needs;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEasy scalability\u003c/li\u003e\n\u003cli\u003ePay for what you use (when no competitions are running the cost of running the application will be negligible)\u003c/li\u003e\n\u003cli\u003eLots of good online documentation\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBut also because they are exciting technologies which I am keen to work with.\u003c/p\u003e\n\u003cp\u003eMost of my explanations and sample code will in someway showcase my Competition Management App (such as DynamoDB tables with names like Event, Competition, Heat\u0026hellip;). However I aim to distill the topics I am learning into blog posts that demonstrate my work in a way that can benefit all developers (building a variety of applications) using AWS. \u003cbr\u003e\nI will cover widely applicable topics such as;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAccessing DynamoDB tables from Lambdas\u003c/li\u003e\n\u003cli\u003eSetting up DynamoDB event triggers\u003c/li\u003e\n\u003cli\u003eCreating a cognito user pool with some mock users\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"why\"\u003eWhy\u003c/h2\u003e\n\u003cp\u003eIn all areas of software development there is more than one way to skin a fish. However, I believe this is particularly prevalent when it comes to AWS due to the vast array of features and possibilities in this space.\u003c/p\u003e\n\u003cp\u003eI hope I can get some feedback from other developers on my approach and design decisions and maybe benefit from learning alternate approaches, as well as their pros and cons through discussions.\u003c/p\u003e\n\u003cp\u003eMost importantly, I hope that other people who are looking at adopting these technologies can benefit from a documented report of someone building up their AWS skills and mastering Serverless Application development.\u003c/p\u003e\n","description":"","image":"/images/wellcome-to-my-blog/wellcome.jpg","permalink":"https://ziggy6792.github.io/blogs/wellcome-to-my-blog/","title":"Welcome to my blog"}] \ No newline at end of file +[{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eSo you are interested in Web Development? There are of 100s of different tracks that can be followed and I am not going to argue that the track below it the only or even best.\nThis blog post simply outlines;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHow I got into web development, what I found useful and what worked for me.\u003c/li\u003e\n\u003cli\u003eSome useful recourses to get you started with web development in 2024.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"bootcamp\"\u003eBootcamp\u003c/h2\u003e\n\u003ch3 id=\"step-1---javascript-fundamentals\"\u003eStep 1 - JavaScript Fundamentals\u003c/h3\u003e\n\u003cp\u003eFirst step is to get familiar with coding in general and learning some javascript.\nCoding is an infinite virtual jigsaw puzzle where you can build whatever you can imagine.\nIf the prospect of this excites you, it serves as a promising indicator to persist and delve deeper into the world of coding.\u003c/p\u003e\n\u003ch4 id=\"recourses\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.codecademy.com/courses/introduction-to-javascript/informationals/learn-javascript-welcome\"\u003eCodecademy Free Trial\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-2---web-dev-fundamentals\"\u003eStep 2 - Web Dev Fundamentals\u003c/h3\u003e\n\u003cp\u003eThe next step would be to learn web dev fundaments. Topics such as;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eFundamentally how do website work?\u003c/li\u003e\n\u003cli\u003eWhere and how does the code I write get executed?\u003c/li\u003e\n\u003cli\u003eHow do I style a site?\u003c/li\u003e\n\u003cli\u003eResponsive design\u003c/li\u003e\n\u003cli\u003eAuthentication\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"some-tips\"\u003eSome tips\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eFocus on the fun! Do not try to memories 100s of CSS properties. Focus building stuff that you think is cool.\u003c/li\u003e\n\u003cli\u003eYou do not have to spend a lot of money. YouTube series are great! And even Udemy coerces are often heavily reduced. Also if you are a student you may be able\nto get a free/cheap Udemy subscription.\u003c/li\u003e\n\u003cli\u003eReddit and Stack Overflow are probably the best places to get answers to your questions. Reddit for general questions questions (e.g: \u003ca href=\"https://www.reddit.com/r/aws/comments/qcpyiz/cheapest_way_to_integrate_aws_cognito_with/\"\u003ethis\u003c/a\u003e)\nStack Overflow for specific questions (e.g \u003ca href=\"https://stackoverflow.com/questions/69365021/typegoose-mongoose-map-custom-type-to-db\"\u003ethis\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003eI do not rate classes. If you need a class to pleasure you to keep learning. Maybe you are not motivated enough to keep doing this.\nFocus on what you enjoy!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"what-should-i-learn\"\u003eWhat should I learn?\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eLearn a frontend framework. I recommend React.\u003c/li\u003e\n\u003cli\u003eIf you learn React. Learn functional components form the start. Do not waste your time learning the old style class based components\u003c/li\u003e\n\u003cli\u003eYou may want to learn a server side React (i.e: \u003ca href=\"https://nextjs.org/\"\u003eNextJS\u003c/a\u003e) but I would say that is very complicated place to start.\nI would suggest starting with single page applications (i.e: \u003ca href=\"https://vitejs.dev/guide/\"\u003eVite\u003c/a\u003e) and be aware that server side React exists so that you can reach for it in future if you need it\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"recourses-1\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.udemy.com/course/the-web-developer-bootcamp/\"\u003eUdemy - The Web Developer Bootcamp 2024\u003c/a\u003e -\nWhile I haven\u0026rsquo;t personally taken this course, I was thoroughly impressed by my friend\u0026rsquo;s journey. Having transitioned from a school teacher to a web developer,\nthey demonstrated remarkable proficiency within just 1.5 years of embarking on their web development venture.\nThis was their first course they highly recommend it.\u003c/li\u003e\n\u003cli\u003eFor help - \u003ca href=\"https://www.reddit.com/r/aws/\"\u003er/aws\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/javascript/\"\u003er/javascript\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/nextjs/\"\u003er/nextjs\u003c/a\u003e, \u003ca href=\"https://www.reddit.com/r/node/\"\u003er/node\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-3---learn-typescript\"\u003eStep 3 - Learn TypeScript\u003c/h3\u003e\n\u003cp\u003eNow it\u0026rsquo;s time to learn TypeScript. You should really do this as soon as possible. There is an initial learning curve but then immediate payoff which will accelerate\nyour productivity. Learning TypeScript also opens the door to new frameworks and libraries (such as \u003ca href=\"https://nestjs.com/\"\u003eNest\u003c/a\u003e and \u003ca href=\"https://typegraphql.com/\"\u003eTypeGraphQL\u003c/a\u003e) which often have a better developer experience than using JS alone.\nMy biggest regret in my own web dev journey is not learning TypeScript sooner!\u003c/p\u003e\n\u003ch4 id=\"recourses-2\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=jrKcJxF0lAU\"\u003eYouTube - React with TypeScript Crash Course\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/watch?v=Z5iWr6Srsj8\u0026amp;t=3s\"\u003eYouTube - React Typescript Tutorial\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/@bawad\"\u003eYouTube - Anything and Everything from Ben Awad!\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-4---learn-fullstack-typescript-development\"\u003eStep 4 - Learn FullStack TypeScript Development\u003c/h3\u003e\n\u003cp\u003eYou need a lot more than React to build and deploy a modern website.\u003c/p\u003e\n\u003ch4 id=\"some-key-topis\"\u003eSome key topis\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003eWorking with apis. Even if you are not a full stack developer you will inevitably have to use apis in web development. Learn how to make and consume Type Safe APIs!\u003c/li\u003e\n\u003cli\u003eLearn how to deploy websites. Deploying on AWS is a popular choice. You can do this with TypeScript too!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"recourses-3\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/blogs/typescript-journey/\"\u003eTypeScript Journey\u003c/a\u003e - A journey into Modern Web Development, CI/CD and AWS Magic!\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-5---play-around-with-some-sample-repositories\"\u003eStep 5 - Play around with some sample repositories\u003c/h3\u003e\n\u003cp\u003eFind some repos that interest you and play around with them. Be curious and add a couple of cool features.\u003c/p\u003e\n\u003ch4 id=\"recourses-4\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/#samples\"\u003eCode Samples\u003c/a\u003e - Here are some of my sample projects. Check them out and play around.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/\"\u003eGitHub\u003c/a\u003e - There are thousands of projects there to look at! Most cool new libraries (e.g: \u003ca href=\"https://tanstack.com/router/v1\"\u003eTanStack Router\u003c/a\u003e)\nhave stater projects on GitHub. Playing around with them is a great way to learn.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-6---build-a-personal-project\"\u003eStep 6 - Build a Personal Project\u003c/h3\u003e\n\u003cp\u003eBuild something fun! The best way to learn is to build something from scratch and expand your horizons. Get yourself totally stuck and learn how to unblock yourself.\nDo not try to build the next AirBnb. Build something simple and fun that you and your friends/family might use.\u003c/p\u003e\n\u003ch4 id=\"recourses-5\"\u003eRecourses\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"/blogs/wakeboard-competition-app/\"\u003eWakeboard Competition App Project\u003c/a\u003e - My first passion project\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.google.com/document/d/1kjhAq2c5wTl7shTc2lWM8Pe_JBD1edNWy4XQdGbMNiE/edit?usp=sharing\"\u003eCode Assignment\u003c/a\u003e - If you are really stuck for ideas then here\nis a code assignment which I set to evaluate candidates.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"step-7---call-me-\"\u003eStep 7 - Call Me! ☎️\u003c/h3\u003e\n\u003cp\u003eStill having fun!? If you made it this far than you must really enjoy this stuff. Give me a call and let me see if I can find you a real project to work on!\u003c/p\u003e\n","description":"","image":"/images/posts/web-dev-bootcamp/react.jpg","permalink":"https://ziggy6792.github.io/blogs/web-dev-bootcamp/","title":"Fullstack Web Development - Bootcamp"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eModern web development is an exciting challenge, with a myriad of languages, runtimes, and AWS services to conquer. Dive into this interactive workshop, where we\u0026rsquo;ll explore cutting-edge frontend and backend development, all while staying firmly in the TypeScript world.\u003c/p\u003e\n\u003cp\u003eHighlights:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreate modern TypeScript web apps with React, Vite, and Next.JS.\u003c/li\u003e\n\u003cli\u003eMaster TypeScript CI/CD with SST and AWS CDK.\u003c/li\u003e\n\u003cli\u003eDeploy a fullstack application to AWS complete with; Next.js frontend, Node.JS apis, cognito login, S3 upload/download and TypeScript IAC.\u003c/li\u003e\n\u003cli\u003eEnsure type safety from backend to frontend through the capabilities of tRPC.\u003c/li\u003e\n\u003cli\u003eGet a glimpse of the future with Deno and Bun.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eJoin us for an action-packed TypeScript adventure! 🚀🌟\u003c/p\u003e\n\u003ch3 id=\"target-audience\"\u003eTarget Audience\u003c/h3\u003e\n\u003cp\u003eAre you interested in web development with TypeScript? Maybe you already know some React but want to level up to deploying and running fullstack web applications to AWS. This web series is for you!\u003c/p\u003e\n\u003ch3 id=\"links\"\u003eLinks\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.youtube.com/playlist?list=PLz7EK8bcbCdDEcoa0cz4DNXjsFaxCVOfs\"\u003eWeb Series\u003c/a\u003e (YouTube private link, email me for access)\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.meetup.com/tech-meetup/events/296204384\"\u003eMeetup\u003c/a\u003e This web series is based off a workshop delivered in Ho Chi Minh, October 2023.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.google.com/presentation/d/1mYOBD6kO1uWB57J3P9wYvQVRdLzKPNj-/edit?usp=sharing\u0026amp;ouid=115437333129535741316\u0026amp;rtpof=true\u0026amp;sd=true\"\u003eSlides\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/ziggy6792/typeScript-journey\"\u003eCode\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"course-outline\"\u003eCourse Outline\u003c/h2\u003e\n\u003ch4 id=\"1---welcome\"\u003e1 - Welcome\u003c/h4\u003e\n\u003cp\u003eWelcome to my web series “TypeScript Journey” a journey into Modern Web Development, CI/CD and AWS Magic!\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"2---demo-intro--prerequisites\"\u003e2 - Demo Intro \u0026amp; Prerequisites\u003c/h4\u003e\n\u003cp\u003eA look at what we will be building in this web series and prerequisites to get started.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"3---intro-ts-mono-repo\"\u003e3 - Intro TS Mono-Repo\u003c/h4\u003e\n\u003cp\u003eIntroduction to a modern TypeScript Mono-Repository using; Turborepo, yarn workspaces, Eslint, CDK, SST.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"4---add-and-deploy-vite-react-app\"\u003e4 - Add and deploy Vite React App\u003c/h4\u003e\n\u003cp\u003eCreating a Vite React App in a TypeScript Mono-Repo deploying to AWS using Cloud Development Kit (SST).\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"5---add-next-js-app\"\u003e5 - Add Next JS App\u003c/h4\u003e\n\u003cp\u003eCreating a Next JS App in a TypeScript Mono-Repo using Create T3 App.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"6---add-next-auth-cognito\"\u003e6 - Add Next Auth (Cognito)\u003c/h4\u003e\n\u003cp\u003eDeploying Cognito Userpool to AWS using Cloud Development Kit (SST) and configuring sign in with Next Auth.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"7---deploy-next-app-to-aws\"\u003e7 - Deploy Next App to AWS\u003c/h4\u003e\n\u003cp\u003eDeploying Next App to AWS using Open Next. Next App runs serverless on AWS lambda which is a very cheap way to deploy a Next App. We also configure AWS Cognito to accept signin redirect requests from the deployed site. And this highlights the issue of dependency cycles that can occur in IAC.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"8---why-i-like-this-stack\"\u003e8 - Why I Like this Stack\u003c/h4\u003e\n\u003cp\u003eAn overview of some of the advantages of using this stack. Cheap run costs due to serverless architecture, bleeding edge features, fine grained access control through NextAuth and tRPC and Type Safe APIs.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"9---fix-trpc-mutations\"\u003e9 - Fix tRPC Mutations\u003c/h4\u003e\n\u003cp\u003eOpen Next does not support streaming tPRC mutations out of the box. This short video shows a fix.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"10---upload-download-demo\"\u003e10 - Upload Download Demo\u003c/h4\u003e\n\u003cp\u003eImplementing upload/download functionality to the Next app. Adding an S3 bucket to SST app, importing the bucket into Next App and creating an api to generate pre-signed urls. In this demo, bucket name is defined in a common package and imported into SST app and Next app.\n\u003cbr/\u003e\n\u003cbr/\u003e\u003c/p\u003e\n\u003ch4 id=\"11---using-sst-bind-and-summary\"\u003e11 - Using SST Bind and Summary\u003c/h4\u003e\n\u003cp\u003eUsing SST bind to refactor the approach from the previous video. In this demo, bucket name is defined in SST app and then SST bind is run in Next app to generate types and the bucket name to use. Finally a summary of what we have built.\u003c/p\u003e\n","description":"","image":"/images/posts/typescript-journey/meetup.png","permalink":"https://ziggy6792.github.io/blogs/typescript-journey/","title":"TypeScript Journey - Web Series"},{"content":"\u003ch1 id=\"join-alpaca-consultants-network\"\u003eJoin Alpaca Consultants Network\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eEngage in tech discussions\u003c/li\u003e\n\u003cli\u003eAcquire knowledge and solutions\u003c/li\u003e\n\u003cli\u003eGet answers to tech questions\u003c/li\u003e\n\u003cli\u003eRecruiting skilled developers for paid projects\u003c/li\u003e\n\u003c/ul\u003e\n","description":"","image":"/images/posts/join/join-network.png","permalink":"https://ziggy6792.github.io/join/","title":"Join Alpaca Consultants Network"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhire is a Singaporean Tech Startup which helps companies hire great people faster through its platform and a top tier community of hiring ambassadors, saving companies hours on pre-screening and outreach efforts as well as thousands of dollars on job ads and agency fees without compromising candidate quality.\u003c/p\u003e\n\u003ch3 id=\"motivation\"\u003eMotivation\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://www.linkedin.com/in/marsilib/\"\u003eBenjamin Marsili\u003c/a\u003e (founder of Whire) hired me to help him build a recruiting network platform for companies to;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBroadcast jobs from LinkedIn (and other sources)\u003c/li\u003e\n\u003cli\u003eDirectly interview candidates who are referred members of the Whire network\u003c/li\u003e\n\u003cli\u003eMake hiring decisions and then pay for successfully hired candidates\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor more information on Whire visit \u003ca href=\"https://www.whire.net/\"\u003ewhire.net\u003c/a\u003e.\u003cbr\u003e\nTo try the Whire platform yourself, visit \u003ca href=\"https://app.whire.net/\"\u003eapp.whire.net\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"my-engagement-with-whire\"\u003eMy Engagement with Whire\u003c/h2\u003e\n\u003ch3 id=\"initial-phase\"\u003eInitial Phase\u003c/h3\u003e\n\u003cp\u003eBefore engaging with me Whire already had 1 fulltime junior developer and a React (Typescript) UI component library consisting of simple UI components and some full screen layouts. The component library was also complete wth Story Book (UI Component Development Library). Up to this point, the component library was primarily used to show potential investors what they were trying to build.\u003c/p\u003e\n\u003cp\u003eWhire had also started work on a Next.JS web-app using their component library. The app to this point was also for demo purposes only; login/data/integrations were all mocked.\u003c/p\u003e\n\u003ch3 id=\"phase-1-2-weeks\"\u003ePhase 1 (2 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eFor phase 1, Whire\u0026rsquo;s initial aims were;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDeploy existing web-app a cost effective and scalable way\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eI strongly recommended to deploy to AWS for the following reasons:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAWS can do a lot of undifferentiated heavy lifting (e.g: Fargate can be used to deploy and scale the web-app without having to do a lot of configuration)\u003c/li\u003e\n\u003cli\u003eAWS allows a pay as you use payment model\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eNote: Vercel was also considered, however as their was a future plan to integrate other Amazon services it seemed better to keep everything under one roof. Also I had read that Vercel costs are expensive for sites once traffic increases\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eI proposed the following AWS architecture to Ben\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/whire-architecture-phase-1.png\"\u003e\u003cimg src=\"/images/projects/whire/whire-architecture-phase-1.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Architecture Phase 1\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eWeb-app is containerized and pushed to ECR\u003c/li\u003e\n\u003cli\u003eWeb-app deployed managed and auto-scaled using Fargate\u003c/li\u003e\n\u003cli\u003eAWS Code pipeline to automatically build/test and deploy code\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eIn order to execute this plan I completed the following steps;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRestructured the frontend into a mono-repo so that component library and web-app are built and tested independently (using shared extended configurations)\u003c/li\u003e\n\u003cli\u003eAdded (Jest) tests for web-app and component library\u003c/li\u003e\n\u003cli\u003eWrote a docker compose file to build web-app and component library into an image that could be pushed to Amazon ECR\u003c/li\u003e\n\u003cli\u003eCreated a CDK stack to deploy the planned architecture to staging and production environments\u003c/li\u003e\n\u003cli\u003eCreated a CDK pipeline to build, test and deploy the web-app and component library\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"result\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eWhire web-app was successfully deployed on AWS\u003c/p\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1916px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-1/deployed-web-app_hu47c770cf9e83f337bb3d9007024937eb_607014_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-1/deployed-web-app.png\" data-pswp-width=\"1916\"\n data-pswp-height=\"1056\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-1/deployed-web-app_hu47c770cf9e83f337bb3d9007024937eb_607014_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-1/deployed-web-app.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-1/deployed-web-app.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-1/deployed-web-app.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-1/pipeline_hu1f27d3e5234630886d6909584039e8fb_497806_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-1/pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"3394\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-1/pipeline_hu1f27d3e5234630886d6909584039e8fb_497806_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-1/pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-1/pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-1/pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-2-4-weeks\"\u003ePhase 2 (4 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective-1\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eAfter successfully deploying their web-app to AWS. For phase 2, Whire\u0026rsquo;s aims were\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStart working on a backend API to store and serve data about jobs and candidates in the whire network\u003c/li\u003e\n\u003cli\u003eCreate an authentication solution so users could login to Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture-1\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eFor building Whire\u0026rsquo;s api I proposed creating a NodeJS GraphQL api in Typescript and deploying it to Lambda. This has the following advantages.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBuilding everything in Typescript keeps development simple as skills/knowledge/tools are reusable\u003c/li\u003e\n\u003cli\u003eDeploying to Lambda is very cheap and scalable\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003eNote: AppSync was also considered for the GraphQL api. However I find it very awkward to extend beyond out of the box functionality (thought maybe now it has improved).\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor storing Data I suggested using a serverless MongoDB. A schemaless DB was preferable as data such as job posts coming from different sources can be unstructured.\u003c/p\u003e\n\u003cp\u003e\u003cem\u003eNote: The reason for choosing MongoDB and not Amazon\u0026rsquo;s own schemaless DB DynamoDB is that mongo has fantastic Typescript support. Namely, \u003ca href=\"https://typegoose.github.io/typegoose/\"\u003etypegoose\u003c/a\u003e, an object document model written and managed by MongoDB\u0026rsquo;s creators.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eFor authenticating users I suggested AWS Cognito for user login along with AWS Api Gateway to authenticate user api requests.\u003c/p\u003e\n\u003cp\u003eI proposed the following AWS architecture to Ben\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/whire-architecture-phase-2.png\"\u003e\u003cimg src=\"/images/projects/whire/whire-architecture-phase-2.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Architecture Phase 2\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eGraphQL api built with NodeJS/Apollo Server\u003c/li\u003e\n\u003cli\u003eServerless MongoDB\u003c/li\u003e\n\u003cli\u003eAWS Code pipeline to automatically build/test and deploy code\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution-1\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eIn order to execute this plan I completed the following steps;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreated a GraphQL api in NodeJS using Apollo Server and TypeGraphQL\u003c/li\u003e\n\u003cli\u003eConfigured Api Gateway (using CDK) to allow api requests from authenticated cognito users (use access) and allowed IAM roles (role access)\u003c/li\u003e\n\u003cli\u003eCreated a CDK stack to deploy the planned architecture to staging and production environments\u003c/li\u003e\n\u003cli\u003eCreated a CDK pipeline to build, test and deploy the api\u003c/li\u003e\n\u003cli\u003eCoached Whire\u0026rsquo;s junior developer so he could create his own GraphQL queries and mutations\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"result-1\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eWhire api was successfully built and deployed on AWS\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1921px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-2/api-gateway_hu015b6f86db0c3cc0ddb3a9ae399999e8_148013_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-2/api-gateway.png\" data-pswp-width=\"1921\"\n data-pswp-height=\"973\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-2/api-gateway_hu015b6f86db0c3cc0ddb3a9ae399999e8_148013_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-2/api-gateway.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-2/api-gateway.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-2/api-gateway.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/whire/gallery-phase-2/pipeline_huc93bea273eb34a29156b2ddefb9f0405_598639_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/whire/gallery-phase-2/pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"3516\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/whire/gallery-phase-2/pipeline_huc93bea273eb34a29156b2ddefb9f0405_598639_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-phase-2/pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-phase-2/pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-phase-2/pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-3-8-weeks\"\u003ePhase 3 (8 weeks)\u003c/h3\u003e\n\u003ch4 id=\"objective-2\"\u003eObjective\u003c/h4\u003e\n\u003cp\u003eOne of the key features for Whire is referring candidates. For referrals, both the referrer and referee should use LinkedIn to login and share their information.\u003c/p\u003e\n\u003cp\u003eFor phase 3, Whire\u0026rsquo;s aims were;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUsers should be able to login to Whire using their LinkedIn credentials\u003c/li\u003e\n\u003cli\u003eLinkedIn authenticated users should have api access\u003c/li\u003e\n\u003cli\u003eOn logging in for the first time a user should be created in Whire\u0026rsquo;s database\u003c/li\u003e\n\u003cli\u003eUsers LinkedIn profile information (name, nationality, skills etc\u0026hellip;) should be pulled into Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"architecture-2\"\u003eArchitecture\u003c/h4\u003e\n\u003cp\u003eI proposed the following process for user registration, profile setup and sign in.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/whire/auth.png\"\u003e\u003cimg src=\"/images/projects/whire/auth.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eWhire Authentication Flow\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cul\u003e\n\u003cli\u003eUse LinkedIn as federated auth provider with Cognito\u003c/li\u003e\n\u003cli\u003eA Lambda runs on user confirmation (after email validation) to fetch the users profile information. This is non blocking, incase the data can not be retrieved we do not block the user from being onboarded to Whire\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"execution-2\"\u003eExecution\u003c/h4\u003e\n\u003cp\u003eAfter starting to execute this plan we ran into a problem. Cognito does not support LinkedIn login out of the box. Cognito only supports some federated providers (Facebook/Google) as well as OpenId/SAML identity providers (LinkedIn only has an Auth2 provider).\u003c/p\u003e\n\u003cp\u003eI then proposed 3 options\u003c/p\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eOption\u003c/th\u003e\n\u003cth\u003eAdvantages\u003c/th\u003e\n\u003cth\u003eDisadvantages\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eUse a third party solution like Auth0/octa\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eSimple\u003c/strong\u003e to set up LinkedIn login\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eExpensive\u003c/strong\u003e (Auth0 $240/mo - octa $3/MAU)\u003cbr\u003e Outside the AWS ecosystem (bad for developers and CI/CD) \u003cbr\u003e \u003cstrong\u003eAPI Access\u003c/strong\u003e would require a custom lambda authorizer in AWS gateway \u003cbr\u003e \u003cstrong\u003eSign Up events\u003c/strong\u003e would require custom setup outside of AWS\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eMixed approach. Use cognito with Auth0 OpenID provider (use Auth0 as a middleman)\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eSimplest solution\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eBad user experience.\u003c/strong\u003e We don’t control the Auth0 login flow \u003cbr\u003e \u003cstrong\u003eMost expensive solution.\u003c/strong\u003e As we pay for our LinkedIn users twice \u003cbr\u003e \u003cstrong\u003eNo one source of truth\u003c/strong\u003e as users managed in Auth0 and cognito\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eWrite my own OpenId provider.\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eCheap\u003c/strong\u003e \u003cbr\u003e \u003cstrong\u003eEverything kept in AWS\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cstrong\u003eComplex\u003c/strong\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eI chose to write my own OpenId provider, which adapts LinkedIn Auth2 provider to OpenId. Luckily I was able to find an existing project for the \u003ca href=\"https://github.com/TimothyJones/github-cognito-openid-wrapper\"\u003eGithub Auth2 provider\u003c/a\u003e and adapt the code to my needs (LinkedIn apis/scopes). I hosted the custom provider on AWS lambda and added the deployment to CDK along with the additional \u003ca href=\"https://github.com/ziggy6792/linkedin-cognito-openid-wrapper#3-finalise-cognito-configuration\"\u003eCogntio OpenId configuration\u003c/a\u003e. The code more my standalone OpenId Auth2 wrapper can be found \u003ca href=\"https://github.com/ziggy6792/linkedin-cognito-openid-wrapper\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003ch4 id=\"result-2\"\u003eResult\u003c/h4\u003e\n\u003cp\u003eIn the video below you can see Whire\u0026rsquo;s sign up process for referrers and referees. Users are able to sign up to Whire using their LinkedIn credentials and profile data (e.g. full name and profile photo) is pulled into Whire.\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://player.vimeo.com/video/816871864\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" title=\"vimeo video\" webkitallowfullscreen mozallowfullscreen allowfullscreen\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"phase-4-4-weeks\"\u003ePhase 4 (4 weeks)\u003c/h3\u003e\n\u003cp\u003eFor phase 4 I added ATS integration using \u003ca href=\"https://merge.dev/categories/ats-recruiting-api\"\u003emerge.dev\u003c/a\u003e which allows companies to integrate Whire with their existing recruiting flows and tools. In this phase Whire was successfully integrated with \u003ca href=\"https://recruitee.com/\"\u003eRecruitee\u003c/a\u003e and \u003ca href=\"https://www.greenhouse.com/\"\u003eGreenhouse\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n\u003ch2 id=\"impact\"\u003eImpact\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003eWhire Network has sofar grown to; 30 clients, 200 referrers and 100 posted jobs.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eWhether you are starting a new project or restructuring an old stack, his work ethics, drive and sound engineering mind will make the difference between what could have been an expensive failure and a software stack providing a great user as well as developer experience. Working with Simon is investing in your company\u0026rsquo;s sustainable future.\u003c/p\u003e\n— \u003ccite\u003e\u003ca href=\"https://www.linkedin.com/in/harikrishnan-nagarajan-65871a55/\"\u003eBenjamin Marsili\u003c/a\u003e\u003c/cite\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eIn June 2021, I took on a role as CTO of Whire.\u003c/p\u003e\n\u003c/blockquote\u003e\n","description":"","image":"/images/projects/whire/whire.png","permalink":"https://ziggy6792.github.io/projects/whire/","title":"Whire"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://alpacacup.com/events\"\u003eAlpaca Cup\u003c/a\u003e is an event management application for hosting and scoring competitions. The app has already been used in wakeboarding and wakeskating competitions all over the world, and we hope to branch out in other extreme sports in the future (such as skateboarding).\u003c/p\u003e\n\u003cp\u003eAlpaca Cup serves 2 main purposes;\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eSharing event information (who is in each round, when rounds start, who progress to the next round)\u003c/li\u003e\n\u003cli\u003eSharing results for each round in realtime (i.e: live scores as they are entered by judges)\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eTo try the Alpaca Cup yourself, visit \u003ca href=\"https://alpacacup.com/\"\u003ealpacacup.com\u003c/a\u003e.\u003c/p\u003e\n\u003ch3 id=\"related-articles\"\u003eRelated Articles\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/alpaca-cup-architecture\"\u003ehere\u003c/a\u003e for deep dive into the technology behind Alpaca Cup\u003c/li\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/wakeboard-competition-app\"\u003ehere\u003c/a\u003e for deep dive into the original motivations for this project\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"features\"\u003eFeatures\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup\u0026rsquo;s main features are;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRealTime Updates:\u003c/strong\u003e All event data; competitor positions, scores, rankings, heat allocations and more is updated in realtime.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive Scoring:\u003c/strong\u003e Competition judges enter scores in a collaborative realtime draft table before publishing. Competitors/spectators can see their scores live (immediately after judge confirmation).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive Broadcast Integration:\u003c/strong\u003e Providing overlay screens that can be used in event live streams (e.g: using \u003ca href=\"https://www.vmix.com/\"\u003evMix\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCompetition Building:\u003c/strong\u003e Event organizers can build and edit their own competitions.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEvent Timetable:\u003c/strong\u003e Event organizers can organize competition rounds into a timetable and schedule custom items (such as registration and prize ceremony).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eResponsive Mobile First Design:\u003c/strong\u003e Looks great on all devices.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSocial Integration:\u003c/strong\u003e Users can login using Facebook.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eScore export and backup:\u003c/strong\u003e Competition scores are backed up and exported to google sheets.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 742px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/1-live-page_hu733f9afe48e072450eab11d781f9f70a_609132_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/1-live-page.png\" data-pswp-width=\"742\"\n data-pswp-height=\"1502\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/1-live-page_hu733f9afe48e072450eab11d781f9f70a_609132_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/1-live-page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n All event data; competitor positions, scores, rankings, heat allocations and more is updated in realtime.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eRealTime Updates\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 742px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/2-live-scores_hu158a50b560678b0f13c7464066740b45_265341_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/2-live-scores.png\" data-pswp-width=\"742\"\n data-pswp-height=\"1502\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/2-live-scores_hu158a50b560678b0f13c7464066740b45_265341_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/2-live-scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Competition judges enter scores in a collaborative realtime draft table before publishing. Competitors/spectators can see their scores live (immediately after judge confirmation).\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eLive Scoring\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/3-competition-builder_hu8b00e43bd98317dee242d66e9f7b830d_356718_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/3-competition-builder.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/3-competition-builder_hu8b00e43bd98317dee242d66e9f7b830d_356718_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/3-competition-builder.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Event organizers can build and edit their own competitions.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eCompetition Building\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2048px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration_hucaa59dfb1a956a0b2ab2ffd7b954b152_155243_300x300_fill_q75_box_smart1.jpg'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration.jpg\" data-pswp-width=\"2048\"\n data-pswp-height=\"946\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/3a-live-broadcast-integration_hucaa59dfb1a956a0b2ab2ffd7b954b152_155243_300x300_fill_q75_box_smart1.jpg\" width=\"300\" height=\"300\" alt=\"gallery-features/3a-live-broadcast-integration.jpg\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Providing overlay screens that can be used in event live streams (e.g: using [vMix](https://www.vmix.com/))\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eLive Broadcast Integration\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable_hub4925d7dd7122eb41302d9b344f1a4ef_536353_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/4-admin%20manage%20timetable_hub4925d7dd7122eb41302d9b344f1a4ef_536353_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/4-admin manage timetable.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Event organizers can organize competition rounds into a timetable and schedule custom items (such as registration and prize ceremony).\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Timetable\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 3515px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/5-responsive_hua06cd6e77af1d65d214b7926870c326b_1824028_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/5-responsive.png\" data-pswp-width=\"3515\"\n data-pswp-height=\"1549\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/5-responsive_hua06cd6e77af1d65d214b7926870c326b_1824028_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/5-responsive.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Looks great on all devices.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eResponsive Mobile First Design\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 686px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/6-facebook-login_hu68f9c20fa11b725293449ec7ddfe0029_270994_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/6-facebook-login.png\" data-pswp-width=\"686\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/6-facebook-login_hu68f9c20fa11b725293449ec7ddfe0029_270994_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/6-facebook-login.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Users can login using Facebook.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eSocial Integration\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2446px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-features/7-export-results_hu1f36569ebf5ddc3d97334c873a75b260_594964_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-features/7-export-results.png\" data-pswp-width=\"2446\"\n data-pswp-height=\"1388\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-features/7-export-results_hu1f36569ebf5ddc3d97334c873a75b260_594964_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-features/7-export-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Competition scores are backed up and exported to google sheets.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eScore export and backup\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"useful-terms\"\u003eUseful Terms\u003c/h2\u003e\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eTerm\u003c/th\u003e\n\u003cth\u003eDescription\u003c/th\u003e\n\u003cth\u003eExample\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003eEvent\u003c/td\u003e\n\u003ctd\u003eAn event is put on by an organizer and usually takes place over 1 or 2 days.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/event/embily\"\u003eEmbily Open 2023\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eCompetition\u003c/td\u003e\n\u003ctd\u003eEvents consist of competitions/disciplines.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/competition/3f662283-b47e-48f8-915f-bfca763ebe0b\"\u003ePro Men\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eRound\u003c/td\u003e\n\u003ctd\u003eCompetitions consist of rounds. Each round will contain fewer riders than the previous. The final round winners are the competition winners.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/competition/3f662283-b47e-48f8-915f-bfca763ebe0b\"\u003ePro Men - Round 1\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eHeat\u003c/td\u003e\n\u003ctd\u003eRounds consist of heats. The winners of each heat will progress to the next round.\u003c/td\u003e\n\u003ctd\u003e\u003ca href=\"https://alpacacup.com/heat/7d6602e7-9f48-43de-999e-09c61a0431e3\"\u003ePro Men - Round 1 - Heat 1\u003c/a\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003eSeed\u003c/td\u003e\n\u003ctd\u003eA seed is a preliminary ranking given to competitors the purposes of making a fair competition. I.e: so that the best 2 competitors do not face off in round 1\u003c/td\u003e\n\u003ctd\u003e1\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAlpaca Cup has 3 main types of users (user roles)\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCompetitors / Spectators\u003c/li\u003e\n\u003cli\u003eEvent Administrators\u003c/li\u003e\n\u003cli\u003eJudges. There are usually 3 judges for each heat.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"competitors--spectators\"\u003eCompetitors / Spectators\u003c/h3\u003e\n\u003cp\u003eCompetitors / Spectators use the app to see the overall competition structure (number of rounds, how many qualify to the next round etc..).\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/competition-results.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/competition-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/competition-results.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/competition-results.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/event%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/event page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Shows key event info, the competitions taking place registered competitors.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Page: Shows key event info...\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/live%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/live page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/live page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/live page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-competitors/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-competitors/timetable%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-competitors/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-competitors/timetable page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-competitors/timetable page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-competitors/timetable page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"event-administrators\"\u003eEvent Administrators\u003c/h3\u003e\n\u003cp\u003eEvent Administrators use the app to build and manage events.\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1917px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager_hua5a7a87ff55a2c52a93befaf5f3d418e_380001_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager.png\" data-pswp-width=\"1917\"\n data-pswp-height=\"974\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20competition%20manager_hua5a7a87ff55a2c52a93befaf5f3d418e_380001_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin competition manager.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin competition manager.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin competition manager.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event_hu7cb256c6267440e7494815492fc5dc7e_976063_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"976\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20create%20new%20event_hu7cb256c6267440e7494815492fc5dc7e_976063_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin create new event.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin create new event.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin create new event.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event_hu7ab9fa2d59a957643fc0431730365a97_651913_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"975\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20edit%20event_hu7ab9fa2d59a957643fc0431730365a97_651913_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin edit event.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin edit event.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin edit event.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1918px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable_hue27a151a759dce6f6f38059fa626af9a_464123_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable.png\" data-pswp-width=\"1918\"\n data-pswp-height=\"974\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20manage%20timetable_hue27a151a759dce6f6f38059fa626af9a_464123_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin manage timetable.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin manage timetable.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin manage timetable.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores_hu7c76907e69eb6dea637ddcfa149b448d_194096_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"975\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20request%20scores_hu7c76907e69eb6dea637ddcfa149b448d_194096_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin request scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin request scores.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin request scores.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores_hufeb64fab6eb65b85fcd460b3e9879488_199477_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"971\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-event-administrators/admin%20review%20scores_hufeb64fab6eb65b85fcd460b3e9879488_199477_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-event-administrators/admin review scores.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-event-administrators/admin review scores.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-event-administrators/admin review scores.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"judges\"\u003eJudges\u003c/h3\u003e\n\u003cp\u003eJudges use the app score competitors. Judge scoring is a collaborative process and every judge is able to see how other judges are scoring in realtime. Once the judges agree, the scores my be checked by an administrator before becoming public.\u003c/p\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-1_hucdf69c751da77027bde1f9b2b3194bd3_89037_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-1.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-1_hucdf69c751da77027bde1f9b2b3194bd3_89037_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-1.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-1.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-1.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft_huedcbe82e9fdfea4c88609d54c42f831a_150824_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-draft_huedcbe82e9fdfea4c88609d54c42f831a_150824_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2-draft.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2-draft.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2-draft.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded_huacc18f208dd04d3a6bf5d00a6a0909f1_108578_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2-expanded_huacc18f208dd04d3a6bf5d00a6a0909f1_108578_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2-expanded.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2-expanded.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2-expanded.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/projects/alpaca-cup/gallery-judges/judge-score-run-2_huedcbe82e9fdfea4c88609d54c42f831a_123610_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/projects/alpaca-cup/gallery-judges/judge-score-run-2_huedcbe82e9fdfea4c88609d54c42f831a_123610_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-judges/judge-score-run-2.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-judges/judge-score-run-2.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-judges/judge-score-run-2.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup is a state of the art web application build by professional web developers with over 20 years\u0026rsquo; combined experience. For an in depth look into the technology behind Alpaca Cup \u003ca href=\"/blogs/alpaca-cup-architecture/\"\u003esee this article\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"development-team\"\u003eDevelopment Team\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.linkedin.com/in/simonverhoeven067/\"\u003eSimon\u003c/a\u003e: system design, architecture, backend, frontend\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.linkedin.com/in/lorenzoong/\"\u003eLorenzo\u003c/a\u003e: frontend, ui/styling wizard\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"impact\"\u003eImpact\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e42 live competitions completed\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAverage 1000 active users per event\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eSee Alpaca Cup in action at Brezel Wakeboard Contest 2023\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://www.youtube.com/embed/o4QmmCv05Ec\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" allowfullscreen title=\"YouTube Video\"\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\u003cp\u003eSee Alpaca Cup in action at the 2023 Embily Open\u003c/p\u003e\n\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;\"\u003e\n \u003ciframe src=\"https://www.youtube.com/embed/cnMjZtYj6us\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;\" allowfullscreen title=\"YouTube Video\"\u003e\u003c/iframe\u003e\n\u003c/div\u003e\n\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n","description":"","image":"/images/projects/alpaca/alpaca.png","permalink":"https://ziggy6792.github.io/projects/alpaca-cup/","title":"Alpaca Cup"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eThis article shows an overview of the technology and architecture behind \u003ca href=\"https://alpacacup.com/events\"\u003eAlpaca Cup\u003c/a\u003e, an event management application for hosting and scoring competitions.\u003c/p\u003e\n\u003cp\u003eTo try the Alpaca Cup yourself, visit \u003ca href=\"https://alpacacup.com/\"\u003ealpacacup.com\u003c/a\u003e.\u003c/p\u003e\n\u003ch3 id=\"related-articles\"\u003eRelated Articles\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eClick \u003ca href=\"/projects/alpaca-cup\"\u003ehere\u003c/a\u003e for introduction and overview of app features\u003c/li\u003e\n\u003cli\u003eClick \u003ca href=\"/blogs/wakeboard-competition-app\"\u003ehere\u003c/a\u003e for deep dive into the original motivations for this project\u003c/li\u003e\n\u003c/ul\u003e\n\u003c!-- \u003cfigure\u003e\u003cimg src=\"judge%20scoring.png\"\n alt=\"Lighthouse Amrum\" width=\"200\" height=\"300\"/\u003e\u003cfigcaption\u003e\n \u003cp\u003eBla bla bla\u003c/p\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cfigure\u003e\u003cimg src=\"judge%20scoring.png\"\n alt=\"Lighthouse Amrum\" width=\"200\" height=\"300\"/\u003e\u003cfigcaption\u003e\n \u003cp\u003eBla bla bla\u003c/p\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n --\u003e\n\u003ch2 id=\"tools\"\u003eTools\u003c/h2\u003e\n\u003ch3 id=\"amazon-web-services\"\u003eAmazon Web Services\u003c/h3\u003e\n\u003cp\u003eAlpaca Cup makes use of several cloud services and tools provided by Amazon Web Services (AWS), which offer;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eHigh scalability and availability\u003c/li\u003e\n\u003cli\u003ePay as you use payment model (when no competitions are running the cost of running the application will be negligible)\u003c/li\u003e\n\u003cli\u003eMany libraries, packages and tools available to do much of the heavy lifting involved in building modern, robust applications\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key AWS tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/cdk/\"\u003eCloud Development kit\u003c/a\u003e, to rapidly provision and configure cloud services using a CI/CD pipeline\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/cognito/\"\u003eCognito\u003c/a\u003e, to handle user sign up authorization\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/dynamodb/\"\u003eDynamoDB\u003c/a\u003e, to provide millisecond response times to API queries for competition and rider data\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/lambda/\"\u003eLambda\u003c/a\u003e, for hosting application api / executing requests serverlessly\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/api-gateway/\"\u003eAPI Gateway\u003c/a\u003e, to handle backend authentication\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.localstack.cloud/\"\u003eLocalStack\u003c/a\u003e, to run a local DynamoDB database (deployed to a docker container using CDK)\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003clink rel=\"stylesheet\" href=/assets/css/hugo-easy-gallery.css\u003e\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/api-gateway_hu6674fa9b6d3e6181ab2d867db3c2c58a_476650_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/api-gateway.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"971\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/api-gateway_hu6674fa9b6d3e6181ab2d867db3c2c58a_476650_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/api-gateway.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/api-gateway.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/api-gateway.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1919px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB_hubbdf9b99a737ace09b2f6f42d076a3ed_529145_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB.png\" data-pswp-width=\"1919\"\n data-pswp-height=\"969\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/DynamoDB_hubbdf9b99a737ace09b2f6f42d076a3ed_529145_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/DynamoDB.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/DynamoDB.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/DynamoDB.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 2032px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-aws/localstack_hu660e0c719cbff297f49dc0a43670323e_485114_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-aws/localstack.png\" data-pswp-width=\"2032\"\n data-pswp-height=\"1167\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-aws/localstack_hu660e0c719cbff297f49dc0a43670323e_485114_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-aws/localstack.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-aws/localstack.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-aws/localstack.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"typescript-everywhere\"\u003eTypescript Everywhere\u003c/h2\u003e\n\u003cp\u003eAlpaca Cup is build on the \u003ca href=\"https://creativedesignsguru.com/typescript-everywhere/\"\u003eTypescript Everywhere\u003c/a\u003e stack this makes development much simpler. The stack consist of;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eNodeJS Backend\u003c/li\u003e\n\u003cli\u003eReactJS Frontend\u003c/li\u003e\n\u003cli\u003eInfrastructure as Code (AWS CDK)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"nodejs-backend\"\u003eNodeJS Backend\u003c/h3\u003e\n\u003cp\u003eThe backend is built using NodeJS, which offers;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eVery fast execution times on AWS Lambda\u003c/li\u003e\n\u003cli\u003eGreat simplicity when working with asynchronous operations. Alpaca Cup has to interface with many AWS services asynchronously (Dynamo DB calls, fetching keys from Secrets Manager, auth requests to Cognito).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key ReactJS libraries tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://aws.amazon.com/sdk-for-javascript/\"\u003eAWS SDK\u003c/a\u003e to integrate with AWS services\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://jestjs.io/\"\u003eJest\u003c/a\u003e, to run integration tests that run against a local test db\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.apollographql.com/docs/apollo-server/\"\u003eApollo Server\u003c/a\u003e to create a GraphQL api for serving requests to fetch and update event/competition data.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://typegraphql.com/\"\u003eType GraphQL\u003c/a\u003e is a library that extends Apollo Server, removing a lot of boilerplate code and providing a fantastic developer experience. The main idea of TypeGraphQL it to use Typescript classes and decorators to provide one source of truth by defining GraphQL schema.\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/shiftcode/dynamo-easy\"\u003eDynamo Easy\u003c/a\u003e provides a Object Data Model as an abstraction layer for working with Dynamo DB tables\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"reactjs-frontend\"\u003eReactJS Frontend\u003c/h3\u003e\n\u003cp\u003eThe frontend is built using ReactJS, which offers;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA robust and flexible approach to handling dynamic content and rich stateful interactions (e.g. managing and rendering competition state)\u003c/li\u003e\n\u003cli\u003eA really fun development experience. Many developers, including myself, really enjoy ReactJS programming. This also ensures a rich and growing ecosystem of React libraries and tools to support present and future development\u003c/li\u003e\n\u003cli\u003ePotential to create a React Native Mobile App in the future, while reusing (and not replacing) a lot of solution code\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHere are the key ReactJS libraries tools being utilized;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.amplify.aws/\"\u003eAWS Amplify\u003c/a\u003e, to connect to cloud resources\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://jestjs.io/\"\u003eJest\u003c/a\u003e, to test critical app components\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.apollographql.com/docs/react/\"\u003eApollo Client\u003c/a\u003e, to store api state coming from GraphQL api\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://aws-amplify.github.io/\"\u003eRedux\u003c/a\u003e, to store app global state (such as user authentication state)\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://material-ui.com/\"\u003eMaterial UI\u003c/a\u003e, to make use of many out of the box UI components with a common theme and appearance\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.npmjs.com/package/axios\"\u003eAxios\u003c/a\u003e, to wrap calls to api and apply middleware (such as adding AWS authentication credentials)\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/competition-results.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/competition-results_hubb7d1f6f54493c7334ddf079d6405445_221406_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/competition-results.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/competition-results.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/competition-results.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/event%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/event%20page_hubb7d1f6f54493c7334ddf079d6405445_413809_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/event page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n Shows key event info, the competitions taking place registered competitors.\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003eEvent Page: Shows key event info...\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/live%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/live%20page_hubb7d1f6f54493c7334ddf079d6405445_241552_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/live page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/live page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/live page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 750px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page.png\" data-pswp-width=\"750\"\n data-pswp-height=\"1334\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/gallery-react-app/timetable%20page_hubb7d1f6f54493c7334ddf079d6405445_165058_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"gallery-react-app/timetable page.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n gallery-react-app/timetable page.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003egallery-react-app/timetable page.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch3 id=\"infrastructure-as-code-aws-cdk\"\u003eInfrastructure as Code (AWS CDK)\u003c/h3\u003e\n\u003cp\u003eAWS Cloud Development kit is used to rapidly provision and configure cloud services.\u003c/p\u003e\n\u003cp\u003eHere are the key CDK components being utilized.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.Stage.html\"\u003eCDK Stages\u003c/a\u003e to configure separate staging and production builds\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://docs.aws.amazon.com/cdk/v2/guide/cdk_pipeline.html\"\u003eCDK Pipelines\u003c/a\u003e are used to automatically deploy app updates safely in a consistent build environment. Merging to master triggers a staging build which is deployed if all build checks pass (typechecking/lint/build/test). There is then a manual approval step to push deployment to production.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003cdiv class=\"gallery caption-position-bottom caption-effect-slide hover-effect-zoom hover-transition\" itemscope itemtype=\"http://schema.org/ImageGallery\"\u003e\n \u003cdiv class=\"pswp-gallery pswp-gallery--single-column\" id=\"photoswipe5-gallery\"\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/infra/backend-pipeline_hu917c2c6d960bd9704045968688b992d4_413353_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/infra/backend-pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"2708\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/infra/backend-pipeline_hu917c2c6d960bd9704045968688b992d4_413353_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"infra/backend-pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n infra/backend-pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003einfra/backend-pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n\n \u003cdiv class=\"box\" style=\"max-width: 1920px;\"\u003e\n \u003cfigure itemprop=\"associatedMedia\" itemscope itemtype=\"http://schema.org/ImageObject\"\u003e\n \u003cdiv class=\"img\" style=\"background-image: url('/blogs/alpaca-cup-architecture/infra/frontend-pipeline_hub4c1f49c149ebc70605b7772ada922e4_457082_300x300_fill_box_smart1_3.png'); background-repeat:no-repeat;\" \u003e\n \u003ca href=\"/blogs/alpaca-cup-architecture/infra/frontend-pipeline.png\" data-pswp-width=\"1920\"\n data-pswp-height=\"2728\" target=\"_blank\"\u003e\n \u003cimg src=\"/blogs/alpaca-cup-architecture/infra/frontend-pipeline_hub4c1f49c149ebc70605b7772ada922e4_457082_300x300_fill_box_smart1_3.png\" width=\"300\" height=\"300\" alt=\"infra/frontend-pipeline.png\"\u003e\n \u003cspan class=\"pswp-caption-content\"\u003e\n infra/frontend-pipeline.png\u003cbr\u003e\n \u003c/span\u003e\n \u003c/a\u003e\n \u003c/div\u003e\n \u003cfigcaption\u003e\n \u003cp\u003einfra/frontend-pipeline.png\u003c/p\u003e\n \u003c/figcaption\u003e\n \u003c/figure\u003e\n \u003c/div\u003e\n \u003c/div\u003e\n\u003c/div\u003e\n\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003c/h2\u003e\n\u003cp\u003eHere’s a map of the services and tools we are using and how they connect to each-other.\u003c/p\u003e\n\u003cfigure\u003e\u003ca href=\"/images/projects/alpaca/alpaca-cup-architecture.png\"\u003e\u003cimg src=\"/images/projects/alpaca/alpaca-cup-architecture.png\"/\u003e\u003c/a\u003e\u003cfigcaption\u003e\n \u003ch4\u003eAlpaca Cup Architecture\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\n\n\n\n \n\n\n\u003cscript type=\"module\"\u003e\nimport PhotoSwipeLightbox from '/assets/photoswipe5/photoswipe-lightbox.esm.js';\nimport PhotoSwipeDynamicCaption from '/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm.js';\n\nconst lightbox = new PhotoSwipeLightbox({\n gallery: '#photoswipe5-gallery',\n children: 'a',\n pswpModule: () =\u003e import('/assets/photoswipe5/photoswipe.esm.js'),\n bgOpacity: 0.95,\n loop: false,\n});\nlightbox.init();\n\nconst captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {\n \n type: 'auto',\n});\n\u003c/script\u003e\n\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe5/photoswipe.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/assets/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css\"\u003e\n\n","description":"","image":"/images/posts/alpaca-cup-architecture/alpaca-web-developer.png","permalink":"https://ziggy6792.github.io/blogs/alpaca-cup-architecture/","title":"Alpaca Cup Architecture"},{"content":"","description":"","image":null,"permalink":"https://ziggy6792.github.io/gallery/","title":"Image Gallery"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eAmplify offers the ability to add Lambda functions which use other configured resources including API objects backed by DynamoDb. This article will focus on creating a lambda function with the Amplify CLI that has access to the DynamoDB tables setup for an \u003ca href=\"https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js#create-the-graphql-api\"\u003eAppSync GraphQL API\u003c/a\u003e. \u003cbr\u003e\nSpecifically we will look at;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreating a Lambda function with Amplify CLI configured to use DynamoDB table resources\u003c/li\u003e\n\u003cli\u003eThe resource provisioning mechanism and potential problems with creating Lambdas created through Amplify\u003c/li\u003e\n\u003cli\u003eWriting \u0026amp; testing a sample Lambda handler to demonstrate its capabilities\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eWHY?\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePerhaps you want to write some custom graphQL \u003ca href=\"https://docs.amplify.aws/cli/graphql-transformer/directives#function\"\u003e@function\u003c/a\u003e resolvers that interact with your API DynamoDB tables directly (e.g. add a batch of items to a table)\u003c/li\u003e\n\u003cli\u003eOr, set up triggers on your tables (e.g. to cascade deletes to orphaned items)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"pre-requisites\"\u003ePre-requisites\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eInstall Amplify CLI (version 4.16.1 or higher)\u003c/li\u003e\n\u003cli\u003eRun Amplify Init\u003c/li\u003e\n\u003cli\u003eCreate GraphQL API\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"creating-a-lambda-function-with-amplify-cli\"\u003eCreating a Lambda function with Amplify CLI\u003c/h2\u003e\n\u003cp\u003eUse the following commands to create a Lambda function with the Amplify CLI. After selecting the category \u003ccode\u003estorage\u003c/code\u003e, you will be prompted to select DynamoDB tables wich exist for the GraphQL API \u003ca href=\"https://docs.amplify.aws/cli/graphql-transformer/directives#model\"\u003e@model\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e: You will need Amplify CLI version 4.16.1 or higher.\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=createLambdaFunction.txt\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eNBMAC0056:SwtNinja verhoeven$ amplify add function\nUsing service: Lambda, provided by: awscloudformation\n? Provide a friendly name for your resource to be used as a label for this category in the project: doSomethingToDBTables\n? Provide the AWS Lambda function name: doSomethingToDBTables\n? Choose the function runtime that you want to use: NodeJS\n? Choose the function template that you want to use: Hello World\n? Do you want to access other resources created in this project from your Lambda function? Yes\n? Select the category storage\n? Storage has 8 resources in this project. Select the one you would like your Lambda to access Event:@model(appsync), Competition:@model(appsync), Heat:@model(appsync), SeedSlot:@model(appsync), Ride\nrAllocation:@model(appsync), User:@model(appsync)\n? Select the operations you want to permit for Event:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for Competition:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for Heat:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for SeedSlot:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for RiderAllocation:@model(appsync) create, read, update, delete\n? Select the operations you want to permit for User:@model(appsync) create, read, update, delete\n\nYou can access the following resource attributes as environment variables from your Lambda function\n API_COMPAPI_COMPETITIONTABLE_ARN\n API_COMPAPI_COMPETITIONTABLE_NAME\n API_COMPAPI_EVENTTABLE_ARN\n API_COMPAPI_EVENTTABLE_NAME\n API_COMPAPI_GRAPHQLAPIIDOUTPUT\n API_COMPAPI_HEATTABLE_ARN\n API_COMPAPI_HEATTABLE_NAME\n API_COMPAPI_RIDERALLOCATIONTABLE_ARN\n API_COMPAPI_RIDERALLOCATIONTABLE_NAME\n API_COMPAPI_SEEDSLOTTABLE_ARN\n API_COMPAPI_SEEDSLOTTABLE_NAME\n API_COMPAPI_USERTABLE_ARN\n API_COMPAPI_USERTABLE_NAME\n ENV\n REGION\n? Do you want to invoke this function on a recurring schedule? No\n? Do you want to edit the local lambda function now? Yes\nPlease edit the file in your editor: \u0026lt;project-dir\u0026gt;/amplify/backend/function/doSomethingToDBTables/src/index.js\n? Press enter to continue\nSuccessfully added resource doSomethingToDBTables locally.\n\nNext steps:\nCheck out sample function code generated in \u0026lt;project-dir\u0026gt;/amplify/backend/function/doSomethingToDBTables/src\n\u0026#34;amplify function build\u0026#34; builds all of your functions currently in the project\n\u0026#34;amplify mock function \u0026lt;functionName\u0026gt;\u0026#34; runs your function locally\n\u0026#34;amplify push\u0026#34; builds all of your local backend resources and provisions them in the cloud\n\u0026#34;amplify publish\u0026#34; builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud\n\nNBMAC0056:SwtNinja verhoeven$ amplify push\n✔ Successfully pulled backend environment compapi from the cloud.\n\nCurrent Environment: compapi\n\n| Category | Resource name | Operation | Provider plugin |\n| -------- | --------------------- | --------- | ----------------- |\n| Function | doSomethingToDBTables | Create | awscloudformation |\n| Api | compapi | No Change | awscloudformation |\n? Are you sure you want to continue? Yes\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"potential-maximum-policy-size-issue-when-running-amplify-push\"\u003ePotential maximum policy size issue when running amplify push\u003c/h2\u003e\n\u003cp\u003eWhen running amplify push at the end of the last step you may see the following \u003ca href=\"https://github.com/aws-amplify/amplify-cli/issues/1699\"\u003eissue\u003c/a\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Maximum policy size of 10240 bytes exceeded for role \u003ccode\u003e\u0026lt;apiName\u0026gt;LambdaRole26741da9-\u0026lt;apiName\u0026gt;\u003c/code\u003e\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis issue occurs when the policy created for the lambda role (the IAM role created to run your new lambda function) exceeds the maximum size allowed for role inline policies.\u003c/p\u003e\n\u003ch3 id=\"option1-reducing-the-number-of-tables-added-to-the-role-policy\"\u003eOption1: Reducing the number of tables added to the role policy\u003c/h3\u003e\n\u003cp\u003eIf possible, you should reduce the tables and actions granted in the previous step (following the best practice that each lambda only has access to the actions and resources it expressly needs). However, this may not be possible. In my case, I wanted to write a lambda function that cascaded deletes through my table schema. Therefore, my lambda function needs to have access to all of my API tables.\u003c/p\u003e\n\u003ch3 id=\"option2-customizing-local-cloudformation-template-generated-by-amplify\"\u003eOption2: Customizing local CloudFormation template generated by Amplify\u003c/h3\u003e\n\u003cp\u003eTo understand a work around for this issue, it is important to explain in more detail what \u003ccode\u003eamplify add function\u003c/code\u003e is actually doing. This method creates a local folder for the function containing a CloudFormation template. E.g;\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003eamplify\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003ebackend\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003efunction\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003edoSomethingToDBTables\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003edoSomethingToDBTables\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003ecloudformation\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#a6e22e\"\u003etemplate\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ejson\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003eThis template describes the lambda to be created when amplify push is next run. However, Amplify is not very clever when it comes to creating the templates which create role inline policies. The Amplify created CloudFormation template creates a separate inline policy statement for each DynamoDB table (even if the allowed actions are the same).\u003c/p\u003e\n\u003cp\u003eFor example; granting these actions;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Put*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Create*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:BatchWriteItem\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Get*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:BatchGetItem\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:List*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Describe*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Scan\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Query\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Update*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:RestoreTable*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026quot;dynamodb:Delete*\u0026quot;\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eTo these tables;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCompetition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eUser-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eEvent-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eHeat-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eRiderAllocation-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003cli\u003eSeedSlot-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eWill create the following access policy list of statements;\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=accessPolicyExample1.json\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e{\n \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;,\n \u0026#34;Statement\u0026#34;: [\n {\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/Competition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/Competition-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n },{\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/User-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/User-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n },\n ...Repeat For Each Table\n ]\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWhich is actually equivalent to the following single statement (using the pattern resource identifier: \u003ccode\u003e*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u003c/code\u003e matching every table for \u003cstrong\u003ethis particular GraphQL API Environment\u003c/strong\u003e);\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=accessPolicyExample2.json\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e{\n \u0026#34;Version\u0026#34;: \u0026#34;2012-10-17\u0026#34;,\n \u0026#34;Statement\u0026#34;: [\n {\n \u0026#34;Action\u0026#34;: [\n \u0026#34;dynamodb:Put*\u0026#34;,\n \u0026#34;dynamodb:Create*\u0026#34;,\n \u0026#34;dynamodb:BatchWriteItem\u0026#34;,\n \u0026#34;dynamodb:Get*\u0026#34;,\n \u0026#34;dynamodb:BatchGetItem\u0026#34;,\n \u0026#34;dynamodb:List*\u0026#34;,\n \u0026#34;dynamodb:Describe*\u0026#34;,\n \u0026#34;dynamodb:Scan\u0026#34;,\n \u0026#34;dynamodb:Query\u0026#34;,\n \u0026#34;dynamodb:Update*\u0026#34;,\n \u0026#34;dynamodb:RestoreTable*\u0026#34;,\n \u0026#34;dynamodb:Delete*\u0026#34;\n ],\n \u0026#34;Resource\u0026#34;: [\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi\u0026#34;,\n \u0026#34;arn:aws:dynamodb:ap-southeast-1:694710432912:table/*-xl3qodhqsfdmhe5psj4vqa7wsy-compapi/index/*\u0026#34;\n ],\n \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34;\n }\n ]\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThe difference with using the second example however is that; generating policies this way will not cause policies to grow in size with the number of API tables and therefore will not easily hit the maximum policy size and cause the maximum policy size issue.\u003c/p\u003e\n\u003cp\u003eTherefore, a workaround for the maximum policy size issue is to modify your CloudFormation template to consolidate the assignment of actions to tables. Exactly how to modify the CloudFormation template will depend on the specific access rights you want to grant to your lambda. A common example however would be if you wanted to grant the same access to all API tables for a particular environment as shown above.\u003c/p\u003e\n\u003ch2 id=\"modify-cloudformation-template-to-give-access-to-all-amplify-api-tables\"\u003eModify CloudFormation template to give access to all Amplify API tables\u003c/h2\u003e\n\u003cp\u003eUsing \u003ca href=\"https://stedolan.github.io/jq\"\u003ejq\u003c/a\u003e we can first remove all but the first policy statement from the generated \u003ccode\u003edoSomethingToDBTables-cloudformation-template.json\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ejq \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;.Resources.AmplifyResourcesPolicy.Properties.PolicyDocument.Statement[0] as $stmt0 |\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e .Resources.AmplifyResourcesPolicy.Properties.PolicyDocument.Statement = [$stmt0]\u0026#39;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e doSomethingToDBTables-cloudformation-template.json \u0026gt;output.json\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003e\u003cstrong\u003eNOTE\u003c/strong\u003e: \u003ccode\u003ejq\u003c/code\u003e currently does not support in-place editing of files\u003c/p\u003e\n\u003cp\u003eThis will create a new file \u003ccode\u003eoutput.json\u003c/code\u003e in the same directory, manually edit this file replacing the tableName specific resource identifier with the pattern identifier (\u003ccode\u003e*-\u0026lt;ApiName\u0026gt;-\u0026lt;EnvName\u0026gt;\u003c/code\u003e). Here\u0026rsquo;s a sample diff;\u003c/p\u003e\n\u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=editCloudFormationTemplate.diff\"\u003e\u003c/script\u003e\n\n\u003cp\u003eNext, replace original \u003ccode\u003edoSomethingToDBTables-cloudformation-template.json\u003c/code\u003e with \u003ccode\u003eoutput.json\u003c/code\u003e.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\u003c/span\u003e\u003cspan\u003erm doSomethingToDBTables-cloudformation-template.json\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\u003c/span\u003e\u003cspan\u003emv output.json doSomethingToDBTables-cloudformation-template.json\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003e\u003ca href=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0#file-dosomethingtodbtables-cloudformation-template-json\"\u003eHere\u003c/a\u003e is an example of my final CloudFormation template.\u003c/p\u003e\n\u003cp\u003eFinally, run \u003ccode\u003eamplify push\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eGo to your ec2 console. You should see the following created lambda function;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Created-Function.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eCreated Lambda Function\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eAlong with the following attached role and access policy.\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Created-Role.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eCreated Role\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003ch2 id=\"test-your-new-function-write-to-dynamodb-from-lambda-function\"\u003eTest your new function. Write to DynamoDB from Lambda Function\u003c/h2\u003e\n\u003cp\u003eTo test access to dynamoDB from your new Lambda function, copy the following code (from line 23) into your local lambda function index.js, replacing the hello world function that was created \u003ca href=\"/posts/amplify-lambda-dynamodb/#creating-a-lambda-function-with-amplify-cli\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n\u003c!-- \u003cscript type=\"application/javascript\" src=\"https://gist.github.com/ziggy6792/f8ea26516f51fbb7bf2ee024f9d054f0.js?file=lambda-function-index.js\"\u003e\u003c/script\u003e\n --\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e/* Amplify Params - DO NOT EDIT\n\tAPI_COMPAPI_BPOSTTABLE_ARN\n\tAPI_COMPAPI_BPOSTTABLE_NAME\n\tAPI_COMPAPI_COMPETITIONTABLE_ARN\n\tAPI_COMPAPI_COMPETITIONTABLE_NAME\n\tAPI_COMPAPI_CUSTOMERTABLE_ARN\n\tAPI_COMPAPI_CUSTOMERTABLE_NAME\n\tAPI_COMPAPI_EVENTTABLE_ARN\n\tAPI_COMPAPI_EVENTTABLE_NAME\n\tAPI_COMPAPI_GRAPHQLAPIIDOUTPUT\n\tAPI_COMPAPI_HEATTABLE_ARN\n\tAPI_COMPAPI_HEATTABLE_NAME\n\tAPI_COMPAPI_RIDERALLOCATIONTABLE_ARN\n\tAPI_COMPAPI_RIDERALLOCATIONTABLE_NAME\n\tAPI_COMPAPI_SEEDSLOTTABLE_ARN\n\tAPI_COMPAPI_SEEDSLOTTABLE_NAME\n\tAPI_COMPAPI_USERTABLE_ARN\n\tAPI_COMPAPI_USERTABLE_NAME\n\tENV\n\tREGION\nAmplify Params - DO NOT EDIT */\n\nvar AWS = require(\u0026#39;aws-sdk\u0026#39;);\nvar uuid = require(\u0026#39;uuid\u0026#39;);\n\nAWS.config.update({ region: process.env.REGION });\n\nvar ddb = new AWS.DynamoDB({ apiVersion: \u0026#39;2012-08-10\u0026#39; });\n\nexports.handler = async (event, context, callback) =\u0026gt; {\n\tlet items = [{id:uuid.v4(),name:\u0026#34;Hello World\u0026#34;}];\n\ttry {\n\t\tawait write(process.env.API_COMPAPI_EVENTTABLE_NAME,items)\n\t} catch (err) {\n\t\tcallback(err)\n\t}\n\treturn callback(null,items);\n};\n\nasync function write(tableName, items) {\n\tvar itemsToWrite = items\n\tvar params = {\n\t\tRequestItems: {\n\t\t\t[tableName]:\n\t\t\t\titemsToWrite.map((item) =\u0026gt; {\n\t\t\t\t\treturn ({\n\t\t\t\t\t\tPutRequest: {\n\t\t\t\t\t\t\tItem: AWS.DynamoDB.Converter.marshall(item)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t}\n\n\t};\n\tconst data = await ddb.batchWriteItem(params).promise();\n}\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eIMPORTANT\u003c/strong\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDo not replace/edit the Amplify auto generated variables (lines 1 to 21), keep your own variables separate from this auto-generated code to ensure they are not overwritten.\u003c/li\u003e\n\u003cli\u003eReplace tableName (line 33) with one of your own resource attributes as environment variables that was created \u003ca href=\"/posts/amplify-lambda-dynamodb/#creating-a-lambda-function-with-amplify-cli\"\u003ehere\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eYou will also need to cd to you local lambda function directory and install npm package uuid locally\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\u003c/span\u003e\u003cspan\u003ecd \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;amplify/backend/function/doSomethingToDBTables/src/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\u003c/span\u003e\u003cspan\u003enpm i --save uuid\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003eNext run \u003ccode\u003eamplify push\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eNow you can test your function in the AWS console;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Test-Function.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eTest Function\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cp\u003eAnd then verify that an item was created in the specified table;\u003c/p\u003e\n\u003cfigure\u003e\u003cimg src=\"/images/posts/Accessing-DynamoDB-Tables/Test-Function-Created-Item.png\"/\u003e\u003cfigcaption\u003e\n \u003ch4\u003eTest Function Success - Created Item\u003c/h4\u003e\n \u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003c!-- \u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 1\u003c/span\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 2\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;name\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;doSomethingToDBTables\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 3\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;version\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;2.0.0\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 4\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;description\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Lambda function generated by Amplify\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 5\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;main\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;index.js\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 6\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;license\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Apache-2.0\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 7\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;dependencies\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex; background-color:#3c3d38\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 8\u003c/span\u003e\u003cspan\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026#34;lodash\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;latest\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 9\u003c/span\u003e\u003cspan\u003e }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e10\u003c/span\u003e\u003cspan\u003e}\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e --\u003e\n","description":"","image":"/images/posts/Accessing-DynamoDB-Tables/Amplify.png","permalink":"https://ziggy6792.github.io/blogs/amplify-lambda-dynamodb/","title":"Accessing Amplify GraphQL API objects from Lambda functions"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003c!-- ### About Us\n\nSimon + Vincent --\u003e\n\u003ch3 id=\"motivation\"\u003eMotivation\u003c/h3\u003e\n\u003cp\u003eCable Wakeboarding is an extreme water sport where participants (riders) are pulled (at speeds of 30+ kph) around a lake by an overhead cable (very similar to a cable car system). Much like skate parks, wake parks have obstacles (such as jumps and rails) built along the cable path. In competition, riders are judged subjectively on their ability to perform ticks (such as flips) both on and off these obstacles. There are 700+ Wakeparks in the world (60+ in Asia).\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.singaporewakepark.com/home/\"\u003eSingapore Wakepark\u003c/a\u003e (SWP) and other parks host about 4 competitions per year with an average turn out of 100 riders and 300 spectators with a SGD 60 admission fee. A typical competition consists of several disciplines (e.g. “Men’s Beginner”) containing rounds and heats (e.g. “Round 2 - Semi Final 1”). Riders are scored and ranked in each round, hoping to progress to the final, where the top 3 are awarded a price (1st place upwards of SGD 1000).\u003c/p\u003e\n\u003cp\u003eFor competition organizers there are currently 2 options when it comes to managing a competition;\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ePay a federation like the IWWF to manage the competition and use their event management system. Disadvantages;\n\u003cul\u003e\n\u003cli\u003eYearly member subscription fees\u003c/li\u003e\n\u003cli\u003ePercentage taken from rider admission fees\u003c/li\u003e\n\u003cli\u003eCan only use judges affiliated with the federation (sometimes judges need to be flown in at an expense to the park)\u003c/li\u003e\n\u003cli\u003eLittle control over the structure of competition\u003c/li\u003e\n\u003cli\u003eThe applications provided by federations are not very good; not mobile optimized, results are not pushed automatically (need to refresh page), results are not real-time\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eManage the competition on your own. Receiving admissions by email, manually creating heat tables using excel, sharing screen shots of start lists on Facebook, then printing round results and pinning them on a notice board. Disadvantages;\n\u003cul\u003e\n\u003cli\u003eVery time consuming and rigid format\u003c/li\u003e\n\u003cli\u003eVery little feedback to interested parties on the state of the competition as it takes place (e.g. current round results, next round start list)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"our-goals\"\u003eOur Goals\u003c/h3\u003e\n\u003cp\u003eWe are building a web application to be used by organizers, riders, judges and spectators with the following goals;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eProvide a cheap, intuitive and flexible workflow to organizers for creating and hosting competitions\u003c/li\u003e\n\u003cli\u003eGive maximum control to organizers over their competition structure (number of disciplines, number of heats per round etc.)\u003c/li\u003e\n\u003cli\u003eGive real-time monitoring metrics to interested parties. E.g. how long will a competition take, is it running behind schedule (adverse weather can delay a schedule)\u003c/li\u003e\n\u003cli\u003eGive real-time feedback to interested parties on the state of a competition (e.g: how many points the current rider needs to proceed to the next round). This is particularly useful for competition commentators who rely on this real-time information to keep the spectators engaged and excited.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"solution\"\u003eSolution\u003c/h2\u003e\n\u003ch3 id=\"our-stakeholders\"\u003eOur Stakeholders\u003c/h3\u003e\n\u003cp\u003eAll those involved in Wakeboard Competitions;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eOrganizers e.g. Singapore Wakepark Owner\u003c/li\u003e\n\u003cli\u003eRiders\u003c/li\u003e\n\u003cli\u003eSpectators (mostly the families of riders, especially younger riders who often have a whole family to support them)\u003c/li\u003e\n\u003cli\u003eJudges\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"requirements\"\u003eRequirements\u003c/h3\u003e\n\u003cp\u003eAfter talking to our stakeholders, here are some of the key features of the app we are building;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAs an organizer I want to create competitions and heats\u003c/li\u003e\n\u003cli\u003eAs an organizer I want to assign starting \u003ca href=\"https://en.wikipedia.org/wiki/Seed_(sports)\"\u003eseeds\u003c/a\u003e to riders\u003c/li\u003e\n\u003cli\u003eAs an organizer I want to allocate registered riders into the first round \u003ca href=\"https://www.merriam-webster.com/dictionary/qualifying%20heat\"\u003eheats\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eAs a rider I want to sign up to a competition\u003c/li\u003e\n\u003cli\u003eAs a rider I want to see all the competitions and heats I am assigned to\u003c/li\u003e\n\u003cli\u003eAs a rider I want to track my overall progression in real-time (as soon as a result comes in from the judges I should receive a notification with my updated place on the scoreboard)\u003c/li\u003e\n\u003cli\u003eAs a judge I want to see the current heat I am scoring\u003c/li\u003e\n\u003cli\u003eAs a judge I want to be able to input scores for riders as they compete\u003c/li\u003e\n\u003cli\u003eAs a judge I want to be able to end a heat and have riders allocated automatically to the next round (based on the competition structure defined by the organizer)\u003c/li\u003e\n\u003cli\u003eAs a user I want to track the overall progression of all riders in real-time\u003c/li\u003e\n\u003cli\u003eAs a user I want to see real-time metrics. E.g. is a competition running behind schedule\u003c/li\u003e\n\u003c/ul\u003e\n","description":"","image":"/images/posts/wakeboard-competition-app/wakeboard-bw.jpg","permalink":"https://ziggy6792.github.io/blogs/wakeboard-competition-app/","title":"Wakeboard Competition App Project"},{"content":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn this blog I aspire to detail my journey in adopting AWS Serverless Architecture;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAmplify\u003c/li\u003e\n\u003cli\u003eLambda\u003c/li\u003e\n\u003cli\u003eCognito\u003c/li\u003e\n\u003cli\u003eAppSync\u003c/li\u003e\n\u003cli\u003eDynamoDB\u003c/li\u003e\n\u003cli\u003eAPI Gateway\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eI am creating a Competition Management App for event organizers and competitors at \u003ca href=\"http://www.singaporewakepark.com/home/\"\u003eSingapore Wakepark\u003c/a\u003e. This is a full stack serverless web application (ReactJS front-end, GraphQL API (AppSync + Node JS Lambda) back-end. I chose these technologies for the job because I believe them to be a good fit to my needs;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEasy scalability\u003c/li\u003e\n\u003cli\u003ePay for what you use (when no competitions are running the cost of running the application will be negligible)\u003c/li\u003e\n\u003cli\u003eLots of good online documentation\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBut also because they are exciting technologies which I am keen to work with.\u003c/p\u003e\n\u003cp\u003eMost of my explanations and sample code will in someway showcase my Competition Management App (such as DynamoDB tables with names like Event, Competition, Heat\u0026hellip;). However I aim to distill the topics I am learning into blog posts that demonstrate my work in a way that can benefit all developers (building a variety of applications) using AWS. \u003cbr\u003e\nI will cover widely applicable topics such as;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAccessing DynamoDB tables from Lambdas\u003c/li\u003e\n\u003cli\u003eSetting up DynamoDB event triggers\u003c/li\u003e\n\u003cli\u003eCreating a cognito user pool with some mock users\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"why\"\u003eWhy\u003c/h2\u003e\n\u003cp\u003eIn all areas of software development there is more than one way to skin a fish. However, I believe this is particularly prevalent when it comes to AWS due to the vast array of features and possibilities in this space.\u003c/p\u003e\n\u003cp\u003eI hope I can get some feedback from other developers on my approach and design decisions and maybe benefit from learning alternate approaches, as well as their pros and cons through discussions.\u003c/p\u003e\n\u003cp\u003eMost importantly, I hope that other people who are looking at adopting these technologies can benefit from a documented report of someone building up their AWS skills and mastering Serverless Application development.\u003c/p\u003e\n","description":"","image":"/images/wellcome-to-my-blog/wellcome.jpg","permalink":"https://ziggy6792.github.io/blogs/wellcome-to-my-blog/","title":"Welcome to my blog"}] \ No newline at end of file