You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
subtitle: A journey tring to restore supabase RLS when using Drizzle ORM using tRPC middlewares
lede: An experiment trying to fix Supabase RLS not working when using a direct db connection through an ORM, in a context of a tRPC, we can use middleware to create a wrapper around db calls and inject postgres set_config cmd to impersonate the correct roles and claims.
Edit:
This post has become obsolete with the current versions of the used services, though it was a good experiment that I've enjoyed, now Supabase have a good support for DrizzleORM, and with the Next.JS 15 server actions, you could basically replicate what tRPC have.
I have built a boilerplate template that make use of the latest version of DrizzleORM, Server Actions, a replicated middleware implementation similar to tRPC's and much more: Locastic/next-web3-hybrid-starter
Intro
First of all, let's go througt some basics, what are those:
Supabase: it's a DaaS open-source project that aims to simplify running and hosting Postgres databases.
RLS: Role-Level Security, it's a postgres security functionality that aims to protect postgres data on the row level, Supabase uses it as one of their selling points for their implementation.
Drizzle: is a new typescript ORM that memicks real SQL syntaxt without any runtime and overheads (like prisma ORM).
tRPC: is typescript typesafe api "builder" that seemlisely integrates with your frontend.
Issue
So I've been fighting this issue for months not only with Drizzle but with Prisma before, and my solution has been to either ignore RLS entirely or use the Supabase js library.
The issue is that Supabase RLS is only working when we use their JS SDK, if you choose otherwise, then you're on your own, it's the ORM job to figure out how roles and claims to be managed and that's what has been the case for Prisma, they now have their own plugin to handle that, for Drizzle, there have been some talks like drizzle-team/drizzle-orm/issues/594, porsager/postgres/issues/559 and also some tries like rphlmr/drizzle-supabase-rls.
So a more detailed summary of the issue: if you use an ORM, the default postgres role that will be used is postgres which is the role with admin privileges, that role will be ignored by Supabase RLS, so all your requests will go right through as if RLS is not enabled.
A quick overview about supabase RLS internals, supabase has default internal functions that will be used whenever you want to setup RLS in supabase dashboard, these postgres functions are: auth.email(), auth.jwt(), auth.role(), auth.uid(), calling them will check request.jwt.claim(s) setting and read off the jwt to extract information about the user that's making the postgres request.
That's one thing we need to manually inject, the next thing is the postgres role, we need to change the role from postgres to either authenticated or anon.
For this, there are several solutions proposed on the aforementioned discussions above, but IMHO the best one is to use tRPC middlewares as they're best suited for cases like this.
Solution
Let's go through this, in a default t3 stack setup, we have the following files:
import{db}from"@/server/db";exportconstcreateTRPCContext=async(opts: {headers: Headers})=>{constsession=awaitgetServerAuthSession();return{
db,
session,
...opts,};};/*...*/exportconstprotectedProcedure=t.procedure.use(({ ctx, next })=>{if(!ctx.session){thrownewTRPCError({code: "UNAUTHORIZED"});}returnnext({ctx: {// infers the `session` as non-nullablesession: { ...ctx.session},},});});
Now we need to wrap the next() callback with the needed logic to change the default postgres role, we need to do the following:
Inject jwt claims in order to make supabase be able to use auth.* functions.
Change role to either authenticated or anon.
exportconstprotectedProcedure=t.procedure.use(({ ctx, next })=>{returnctx.db.transaction(async(tx)=>{if(!ctx.session){thrownewTRPCError({code: "UNAUTHORIZED"});}// add jwt claims, it should be JSON stringified with the need// information (uid, email, role, ..)awaittx.execute(sql.raw(`SELECT set_config('request.jwt.claims', '{claims}', TRUE)`,),);// Change default role to either 'authenticated' or 'anon'awaittx.execute(sql.raw(`SET ROLE '{role}'`));constres=awaitnext({ctx: {// override default `db` with `tx`db: tx,// infers the `session` as non-nullablesession: { ...ctx.session},},});// reset default claims and roleawaittx.execute(sql`SELECT set_config('request.jwt.claims', NULL, TRUE)`);awaittx.execute(sql`RESET ROLE`);returnres;});});
A more refurbished version could be like this:
/** * Admin (postgres) procedure */exportconstadminProcedure=t.procedure;/** * Public (anon | authenticated) procedure */exportconstpublicProcedure=t.procedure.use(({ ctx, next })=>{returnctx.db.transaction(async(tx)=>{if(ctx.session){awaittx.execute(sql.raw(`SELECT set_config('request.jwt.claims', '${JSON.stringify(ctx.session)}', TRUE)`,),);awaittx.execute(sql.raw(`SET ROLE 'authenticated'`));}else{awaittx.execute(sql.raw(`SET ROLE 'anon'`));}constres=awaitnext({ctx: {// override default `db` with `tx`db: tx,},});if(ctx.session){awaittx.execute(sql`SELECT set_config('request.jwt.claims', NULL, TRUE)`,);}awaittx.execute(sql`RESET ROLE`);returnres;});});/** * Protected (authenticated) procedure */exportconstprotectedProcedure=t.procedure.use(({ ctx, next })=>{returndb.transaction(async(tx)=>{if(!ctx.session){thrownewTRPCError({code: "UNAUTHORIZED"});}awaittx.execute(sql.raw(`SELECT set_config('request.jwt.claims', '${JSON.stringify(ctx.session)}', TRUE)`,),);awaittx.execute(sql.raw(`SET ROLE 'authenticated'`));constres=awaitnext({ctx: {// override default `db` with `tx`db: tx,// infers the `session` as non-nullablesession: { ...ctx.session},},});awaittx.execute(sql`SELECT set_config('request.jwt.claims', NULL, TRUE)`);awaittx.execute(sql`RESET ROLE`);returnres;});});
Caveats
Now with this, one should be careful of edge cases, this should not be THE solution for this issue but rather, a workaround, a more legit solution should come from the ORM itself, hopefully it will come soon and this workaround will become absolete.
If you ever stambled on this, I hope it was helpful.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
subtitle: A journey tring to restore supabase RLS when using Drizzle ORM using tRPC middlewares
lede: An experiment trying to fix Supabase RLS not working when using a direct db connection through an ORM, in a context of a tRPC, we can use middleware to create a wrapper around db calls and inject postgres
set_configcmd to impersonate the correct roles and claims.Edit:
This post has become obsolete with the current versions of the used services, though it was a good experiment that I've enjoyed, now Supabase have a good support for DrizzleORM, and with the Next.JS 15 server actions, you could basically replicate what tRPC have.
I have built a boilerplate template that make use of the latest version of DrizzleORM, Server Actions, a replicated middleware implementation similar to tRPC's and much more: Locastic/next-web3-hybrid-starter
Intro
First of all, let's go througt some basics, what are those:
Issue
So I've been fighting this issue for months not only with Drizzle but with Prisma before, and my solution has been to either ignore RLS entirely or use the Supabase js library.
The issue is that Supabase RLS is only working when we use their JS SDK, if you choose otherwise, then you're on your own, it's the ORM job to figure out how roles and claims to be managed and that's what has been the case for Prisma, they now have their own plugin to handle that, for Drizzle, there have been some talks like drizzle-team/drizzle-orm/issues/594, porsager/postgres/issues/559 and also some tries like rphlmr/drizzle-supabase-rls.
So a more detailed summary of the issue: if you use an ORM, the default postgres role that will be used is
postgreswhich is the role with admin privileges, that role will be ignored by Supabase RLS, so all your requests will go right through as if RLS is not enabled.A quick overview about supabase RLS internals, supabase has default internal functions that will be used whenever you want to setup RLS in supabase dashboard, these postgres functions are:
auth.email(),auth.jwt(),auth.role(),auth.uid(), calling them will checkrequest.jwt.claim(s)setting and read off the jwt to extract information about the user that's making the postgres request.That's one thing we need to manually inject, the next thing is the postgres role, we need to change the role from
postgresto eitherauthenticatedoranon.For this, there are several solutions proposed on the aforementioned discussions above, but IMHO the best one is to use tRPC middlewares as they're best suited for cases like this.
Solution
Let's go through this, in a default t3 stack setup, we have the following files:
Now we need to wrap the
next()callback with the needed logic to change the default postgres role, we need to do the following:auth.*functions.authenticatedoranon.A more refurbished version could be like this:
Caveats
Now with this, one should be careful of edge cases, this should not be THE solution for this issue but rather, a workaround, a more legit solution should come from the ORM itself, hopefully it will come soon and this workaround will become absolete.
If you ever stambled on this, I hope it was helpful.
Beta Was this translation helpful? Give feedback.
All reactions