DEV Community

Cover image for Build a Secure E-Commerce App with SuperTokens and Hasura GraphQL
Ankur Tyagi
Ankur Tyagi

Posted on • Originally published at theankurtyagi.com

Build a Secure E-Commerce App with SuperTokens and Hasura GraphQL

This tutorial will show you how to develop a secure e-commerce store using SuperTokens authentication in a React.js App.

We'll use a modern stack that includes React, Hasura GraphQL, and SuperTokens.

The source code for the App we're working on is available to view here.

By learning how to combine all of these features, you should be able to apply what you've learned here to create your ideas. Understanding the fundamental building blocks allows you to take this knowledge with you and use it in any way you see fit in the future.


Using SuperTokens to Authenticate the Hasura endpoint

SuperTokens provides authentication, and Hasura exposes a single GraphQL endpoint that you use on the frontend to send GraphQL queries and access data. Because it is a public API by default, SuperTokens will make it secure or private.

You will integrate SuperTokens with Hasura. Tokens generated from SuperTokens will be sent from the UI side in request headers to Hasura, where they will be validated.


What is SuperTokens?

SuperTokens is an open-source AuthO alternative that allows you to set up authentication in less than 30 minutes.

Over the last few months, SuperTokens has grown in popularity and adoption among developers in my network. And many of the developers I've talked to about it like the fact that it's open-source.

When you start a new project, SuperTokens provides user authentication. From there, you can quickly implement additional features in your App.


Why use SuperTokens?

SuperTokens is an open-source alternative with the following features:

  • SuperTokens is open source, which means they can be used for free, indefinitely, with no restrictions on the number of users.

  • An on-premises deployment that gives you complete control over your user data by utilizing your database.

  • An all-in-one solution that includes login, signups, user and session management without the complexities of OAuth protocols.

  • Ease of use and increased security.

  • Customisable: Anyone can contribute to the improvement of SuperTokens!


What is Hasura?

  • Hasura makes it possible to create a real-time GraphQL API for your application without writing any backend code.

  • Hasura is a GraphQL Engine that converts your database into a real-time, instant GraphQL API.

  • You can also use the Remote Schemas and Actions to integrate your own GraphQL APIs into Hasura.

  • Hasura is a permission-based system.


TL;DR

Here are the links to quickly access the source code or learn more about both products.


Let's get started

To get started, first create a new React.js App:

npx create-react-app my-app
cd my-app
npm start

Enter fullscreen mode Exit fullscreen mode

To implement SuperTokens Authentication, we have two options.

  • Unlimited users, self-hosted, and free for life
  • Free up to 5K monthly active users on SaaS (hosted by SuperTokens). After that, $29 per month for every 5K users (up to 50K MAUs)

Pricing.JPG


Create a managed service with SuperTokens

To create a SuperTokens Managed Service, click the blue "Create an App" button, which will take you to an account creation page. Then, by following the instructions, you can select an availability region for your managed service.

You'll see the following UI after creating a SuperTokens Managed Service, which contains a default development environment.

image(2).png


Set up of the Hasura cloud

Hasura can be used in two different ways. You can use it locally with Docker or on Hasura Cloud. Hasura Cloud is used throughout the tutorial.

If you're new to Hasura, you'll need to create an account and a project. If you follow this guide, you should be up and running in no time.

You should be in the project dashboard after setting up your account and project.

image(3).png

image(4).png


Creating/Importing a database in Hasura

The first step is to connect the database with Hasura. Next, select the "Connect Database" option as indicated in the image below. This will take you to the database page, where you can connect to an existing database or create one from scratch.

image(5).png

image(6).png

This tutorial will connect the database we created using SuperTokens to managed services.

After connecting the database, you can access it by clicking on "public," as shown in the figure below.

image(7).png

In this blog, I'm connecting Hasura to a SuperTokens cloud database, but in production, SuperTokens doesn't allow the database, so the database should be a separate entity, which is the recommended approach. I'm only using it for this demo app.


Using Hasura to make tables

Now that you've connected the database, it's time to create the tables.

You will create a few more tables in this step:

  • user_cart
  • products
  • user_wishlist
  • merchants
  • orders
  • categories

drawSQL-export-2022-03-15_23_20.png


Managing permissions in Hasura

Hasura lets you define access control rules at three different levels:

Table level, Action level, and Role level are examples of levels.

Role-level examples are used in this app.

You can find detailed instructions in the documentation link


SuperTokens Frontend.init()

SuperTokens lets us automatically add request interceptors to save and store tokens for enduring user sessions by initializing them on the frontend.

We'll use the pre-built *EmailPassword * recipe to access the SuperTokens demo app.

Let's add the following code block to the top of the index.tsx to initialize the Supertokens client on the React app.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import SuperTokens from 'supertokens-auth-react';
import Session, { SessionAuth } from 'supertokens-auth-react/recipe/session';
import { BrowserRouter } from 'react-router-dom';
import EmailPassword from 'supertokens-auth-react/recipe/emailpassword';
import { getApiDomain, getWebsiteDomain } from './utils/utils';
import App from './App';
import reportWebVitals from './reportWebVitals';

SuperTokens.init({
  appInfo: {
    appName: 'Shopping App',
    apiDomain: getApiDomain(),
    websiteDomain: getWebsiteDomain(),
  },
  recipeList: [
    EmailPassword.init({
      getRedirectionURL: async (context) => {
        if (context.action === 'SUCCESS') {
          return '/home';
        }
        return undefined;
      },
      emailVerificationFeature: {
        mode: 'REQUIRED',
      },
    }),
    Session.init(),
  ],
});

Enter fullscreen mode Exit fullscreen mode

Our session context can be accessed after wrapping the App() component with the EmailPaswordAuth wrapper.


SuperTokens Backend.init()

SuperTokens takes care of many things for you and abstracts them away. When calling supertokens.init, we must specify the override config value to override the default implementation. Each recipe in the recipeList has an override config that may be used to alter its behavior.

supertokens.init({
    framework: 'express',
    supertokens: {
        connectionURI: process.env.API_TOKENS_URL,
        apiKey: process.env.API_KEY,
    },
    appInfo: {
        appName: 'SuperTokens Demo App',
        apiDomain,
        websiteDomain,
    },
    recipeList: [EmailPassword.init({}), Session.init({
        jwt: {
            enable: true,
            /*
             * This is an example of a URL that ngrok generates when
             * you expose localhost to the internet
             */
            issuer: process.env.API_JWT_URL,
        },
    })],
});

Enter fullscreen mode Exit fullscreen mode

Learn how to customize SuperTokens APIs here.


SuperTokens managed services Architecture

The architecture diagram for SuperTokens managed services version 👇

image.png

An example of how the three components interact for a sign-in and sign-out flow (using email and password) is shown below

image.png


Integrating SuperTokens with Hasura

The token issuer URL must be added to Hasura env variables to integrate SuperTokens with Hasura. Because we'll be calling the Hasura endpoint from our local, we'll need to expose it to the internet. To do so, we'll use ng-rock, and we'll also need to enable JWT in SuperTokens.

ngrok is a well-known tunneling tool that allows you to connect a locally running application to the internet. By clicking here, you can get it for free and with all of the functionality you need, as shown in the diagram below.

ng rock.JPG

Follow the documentation, which includes step-by-step instructions, to set up Hasura environment variables.

Set up Hasura environment variables

Hasura's env variables on the cloud are configured with SuperTokens JWT URls, as shown in the diagram below.

  recipeList: [EmailPassword.init({}), Session.init({
    jwt: {
      enable: true,
      /*
                * This is an example of a URL that ngrok generates when
                * you expose localhost to the internet
                */
      issuer: process.env.API_JWT_URL,
    },

Enter fullscreen mode Exit fullscreen mode
REACT_APP_API_PORT=3002
REACT_APP_API_GRAPHQL_URL=https://supertokens.hasura.app/v1/graphql
API_KEY=SSugiN8EMGZv=fL33=yJbycgI7UmSd
API_TOKENS_URL=https://0def13719ed411ecb83cf5e5275e2536-ap-southeast-1.aws.supertokens.io:3568
API_JWT_URL=http://ec87-223-185-12-185.ngrok.io/auth

Enter fullscreen mode Exit fullscreen mode

env.JPG

update.JPG


To send Hasura JWT claims in a SuperTokens-generated token

We need to share user role-related information with Hasura for role-based permission. This may be done in the SuperTokens by overriding the existing token, as seen in the code spinet below.

  override: {
      functions(originalImplementation) {
        return {
          ...originalImplementation,
          async createNewSession(sessionInput) {
            const input = sessionInput;
            input.accessTokenPayload = {
              ...input.accessTokenPayload,
              'https://hasura.io/jwt/claims': {
                'x-hasura-user-id': input.userId,
                'x-hasura-default-role': 'user',
                'x-hasura-allowed-roles': ['user', 'anonymous', 'admin'],
              },
            };

            return originalImplementation.createNewSession(input);
          },
        };
      },
    },

Enter fullscreen mode Exit fullscreen mode

Hasura will validate authorization using the headers listed below.

x-hasura-user-id
x-hasura-default-role
x-hasura-allowed-roles

Enter fullscreen mode Exit fullscreen mode

In the UI, how do you use the Hasura endpoint?

We're using the apollo/client npm package to access the Hasura GrpahQL endpoint.

Adding apollo/client to our app:

import React from 'react';
import './App.scss';
import { useSessionContext } from 'supertokens-auth-react/recipe/session';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
} from '@apollo/client';
import AppRoutes from './shared/components/routes/AppRoutes';

function App() {
  const { accessTokenPayload } = useSessionContext();
  const client = new ApolloClient({
    uri: `${process.env.REACT_APP_API_GRAPHQL_URL}`,
    cache: new InMemoryCache(),
    headers: {
      Authorization: `Bearer ${accessTokenPayload?.jwt}`,
      'Content-Type': 'application/json',
    },
  });
  return (
    <div className="App">
      <ApolloProvider client={client}>
        <AppRoutes />
      </ApolloProvider>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

We are sending a token generated by SuperTokens in Authorization: Bearer $accessTokenPayload?.jwt


Let's have a look at all of the project dependencies that were used in the development of this app

"dependencies": {
    "@apollo/client": "^3.5.9",
    "@emotion/react": "^11.8.1",
    "@emotion/styled": "^11.8.1",
    "@material-ui/icons": "^4.11.2",
    "@mui/icons-material": "^5.4.4",
    "@mui/lab": "^5.0.0-alpha.72",
    "@mui/material": "^5.4.3",
    "@mui/styles": "^5.4.4",
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.3",
    "@testing-library/user-event": "^13.5.0",
    "@types/express": "^4.17.13",
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.25",
    "@types/react": "^17.0.39",
    "@types/react-dom": "^17.0.11",
    "axios": "^0.26.0",
    "body-parser": "^1.19.2",
    "cors": "^2.8.5",
    "dotenv": "^16.0.0",
    "graphql": "^16.3.0",
    "helmet": "^5.0.2",
    "morgan": "^1.10.0",
    "nodemon": "^2.0.15",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.5.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^6.2.1",
    "react-scripts": "5.0.0",
    "sass": "^1.49.8",
    "supertokens-auth-react": "^0.18.7",
    "supertokens-node": "^9.0.0",
    "typescript": "^4.5.5",
    "web-vitals": "^2.1.4"
  },

Enter fullscreen mode Exit fullscreen mode

Let's talk about the React components we built for the E-commerce app.

The folder structure in Visual Studio Code looks like this:

folder.JPG


Create the product list component (ProductList.tsx)

This component displays a list of all of the products.

import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import {
  useQuery,
  gql,
} from '@apollo/client';
import Skeleton from '@mui/material/Skeleton';
import Card from '@mui/material/Card';
import ProductItem from '../product-item/ProductItem';
import { Product } from '../models/Product';
import useToast from '../../hooks/useToast';

const PRODUCT_LIST = gql`query{products {id category_id  merchant_id  name  price product_img_url status}user_whishlist {
    product_id
  }}`;

function ProductList() {
  const { loading, error, data } = useQuery(PRODUCT_LIST);
  const { addToast } = useToast();
  if (error) {
    addToast('Unable to load.....');
    return null;
  }
  return (
    <Box sx={{ flexGrow: 1, padding: '20px' }}>
      <Grid container spacing={6}>
        {
           !loading ? data?.products?.map((product: Product) => (
             <Grid item xs={3}>
               <ProductItem
                 productData={product}
                 whishlisted={data?.user_whishlist
                   .some((item: any) => item.product_id === product.id)}
               />
             </Grid>
           )) : (
             <Grid item xs={3}>
               <Card style={{ padding: '10px' }}>
                 <Skeleton variant="rectangular" height={50} style={{ marginBottom: '10px' }} />
                 <Skeleton variant="rectangular" height={200} style={{ marginBottom: '10px' }} />
                 <Skeleton variant="rectangular" height={40} width={100} style={{ margin: '0 auto' }} />
               </Card>
             </Grid>
           )
        }
      </Grid>
    </Box>
  );
}

export default ProductList;


Enter fullscreen mode Exit fullscreen mode

Create the product details component (ProductDetails.tsx)

When a user clicks on any products on the ProductList page, this component displays all of the product's details and specifications.

/* eslint-disable no-unused-vars */
import React, { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import { makeStyles } from '@mui/styles';
import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
import {
  useQuery,
  gql,
  useMutation,
} from '@apollo/client';
import CardActions from '@mui/material/CardActions';
import LoadingButton from '@mui/lab/LoadingButton';
import Skeleton from '@mui/material/Skeleton';
import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import { useParams, useNavigate } from 'react-router-dom';
import ProductSpecifications from '../product-specifications/ProductSpecifications';

const FETCH_PRODUCT = gql`query getProduct($pid: Int!) {
  products(where: {id: {_eq: $pid}}) {
    category_id
    id
    merchant_id
    name
    price
    product_img_url
    status
    descriptions
  }
  user_cart(where: {product_id: {_eq: $pid}}) {
    product_id
  }
}

`;

const ADD_TO_CART = gql`mutation addToCart($pid: Int!, $price: Int!) {
  insert_user_cart_one(object: {product_id: $pid, price: $price}) {
    product_id
  }
}
`;

const useStyles: any = makeStyles(() => ({
  productImg: {
    height: '416px',
    width: '200px',
    marginLeft: 'auto',
    marginRight: 'auto',
    padding: '10px',
  },
  addtoCartBtn: {
    backgroundColor: '#ff9f00',
    fontWeight: 'bold',
    fontSize: '16px',
  },
  buyNowBtn: {
    backgroundColor: '#fb641b',
    fontWeight: 'bold',
    fontSize: '16px',
  },
  textLeft: {
    textAlign: 'left',
  },
  offerHeader: {
    fontSize: '16px',
    fontWeight: '500',
    color: '#212121',
    textAlign: 'left',
  },
  offerList: {
    textAlign: 'left',
    lineHeight: '1.43',
    paddingLeft: '0',
  },
  specHeader: {
    fontSize: '24px',
    fontWeight: '500',
    lineHeight: '1.14',
    textAlign: 'left',
    color: '#212121',
  },
  cardWrapper: {
    padding: '20px',
  },
  currencyTxt: {
    fontSize: '28px',
    textAlign: 'left',
    fontWeight: 'bold',
  },
  offerImg: {
    height: '18px',
    width: '18px',
    position: 'relative',
    top: '6px',
    marginRight: '10px',
  },
  offerListWrapper: {
    listStyle: 'none',
  },
  pb0: {
    paddingBottom: '0',
  },
  currIcon: {
    position: 'relative',
    top: '5px',
    fontWeight: 'bold',
    fontSize: '28px',
  },
  cardActions: {
    display: 'flex',
    justifyContent: 'center',
  },
  productCard: {
    cursor: 'pointer',
  },
}));

export default function ProductDetails() {
  const { pid } = useParams();
  const { loading, data, error } = useQuery(FETCH_PRODUCT, {
    variables: {
      pid,
    },
  });
  const [addToCart, {
    loading: AddLoader,
    data: AddData, error: AddError,
  }] = useMutation(ADD_TO_CART);
  const product = data?.products[0];
  const [addToCartLoader, setAddToCartLoader] = useState(false);
  const classes = useStyles();
  const [cartBtnTxt, setCartBtnTxt] = useState('ADD TO CART');
  const navigate = useNavigate();
  useEffect(() => {
    setCartBtnTxt(data?.user_cart.length > 0 ? 'GO TO CART' : 'ADD TO CART');
  }, [data]);
  const addToCartHandler = async () => {
    if (data?.user_cart.length > 0) {
      navigate('/cart');
    } else {
      setCartBtnTxt('GOING TO CART');
      setAddToCartLoader(true);
      await addToCart({
        variables: {
          pid,
          price: product.price,
        },
      });
      navigate('/cart');
    }
  };
  return (
    <Box sx={{ padding: '20px' }}>
      <Grid container spacing={6}>
        <Grid item xs={4}>
          <Card className={classes.cardWrapper}>
            {!loading ? (
              <CardMedia
                className={classes.productImg}
                component="img"
                image={product.product_img_url}
                alt="Paella dish"
              />
            ) : <Skeleton animation="wave" variant="rectangular" height="416px" />}
            <CardActions className={classes.cardActions}>
              {!loading ? (
                <>
                  <LoadingButton
                    variant="contained"
                    disableElevation
                    size="large"
                    loading={addToCartLoader}
                    loadingPosition="start"
                    className={classes.addtoCartBtn}
                    startIcon={<AddShoppingCartIcon />}
                    onClick={addToCartHandler}
                  >
                    {cartBtnTxt}
                  </LoadingButton>
                  <LoadingButton
                    variant="contained"
                    disableElevation
                    size="large"
                    className={classes.buyNowBtn}
                  >
                    BUY NOW
                  </LoadingButton>
                </>
              ) : (
                <>
                  <Skeleton animation="wave" variant="rectangular" height="43px" width="190px" />
                  <Skeleton animation="wave" variant="rectangular" height="43px" width="190px" />
                </>
              )}
            </CardActions>
          </Card>

        </Grid>
        <Grid item xs={8}>
          <Card>
            {!loading ? <CardHeader className={`${classes.textLeft} ${classes.pb0}`} title={product.name} /> : <Skeleton animation="wave" variant="rectangular" height="43px" />}
            <CardContent className={classes.pb0}>
              {!loading ? (
                <>
                  <Typography color="text.primary" className={classes.currencyTxt}>
                    <CurrencyRupeeIcon className={classes.currIcon} />
                    {product?.price}
                  </Typography>
                  {product?.descriptions?.offers?.length > 0 && (
                  <div className={classes.offers}>
                    <p className={classes.offerHeader}>Available Offers</p>
                    <ul className={classes.offerList}>
                      {
                            product?.descriptions?.offers.map((item: string) => (
                              <li className={classes.offerListWrapper}>
                                <span><img className={classes.offerImg} alt="" src="/images/offer.png" /></span>
                                {item}
                              </li>
                            ))
                        }
                    </ul>
                  </div>
                  ) }
                  <div>
                    <p className={classes.specHeader}>Specifications</p>
                    <ProductSpecifications header="General" specs={product?.descriptions?.specifications?.general} />
                    <ProductSpecifications header="Display Features" specs={product?.descriptions?.specifications?.display} />
                  </div>
                </>
              ) : <Skeleton animation="wave" variant="rectangular" height="43px" width="190px" />}
            </CardContent>
          </Card>
        </Grid>
      </Grid>
    </Box>
  );
}


Enter fullscreen mode Exit fullscreen mode

Create the cart list component (CartList.tsx)

This component displays a list of the products you have added to your cart.

/* eslint-disable no-unused-vars */
import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import {
  useQuery,
  gql,
} from '@apollo/client';
import Skeleton from '@mui/material/Skeleton';
import Button from '@mui/material/Button';
import { useNavigate } from 'react-router-dom';
import CartItem from '../cart-item/CartItem';
import PriceDetails from '../price-details/PriceDetails';
// import CardMedia from '@mui/material/CardMedia';
const PRODUCTS_IN_CART = gql`query getProductsInCart {
  user_cart {
    cartProducts {
      category_id
      name
      price
      product_img_url
      id
    }
    price
    discount
  }

}

`;
export default function CartList() {
  const {
    data, loading, error, refetch,
  } = useQuery(PRODUCTS_IN_CART);
  const navigate = useNavigate();

  const refereshCart = () => {
    refetch();
  };
  if (!loading && data.user_cart.length === 0) {
    return (
      <Box>
        <Card>
          <CardHeader sx={{ textAlign: 'left', paddingLeft: '33px' }} title="My Cart" />
          <CardContent>
            <img style={{ height: '162px' }} alt="" src="/images/empty.png" />
            <p>Your Cart is empty</p>
            <Button variant="contained" onClick={() => navigate('/home')}>Shop Now</Button>
          </CardContent>
        </Card>
      </Box>
    );
  }
  return (
    <Box sx={{ padding: '20px' }}>
      <Grid container spacing={6}>
        <Grid item xs={7}>
          <Card>
            {!loading ? (
              <>
                <CardHeader sx={{ borderBottom: '1px solid #efefef', textAlign: 'left', paddingLeft: '33px' }} title={`My Cart (${data.user_cart.length})`} />
                <CardContent sx={{ padding: '0' }}>
                  {data.user_cart.map((item: any) => (
                    <CartItem
                      refereshCart={refereshCart}
                      product={item.cartProducts}
                    />
                  ))}
                </CardContent>
              </>
            ) : <Skeleton animation="wave" variant="rectangular" height="416px" />}
          </Card>
        </Grid>
        <Grid item xs={5}>
          <Card>
            {!loading ? (
              <CardContent sx={{ padding: '0' }}>
                <PriceDetails priceDetails={data.user_cart} />
              </CardContent>
            ) : <Skeleton animation="wave" variant="rectangular" height="416px" />}
          </Card>
        </Grid>
      </Grid>
    </Box>
  );
}


Enter fullscreen mode Exit fullscreen mode

Create the price details component (PriceDetails.tsx)

This component displays the price calculation for all of the products that are currently in the shopping cart.

import React from 'react';
import { makeStyles } from '@mui/styles';

const useStyles = makeStyles({
  detailsHeader: {
    fontSize: '24px',
    fontWeight: '500',
    textAlign: 'left',
    color: '#878787',
    borderBottom: '1px solid #efefef',
    padding: '16px',
  },
  prcieWrapper: {
    display: 'flex',

  },
  priceContent: {
    width: '50%',
    padding: '16px',
    textAlign: 'left',
    fontSize: '22px',
  },
});

export default function PriceDetails({ priceDetails }: { priceDetails: any}) {
  const classes = useStyles();
  const total = priceDetails.reduce((prev: any, curr: any) => ({
    price: prev.price + curr.price,
    discount: prev.discount + curr.discount,
  }));
  return (
    <div>
      <div className={classes.detailsHeader}>
        PRICE DETAILS
      </div>
      <div className={classes.prcieWrapper}>
        <div className={classes.priceContent}>Price</div>
        <div className={classes.priceContent}>{total.price}</div>
      </div>
      <div className={classes.prcieWrapper}>
        <div className={classes.priceContent}>Discount</div>
        <div className={classes.priceContent}>
          -
          {total.discount}
        </div>
      </div>
      <div className={classes.prcieWrapper}>
        <div className={classes.priceContent}>Delivery Charges</div>
        <div className={classes.priceContent}>-</div>
      </div>
      <div className={classes.prcieWrapper}>
        <div className={classes.priceContent}>Total Amount</div>
        <div className={classes.priceContent}>
          {Number(total.price)
        - Number(total.discount)}

        </div>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Finally, this is how the app will appear once you sign in.

image.png

If you have any problems implementing the workflow after reading this article, don't hesitate to get in touch with me on Twitter or ping your questions to the SuperTokens Discord channel.


Conclusion

That was the end of this blog.

A big thank you to the SuperTokens team for spearheading this excellent open-source authentication project and developing this integration functionality with Hasura.

This project's final code can be found here.

Today, I hope you learned something new and if you did, please like/share it so that others can see it.

Thank you for being a regular reader; you're a big part of why I've been able to share my life/career experiences with you.

Let me know how you plan to use SuperTokens in your next project.

For the most up-to-date information, follow SuperTokens on Twitter.

Top comments (0)