DEV Community 👩‍💻👨‍💻

Cover image for The guide to create (offline) multi tenant apps with Expo and AWS Amplify
rpostulart for AWS Community Builders

Posted on • Updated on

The guide to create (offline) multi tenant apps with Expo and AWS Amplify

Cover photo by David Watkis on Unsplash

Multi tenant

A tenant is a group of users which have the same access rights. With multi tenant you design your application in a way that different tenants can make use of the same software without interfering each other and be in control about their own data assets without the risks that others can access this.

Alt Text

Software as a Service (SaaS)

There are different type of apps that we all use in our daily lives. One type of applications are Software as a Service (SaaS) applications. The characteristics of SaaS is that the hardware and software is abstracted away from you. You don't need to manage this and only consume the features that are provided. You make free use of these apps or you take a subscription model where you pay a monthly / yearly / whatever fee.

These applications are designed in a way that there is a software version running where multiple users can make use of in a secure and isolated way.

Slack

You can see Slack as an example of a Multi tenant Saas Solution. The software and hardware is running somewhere (in their private networks or public cloud). As a company you are able to get a subscription and add the users of your company. Your users can work together and share data in slack which is not accessible to other tenants.
Alt Text

Purpose of this guide

In this guide I will explain how you can set up a multi tenant application with AWS Amplify like Slack.

The power of AWS Amplify is that it connects to different front end frameworks. So, as soon as you have set up this backend you are able to build your front end application on top of the multi tenant backend.

We will build an Expo app. This is easy to set up and test your app.

There are different possibilities of a multi tenant implementation:

1) AWS account per tenant
2) Same AWS account and different DynamoDB tables per tenant
3) Same AWS account and one table for all tenants

I will focus on option 3 because this is the less complex implementation and still secure.

To achieve the implementation there are again different options:

The ideal implementation

The ideal implementation is the one where the server(less) side will check if the user is part of the tenant and it is in the right cognito group. This can be done via the VTL templates in AWS Appsync.

Alt Text

Let's assume we will create this model:

type channel
  @model
  @key(
    name: "channelByTenant"
    fields: ["tenant"]
    queryField: "channelByTenant"
  )
  @auth(
    rules: [
      { allow: owner, ownerField: "owner" }
      { allow: owner, ownerField: "tenant", identityClaim: "custom:tenant" }
      { allow: groups, groups: ["user"], operations: [read] }
      {
        allow: groups
        groups: ["editors"]
        operations: [create, read, update, delete]
      }
    ]
  ) {
  id: ID!
  name: String!
  messages: [message] @connection(keyName: "messageByChannel", fields: ["id"])
  tenant: ID!
  owner: ID!
}
Enter fullscreen mode Exit fullscreen mode

When create a schema like above many templates are generated for you out of the box. As you can see in this schema I apply 4 auth rules.

The above model will allow owner, tenants and users in the group editors to create, read, update and delete and the users in the group users to read.

The problem with the generated templates is that it is applying OR conditions on the rules, like:

#if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) )
    $util.unauthorized()
  #end
Enter fullscreen mode Exit fullscreen mode

which means

$isStaticGroupAuthorized => check cognito groups 
$isDynamicGroupAuthorized => check owners and tenants
$isOwnerAuthorized => not applicable now in the model
Enter fullscreen mode Exit fullscreen mode

but we want this multi tenant validation:

// if is not part of tenant, set unauthorized
if( tenant !== custom.tenantid ) {
   unauthorized()
} 

// ELSE if is not the owner or part of the groups, set unauthorized
if( user !== owner || user is not in group users or editors) {
    unauthorized()
}

Enter fullscreen mode Exit fullscreen mode

Of course it is possible to change all the VTL templates, but for one model there are many templates created (see next screenshot) and if you are going to adjust them manually then you will end up with maintainability issues. Imaging if you have more models in your schema.

Each time you adjust your model then your templates are generated again and you have to apply the adjustments again.

Alt Text

Github issue

There is an issue created where this will be solved:
https://github.com/aws-amplify/amplify-cli/issues/317

If this is implemented by the Amplify or Appsync team we can maintain the rules and tenant set up from the schema and based on that the templates will be generated.

Our model will look like this then:

@auth(rules: [
    { and: [
        { allow: owner, ownerField: "tenant", identityClaim: "custom:tenant" } 
        { or: [
            { allow: owner, ownerField: "owner" }   
            { allow: groups, groups: ["user"], operations: [read] }
            { allow: groups groups: ["editors"] operations: [create, read, update, delete] }
        ]}
    ]}
])
Enter fullscreen mode Exit fullscreen mode

Ok ... what to do now?

I am very happy that since two weeks ago datastore is offering selective sync. Which means that we only sync data between the cloud and our app that is part of the tenants. The other tenant data we will leave in the cloud and therefor it is not available to the clients.

AWS Services

I use different AWS services to accomplish this Multi tenant set up.

AWS Cognito

When a tenant creates an account will it be stored in Cognito. This is a service from AWS that handles user and access management for you. Cognito will create the user with an unique user id. This user id will be the tenant ID.

AWS Amplify

AWS Amplify is a library that acts as the glue between front end tools and the backend on AWS. You can use AWS Amplify for setting up your API's, storage, authentication and authorization, database, data store and more. Via AWS Amplify we will design who can access what data and deploy the cloud backend.

A great feature of AWS Amplify is datastore. This is a capability that enables offline data access, manage the syncing of data between clients and cloud and take care of syncing conflicts.

AWS Appsync

AWS AppSync is a fully managed service that makes it easy to develop GraphQL APIs by handling the heavy lifting of securely connecting to data sources like AWS DynamoDB, Lambda, and more.

When you make an API requests the logic of the request is in the resolvers. A resolver has a request mapping and response mapping template. Via the response mapping template we will apply logic that checks if the user is in the right auth group to access the data and if he is part of the right tenant.

AWS DynamoDB

DynamoDB is a NoSQL key-value pair database. It is database that is completely managed for you and that will be deployed via the Appsync schema.

Getting Started

We are going to create a mini Slack app. As a tenant you can add users and channels. The users in your tenant can create messages, only, in the channels which are part of the tenant.

Set up AWS Amplify

We first need to have the AWS Amplify CLI installed. The Amplify CLI is a command line tool that allows you to create & deploy various AWS services.

To install the CLI, we'll run the following command:

$ npm install -g @aws-amplify/cli

Enter fullscreen mode Exit fullscreen mode

Next, we'll configure the CLI with a user from our AWS account:

$ amplify configure

Enter fullscreen mode Exit fullscreen mode

For a video walkthrough of the process of configuring the CLI, click

Set up React Native

First, we'll create the React Native application we'll be working with.
Run these commands in the root dir (so not in your reactJS dir)

$ npx expo init multitenantAPP

> Choose a template: blank

$ cd multitenantAPP

$ npm install aws-amplify aws-amplify-react-native @react-native-community/netinfo

Enter fullscreen mode Exit fullscreen mode

Init your Amplify Project

Now we can initialize a new Amplify project from within the root of our React Native application:

$ amplify init
Enter fullscreen mode Exit fullscreen mode

Here we'll be guided through a series of steps:

  • Enter a name for the project: multitenantapp (or your preferred project name)
  • Enter a name for the environment: dev (use this name, because we will reference to it)
  • Choose your default editor: Visual Studio Code (or your text editor)
  • Choose the type of app that you're building: javascript
  • What javascript framework are you using: react-native
  • Source Directory Path: src
  • Distribution Directory Path: build
  • Build Command: npm run-script build
  • Start Command: npm run-script start
  • Do you want to use an AWS profile? Y
  • Please choose the profile you want to use: YOUR_USER_PROFILE
  • Now, our Amplify project has been created & we can move on to the next steps.

Add Auth to your project

Amplify add auth
Enter fullscreen mode Exit fullscreen mode

Follow these steps:

  • Do you want to use the default authentication and security configuration? Manual configuration
  • User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)
  • Please provide a friendly name for your resource that will be used to label this category in the project: Enter
  • Please enter a name for your identity pool: Enter
  • Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
  • Do you want to enable 3rd party authentication providers in your identity pool? No
  • Please provide a name for your user pool:
  • How do you want users to be able to sign in? Username
  • Do you want to add User Pool Groups? Yes
  • Provide a name for your user pool group: users
  • Do you want to add another User Pool Group: Y
  • Provide a name for your user pool group: editors
  • Do you want to add another User Pool Group: N
  • Sort the user pool groups in order of preference: Enter
  • Do you want to add an admin queries API? No
  • Multifactor authentication (MFA) user login options: OFF
  • Email based user registration/forgot password: Enabled(Requires per-user email entry at registration)
  • Please specify an email verification subject: Enter
  • Please specify an email verification message: Enter
  • Do you want to override the default password policy for this User Pool? Enter
  • What attributes are required for signing up? Select email and name by pressing space bar and press enter when finished
  • Specify the app's refresh token expiration period (in days): Enter
  • Do you want to specify the user attributes this app can read and write? Y
  • Do you want to enable any of the following capabilities? Enter
  • Do you want to enable any of the following capabilities? Select Add user to group
  • Do you want to use an OAuth flow? No
  • Do you want to configure Lambda Triggers for Cognito? N
  • Enter the name of the group to which users will be added. users
  • Do you want to edit your add-to-group function now? N

So that is it. We need to add a custom tenantid field now. Go to:

Amplify > backend > auth > multitenant......

open the parameters.json and replace this code:

 "userpoolClientWriteAttributes": [
        "email",
        "name"
    ],
 "userpoolClientReadAttributes": [
        "email",
        "name"
    ],

Enter fullscreen mode Exit fullscreen mode

with this code:

 "userpoolClientWriteAttributes": [
        "email",
        "name",
        "custom:tenantid"
    ],
 "userpoolClientReadAttributes": [
        "email",
        "name",
        "custom:tenantid"
    ],
Enter fullscreen mode Exit fullscreen mode

open the .....cloudformation-template.yml and replace this code:

 Schema: 

        -
          Name: email
          Required: true
          Mutable: true

        -
          Name: name
          Required: true
          Mutable: true
Enter fullscreen mode Exit fullscreen mode

with this code:

 Schema: 

        -
          Name: email
          Required: true
          Mutable: true

        -
          Name: name
          Required: true
          Mutable: true

        - 
          Name: tenantid
          Mutable: true
          AttributeDataType: String
Enter fullscreen mode Exit fullscreen mode

push to the cloud first:

Amplify push
Enter fullscreen mode Exit fullscreen mode

Let's create 4 users

Note: make sure you replace certain values with your own values. Do not apply quotes to string values.

Make a the tenant user

Go to the terminal and apply this command:

aws cognito-idp admin-create-user --profile <profileName you made in the Set up AWS Amplify chapter > \
    --user-pool-id <CognitoUserPoolID> \
    --username <EMAIL> \
    --temporary-password temppass \
    --user-attributes 
 Name=email,Value=<EMAIL> \
    --message-action SUPPRESS
Enter fullscreen mode Exit fullscreen mode

make a note of the sub value, something like this: 0cc05b9c-9edf-4be8-87b6-25d7c4e750b6

This ID of the first user, the tenant, will be used when creating the next tenant users.

Make a tenant user in the group users

aws cognito-idp admin-create-user --profile <profileName you made in the Set up AWS Amplify chapter > \
    --user-pool-id <CognitoUserPoolID> \
    --temporary-password temppass \
    --username <EMAIL> \
    --user-attributes 
 Name=email,Value=<EMAIL> 
\
    --message-action SUPPRESS
Enter fullscreen mode Exit fullscreen mode

Now update the attributes:

aws cognito-idp admin-update-user-attributes --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --user-attributes Name=custom:tenantid,Value=<ID THAT YOU HAVE NOTED> Name=name,Value=<A NAME>
Enter fullscreen mode Exit fullscreen mode

Add this user to the "users" group

aws cognito-idp admin-add-user-to-group --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --group-name users
Enter fullscreen mode Exit fullscreen mode

Make a tenant user in the group editors

aws cognito-idp admin-create-user --profile <profileName you made in the Set up AWS Amplify chapter > \
    --user-pool-id <CognitoUserPoolID> \
    --temporary-password temppass \
    --username <EMAIL> \
    --user-attributes 
 Name=email,Value=<EMAIL> 
\
    --message-action SUPPRESS
Enter fullscreen mode Exit fullscreen mode

Now update the attributes:

aws cognito-idp admin-update-user-attributes --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --user-attributes Name=custom:tenantid,Value=<ID THAT YOU HAVE NOTED> Name=name,Value=<A NAME>
Enter fullscreen mode Exit fullscreen mode

Add this user to the "users" group

aws cognito-idp admin-add-user-to-group --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --group-name editors
Enter fullscreen mode Exit fullscreen mode

Make a user in another tenant in the group editors

aws cognito-idp admin-create-user --profile <profileName you made in the Set up AWS Amplify chapter > \
    --user-pool-id <CognitoUserPoolID> \
    --temporary-password temppass \
    --username <EMAIL> \
    --user-attributes 
 Name=email,Value=<EMAIL> 
\
    --message-action SUPPRESS
Enter fullscreen mode Exit fullscreen mode

Now update the attributes:

aws cognito-idp admin-update-user-attributes --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --user-attributes Name=custom:tenantid,Value=1 Name=name,Value=<A NAME>
Enter fullscreen mode Exit fullscreen mode

Add this user to the "users" group

aws cognito-idp admin-add-user-to-group --profile <profileName> --user-pool-id <CognitoUserPoolID> --username <EMAIL> --group-name editors
Enter fullscreen mode Exit fullscreen mode

Great job! You have set up your authentication and you have created a tenant with two end users and a complete other tenant user. We will first create some data via the Appsync console and validate our implementation before we are going to build the front end application. But before we can do that we first need to deploy our API.

Add Graphql API to your project

Amplify add api
Enter fullscreen mode Exit fullscreen mode

These steps will take place:

  • Select Graphql
  • Enter a name for the API: multitenantapp (your preferred API name)
  • Select an authorisation type for the API: Amazon Cognito User Pool ( Because we are using this app with authenticated users only, but you can choose other options)
  • Select at do you want to use the default authentication and security configuration: Default configuration
  • How do you want users to be able to sign in? Username (with this also the AWS Amplify Auth module will be enabled)
  • Do you want to configure advanced settings? Yes, I want to make some additional changes
  • Configure additional auth types? N
  • Configure conflict detection? Y
  • Select the default resolution strategy Auto merge
  • Do you want to override default per model settings? N
  • Do you have an annotated GraphQL schema? n
  • Choose a schema template: Single object ...
  • Do you want to edit the schema now?: n

Your API and your schema definition have been created now. You can find it in you project directory:

Amplify > backend > api > name of your api

Open the schema.graphql file and replace the code with this code.

type channel
  @model
  @key(
    name: "channelByTenant"
    fields: ["tenant"]
    queryField: "channelByTenant"
  )
  @auth(
    rules: [
      { allow: owner, ownerField: "owner" }
      { allow: groups, groups: ["users"], operations: [read] }
      {
        allow: groups
        groups: ["editors"]
        operations: [create, read, update, delete]
      }
    ]
  ) {
  id: ID!
  name: String!
  messages: [message] @connection(keyName: "messageByChannel", fields: ["id"])
  tenant: ID!
  owner: ID!
}

type message
  @model
  @key(
    name: "messageByChannel"
    fields: ["channel"]
    queryField: "messageByChannel"
  )
  @auth(
    rules: [
      { allow: owner, ownerField: "owner" }
      { allow: groups, groups: ["users"], operations: [read, create] }
      {
        allow: groups
        groups: ["editors"]
        operations: [read, update, create, delete]
      }
    ]
  ) {
  id: ID!
  channel: ID!
  user: ID!
  username: String!
  message: String!
  tenant: ID!
  owner: ID!
}



Enter fullscreen mode Exit fullscreen mode

Your backend is set up and can be pushed to the cloud, please run:

amplify push
Enter fullscreen mode Exit fullscreen mode

Follow these steps:

  • Are you sure you want to continue? Y
  • Do you want to generate code for your newly created GraphQL API? Y
  • Choose the code generation language target? Javascript
  • Enter the file name pattern of graphql queries, mutations and subscriptions ENTER
  • Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Y
  • Enter maximum statement depth [increase from default if your schema is deeply nested]? 2

Now create the datastore models which you use in your app. Run this code in the root of your app:

amplify codegen models
Enter fullscreen mode Exit fullscreen mode

Test via the Appsync console

Please log in to the console. Go to the Appsync console, select your project and click on queries.

Alt Text

If you first login with the user from the editor group and create a channel and a few message. Then logout and log in with the user from the other tenant. You will notice that you can see the data of the other tenant.

This is because we don't have the right validation in place in our VTL templates like I mention before. I will update this section as soon a that github issue has been solved.

So for now we will set up this tenant filter via our application. I wanted to show this because although we set multi tenant via the app, everybody still has access if they want.

Set up your React Native App

As you are used from me, I don't focus much on the UX of an application during my blogs. I just want you to show how the basic functionality is working.

Alt Text

Run first

yarn add @react-navigation/native @react-navigation/stack
Enter fullscreen mode Exit fullscreen mode

and

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
Enter fullscreen mode Exit fullscreen mode

Make a file metro.config.js in the root of your project and past this code.

module.exports = {
  resolver: {
    blacklistRE: /#current-cloud-backend\/.*/
  }
};
Enter fullscreen mode Exit fullscreen mode

This will prevent metro to identify duplicate packages and cause errors.

Go to the app.js in the React Native directory and paste this code.

This file will manage the navigation and logout actions

import React from "react";
import { View, Button } from "react-native";
import Amplify, { Auth } from "aws-amplify";
import { withAuthenticator } from "aws-amplify-react-native";
import { DataStore } from "@aws-amplify/datastore";
import Channels from "./src/channels";
import ChannelMessages from "./src/channelMessages";
import LoadAuth from "./src/auth";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import awsconfig from "./src/aws-exports"; // if you are using Amplify CLI

//Amplify.configure(awsconfig);
Amplify.configure({
  ...awsconfig,
  Analytics: {
    disabled: true
  }
});

const Stack = createStackNavigator();

async function logout() {
  await DataStore.clear();
  Auth.signOut();
}

function Appstart() {
  return (
    <View
      style={{
        marginTop: 40,

        display: "flex",
        flex: 1
      }}
    >
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Auth" component={LoadAuth} />
          <Stack.Screen
            name="Channel"
            component={Channels}
            options={{
              headerLeft: null,
              headerTitle: "Channels",
              headerRight: () => (
                <Button onPress={() => logout()} title="Logout" color="#000" />
              )
            }}
          />
          <Stack.Screen name="Messages" component={ChannelMessages} />
        </Stack.Navigator>
      </NavigationContainer>
    </View>
  );
}

export default withAuthenticator(Appstart);

Enter fullscreen mode Exit fullscreen mode

Create this file auth.js in the src directory.

This file identify if a user is authenticated and if so set the datastore selective sync with the tenantid of the user and redirect to the channel overview.

import { Auth } from "aws-amplify";
import { DataStore, syncExpression } from "@aws-amplify/datastore";
import { Channel, Message } from "./models";
import { useNavigation } from "@react-navigation/native";

let tenantid = "";

// use for development purposes, to reset the datastore during each app start
DataStore.clear();
DataStore.configure({
  syncExpressions: [
    syncExpression(Channel, () => {
      return c => c.tenant("eq", tenantid);
    }),
    syncExpression(Message, () => {
      return c => c.tenant("eq", tenantid);
    })
  ]
});

function Datastore() {
  const navigation = useNavigation();
  const initDS = async () => {
    await Auth.currentAuthenticatedUser()
      .then(async result => {
        if (result !== "not authenticated") {
          tenantid = result.attributes["custom:tenantid"];
          await DataStore.start();
          navigation.navigate("Channel");
        }
      })
      .catch(err => {
        console.log(err);
      });
  };

  initDS();

  return null;
}

export default Datastore;

Enter fullscreen mode Exit fullscreen mode

and create a file channels.js in the src directory and paste this code:

import React, { useEffect, useState } from "react";
import {
  View,
  Text,
  SafeAreaView,
  FlatList,
  StyleSheet,
  StatusBar,
  TouchableHighlight,
  TouchableOpacity
} from "react-native";
import { DataStore } from "@aws-amplify/datastore";
import { Auth } from "aws-amplify";
import { Channel } from "./models/";
import { useNavigation } from "@react-navigation/native";

const Item = ({ id, name, navigateToMessage }) => (
  <TouchableOpacity onPress={() => navigateToMessage(id, name)}>
    <View style={styles.item}>
      <Text style={styles.title}>{name}</Text>
    </View>
  </TouchableOpacity>
);

export default function Channels() {
  let subscription;
  const [channels, setChannels] = useState([]);
  const navigation = useNavigation();

  const navigateToMessage = (id, name) => {
    navigation.navigate("Messages", { id, name });
  };

  const loadChannelArray = async () => {
    const result = await DataStore.query(Channel);

    setChannels(result);
  };

  useEffect(() => {
    loadChannelArray();
    subscription = DataStore.observe(Channel).subscribe(() => {
      loadChannelArray();
    });
    return function cleanup() {
      setChannels([]);
      subscription.unsubscribe();
    };
  }, []);

  const renderItem = ({ item }) => (
    <Item
      name={item.name}
      id={item.id}
      navigateToMessage={() => navigateToMessage(item.id, item.name)}
    />
  );

  const onSubmit = async () => {
    const auth = await Auth.currentAuthenticatedUser();

    const identifier = new Date();

    await DataStore.save(
      new Channel({
        name: "New channel " + identifier.getSeconds(),
        owner: auth.signInUserSession.accessToken.payload.sub,
        tenant: auth.attributes["custom:tenantid"]
      })
    );

    loadChannelArray();
  };

  return (
    <SafeAreaView style={styles.container}>
      <View
        style={{
          alignItems: "flex-end"
        }}
      >
        <TouchableHighlight onPress={() => onSubmit()}>
          <Text
            style={{
              fontSize: 16,
              alignItems: "flex-end"
            }}
          >
            Add new channel
          </Text>
        </TouchableHighlight>
      </View>
      <FlatList
        data={channels}
        renderItem={renderItem}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: StatusBar.currentHeight || 0
  },
  item: {
    backgroundColor: "#dead31",
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16
  },
  title: {
    fontSize: 32
  }
});

Enter fullscreen mode Exit fullscreen mode

create a file channelMessages.js in the src directory and paste this code:

This file will manage the messages of a channel

import React, { useEffect, useState } from "react";
import {
  View,
  Text,
  SafeAreaView,
  FlatList,
  StyleSheet,
  StatusBar,
  TouchableHighlight
} from "react-native";
import { DataStore } from "@aws-amplify/datastore";
import { Auth } from "aws-amplify";
import { Message } from "./models/";

const Item = ({ username, message }) => (
  <View style={styles.item}>
    <Text style={styles.title}>Name {username}</Text>
    <Text style={styles.title}>{message}</Text>
  </View>
);

export default function Messages(props) {
  const id = props.route.params.id;

  const [messages, setMessages] = useState([]);

  async function loadMessagesArray() {
    const result = await DataStore.query(Message, c => c.channel("eq", id));
    setMessages(result);
  }

  useEffect(() => {
    const loadMessages = async () => {
      loadMessagesArray();
    };

    loadMessages();
  }, []);

  const renderItem = ({ item }) => (
    <Item username={item.username} message={item.message} />
  );

  const onSubmit = async () => {
    const auth = await Auth.currentAuthenticatedUser();

    const identifier = new Date();

    await DataStore.save(
      new Message({
        channel: id,
        user: auth.signInUserSession.accessToken.payload.sub,
        username: auth.attributes.name,
        message: "This is a new message " + identifier.getSeconds(),
        owner: auth.signInUserSession.accessToken.payload.sub,
        tenant: auth.attributes["custom:tenantid"]
      })
    );

    loadMessagesArray();
  };

  return (
    <SafeAreaView style={styles.container}>
      <View
        style={{
          alignItems: "flex-end"
        }}
      >
        <TouchableHighlight onPress={() => onSubmit()}>
          <Text
            style={{
              fontSize: 16,
              alignItems: "flex-end"
            }}
          >
            Add new message
          </Text>
        </TouchableHighlight>
      </View>
      <FlatList
        data={messages}
        renderItem={renderItem}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: StatusBar.currentHeight || 0
  },
  item: {
    backgroundColor: "#5d90e3",
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16
  },
  title: {
    fontSize: 18
  }
});

Enter fullscreen mode Exit fullscreen mode

You have set up your complete application now. If your run

expo start 
Enter fullscreen mode Exit fullscreen mode

Press I when loaded, this will start Expo on your IOS simulator. Login with the different users and notice the difference between tenants and between users in difference groups.

Repo

You can find my repo on github: https://github.com/rpostulart/multitenant

Conclusion

I have shown you how with tools like React Native (Expo) and AWS Amplify you can set up Multi tenant applications.

Of course this is not the ideal set up yet, because of the conditional checks are missing from the VTL templates. We can manually add those, but from a maintainability perspective we only want to do this from the schema. As soon as there is an update, I will update this blog.

I hope this was useful to you and I am looking forward for your feedback.

About me

I am a doing (side)projects with technology like AWS Amplify, ReactJS, React Native and EXPO. I am an AWS community builder. I am a huge fan of AWS Amplify and blog about certain topics that come along with it. If you have any questions related to the framework, React of React Native then you can always reach out to me.

Twitter

Alt Text

Do you want to be updated about new blogs?
Follow me on twitter: https://twitter.com/ramonpostulart

Top comments (16)

Collapse
devtghosh profile image
Devjyoti Ghosh

Hey is there a way I could share access for a model with users outside the tenant? My use case would be a user group that can have access to models from various tenants. For eg in asana a user can be added to various different companies projects.

Collapse
rpostulart profile image
rpostulart Author

I believe it should be possible, but then you need to add multiple tenantid's to cognito userattribute. For example an array or string with commas as seperators. In your application you can to the validation

Collapse
devtghosh profile image
Devjyoti Ghosh • Edited on

I don't think this works exactly. Because user needs to have access to specific projects within a company. With multiple tenant ids 2 users within the same user group will have access to all projects within the tenant that is shared to the user group. See the image for an example of what is needed. dev-to-uploads.s3.amazonaws.com/i/...

Thread Thread
rpostulart profile image
rpostulart Author

You can use the user attribute (tenant) in combination with cognito groups (projects) or create multiple user attributes

Thread Thread
devtghosh profile image
Devjyoti Ghosh • Edited on

I don't think that works because you can only have 500 cognito user groups and users will need to have access to specific projects so each project's access will need their own cognito group I think. I have updated the user access diagram maybe that will make it more clear user access pattern diagram

There can also be a user 3 in above diagram that is a freelancer in both Company 1 & 2 and has access to only project A & C

Thread Thread
jcastaneyra profile image
Jose Castaneyra

The number of Cognito groups now is 10,000.

And a user can belong to 100 groups.

docs.aws.amazon.com/cognito/latest...

Collapse
yudhiesh1997 profile image
Yudhiesh Ravindranath

I am having an issue when using the mobile application, I cannot seem to login to the accounts I created. It just says I need to "Validate that amazon-cognito-identity-js has been linked". I cannot seem to find anywhere how to solve this.

Collapse
devtghosh profile image
Devjyoti Ghosh

The users show up on cognito console?

Collapse
rpostulart profile image
rpostulart Author

Looks like a framework error, have you created an issue on github ?

Collapse
yudhiesh1997 profile image
Yudhiesh Ravindranath

It is weird because yesterday I had other issues and was able to sign in but now when I redid the whole project this is an issue.

Thread Thread
yudhiesh1997 profile image
Yudhiesh Ravindranath • Edited on

Another issue is that when I try to sign in from AppSync in the AWS Console through User Pools I have to create a new password for the user but when I do it is saying that "email is missing" even though it is only asking for the password.

Collapse
devtghosh profile image
Devjyoti Ghosh

Hey thanks for the great article. I had a question how would the dataflow work when creating users within groups from the frontend with amplify. Is there a way I can mention the tenantid and CognitoUserPoolID that a user belongs to?

Collapse
rpostulart profile image
rpostulart Author

Of course you can. You can update the user attributes. It is explained here: docs.amplify.aws/lib/auth/manageus...

The userpool is configured with amplify

Collapse
devtghosh profile image
Devjyoti Ghosh

How would you get a company's tenant id though while signing up a user to the editors group for ex? My imagining is the main company Admin/Tenant signs up and then to add users to his organization they adds their email address and it temporarily signsup the user with the relevant tenant id & group details & sends an email to the users containing their userid and temporary password that they need to change. Kind of like how IAM works.

Thread Thread
rpostulart profile image
rpostulart Author

Indeed, that is how I would do it.

Collapse
ricobanga profile image
Henry de la Martinière • Edited on

This is great ! avoiding @auth owner to filter rows allows to keep subscription working properly. But if a tenant id goes public, you'll have a security issue

🌚 Life is too short to browse without dark mode