How to add auth in your app using supertokens
Adding the option to the users to log in can be very simple or very tricky. In this post I'll show the steps to set up supertokens in a react app with fastapi (python) on the backenn. The supertokens instance will be selfhosted in coolify.
Intro
Adding the option to the users to log in can be very simple or very tricky. In this post I'll show the steps to set up auth with supertoken in a react app with fastapi (python) on the backenn.
The supertokens instance will be selfhosted in coolify.
The example will be shown for the site translateprompt.com
Creating the supertokens instance
On coolify project we’ll add a new resource, this is an instance supertokens with postgresql.
Once it’s created, you can connect to the database by going to postgres → settings → edit public port → make it publicly available.
then with the credentials from the configuration page and the url and port you should be able to connect.
Test that the supertokens instance is running by going to the link where it’s deployed, you should be able to see “Hello”.
Now we have two different resources deployed
Supertokens db
Here will all the users, permissions and everything that composes the auth be savedSupertoken instance
This includes the logic that handles all the operations that will save the data to the DB and handles all the auth
Backend
For the backend, in this guide we will not follow the default setup from supertokens where just the password and email are used for the setup, here we’ll also require that the users enter the username where they describe. Following this code the username will be available.
As we are using python and fastapi, we’ll install the following dependency.
uv add supertokens-python
And create the following file
from fastapi import APIRouter, Depends
from supertokens_python import InputAppInfo, SupertokensConfig, init
# from supertokens_python.recipe.usermetadata.syncio import get_user_metadata
from supertokens_python.recipe import emailpassword, session
from supertokens_python.recipe.emailpassword.interfaces import (
SignUpPostOkResult,
)
from supertokens_python.recipe.session import SessionContainer
from supertokens_python.recipe.session.framework.fastapi import verify_session
from supertokens_python.recipe.usermetadata.asyncio import (
get_user_metadata,
update_user_metadata,
)
router = APIRouter(prefix="/auth", tags=["auth"])
def apis_override(original_implementation: emailpassword.interfaces.APIInterface):
# Get a reference to the original sign_up_post
original_sign_up_post = original_implementation.sign_up_post
# Use *args and **kwargs to be compatible with any SDK version
async def sign_up_post(*args, **kwargs):
# 1. Call the original implementation, passing through all arguments
# that were given to us. This is the key part.
response = await original_sign_up_post(*args, **kwargs)
# 2. Check if the sign up was successful
if isinstance(response, SignUpPostOkResult):
# The user was created successfully
user_id = response.user.id
# 3. Extract form_fields from the arguments. It is always the
# first positional argument (at index 0).
form_fields = args[0]
username = None
for field in form_fields:
if field.id == "username":
username = field.value
break
if username is not None:
# 4. Save the username in the user's metadata
await update_user_metadata(user_id, {"username": username})
# 5. Return the original response
return response
# Replace the original implementation with our overridden one
original_implementation.sign_up_post = sign_up_post
return original_implementation
init(
app_info=InputAppInfo(
app_name="TranslatePrompt",
api_domain="<backend_url>", # FastAPI domain
website_domain="<frontend_url>", # React domain
api_base_path="/auth",
website_base_path="/auth",
),
supertokens_config=SupertokensConfig(
connection_uri="<supertokens_instance>" # your core instance URL
),
recipe_list=[
emailpassword.init(
sign_up_feature=emailpassword.InputSignUpFeature(
form_fields=[emailpassword.InputFormField(id="username")]
),
override=emailpassword.InputOverrideConfig(apis=apis_override),
),
session.init(), # session management
],
framework="fastapi",
)
@router.get("/user")
async def get_user_info(session: SessionContainer = Depends(verify_session())):
user_id = session.get_user_id()
metadata_result = await get_user_metadata(user_id)
return {"user_id": user_id, "metadata": metadata_result.metadata}
this is the auth file I am using to handle just the auth routes, but we also have to add the middleware, this will be done on the fastapi instance.
from supertokens_python.framework.fastapi import get_middleware
app = FastAPI(
title="TranslatePrompt",
description="Translateprompt is a site that allows users to translate with a glossary",
version="1.0.0",
)
app.add_middleware(get_middleware())
SuperTokens automatically exposes its auth API routes under /auth/*
in your FastAPI app once you’ve added the middleware:
That means you now have endpoints like:
POST /auth/signin
POST /auth/signup
POST /auth/signout
POST /auth/session/refresh
You don’t need to implement these yourself, they are provided by SuperTokens.
IMPORTANT!!
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.
So if you have something like this it will not work
from supertokens_python.framework.fastapi import get_middleware
app.add_middleware(get_middleware())
# Nees to be after the specific supertokens middleware
app.add_middleware(
CORSMiddleware,
allow_origins="*",
This is the correct version
app.add_middleware(
CORSMiddleware,
allow_origins="<FRONTEND_URL>",
Test the backend
You can test it by running the following curl request
curl -i -X POST http://<backend_url>/auth/signup \
-H "Content-Type: application/json" \
-d '{"formFields":[{"id":"email","value":"test@example.com"},{"id":"password","value":"mypassword1"}]}'
You should receive something like this and see the user added in the database under the column emailpassword_users
{"status":"OK","user":{"id":"43466338-87be-40ac-b1a2-0a97e...
Frontend
First we’ll have to initialise the supertokens instance
SuperTokens.init({
appInfo: {
// learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
appName: "TranslatePrompt",
apiDomain: "<backend_url>",
websiteDomain: "<frontend_url>",
apiBasePath: "/auth", # (backend) By default, the frontend SDK queries {apiDomain}/auth/*. If you want to change the /auth to something else, then you must set this value
websiteBasePath: "/auth" # (frontend) By default, the login UI appears on {websiteDomain}/auth. Other authentication-related user interfaces appear on {websiteDomain}/auth/*.
},
recipeList: [
EmailPasswordReact.init({
signInAndUpFeature: {
signUpForm: {
formFields: [
{
id: "username",
label: "Username",
placeholder: "username"
}
]
}
}
}),
SessionReact.init()
]
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<SuperTokensWrapper>
<App />
</SuperTokensWrapper>
</StrictMode>
);
We’ll also create an auth.ts hook to be able to know if the user is logged from any component in the frontend.
useAuth.ts
// hooks/useAuth.ts
import {useEffect, useState} from "react";
import {
signOut,
useSessionContext
} from "supertokens-auth-react/recipe/session";
import axiosInstance from "@/api/axiosConfig";
const BASE_URL = import.meta.env.VITE_BACKEND_URL;
const AUTH_BASE_URL = `${BASE_URL}/auth`;
export function useAuth() {
const session = useSessionContext() as any;
const [userData, setUserData] = useState({
username: null,
loading: false,
error: null as any
});
useEffect(() => {
// This flag prevents state updates if the component unmounts
// during the async API call.
let isMounted = true;
const fetchUserData = async () => {
// Only fetch if we have a confirmed session and user ID,
// and we aren't already fetching.
if (session.doesSessionExist && session.userId) {
setUserData({username: null, loading: true, error: null});
try {
const response = await axiosInstance.get(`${AUTH_BASE_URL}/user`);
if (isMounted) {
setUserData({
username: response.data.metadata.username,
loading: false,
error: null
});
}
} catch (error) {
console.error("Failed to fetch user data:", error);
if (isMounted) {
setUserData({username: null, loading: false, error: error});
}
}
}
};
console.log("fetchUserData", fetchUserData);
fetchUserData();
return () => {
isMounted = false;
};
// This effect should ONLY re-run when the user's session status or ID changes.
}, [session.doesSessionExist, session.userId]);
const handleLogout = async () => {
try {
await signOut();
window.location.href = "/";
} catch (error) {
console.error("Logout error:", error);
}
};
// The final loading state is true if the session is loading OR our user data is loading.
const isLoading = session.loading || userData.loading;
return {
loading: isLoading,
loggedIn: session.doesSessionExist,
logout: handleLogout,
userId: session.userId,
userName: userData.username,
error: userData.error // Expose error state for UI to optionally use
};
}
Then this can be used like this
const {loggedIn, userName} = useAuth();
return (
<div className="max-w-7xl mx-auto py-8">
{loggedIn && <p>Welcome {userName}</p>}
Related Posts
How to add third parthy login with Supertokens (Google)
Step by step of how to integrate a google login in your application. Create the credentials in google and integrate everything in your application.
Create a chatbot: Part 3 - Adding the frontend
This post explains how to use assistant ui to render a chatbot and handle the calls with the backend
How to add pricing with Lemonsqueezy (python fastapi + React)
How to add pricing with lemonsqueezy, handle the webhooks, automatic redirects and changes to the DB
Let's connect !!
Get in touch if you want updates, examples, and insights on how AI agents, Langchain and more are evolving and where they’re going next.