Micro-frontends with AWS Amplify and GraphQL

Antonio Lagrotteria
8 min readApr 27, 2021

After establishing a micro-frontend architecture and its deployment via Amplify in two previously published stories, it’s now time to focus on the Amplify supported backend capabilities. We will setup a simple multi-tenant application with a payments service based on AWS AppSync, a managed GraphQL API. Whole code can be found on Github.

Focus architecture for Payments micro-frontend.

Goal

Scope of this story is to:

  • Support CRUD operations to the payments micro-frontend by using Amplify AppSync to consume a payments API backed by GraphQL and DynamoDB.
  • Introduce an element of multi-tenancy architecture to allow companies’ users to manage their unconfirmed company payments.

User interface will differ a lot from previous stories, thought we won’t go too much in details about its look and feel. End result can be seen below.

End result user flow.

Prerequisites

If you are just interested to multi-tenancy and Amplify’s AppSync PoC, just continue. If you want to check how we ended up here, you can check Part 1 and Part 2 of this mini series.

Introduce APIs to Payments

In order to expose our Payments API, we will use Amplify to leverage AWS AppSync, which combines data into a single GraphQL endpoint. Let’s add APIs by executing the below command:

cd mfe-payments
amplify add api

Amplify prompt will ask — as usual — several questions. Concerning security, there are 4 ways to authenticate and authorize access to GraphQL API. I based my implementation on Amazon Cognito User Pools, as the application will receive JSON web tokens (JWTs) used to authorize requests with our AppSync app. You will be also asked to generate a single “Todo” model as starting type, please choose it. Thought the Todo model is not useful per se, let’s push it to AWS to see what the command has provisioned.

amplify push

Once deployment is done, let’s see what happened on both code-wise and on cloud.

On the code side we got:

  • an api and auth section inside the backend-config.json file, to support deployment
  • a new api folder got created under path amplify/backend/api.
  • a CloudFormation template which will provision AppSync configurations, IAM roles and lambdas to support the infrastructure for the GraphQL endpoint.
  • a dummy schema.graphql containing an annotated Todo model, which we will update shortly.
Generated code by add command.

On AWS side, the CloudFormation stack has:

  • created a DynamoDB table to store the model.
  • created an AppSync configuration with a Datasource to access the above mentioned table. This also uses a number of resources (S3 deployment buckets, IAM roles, Lambdas, Cognito user pools) to glue our API with authorization mechanisms and store artifacts such as the GraphQL schema.
  • exposed a GraphQL endpoint which can be POSTed to perform any CRUD actions.
  • generated up to 8 GraphQL resolvers handling queries, mutations, and subscriptions on the type annotated with the @model directive.
Part of resources generated by CloudFormation stack.

Now that we got an overview of the provisioned resources, let’s modify the GraphQL schema to model the payment type. If in doubt or curious GraphQL models’ syntax and explanations, learn more here.

The Payment model.

Above is a simple code which infers and involves a lot of complex information, with focus in next sections about:

Supporting multi-tenancy

As simple as that: you do not want to build applications (or even PoCs) where independent companies can access each others’ private data. A multi-tenancy architecture is one of the foundations of the Saas model, where an application supports multiple customers via a single instance of a software and infrastructure.

Saas model with multi-tenancy.

For our PoC, we will associate users with their tenant (aka the company they belong to) by introducing a tenantId attribute as part the payment model. This will ensure:

  • unique tenant identification for the logged in user
  • tenancy isolation. As data is shared to the same database instance, tenant related data should be isolated from other data and accessed securely.

The relationship between tenantId and AppSync annotations is done by setting ownerField to be the tenantId field of the model, which will be compared with a custom field present in the IDToken as part of the JWT token as part of the GraphQL endpoint request (more details later on this).

This new field requires a new attribute in Cognito. As shown below, you just access the attributes section and add any custom field you wish. In my case I added a tenantId (the identifier for the tenant/company) and its name tenantName:

Create custom attributes in Cognito Users Pools.

For the purpose of this demo, each company’s user will be able to modify only items belonging to the tenant they are part of. Now that we know what a tenant id is in our context, let us focus on the models’ annotations.

Magic @nnotations

AppSync supports a bunch of annotations available, but to keep it simple, I decided to focus only @modeland @auth.

The @model itself is the main responsible for all the provisioned resources described above (AppSync configuration, GraphQL artifacts and DB storage).

The @authis a fundamental piece in order to protect your payment model from malicious or unauthorized access.

Auth annotation with defined rule.

There are various combinations and granularity in how you can authenticate and authorize your resources. In my case I am allowing access to the owner authenticated user to perform all CRUD operations and only to the ones that belong to the same tenant/company. As AppSync goes hand in hand with authorization, if you try to call API at this stage you would get unauthorized errors. Since we already have a Cognito Users Pool coming from parent shell app, we will share it with our payments module:

amplify import auth
amplify push

Before trying to call the deployed API, I have created a couple of users with below script, as the endpoint require authentication prior their execution.

The above script will create two users belonging to two different tenants, A and B. This is needed because, as shown in below gif, in order to perform GraphQL CRUD operations, you must be authenticated. First, I use the user credentials from Cognito to do that. Then I create a Payment model which gets stored to DynamoDB. Finally, I keep track of its ID (needed to delete it later on), list it, delete it and finally list it again.

CRUD operations on payment model.

Pretty nice thing to notice: when creating a payment with a wrong tenant ID associated to the logged in user, the API returns 400 as being unauthorized.

Unauthorized mutation.

Now that we made sure our API authorization and persistence capabilities work, let’s connect them to the frontend.

UI frontend changes

We will not go into details about frontend changes which you can check in great details in the Github repo. Simply, I introduce a dependency towards angular material components which I have used to beautify our app from our first blog part. The biggest changes introduced in frontend are:

  • Redesign parent app with Menu based on angular-material components which routes to accounts and payments Web Components.
  • Introduced paged table in payments to show off CRUD by using angular component forms for forms, table and validations.

Access tokens vs ID tokens

As mentioned earlier, access tokens are typically the default approach for GraphQL to authenticate requests, but as we have introduced custom fields in order to handle multi tenancy, we need some tweak in order to use the IDToken information instead. IDTokens differs from Access tokens as they contain claims about the identity of the authenticated user, our tenantId in our case. To make GraphQL to authenticate requests via IDToken rather than access token you need to add the below snippet to the main.ts Payments class.

Once we are able to use the appropriate JWT token, I just exposed a PaymentsService to encapsulate the GraphQL APIs call. Below example shows how to get a list of payments, using the tenantId information retrieved in the current user info session and passing it to the query resolver in order to fetch data from DynamoDB. All queries and its types were magically generated by Amplify earlier.

As both parent and payments modules were changed, you will need to publish changes for both:

amplify publish 

Demo time!

We made it! You can visit demo here by using the 2 users I previously created fit tenants A and B (userTenantA/passwordA & userTenantB/passwordB).

Frontend Limitations & Improvements

  • When I tried to publish the angular bundle, you may need to update the budget limit (do not worry, no money involved here :P) in the angular.json
  • Lack of global object not defined. I just needed to add a few lines here
  • Add creation of tenants in UI, rather than programmatically. This could imply rewriting the signing flow by dropping the withAuthentication HOC in fa out of programmatic use of Auth APIs. Can be fun to try that out!
  • Sharing styles in micro-frontend will be tricky at scale: make sure you invest some time into a strategy on how to share common style guide and shared code from parent to child components unless you just will reference all child styles into parent html (as I did fit the demo).

Backend Limitations & Improvements

  • To protect your GraphQL endpoints from XSS attacks and company, leverage AWS WAF to protect network access.
  • Integration with Leading keys check on Dynamo DB with Amplify workflow, to increase security and tenancy isolation at IAM level.
  • Multitenancy: more granular use cases and more deep dive into annotations.
  • Evolve and or

Hope you enjoyed this read!

--

--

Antonio Lagrotteria

Engineering Manager | Full-Stack Architect | Team/Tech Lead with a passion for frontend, backend and cloud | AWS Community Builder