DEV Community

Emmanuel Onwuegbusi
Emmanuel Onwuegbusi

Posted on

Setup User Auth for your Reflex Python app using supabase (supabase-py)

In this article, we will configure user authentication for your reflex app using supabase-py.

Supabase is an open-source Firebase alternative. It provides you with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings so as to develop a scalable system in less time.

supabase-py is Supabase client for Python. You can use supabase-py to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.

Outline

  • Register for supabase
  • Create a new supabase project
  • Get your Project URL and API key (anon public)
  • Create a new folder, open it with a code editor
  • Create a virtual environment and activate
  • Install requirements
  • reflex setup
  • .env file
  • auth_supabase.py
  • base_state.py
  • login.py
  • registration.py
  • .gitignore
  • run app
  • conclusion

Register for supabase

You will need to create an account with Supabase. You can either go for their free or pro or team or enterprise plan depending on the scale of your project. For this tutorial, I will go with the free plan but you can choose other plans.

Go to https://supabase.com/pricing and select a plan, then signup.
supabase pricing

Create a new supabase project

Go to https://supabase.com/dashboard/projects to create a new supabase project to set up a dedicated environment for your application or project. You will give your project a name, and Database password.
supabase new project

Get your Project URL and API key (anon public)

Once your project is created, you will see your Project URL and API key (anon public). You can copy them and save to a safe file.

Create a new folder, open it with a code editor

We will build the auth project now. Create a new folder on your computer and name it auth_supabase then open it with a code editor like VS Code.

Create a virtual environment and activate

Open the terminal. Use the following command to create a virtual environment .venv and activate it:

python3 -m venv .venv
Enter fullscreen mode Exit fullscreen mode
source .venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Install requirements

We will install reflex to build the app, supabase the client for Python, and Python-dotenv to read key-value pairs from a .env file and set them as environment variables.
Run the following command in the terminal:

pip install reflex==0.3.1 supabase==1.2.0 python-dotenv==1.0.0
Enter fullscreen mode Exit fullscreen mode

reflex setup

Now, we need to create the project using reflex. Run the following command to initialize the template app in auth_supabase directory.

reflex init --template blank
Enter fullscreen mode Exit fullscreen mode

The above command will create the following file structure in auth_supabase directory:

authz

You can run the app using the following command in your terminal to see a welcome page when you go to http://localhost:3000/ in your browser

reflex run
Enter fullscreen mode Exit fullscreen mode

.env file

Create a new file .env in your project root directory and add the following to set your API key environment variables

SUPABASE_URL=""
SUPABASE_KEY=""
Enter fullscreen mode Exit fullscreen mode

In the empty string of SUPABASE_URL and SUPABASE_KEY place your Project URL and API key (anon public) you saved initially. These will help us communicate to supabase.

auth_supabase.py

We will build the homepage of the app. Go to the auth_supabase subdirectory and open the auth_supabase.py file. Replace with the following code:

"""App module to demo authentication with supabase."""
import reflex as rx

from .base_state import State

from .registration import registration_page as registration_page
from .login import require_login


import supabase
from dotenv import load_dotenv
import os

# load env
load_dotenv()

# setup supabase
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase_client = supabase.Client(supabase_url, supabase_key)


def index() -> rx.Component:
    """Render the index page.

    Returns:
        A reflex component.
    """
    # is_authenticated = supabase_client.auth.get_session() is not None
    # print('this is_authenticated',is_authenticated)

    return rx.fragment(
        rx.color_mode_button(rx.color_mode_icon(), float="right"),
        rx.vstack(
            rx.heading("Welcome to my homepage!", font_size="2em"),
            rx.cond(
                State.is_hydrated & ~State.is_authenticated,
                rx.link("Register", href="/register"),),


            rx.cond(
                State.is_hydrated & ~State.is_authenticated,
                rx.link("Login", href="/login"),),

            rx.cond(
                State.is_hydrated & State.is_authenticated,
                rx.link("Protected Page", href="/protected"),),

            rx.cond(
                State.is_hydrated & State.is_authenticated,
                rx.link("Logout", href="/", on_click=State.do_logout),),
            spacing="1.5em",
            padding_top="10%",
        ),
    )


@require_login
def protected() -> rx.Component:
    """Render a protected page.

    The `require_login` decorator will redirect to the login page if the user is
    not authenticated.

    Returns:
        A reflex component.
    """
    return rx.vstack(
        rx.heading(
            "Protected Page", font_size="2em"
        ),
        rx.link("Home", href="/"),
        rx.link("Logout", href="/", on_click=State.do_logout),
    )


app = rx.App()
app.add_page(index)
app.add_page(protected)
app.compile()
Enter fullscreen mode Exit fullscreen mode

The above code imports various modules, including "reflex" for web components and state management, "supabase" for interacting with the Supabase service, and "dotenv" for loading environment variables.

The load_dotenv() function loads the declared environment variables from the .env file, which typically contains sensitive information like API keys. The supabase_url and supabase_key are then retrieved from these environment variables. The supabase_client is created using these credentials.

The index() function defines the main page of the application. It checks the user's authentication state using the State object and conditionally displays links to register, login, or access a protected page based on whether the user is authenticated or not.

The protected() function is a protected page that requires authentication. It uses the @require_login decorator to ensure that only authenticated users can access it. It provides links to the homepage and a logout option.

Finally, an rx.App object is created, and the "index" and protected pages are added to it. The app is then compiled to run the web application.
The above code renders the following page:
authshomepage

base_state.py

Create a new file base_state.py in the auth_supabase subdirectory and add the following code.

import reflex as rx


import supabase
from dotenv import load_dotenv
import os

# load env
load_dotenv()

# setup supabase
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase_client = supabase.Client(supabase_url, supabase_key)



class State(rx.State):

    is_authenticated: bool = supabase_client.auth.get_session() is not None


    def do_logout(self) -> None:
        """signout."""

        res = supabase_client.auth.get_session()
        if res is not None:
            res = supabase_client.auth.sign_out()
            self.is_authenticated = False
Enter fullscreen mode Exit fullscreen mode

The above code declares a "State" class, which inherits from rx.State to manage the application's state. It has a boolean attribute is_authenticated which is set based on the presence of an active session obtained from Supabase authentication.

The do_logout method within the State class is responsible for signing the user out. It checks if there is an active session, and if so, it calls the Supabase client's sign_out method to log the user out and sets the is_authenticated attribute to False to indicate that the user is no longer authenticated.

login.py

Create a new file login.py in the auth_supabase subdirectory and add the following code.

"""Login page and authentication logic."""
import reflex as rx

from .base_state import State


import supabase
from dotenv import load_dotenv
import os

load_dotenv()

supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase_client = supabase.Client(supabase_url, supabase_key)



LOGIN_ROUTE = "/login"
REGISTER_ROUTE = "/register"


class LoginState(State):
    """Handle login form submission and redirect to proper routes after authentication."""

    error_message: str = ""
    redirect_to: str = ""

    is_loading: bool = False

    def on_submit(self, form_data) -> rx.event.EventSpec:
        """Handle login form on_submit.

        Args:
            form_data: A dict of form fields and values.
        """

        # set the following values to spin the button
        self.is_loading = True
        yield

        self.error_message = ""
        email = form_data["email"]
        password = form_data["password"]

        try:
            supabase_client.auth.sign_in_with_password({"email": email, "password": password})
            self.error_message = ""
            self.is_authenticated = supabase_client.auth.get_session() is not None
            return LoginState.redir()  # type: ignore
        except:
            self.error_message = "There was a problem logging in, please try again."

            # reset state variable again
            self.is_loading = False
            yield


    def redir(self) -> rx.event.EventSpec | None:
        """Redirect to the redirect_to route if logged in, or to the login page if not."""
        if not self.is_hydrated:
            # wait until after hydration
            return LoginState.redir()  # type: ignore
        page = self.get_current_page()

        # is_authenticated = supabase_client.auth.get_session() is not None
        # print('is_authenticated',is_authenticated)

        if not self.is_authenticated and page != LOGIN_ROUTE:
            self.redirect_to = page

            # reset state variable again
            self.is_loading = False
            yield

            return rx.redirect(LOGIN_ROUTE)
        elif page == LOGIN_ROUTE:

            # reset state variable again
            self.is_loading = False
            yield

            return rx.redirect(self.redirect_to or "/")



@rx.page(route=LOGIN_ROUTE)
def login_page() -> rx.Component:
    """Render the login page.

    Returns:
        A reflex component.
    """
    login_form = rx.form(
        rx.input(placeholder="email", id="email", type_="email"),
        rx.password(placeholder="password", id="password"),
        rx.button("Login", type_="submit", is_loading=LoginState.is_loading),
        width="80vw",
        on_submit=LoginState.on_submit,
    )

    return rx.fragment(
        rx.cond(
            LoginState.is_hydrated,  # type: ignore
            rx.vstack(
                rx.cond(  # conditionally show error messages
                    LoginState.error_message != "",
                    rx.text(LoginState.error_message),
                ),
                login_form,
                rx.link("Register", href=REGISTER_ROUTE),
                padding_top="10vh",
            ),
        )
    )



def require_login(page: rx.app.ComponentCallable) -> rx.app.ComponentCallable:
    """Decorator to require authentication before rendering a page.

    If the user is not authenticated, then redirect to the login page.

    Args:
        page: The page to wrap.

    Returns:
        The wrapped page component.
    """

    def protected_page():
        # is_authenticated = supabase_client.auth.get_session() is not None
        return rx.fragment(
            rx.cond(
                State.is_hydrated & State.is_authenticated, # type: ignore
                page(),
                rx.center(
                    # When this spinner mounts, it will redirect to the login page
                    rx.spinner(on_mount=LoginState.redir),
                ),
            )
        )

    protected_page.__name__ = page.__name__
    return protected_page
Enter fullscreen mode Exit fullscreen mode

Above, the LoginState class manages the state of the login page. It handles form submission with the on_submit method, which sets the is_loading flag to indicate form submission and attempts to sign in the user using Supabase. If successful, it sets the user as authenticated, and if there's an error, it displays an error message. The redir method handles page redirection based on the user's authentication status.

The login_page() function defines the login page itself, rendering a login form and displaying error messages if present. It also provides a link to the registration page.

The require_login decorator function ensures that only authenticated users can access certain pages. It wraps the protected page and redirects unauthenticated users to the login page.
The above code renders the following page:
authlogin

registration.py

Create a new file registration.py in the auth_supabase subdirectory and add the following code.

"""New user registration form and validation logic."""
from __future__ import annotations

import asyncio
from collections.abc import AsyncGenerator

import reflex as rx

from .base_state import State
from .login import LOGIN_ROUTE, REGISTER_ROUTE
# from .user import User
import re


import supabase
from dotenv import load_dotenv
import os

load_dotenv()

supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase_client = supabase.Client(supabase_url, supabase_key)

class RegistrationState(State):
    """Handle registration form submission and redirect to login page after registration."""

    success: bool = False
    error_message: str = ""

    is_loading: bool = False

    async def handle_registration(
        self, form_data
    ) -> AsyncGenerator[rx.event.EventSpec | list[rx.event.EventSpec] | None, None]:
        """Handle registration form on_submit.

        Set error_message appropriately based on validation results.

        Args:
            form_data: A dict of form fields and values.
        """

        # set the following values to spin the button
        self.is_loading = True
        yield


        email = form_data["email"]
        if not email:
            self.error_message = "email cannot be empty"
            rx.set_focus("email")
            # reset state variable again
            self.is_loading = False
            yield
            return
        if not is_valid_email(email):
            self.error_message = "email is not a valid email address."
            rx.set_focus("email")
            # reset state variable again
            self.is_loading = False
            yield
            return

        password = form_data["password"]
        if not password:
            self.error_message = "Password cannot be empty"
            rx.set_focus("password")
            # reset state variable again
            self.is_loading = False
            yield
            return
        if password != form_data["confirm_password"]:
            self.error_message = "Passwords do not match"
            [
                rx.set_value("confirm_password", ""),
                rx.set_focus("confirm_password"),
            ]
            # reset state variable again
            self.is_loading = False
            yield
            return

        # sign up with supabase
        supabase_client.auth.sign_up({
            "email": email,
            "password": password,
        })

        # Set success and redirect to login page after a brief delay.
        self.error_message = ""
        self.success = True
        self.is_loading = False
        yield
        await asyncio.sleep(3)
        yield [rx.redirect(LOGIN_ROUTE), RegistrationState.set_success(False)]


@rx.page(route=REGISTER_ROUTE)
def registration_page() -> rx.Component:
    """Render the registration page.

    Returns:
        A reflex component.
    """
    register_form = rx.form(
        rx.input(placeholder="email", id="email", type_="email"),
        rx.password(placeholder="password", id="password"),
        rx.password(placeholder="confirm", id="confirm_password"),
        rx.button("Register", type_="submit", is_loading=RegistrationState.is_loading,),
        width="80vw",
        on_submit=RegistrationState.handle_registration,
    )
    return rx.fragment(
        rx.cond(
            RegistrationState.success,
            rx.vstack(
                rx.text("Registration successful, check your mail to confirm signup so as to login!"),
                rx.spinner(),
            ),
            rx.vstack(
                rx.cond(  # conditionally show error messages
                    RegistrationState.error_message != "",
                    rx.text(RegistrationState.error_message),
                ),
                register_form,
                padding_top="10vh",
            ),
        )
    )

def is_valid_email(email):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None
Enter fullscreen mode Exit fullscreen mode

The above code declares a RegistrationState class which manages the state of the registration page. It handles form submission with the handle_registration method, which sets the is_loading flag to indicate form submission and validates the email and password provided. If validation fails, it sets an error message and returns focus to the respective field. If successful, it uses Supabase to sign up the user and sets the success flag, which is followed by a delay and redirection to the login page.

The registration_page() function defines the registration page, rendering the registration form and displaying success messages or error messages based on the registration process. It also provides a link to the login page.

The is_valid_email function checks if an email address is valid based on a regular expression pattern. If it matches, the email is considered valid.
The above code renders the following page:

supabaseregisterauth

.gitignore

Add the following to the .gitignore file:

*.db
*.py[cod]
.web
__pycache__/
.venv/
.env
Enter fullscreen mode Exit fullscreen mode

run app

Run the following in the terminal to start the app:

reflex run
Enter fullscreen mode Exit fullscreen mode

When a user registers, supabase sends a confirm sign-up message to the user's mail. the user's details get saved in supabase users model. You can also login and logout from the web app. You will notice that when you login you will now be able to access a protected page that is meant for only authenticated users. The protected page is added only to show an example.

conclusion

You can get the code: https://github.com/emmakodes/auth_supabase

Note: You may decide to place the supabase setup code in a single file and call from that file.

Top comments (1)

Collapse
 
paulwababu profile image
paulsaul621

Super lovely! Thanks for this!