important
This is a contributors guide and NOT a user guide. Please visit these docs if you are using or evaluating SuperTokens.
Overview
Below is a high level view of the frontend SDKs. An SDK can have one or more recipes in them, and may have all or some of the parts that are mentioned below.
For example, the supertokens-auth-react
frontend SDK has all the parts mentioned below and supports all the recipes. However, the supertokens-website
SDK only has the session recipe and thus does not require the routing module (as of the date this was written).
#
InitUsed to initialise SuperTokens - providing app info and a list of recipes to initalise as well.
Initialising means setting up the routes, calling init
in all the recipes and normalising of user configuration.
#
Path RoutingBy default, all website routes with /auth/*
, will be handled by the SDK.
react-router-dom
#
With For this, the user has to pass reactRouterDom
to our lib and that is used throughout the library to navigate without page reloads.
Each "feature component" has its own route and we pre-create all the routes on init, and insert them into the user's render
function. You can find the code for this here
#
With full page reloadHere we use something like window.location.href=...
when we want to navigate.
#
Recipe RoutingWhen a path that we can handle is loaded, we must determine which recipe to send it to. This is usually done reading the rid
query param of the website route. If the rid
is emailpassword
, then we let the emailpassword
recipe handle it (if it has been initialised).
Sometimes, we cannot rely the query param being there (like when we get an OAuth callback redirect). Then we use other means to know which recipe to use. In the OAuth case, we store the rid
in the state in the session storage.
Sometimes a recipe has sub recipes. In this case, the rid
of the query param would not be a sub recipe's rid
, but would be the super recipe's rid
. This way, the super recipe would get to handle this route, and can further check its child recipes for if they can handle it or not.
#
Recipes#
InitIs called during SDK initialisation. Here we:
- Normalise user config for this recipe
- Create an instance of the recipe implementation and pass it to the override function (in case the user has given one).
- Create instances of any sub recipes that this recipe needs.
#
Recipe InterfaceA recipe interface is an interface that defines all the "core" functions that this recipe's logic / UI can depend on.
For example, for the emailpassword recipe, we would have at least these three functions:
signUp
signIn
doesEmailExist
You can find the full interface for the emailpassword recipe here.
This recipe would need to use other recipes like the emailverification
or the session
recipe, and those would have their own recipe interface.
#
Recipe implementationA recipe implementation is a class that implements the recipe interface for a recipe. This recipe implementation is the default behaviour of that recipe.
You can find the recipe implementation of the emailpassword
recipe here.
Most functions in the recipe implementation usually just query the backend as per the FDI spec and return the result. However, sometimes, they can have very complex logic (as is the case with the session
recipe - see here).
#
Overriding the recipe implementationWe allow users to override the default recipe implementation so that they can customise the behaviour of the SDK as per their auth requirements.
They can do so by providing the implementation of the following function during the init
call for that recipe:
// an example of how they would override the
// signUp function of the emailpassword recipe impl
supertokens.init({
recipeList: [
EmailPassword.init({
override {
functions: (originalImplementation: RecipeInterface): RecipeInterface => {
return {
...originalImplementation,
signUp: (input) => {
// some custom logic
// OR
return originalImplementation.signUp(input);
}
}
};
}
});
]
});
Here, the originalImplementation
is an instance of the recipe implementation of that recipe. However, it is typed as the recipe interface. This allows composability of recipe interfaces - for example, one implementation of the recipe interface can add feature X, and another can add feature Y. The user can use both and get X and Y!
#
Feature React componentsThese are logical components that have no UI. Their job is to use the recipe implementation (either the default one or overriden one) to prepare props in a way that the theme components can use them.
An example of this can be found here.
#
Wrapper React componentsThese are higher order react components that use a recipe to provide some functionality. For example, the SessionAuth
component uses the session recipe to provide a react context to its children. This context can then be read by them to get information about the session (in a very simple way).
#
Theme React componentsThese are mostly UI only components, receiving props from the feature components. They may have some logic, but it would mostly be for the purpose of driving the UI it displays.
Therefore, a complete UI widget comprises of:
<FeatureComponent>
<ThemeComponent />
</FeatureComponent>
#
Overriding the theme react componentsJust like recipe functions override, the user can override named components in the theme component tree. This allows users to customise the UX / UI of the widgets shown. The level of granuality that they can override depends on how many and which components we allow them to override.
For example, if a theme component is:
<EmailPasswordSignInContainer>
<EmailPasswordHeader />
<FormContainer />
<EmailPasswordFooter />
</EmailPasswordSignInContainer>
And we allow users to override EmailPasswordSignInContainer
, EmailPasswordHeader
and EmailPasswordSignInContainer
, then the only way they can change FormContainer
is by overrifing EmailPasswordSignInContainer
.
The way they can override is:
// an example of how they would override the
// signUp function of the emailpassword recipe impl
supertokens.init({
recipeList: [
EmailPassword.init({
override {
components: {
EmailPasswordFooter: ({OriginalComponent, ...props}) => {
return (
<>
<SomeCustomComponent />
<OriginalComponent />
</>
);
}
}
}
});
]
});
Here the OriginalComponent
is the original EmailPasswordFooter
component which the user may or may not decide to use.
#
User facing functionsThese are functions that are meant for users to use. They usually reside in the index.ts
file of a recipe or the whole lib.
Examples of functions are:
Session.doesSessionExist()
EmailPassword.isEmailVerified()
These functions usually just call the recipe's underlying (one or several) recipe implementation's function.
Some of them, like the can use a recipe's sub recipe. This is done because from the user's point of view, they are not directly aware of the subrecipe since they only did EmailPassword.init.
There are also some functions that are not tied to a recipe like:
supertokens.init
supertokens.redirectToAuth
#
StylingFor the supertokens-auth-react SDK, we use the @emotion/react library which allows us to:
- Write CSS in JS
- Allows the user to provide a JS object which can override our default CSS
- Create randomised class names so that our CSS doesn't conflict with the user's CSS (but the other way around is possible).
The user provided JS object (to override our CSS), is exposed to all the theme components via the style context object (usin the react context feature).
Finally, we use a shadow DOM so that the user's CSS doesn't interfere with our CSS - however, this also prevents password managers from working in our forms. So we give users the ability to disable this (across all our components).
#
Querying the backendFor our recipes to work, they need to query the users backend which exposes the FDI APIs (via our backend SDK). This querying happens using fetch
, inside the recipe implementation functions.
For us to know where the user's backend is (which domain and path), the user provides us with an appInfo
object during initialisation that contains the apiDomain
and apiBasePath
values.
#
User facing hook functionsEach recipe has three hook functions:
onHandleEvent
: A user can give this function to get events for user actions. For example, if the end user signs in, a corresponding event is fired using this function.preAPIHook
: A user can give this function to change the request that is sent to the backend. They can use this to add custom headers / change the path / add custom params etc..getRedirectionURL
: This can be used to change how routing works in a recipe. As an example, if one wants to change where the end user goes post login, they can provide this callback.
#
Export pathsThe import statements we expose are very important.
For example, the place where all the ts files are built in the supertokens-auth-react SDK is /lib/build/*
. Therefore, by default, the user would have to use import ... from "supertokens-auth-react/lib/build/recipe/..."
which is ugly.
So we have extra files in the lib which allow users to import like import ... from "supertokens-auth-react/recipe/..."
. As an example, you can see how this is achieved here. The files in this folder don't have any recipe logic - they simply import / export the actual recipe index.ts
files.