Supabase Auth session works, but RLS treats the request as unauthenticated in Flask

What I’m trying to do I’m building a Flask backend that calls Supabase via the async Python client. The user successfully signs in and auth.get_user() returns the expected profile, but any subsequent SELECT on the users table returns zero rows. RLS seems to think the request is anonymous. Environment - OS: MacOS Sequoia 15.5 - Supabase client: supabase>=2.16.0 - Python: 3.13.5 - Flask: 3.1.1 Code snippets Client setup
options = AsyncClientOptions(
storage=FlaskSessionStorage(), # thin wrapper over flask.session
auto_refresh_token=True,
persist_session=True,
)
supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY, options)
options = AsyncClientOptions(
storage=FlaskSessionStorage(), # thin wrapper over flask.session
auto_refresh_token=True,
persist_session=True,
)
supabase = create_client(SUPABASE_URL, SUPABASE_ANON_KEY, options)
Sign-in
# works, saves session

await supabase.auth.sign_in_with_password(
{"email": email, "password": password}
)
# works, saves session

await supabase.auth.sign_in_with_password(
{"email": email, "password": password}
)
Read all users
async def read_all(self):
result = await supabase.table("users").select("*").execute()
return result.data # ← always []
async def read_all(self):
result = await supabase.table("users").select("*").execute()
return result.data # ← always []
Read users route
async def get(self) -> tuple[Response, int]:
all_users = await self.__users.read_all()
# return 200 and json data
async def get(self) -> tuple[Response, int]:
all_users = await self.__users.read_all()
# return 200 and json data
RLS policy
create policy "policy_name"
on public.users
as PERMISSIVE
for SELECT
to authenticated
using (true);
create policy "policy_name"
on public.users
as PERMISSIVE
for SELECT
to authenticated
using (true);
What’s going wrong Even though the session is live and the JWT is attached, Postgres behaves as if the role is anon instead of authenticated, so the SELECT is filtered out by RLS. What I’m looking for 1. An explanation of what exactly causes this problem. 2. Any pointers on which extra header / cookie / claim Supabase expects when using the async Python client in Flask. 3. Known gotchas when combining Flask session storage with the supabase library. 4. A minimal working example (with SUPABASE_ANON_KEY) that proves RLS + Supabase Auth with Python works on the server side.
6 Replies
silentworks
silentworks3mo ago
What does your FlaskSessionStorage look like? also Flask is a sync framework but you said you are using the async supabase client. Please also share your full imports as code partials don't help give much context.
UltraGeoPro
UltraGeoProOP3mo ago
1. from flask import session from gotrue import AsyncSupportedStorage # type: ignore[] class FlaskSessionStorage(AsyncSupportedStorage): def init(self) -> None: self.storage = session async def get_item(self, key: str) -> str | None: return self.storage.get(key) async def set_item(self, key: str, value: str) -> None: self.storage[key] = value async def remove_item(self, key: str) -> None: self.storage.pop(key, None) 2. Flask supports asynchronous methods and can run as an ASGI server. https://flask.palletsprojects.com/en/stable/async-await/ 3. app startup app = asyncio.run(create_app()) asgi_app = WsgiToAsgi(app) if name == "main": config = Config() config.bind = ["0.0.0.0:8000"] asyncio.run(serve(asgi_app, config) register handler class UsersRoutes: logger = getLogger(name) def init(self, db_client: SupabaseClient) -> None: """Initialize users routes.""" self.users = UsersRoute.as_view("users", db_client=db_client) self.user = UserRoute.as_view("user", db_client=db_client) def initialize(self, app: Flask) -> None: """Add users routes to the application.""" users_bp = Blueprint("users", name, url_prefix="/users") users_bp.add_url_rule( "/", view_func=self.users, methods=["GET"], ) users_bp.add_url_rule( "/<uuid:user_id>", view_func=self.__user, methods=["GET", "PATCH"], ) Everything else remains unchanged and fully covers the logic for retrieving a user. If you need any additional code snippets, let me know
silentworks
silentworks3mo ago
The code looks ok to me, not sure why it's not working. I have a flask example app but it's not async and it works perfectly fine. Maybe try and create a minimal reproducible example repo and sare it here. My example is here https://github.com/silentworks/flask-notes, haven't updated it in a while but it should all still work even with the latest version of the python library.
UltraGeoPro
UltraGeoProOP3mo ago
Did you use ANON KEY in your application?
silentworks
silentworks3mo ago
Yes anon key is used
UltraGeoPro
UltraGeoProOP2mo ago
Solved the RLS issue We now spin up fresh clients per request, bind the current Supabase session to them, and stash them in g. Every query runs with the correct JWT, so RLS passes.
# middleware.py
async def before_request(self) -> None:
# :one: per-request clients
g.database, g.storage = await DatabaseClient.create(), await StorageClient.create()

if request.path in self.__allowed: # public routes
return

session = await self.__auth.get_session()
if not session:
raise Unauthorized("Missing session")

user = await self.__auth.get_user()
if not user or not user.confirmed:
raise Unauthorized("Unauthorized")

# :two: attach JWT to the clients
await g.database.set_session(session)
await g.storage.set_session(session)
# middleware.py
async def before_request(self) -> None:
# :one: per-request clients
g.database, g.storage = await DatabaseClient.create(), await StorageClient.create()

if request.path in self.__allowed: # public routes
return

session = await self.__auth.get_session()
if not session:
raise Unauthorized("Missing session")

user = await self.__auth.get_user()
if not user or not user.confirmed:
raise Unauthorized("Unauthorized")

# :two: attach JWT to the clients
await g.database.set_session(session)
await g.storage.set_session(session)
# users_route.py
@staticmethod
@validate(api.ApiUpdateUserOptions)
async def patch(user_id: UUID, body: api.ApiUpdateUserOptions) -> tuple[Response, int]:
db: DatabaseClient = g.database # per-request, already authed
await db.users.update(str(user_id), body.model_dump(exclude_none=True))
return jsonify({"status": 200, "message": "User updated.", "data": None, "error": None}), 200
# users_route.py
@staticmethod
@validate(api.ApiUpdateUserOptions)
async def patch(user_id: UUID, body: api.ApiUpdateUserOptions) -> tuple[Response, int]:
db: DatabaseClient = g.database # per-request, already authed
await db.users.update(str(user_id), body.model_dump(exclude_none=True))
return jsonify({"status": 200, "message": "User updated.", "data": None, "error": None}), 200
Result: each request carries its own token-aware DatabaseClient, so CRUD operations work under RLS without token leakage across concurrent requests.

Did you find this page helpful?