Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Implement invite link based sign up

Discover how to implement an invite based sign up flow with the passwordless recipe.

Overview

In this flow, the admin of the app calls an API to sign up a user and send them an invite link. Once the user clicks on that, they log in and can access the app. If a user has not received an invitation before, their sign in attempt fails.

Before you start

This guide assumes that you have initialized the Passwordless recipe, Session, and User Roles, and have a working application integrated with SuperTokens. The User Roles recipe protects the invitation endpoint in these examples. If you have not, please check the Quickstart Guide.

Steps

1. Add the ability to invite new users

Add a new endpoint that allows you to invite users to your app. You need to first create the new user and then use the passwordless API to send the magic link to them. Additionally, protect the endpoint with a role requirement.

The passwordless API uses the default magic link path, /auth/verify, for the invite link. If you are using the pre-built UI, the frontend SDK automatically logs the user in. For custom UI implementations, use the consumeCode function provided by the frontend SDK to verify the code in the URL and authenticate the user created by the invitation endpoint.

Validate and normalize the email address before creating the user. Configure the framework’s JSON body parser before this route; for Koa, expose the parsed payload as ctx.request.body.

Next.js router
import express from "express";
import { verifySession } from "supertokens-node/recipe/session/framework/express";
import { SessionRequest } from "supertokens-node/framework/express";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

let app = express();

app.post(
  "/create-user",
  verifySession({
    overrideGlobalClaimValidators: async function (globalClaimValidators) {
      return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
    },
  }),
  async (req: SessionRequest, res) => {
    let email = req.body.email;

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    res.send("Success");
  },
);
import Hapi from "@hapi/hapi";
import { verifySession } from "supertokens-node/recipe/session/framework/hapi";
import { SessionRequest } from "supertokens-node/framework/hapi";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

let server = Hapi.server({ port: 8000 });

server.route({
  path: "/create-user",
  method: "post",
  options: {
    pre: [
      {
        method: verifySession({
          overrideGlobalClaimValidators: async function (globalClaimValidators) {
            return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
          },
        }),
      },
    ],
  },
  handler: async (req: SessionRequest, res) => {
    let email = (req.payload.valueOf() as any).email;

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    res.response("Success").code(200);
  },
});
import Fastify from "fastify";
import { verifySession } from "supertokens-node/recipe/session/framework/fastify";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

let fastify = Fastify();

fastify.post(
  "/create-user",
  {
    preHandler: verifySession({
      overrideGlobalClaimValidators: async function (globalClaimValidators) {
        return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
      },
    }),
  },
  async (req, res) => {
    let email = req.body.email;

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    res.code(200).send("Success");
  },
);
import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda";
import { SessionEventV2 } from "supertokens-node/framework/awsLambda";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

async function createUser(awsEvent: SessionEventV2) {
  let email = JSON.parse(awsEvent.body!).email;

  // this will create the user in supertokens if they don't already exist.
  await Passwordless.signInUp({
    tenantId: "public",
    email,
  });

  let inviteLink = await Passwordless.createMagicLink({
    tenantId: "public",
    email,
  });

  // TODO: send inviteLink to user's email
  return {
    statusCode: "200",
    body: "Success",
  };
}

exports.handler = verifySession(createUser, {
  overrideGlobalClaimValidators: async function (globalClaimValidators) {
    return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
  },
});
import KoaRouter from "koa-router";
import { verifySession } from "supertokens-node/recipe/session/framework/koa";
import { SessionContext } from "supertokens-node/framework/koa";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

let router = new KoaRouter();

router.post(
  "/create-user",
  verifySession({
    overrideGlobalClaimValidators: async function (globalClaimValidators) {
      return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
    },
  }),
  async (ctx: SessionContext, next) => {
    let email = ((ctx.request as any).body as { email: string }).email;

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    ctx.status = 200;
    ctx.body = "Success";
  },
);
import { inject, intercept } from "@loopback/core";
import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest";
import { verifySession } from "supertokens-node/recipe/session/framework/loopback";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

class LikeComment {
  constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {}
  @post("/create-user")
  @intercept(
    verifySession({
      overrideGlobalClaimValidators: async function (globalClaimValidators) {
        return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
      },
    }),
  )
  async handler() {
    let email = ""; // TODO: get from request body

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    // TODO: send 200 response to the client
  }
}
import { Controller, Post, UseGuards, Session } from "@nestjs/common";
import { SessionContainer } from "supertokens-node/recipe/session";
import { AuthGuard } from "./auth/auth.guard";
import UserRoles from "supertokens-node/recipe/userroles";
import Passwordless from "supertokens-node/recipe/passwordless";

@Controller()
export class CreateUserController {
  @Post("create-user")
  @UseGuards(
    new AuthGuard({
      overrideGlobalClaimValidators: async function (globalClaimValidators: any) {
        return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")];
      },
    }),
  ) // For more information about this guard please read our NestJS guide.
  async postAPI(@Session() session: SessionContainer): Promise<void> {
    let email = ""; // TODO: get from request body

    // this will create the user in supertokens if they don't already exist.
    await Passwordless.signInUp({
      tenantId: "public",
      email,
    });

    let inviteLink = await Passwordless.createMagicLink({
      tenantId: "public",
      email,
    });

    // TODO: send inviteLink to user's email
    // TODO: send 200 response to the client
  }
}
import (
	"fmt"
	"net/http"

	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/session/claims"
	"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	_ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

		session.VerifySession(&sessmodels.VerifySessionOptions{
			OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
				globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil))
				return globalClaimValidators, nil
			},
		}, createUserAPI).ServeHTTP(rw, r)
	})
}

func createUserAPI(w http.ResponseWriter, r *http.Request) {
	email := "" // TODO: read email from request body

    // This will create the user in supertokens if they don't already exist.
    tenantId := "public"
	_, err := passwordless.SignInUpByEmail(tenantId, email)
	if err != nil {
		http.Error(w, "Could not create invited user", http.StatusInternalServerError)
		return
	}

	inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email)
	if err != nil {
		// TODO: send 500 to the client
		return
	}
	fmt.Println(inviteLink)
	// TODO: send invite link
	// TODO: send 200 to the client
}
import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/session/claims"
	"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	router := gin.New()

	// Wrap the API handler in session.VerifySession
	router.POST("/create-user", verifySession(&sessmodels.VerifySessionOptions{
		OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
			globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil))
			return globalClaimValidators, nil
		},
	}), createUserAPI)
}

// This is a function that wraps the supertokens verification function
// to work the gin
func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc {
	return func(c *gin.Context) {
		session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) {
			c.Request = c.Request.WithContext(r.Context())
			c.Next()
		})(c.Writer, c.Request)
		// we call Abort so that the next handler in the chain is not called, unless we call Next explicitly
		c.Abort()
	}
}

func createUserAPI(c *gin.Context) {
	email := "" // TODO: read email from request body

    // This will create the user in supertokens if they don't already exist.
    tenantId := "public"
	_, err := passwordless.SignInUpByEmail(tenantId, email)
	if err != nil {
		c.String(http.StatusInternalServerError, "Could not create invited user")
		return
	}

	inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email)
	if err != nil {
		// TODO: send 500 to the client
		return
	}
	fmt.Println(inviteLink)
	// TODO: send invite link
	// TODO: send 200 to the client
}
import (
	"fmt"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/session/claims"
	"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	r := chi.NewRouter()

	// Wrap the API handler in session.VerifySession
	r.Post("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{
		OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
			globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil))
			return globalClaimValidators, nil
		},
	}, createUserAPI))
}

func createUserAPI(w http.ResponseWriter, r *http.Request) {
	email := "" // TODO: read email from request body

    // This will create the user in supertokens if they don't already exist.
    tenantId := "public"
	_, err := passwordless.SignInUpByEmail(tenantId, email)
	if err != nil {
		http.Error(w, "Could not create invited user", http.StatusInternalServerError)
		return
	}

	inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email)
	if err != nil {
		// TODO: send 500 to the client
		return
	}
	fmt.Println(inviteLink)
	// TODO: send invite link
	// TODO: send 200 to the client
}
import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/session/claims"
	"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
	router := mux.NewRouter()

	// Wrap the API handler in session.VerifySession
	router.HandleFunc("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{
		OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) {
			globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil))
			return globalClaimValidators, nil
		},
	}, createUserAPI)).Methods(http.MethodPost)
}

func createUserAPI(w http.ResponseWriter, r *http.Request) {
	email := "" // TODO: read email from request body

    // This will create the user in supertokens if they don't already exist.
    tenantId := "public"
	_, err := passwordless.SignInUpByEmail(tenantId, email)
	if err != nil {
		http.Error(w, "Could not create invited user", http.StatusInternalServerError)
		return
	}

	inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email)
	if err != nil {
		// TODO: send 500 to the client
		return
	}
	fmt.Println(inviteLink)
	// TODO: send invite link
	// TODO: send 200 to the client
}
from fastapi import Depends

from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.userroles import UserRoleClaim


@app.post('/create-user')
async def create_user(session: SessionContainer = Depends(verify_session(
    override_global_claim_validators=lambda global_validators, session, user_context: global_validators +
    [UserRoleClaim.validators.includes("admin")]
))):
    email = ""  # TODO: read from request body.

    # this will creat the user in supertokens if they don't already exist
    await signinup("public", email, None)

    invite_link = await create_magic_link("public", email, None)

    print(invite_link)
    # TODO: send invite_link to email
    # TODO: send 200 responspe to client
from supertokens_python.recipe.passwordless.syncio import create_magic_link, signinup
from supertokens_python.recipe.session.framework.flask import verify_session
from supertokens_python.recipe.userroles import UserRoleClaim


@app.route('/create_user', methods=['POST'])
@verify_session(
    override_global_claim_validators=lambda global_validators, session, user_context: global_validators +
    [UserRoleClaim.validators.includes("admin")]
)
def create_user():
    email = ""  # TODO: read from request body.

    # this will creat the user in supertokens if they don't already exist
    signinup("public", email, None)

    invite_link = create_magic_link("public", email, None)

    print(invite_link)
    # TODO: send invite_link to email
    # TODO: send 200 responspe to client
from django.http import HttpRequest

from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup
from supertokens_python.recipe.session.framework.django.asyncio import verify_session
from supertokens_python.recipe.userroles import UserRoleClaim


@verify_session(
    override_global_claim_validators=lambda global_validators, session, user_context: global_validators +
    [UserRoleClaim.validators.includes("admin")]
)
async def create_user(request: HttpRequest):
    email = ""  # TODO: read from request body.

    # this will creat the user in supertokens if they don't already exist
    await signinup("public", email, None)

    invite_link = await create_magic_link("public", email, None)

    print(invite_link)
    # TODO: send invite_link to email
    # TODO: send 200 responspe to client

2. Check if a user was invited

Update the backend SDK API function to only allow sign up requests from invited users. To do this you need to check if a user exists in SuperTokens.

import Passwordless from "supertokens-node/recipe/passwordless";
import supertokens from "supertokens-node";

Passwordless.init({
  contactMethod: "EMAIL_OR_PHONE",
  flowType: "MAGIC_LINK",
  override: {
    apis: (originalImplementation) => {
      return {
        ...originalImplementation,
        createCodePOST: async function (input) {
          if ("email" in input) {
            let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
              email: input.email,
            });
            let existingPasswordlessUser = existingUsers.find(
              (user) =>
                user.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !==
                undefined,
            );
            if (existingPasswordlessUser === undefined) {
              // this is sign up attempt
              return {
                status: "GENERAL_ERROR",
                message: "Sign up disabled. Please contact the admin.",
              };
            }
          } else {
            let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
              phoneNumber: input.phoneNumber,
            });
            let existingPasswordlessUser = existingUsers.find(
              (user) =>
                user.loginMethods.find(
                  (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless",
                ) !== undefined,
            );
            if (existingPasswordlessUser === undefined) {
              // this is sign up attempt
              return {
                status: "GENERAL_ERROR",
                message: "Sign up disabled. Please contact the admin.",
              };
            }
          }
          return await originalImplementation.createCodePOST!(input);
        },
      };
    },
  },
});
import (
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

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
							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
							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
			},
		},
	})
}
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


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
                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
                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,
            ),
        )
    ],
)

See also

API reference

API schema and response details