rphlmr / supa-fly-stack

The Remix Stack for deploying to Fly with Supabase, authentication, testing, linting, formatting, etc.
MIT License
289 stars 26 forks source link

feat: make supabase admin client a request scoped instance + rework env mana… #43

Closed rphlmr closed 1 year ago

rphlmr commented 1 year ago

Previously, supabaseAdmin (which has full admin privileges) was a singleton. In a moment of inattention and depending on how it's used, It could result in a Supabase session leak

Previously :

supabaseAdmin.auth.api.signInWithEmail() // ✅ no user session is stored in client instance

// ⚠️ Danger zone
supabaseAdmin.auth.signIn() // ❌ user session is stored in client instance and is shared over multiple concurrent requests and from different users
// somewhere else in a `loader`/ `action`
supabaseAdmin.auth.user() // can be you or the last signIn user

Now, getSupabaseAdmin() returns a unique client :

 getSupabaseAdmin().auth.api.signInWithEmail() // ✅ no user session is stored in client instance
 getSupabaseAdmin().auth.signIn() // ✅ user session is stored in client instance but client instance is unique for each request

❌ This is not the way ❌ :

// supabase-client.server.ts
export const supabaseAdmin = getSupabaseAdmin(); // ❌
export const supabaseClient = getSupabase(); // ❌

💡As much as you can, prefer using non admin supabase client getSupabase() until service_role is required for managing your DB (example that requires supabase admin client)

getSupabaseAdmin() only works in server land (throw in browser land)

getSupabase() is safe in server land and browser land, it accepts a user accessToken (To use RLS features)


Issue Demo : why getting a new client per request will save your production

https://user-images.githubusercontent.com/20722140/184351345-2e637f59-1471-4cda-92ee-a5438a5cdc8f.mp4

NB :

Given this implementation Supabase sdk init : ```js // supabase.server.ts export supabaseAdmin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE); export supabase = createClient(SUPABASE_URL, SUPABASE_ANON_PUBLIC); ``` By inadvertence, you sign in your user like that : ```js // ❌ Don't do that // route/login.tsx > loader // import { supabase } from "~/supabase.server" const { session, error } = await supabase.auth.signIn({ email, password, }); ``` instead of : ```js // route/login.tsx > loader // import { supabase } from "~/supabase.server" const { session, error } = await supabase.auth.api.ignInWithEmail( email, password, ); ``` ```js // route/notes.tsx > loader // import { supabase } from "~/supabase.server" // fetch notes with RLS const { data: notes, error } = await supabase .from("Note") .select("id, title"); return json({ notes }); ``` And this is what happen : on refresh, another user can see your notes 😱