---
title: Implement allow list based sign up
description: Discover how to implement an allow list based sign up flow with the passwordless recipe.
sidebar:
  order: 80
---

## Overview

In this flow, you create a list of emails or phone numbers that are allowed to sign up.
Based on that users can go through the passwordless flow.

## Before you start

This guide 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 allow list.
You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step.


## Steps

### 1. Add a way to keep track of allowed emails or phone numbers

Start by maintaining an allow list of emails.
Use transactional application storage for a production allow list. The User Metadata examples below are suitable for a simple prototype, but their read-modify-write updates are not atomic: concurrent additions can overwrite each other.

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

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

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

async function isEmailAllowed(tenantId: string, email: string) {
  let existingData = await UserMetadata.getUserMetadata(`${tenantId}:emailAllowList`);
  let allowList: string[] = existingData.metadata.allowList || [];
  return allowList.includes(email);
}

async function addPhoneNumberToAllowlist(tenantId: string, phoneNumber: string) {
  const metadataKey = `${tenantId}:phoneNumberAllowList`;
  let existingData = await UserMetadata.getUserMetadata(metadataKey);
  let allowList: string[] = existingData.metadata.allowList || [];
  allowList = [...allowList, phoneNumber];
  await UserMetadata.updateUserMetadata(metadataKey, {
    allowList,
  });
}

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

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

func stringListFromMetadata(value interface{}) ([]string, error) {
	if value == nil {
		return []string{}, nil
	}
	items, ok := value.([]interface{})
	if !ok {
		return nil, fmt.Errorf("allowList metadata is not an array")
	}
	result := make([]string, 0, len(items))
	for _, item := range items {
		text, ok := item.(string)
		if !ok {
			return nil, fmt.Errorf("allowList metadata contains a non-string value")
		}
		result = append(result, text)
	}
	return result, nil
}

func addEmailToAllowlist(tenantId, email string) error {
	metadataKey := tenantId + ":emailAllowList"
	existingData, err := usermetadata.GetUserMetadata(metadataKey)
	if err != nil {
		return err
	}
	allowList, err := stringListFromMetadata(existingData["allowList"])
	if err != nil {
		return err
	}
	allowList = append(allowList, email)
	_, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{
		"allowList": allowList,
	})
	return err
}

func isEmailAllowed(tenantId, email string) (bool, error) {
	existingData, err := usermetadata.GetUserMetadata(tenantId + ":emailAllowList")
	if err != nil {
		return false, err
	}
	allowList, err := stringListFromMetadata(existingData["allowList"])
	if err != nil {
		return false, err
	}
	for _, allowedEmail := range allowList {
		if allowedEmail == email {
			return true, nil
		}
	}
	return false, nil
}

func addPhoneNumberToAllowlist(tenantId, phoneNumber string) error {
	metadataKey := tenantId + ":phoneNumberAllowList"
	existingData, err := usermetadata.GetUserMetadata(metadataKey)
	if err != nil {
		return err
	}
	allowList, err := stringListFromMetadata(existingData["allowList"])
	if err != nil {
		return err
	}
	allowList = append(allowList, phoneNumber)
	_, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{
		"allowList": allowList,
	})
	return err
}

func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) {
	existingData, err := usermetadata.GetUserMetadata(tenantId + ":phoneNumberAllowList")
	if err != nil {
		return false, err
	}
	allowList, err := stringListFromMetadata(existingData["allowList"])
	if err != nil {
		return false, err
	}
	for _, allowedPhoneNumber := range allowList {
		if allowedPhoneNumber == phoneNumber {
			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,
)


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

async def is_email_allowed(tenant_id: str, email: str):
    metadataResult = await get_user_metadata(f"{tenant_id}:emailAllowList")
    allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
    return email in allow_list

async def add_phone_number_to_allow_list(tenant_id: str, phone_number: str):
    metadata_key = f"{tenant_id}:phoneNumberAllowList"
    metadataResult = await get_user_metadata(metadata_key)
    allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
    allow_list.append(phone_number)
    await update_user_metadata(metadata_key, {
        "allowList": allow_list
    })

async def is_phone_number_allowed(tenant_id: str, phone_number: str):
    metadataResult = await get_user_metadata(f"{tenant_id}:phoneNumberAllowList")
    allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
    return phone_number in allow_list
```
</Tab>
</CodeGroup>


:::info[Multi Tenancy]
The helpers separate prototype metadata by `tenantId`, which the API overrides provide. User Metadata has no `tenantId` argument, so never use one shared synthetic key across tenants. For production, enforce tenant isolation and atomic updates in application storage.
:::


### 2. Check if the user is on the allow list


Update the backend SDK API function to only allow sign up requests from users that are on the allow list.
To do this you need to use the check functions from the previous code snippet.


<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx check=false reason="This example uses allow-list helpers defined in the preceding application code."
import Passwordless from "supertokens-node/recipe/passwordless";
import supertokens from "supertokens-node";

Passwordless.init({
  override: {
    apis: (originalImplementation) => {
      return {
        ...originalImplementation,
        createCodePOST: async function (input) {
          if ("email" in input) {
            let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
              email: input.email,
            });
            let userWithPasswordles = existingUsers.find(
              (u) =>
                u.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !==
                undefined,
            );
            if (userWithPasswordles === undefined) {
              // this is sign up attempt
              if (!(await isEmailAllowed(input.tenantId, input.email))) {
                return {
                  status: "GENERAL_ERROR",
                  message: "Sign up disabled. Please contact the admin.",
                };
              }
            }
          } else {
            let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
              phoneNumber: input.phoneNumber,
            });
            let userWithPasswordles = existingUsers.find(
              (u) =>
                u.loginMethods.find(
                  (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless",
                ) !== undefined,
            );
            if (userWithPasswordles === undefined) {
              // this is sign up attempt
              if (!(await isPhoneNumberAllowed(input.tenantId, input.phoneNumber))) {
                return {
                  status: "GENERAL_ERROR",
                  message: "Sign up disabled. Please contact the admin.",
                };
              }
            }
          }
          return await originalImplementation.createCodePOST!(input);
        },
      };
    },
  },
});
```
</Tab>
<Tab title="Go" value="go">
```go
import (
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func isEmailAllowed(tenantId, email string) (bool, error) {
	// ... from previous code snippet
	return false, nil
}

func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) {
	// ... from previous code snippet
	return false, nil
}

func main() {
	passwordless.Init(plessmodels.TypeInput{
		Override: &plessmodels.OverrideStruct{
			APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface {
				originalCreateCodePOST := *originalImplementation.CreateCodePOST

				(*originalImplementation.CreateCodePOST) = func(email, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) {

					if email != nil {
						existingUser, err := passwordless.GetUserByEmail(tenantId, *email)
						if err != nil {
							return plessmodels.CreateCodePOSTResponse{}, err
						}
						if existingUser == nil {
							// sign up attempt
							emailAllowed, err := isEmailAllowed(tenantId, *email)
							if err != nil {
								return plessmodels.CreateCodePOSTResponse{}, err
							}
							if !emailAllowed {
								return plessmodels.CreateCodePOSTResponse{
									GeneralError: &supertokens.GeneralErrorResponse{
										Message: "Sign ups are disabled. Please contact the admin.",
									},
								}, nil
							}
						}
					} else {
						existingUser, err := passwordless.GetUserByPhoneNumber(tenantId, *phoneNumber)
						if err != nil {
							return plessmodels.CreateCodePOSTResponse{}, err
						}
						if existingUser == nil {
							// sign up attempt
							phoneNumberAllowed, err := isPhoneNumberAllowed(tenantId, *phoneNumber)
							if err != nil {
								return plessmodels.CreateCodePOSTResponse{}, err
							}
							if !phoneNumberAllowed {
								return plessmodels.CreateCodePOSTResponse{
									GeneralError: &supertokens.GeneralErrorResponse{
										Message: "Sign ups are disabled. Please contact the admin.",
									},
								}, nil
							}
						}
					}
					return originalCreateCodePOST(email, phoneNumber, tenantId, options, userContext)
				}

				return originalImplementation
			},
		},
	})
}
```
</Tab>
<Tab title="Python" value="python">
```python check=false reason="This example omits surrounding application and SuperTokens configuration."
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 passwordless
from supertokens_python.recipe.passwordless.interfaces import (
    APIInterface,
    APIOptions,
)
from supertokens_python.recipe.session.interfaces import SessionContainer
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


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


def override_passwordless_apis(original_implementation: APIInterface):
    original_create_code_post = original_implementation.create_code_post

    async def create_code_post(
        email: Union[str, None],
        phone_number: Union[str, None],
        session: Optional[SessionContainer],
        should_try_linking_with_session_user: Union[bool, None],
        tenant_id: str,
        api_options: APIOptions,
        user_context: Dict[str, Any],
    ):
        if email is not None:
            existing_user = await list_users_by_account_info(
                tenant_id, AccountInfoInput(email=email)
            )
            user_with_passwordless = next(
                (
                    user
                    for user in existing_user
                    if any(
                        login_method.recipe_id == "passwordless"
                        and login_method.has_same_email_as(email)
                        for login_method in user.login_methods
                    )
                ),
                None,
            )

            if user_with_passwordless is None:
                # sign up attempt
                if not (await is_email_allowed(tenant_id, email)):
                    return GeneralErrorResponse(
                        "Sign ups disabled. Please contact admin."
                    )
        else:
            assert phone_number is not None
            existing_user = await list_users_by_account_info(
                tenant_id, AccountInfoInput(phone_number=phone_number)
            )
            user_with_passwordless = next(
                (
                    user
                    for user in existing_user
                    if any(
                        login_method.recipe_id == "passwordless"
                        and login_method.has_same_phone_number_as(phone_number)
                        for login_method in user.login_methods
                    )
                ),
                None,
            )

            if user_with_passwordless is None:
                # sign up attempt
                if not (await is_phone_number_allowed(tenant_id, phone_number)):
                    return GeneralErrorResponse(
                        "Sign ups disabled. Please contact admin."
                    )

        return await original_create_code_post(
            email,
            phone_number,
            session,
            should_try_linking_with_session_user,
            tenant_id,
            api_options,
            user_context,
        )

    original_implementation.create_code_post = create_code_post
    return original_implementation


init(
    app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."),
    framework="...",
    recipe_list=[
        passwordless.init(
            flow_type="USER_INPUT_CODE",
            override=passwordless.InputOverrideConfig(
                apis=override_passwordless_apis,
            ),
        )
    ],
)
```
</Tab>
</CodeGroup>


---

## See also

<CardGroup cols={3}>
  <Card title="Customize the magic link" href="/authentication/passwordless/customize-the-magic-link" />
  <Card title="Customize the OTP" href="/authentication/passwordless/customize-the-otp" />
  <Card title="Hooks and overrides" href="/authentication/passwordless/hooks-and-overrides" />
  <Card title="Email and SMS behavior" href="/authentication/passwordless/configure-email-and-sms-behavior" />
  <Card title="Invite link sign up" href="/authentication/passwordless/invite-link-flow" />
</CardGroup>
