---
title: Implement a custom invite flow
description: Implement a list of approved email addresses for third-party sign-ups using the SuperTokens user metadata recipe.
sidebar:
  order: 8
---

## Overview

This guide shows you how to disable public sign-ups to allow only certain people to access your app.
For third-party login, maintain a list of approved email addresses and validate users against it.

## Before you start

The tutorial assumes that you already have a working application integrated with **SuperTokens**.
If you have not, please check the [Quickstart Guide](/quickstart).

### Prerequisites

This guide uses the `UserMetadata` recipe to store the list of approved email addresses.
You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step.

## Steps

### 1. Implement the approved email list

You can store this list in your own database or use the metadata feature provided by SuperTokens.

The following code samples show you how to save the approved email list in user metadata.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import UserMetadata from "supertokens-node/recipe/usermetadata";

function allowlistKey(tenantId: string) {
  return `emailAllowList:${tenantId}`;
}

async function addEmailToAllowlist(tenantId: string, email: string) {
  let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId));
  let allowList: string[] = existingData.metadata.allowList || [];
  allowList = [...allowList, email];
  await UserMetadata.updateUserMetadata(allowlistKey(tenantId), {
    allowList,
  });
}

async function isEmailAllowed(tenantId: string, email: string) {
  let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId));
  let allowList: string[] = existingData.metadata.allowList || [];
  return allowList.includes(email);
}
```
</Tab>
<Tab title="Go" value="go">
```go
import (
	"fmt"

	"github.com/supertokens/supertokens-golang/recipe/usermetadata"
)

func allowlistKey(tenantID string) string {
	return fmt.Sprintf("emailAllowList:%s", tenantID)
}

func getAllowList(metadata map[string]interface{}) []string {
	allowList := []string{}
	rawAllowList, ok := metadata["allowList"].([]interface{})
	if !ok {
		return allowList
	}

	for _, value := range rawAllowList {
		email, ok := value.(string)
		if ok {
			allowList = append(allowList, email)
		}
	}
	return allowList
}

func addEmailToAllowlist(tenantID string, email string) error {
	existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID))
	if err != nil {
		return err
	}
	allowList := getAllowList(existingData)
	allowList = append(allowList, email)
	_, err = usermetadata.UpdateUserMetadata(allowlistKey(tenantID), map[string]interface{}{
		"allowList": allowList,
	})
	return err
}

func isEmailAllowed(tenantID string, email string) (bool, error) {
	existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID))
	if err != nil {
		return false, err
	}
	allowList := getAllowList(existingData)
	for _, allowedEmail := range allowList {
		if allowedEmail == email {
			return true, nil
		}
	}
	return false, nil
}
```
</Tab>
<Tab title="Python" value="python">
```python
from typing import List

from supertokens_python.recipe.usermetadata.asyncio import (
    get_user_metadata,
    update_user_metadata,
)


def allowlist_key(tenant_id: str):
    return f"emailAllowList:{tenant_id}"


async def add_email_to_allow_list(tenant_id: str, email: str):
    metadataResult = await get_user_metadata(allowlist_key(tenant_id))
    allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
    allow_list.append(email)
    await update_user_metadata(allowlist_key(tenant_id), {
        "allowList": allow_list
    })

async def is_email_allowed(tenant_id: str, email: str):
    metadataResult = await get_user_metadata(allowlist_key(tenant_id))
    allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
    return email in allow_list
```
</Tab>
</CodeGroup>


### 2. Check if the email is allowed

Update the backend SDK API function to only allow sign-up requests from users whose email addresses are on the approved list.
Use the check functions from the previous code snippet.

The overrides reject a provider response without an email before the SDK can generate a synthetic email for a provider
configured not to require one.


<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
import ThirdParty from "supertokens-node/recipe/thirdparty";
import supertokens from "supertokens-node";

class SignUpNotAllowedError extends Error {}

ThirdParty.init({
  override: {
    functions: (originalImplementation) => {
      return {
        ...originalImplementation,
        signInUp: async function (input) {
          let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
            thirdParty: {
              id: input.thirdPartyId,
              userId: input.thirdPartyUserId,
            },
          });
          if (existingUsers.length === 0) {
            if (!input.isVerified) {
              throw new SignUpNotAllowedError();
            }
            if (!(await isEmailAllowed(input.tenantId, input.email))) {
              throw new SignUpNotAllowedError();
            }
          }
          return originalImplementation.signInUp(input);
        },
      };
    },
    apis: (originalImplementation) => {
      return {
        ...originalImplementation,
        signInUpPOST: async function (input) {
          try {
            const provider = input.provider;
            const providerWithEmailCheck =
              provider.type === "oauth2"
                ? {
                    ...provider,
                    getUserInfo: async (getUserInfoInput: Parameters<typeof provider.getUserInfo>[0]) => {
                      const userInfo = await provider.getUserInfo(getUserInfoInput);
                      if (userInfo.email === undefined) {
                        throw new SignUpNotAllowedError();
                      }
                      return userInfo;
                    },
                  }
                : {
                    ...provider,
                    getUserInfo: async (getUserInfoInput: Parameters<typeof provider.getUserInfo>[0]) => {
                      const userInfo = await provider.getUserInfo(getUserInfoInput);
                      if (userInfo.email === undefined) {
                        throw new SignUpNotAllowedError();
                      }
                      return userInfo;
                    },
                  };

            return await originalImplementation.signInUpPOST!({
              ...input,
              provider: providerWithEmailCheck,
            });
          } catch (err: unknown) {
            if (err instanceof SignUpNotAllowedError) {
              return {
                status: "GENERAL_ERROR",
                message: "Sign-ups are disabled. Please contact the admin.",
              };
            }
            throw err;
          }
        },
      };
    },
  },
});
```
</Tab>
<Tab title="Go" value="go">

Pass the `isEmailAllowed` helper from the previous step to `initThirdPartyWithInvites`, and include the returned recipe in your SuperTokens `RecipeList`. Add your provider configuration to the `TypeInput` below.


```go
import (
	"errors"

	"github.com/supertokens/supertokens-golang/recipe/thirdparty"
	"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

var errSignUpNotAllowed = errors.New("sign up not allowed")

func initThirdPartyWithInvites(isEmailAllowed func(tenantID, email string) (bool, error)) supertokens.Recipe {
	return thirdparty.Init(&tpmodels.TypeInput{
		Override: &tpmodels.OverrideStruct{
			APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface {
				originalSignInUpPOST := *originalImplementation.SignInUpPOST

				(*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) {
					providerWithInviteCheck := *provider
					originalGetUserInfo := provider.GetUserInfo
					providerWithInviteCheck.GetUserInfo = func(oAuthTokens tpmodels.TypeOAuthTokens, userContext supertokens.UserContext) (tpmodels.TypeUserInfo, error) {
						userInfo, err := originalGetUserInfo(oAuthTokens, userContext)
						if err != nil {
							return tpmodels.TypeUserInfo{}, err
						}

						if userInfo.Email == nil {
							return tpmodels.TypeUserInfo{}, errSignUpNotAllowed
						}

						existingUser, err := thirdparty.GetUserByThirdPartyInfo(tenantId, provider.ID, userInfo.ThirdPartyUserId, userContext)
						if err != nil {
							return tpmodels.TypeUserInfo{}, err
						}

						if existingUser == nil {
							if !userInfo.Email.IsVerified {
								return tpmodels.TypeUserInfo{}, errSignUpNotAllowed
							}

							allowed, err := isEmailAllowed(tenantId, userInfo.Email.ID)
							if err != nil {
								return tpmodels.TypeUserInfo{}, err
							}
							if !allowed {
								return tpmodels.TypeUserInfo{}, errSignUpNotAllowed
							}
						}

						return userInfo, nil
					}

					resp, err := originalSignInUpPOST(&providerWithInviteCheck, input, tenantId, options, userContext)

					if errors.Is(err, errSignUpNotAllowed) {
						return tpmodels.SignInUpPOSTResponse{
							GeneralError: &supertokens.GeneralErrorResponse{
								Message: "Sign-ups are disabled. Please contact the admin.",
							},
						}, nil
					}

					return resp, err
				}

				return originalImplementation
			},
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="This example omits surrounding application and SuperTokens configuration."
from copy import copy
from typing import Any, Dict, Optional, Union

from supertokens_python import InputAppInfo, init
from supertokens_python.asyncio import list_users_by_account_info
from supertokens_python.recipe import thirdparty
from supertokens_python.recipe.session.interfaces import SessionContainer
from supertokens_python.recipe.thirdparty.interfaces import (
    APIInterface,
    APIOptions,
    RecipeInterface,
)
from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo
from supertokens_python.recipe.thirdparty.types import (
    RawUserInfoFromProvider,
    ThirdPartyInfo,
    UserInfo,
)
from supertokens_python.types import GeneralErrorResponse
from supertokens_python.types.base import AccountInfoInput


async def is_email_allowed(tenant_id: str, email: str):
    # from previous code snippet..
    return False


class SignUpNotAllowedError(Exception):
    pass


def override_thirdparty_functions(original_implementation: RecipeInterface):
    original_thirdparty_sign_in_up = original_implementation.sign_in_up

    async def thirdparty_sign_in_up(
        third_party_id: str,
        third_party_user_id: str,
        email: str,
        is_verified: bool,
        oauth_tokens: Dict[str, Any],
        raw_user_info_from_provider: RawUserInfoFromProvider,
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Union[bool, None],
        tenant_id: str,
        user_context: Dict[str, Any],
    ):
        existing_users = await list_users_by_account_info(
            tenant_id,
            AccountInfoInput(
                third_party=ThirdPartyInfo(
                    third_party_user_id=third_party_user_id,
                    third_party_id=third_party_id,
                )
            ),
        )
        if len(existing_users) == 0:
            if not is_verified:
                raise SignUpNotAllowedError()
            if not await is_email_allowed(tenant_id, email):
                raise SignUpNotAllowedError()

        return await original_thirdparty_sign_in_up(
            third_party_id,
            third_party_user_id,
            email,
            is_verified,
            oauth_tokens,
            raw_user_info_from_provider,
            session,
            should_try_linking_with_session_user,
            tenant_id,
            user_context,
        )

    original_implementation.sign_in_up = thirdparty_sign_in_up

    return original_implementation


def override_thirdparty_apis(original_implementation: APIInterface):
    original_sign_in_up_post = original_implementation.sign_in_up_post

    async def thirdparty_sign_in_up_post(
        provider: Provider,
        redirect_uri_info: Optional[RedirectUriInfo],
        oauth_tokens: Optional[Dict[str, Any]],
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Union[bool, None],
        tenant_id: str,
        api_options: APIOptions,
        user_context: Dict[str, Any],
    ):
        provider_with_email_check = copy(provider)
        original_get_user_info = provider.get_user_info

        async def get_user_info_with_email_check(
            oauth_tokens: Dict[str, Any], user_context: Dict[str, Any]
        ) -> UserInfo:
            user_info = await original_get_user_info(oauth_tokens, user_context)
            if user_info.email is None:
                raise SignUpNotAllowedError()
            return user_info

        setattr(
            provider_with_email_check,
            "get_user_info",
            get_user_info_with_email_check,
        )

        try:
            return await original_sign_in_up_post(
                provider_with_email_check,
                redirect_uri_info,
                oauth_tokens,
                session,
                should_try_linking_with_session_user,
                tenant_id,
                api_options,
                user_context,
            )
        except SignUpNotAllowedError:
            return GeneralErrorResponse(
                "Sign-ups are disabled. Please contact the admin."
            )

    original_implementation.sign_in_up_post = thirdparty_sign_in_up_post
    return original_implementation


init(
    app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
    framework="...",
    recipe_list=[
        thirdparty.init(
            override=thirdparty.InputOverrideConfig(
                apis=override_thirdparty_apis, functions=override_thirdparty_functions
            ),
        )
    ],
)
```
</Tab>
</CodeGroup>


## See also

<CardGroup cols={3}>
  <Card title="Built-in providers" href="/authentication/social/custom-providers" />
  <Card title="Custom providers" href="/authentication/social/custom-providers" />
  <Card title="Multiple clients on the same provider" href="/authentication/social/add-multiple-clients-for-the-same-provider" />
  <Card title="Hooks and overrides" href="/authentication/social/hooks-and-overrides" />
</CardGroup>
