Remix Loaders allow us to fetch data server-side, before rendering a component. This works great if you are implementing your authorization rules for data fetching in the loader function, however, Supabase allows us to use Row Level Security policies to write access policies alongside the data in the database.
By default, supabase-js
stores session data in localStorage
, which exists only within the user's browser. If we want this session to be available within Loader or Action functions in Remix, we need to store the session in a cookie. Cookies are automatically sent with every request to the server.
In this lesson, we look at using the Supabase Auth Helpers package for Remix to automate this process, and swap out the storage mechanism for the Supabase client, to use cookies to store session data.
Additionally, we refactor our application to use the new createServerClient
and createBrowserClient
functions, making cookies the single source of truth about the user's current session, across the server and client-side of our Remix app.
Install Remix Auth Helpers
npm i @supabase/auth-helpers-remix
Create server-side Supabase client
import { createServerClient } from "@supabase/auth-helpers-remix";
import type { Database } from "db_types";
export default ({
request,
response,
}: {
request: Request;
response: Response;
}) =>
createServerClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
);
Create client-side Supabase client (simplified)
import { useState } from "react";
import { createBrowserClient } from "@supabase/auth-helpers-remix";
export const loader = async ({ request }: LoaderArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
};
return json({ env });
};
const { env, session } = useLoaderData<typeof loader>();
const [supabase] = useState(() =>
createBrowserClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
);
return <Outlet context={{ supabase }} />;
Use Supabase in a loader
export const loader = async ({ request }: LoaderArgs) => {
const response = new Response();
const supabase = createServerSupabase({ request, response });
const { data } = await supabase.from("messages").select();
return json({ data }, { headers: response.headers });
};
Use Supabase in an action
export const action = async ({ request }: ActionArgs) => {
const response = new Response();
const supabase = createServerSupabase({ request, response });
const { data } = await supabase.from("messages").insert({ content: "hello" });
return json({ data }, { headers: response.headers });
};
Use Supabase in a component
import { useOutletContext } from "@remix-run/react";
import type { SupabaseOutletContext } from "~/root";
export default function Login() {
const { supabase } = useOutletContext<SupabaseOutletContext>();
const handleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: "github",
});
};
return <button onClick={handleLogin}>Login</button>;
}
Instructor: [0:00] Our application is correctly showing no results when we logged out. However, if we login and try to refresh, we're still seeing no results. What's going on here? [0:09] Let's make this a little bit clearer by importing our Supabase client from utils/supabase server. Now down in our loader, let's make a request to Supabase to get our current session. We can do this by calling supabase.auth.getSession.
[0:27] Then let's also return this new session from our loader, which means in our component we can now destructure this from what we get back from our use loaded data hook and then let's console log and object that prints out our server session. Then after our use state here, let's add another useEffect, which we need to also bring in from React.
[0:47] Since useEffect runs client-side, we're going to use that singleton instance of Supabase that we declared above. We're just basically doing the same thing. We're calling .auth.getSession. When we get back that session, we're just console logging it out, but this time, with the key client, just so that we can help distinguish that from our server session here.
[1:07] Let's save this, refresh our application, and open up the console. Now, if we look at the state of our server session, we'll see that it's set to null. However, if we have a look at our client, we'll see our user's session data. That's because by default, Supabase stores our session information in Local Storage. However, our loader function in Remix gets executed on the server, which has no idea about Local Storage.
[1:33] If we want this server to also be able to use our Supabase session, we need to install the Auth Helpers package. Let's type npm install @supabase/auth-helpers-remix. Once that's finished, we can run our development environment again.
[1:49] To create a supabase client on the server, we want to import the createServerClient function from @supabase/auth-helpers-remix. Let's call that function to create our new supabase client. Since this is going to use cookies instead of Local Storage to manage the user's session, we need to refactor this file to export out a function so we can take in a request and a response.
[2:16] The type for our request is going to be a Request. The type for our response is going to be, you guessed it, Response, with a capital R. We then need to pass these values across to our createServerClient function. TypeScript tells us that we need to fix up our imports.
[2:33] Let's come up to the top of our root.tsx file and refactor this to instead bring in a function called createServerSupabase. Down in our loader function, we can create a new supabase client by calling our createServerSuperbase function, which then needs to take in a request and a response.
[2:53] We can get our request from our loader arguments here. However, our response, we're going to need to create above. Let's say const response is equal to a new Response. This supabase client may need to refresh the access token when making a request to Supabase.
[3:09] It needs to be able to set cookie headers in order to update the session that's used both across the server and the client. Therefore, whenever we're creating a supabase client on the server side, in our loaders or actions, we need to remember to return headers that are set to these response.headers.
[3:27] Since our server superbase client is now using cookies for authentication, we also need to use cookies for the client that's used across our components.
[3:36] Rather than calling createClient, we want to call createBrowserClient, which also comes in from that Auth Helpers package, which means we can also remove our createClient import from supabase-js and move our import statement from our Auth Helpers above our types.
[3:51] We also need to update our import in routes/index. tsx and update the import to be a createServerSupabase function. We then want to bring in our request from our LoaderArgs. Create a new empty response. Create a new supabase client by calling createServerSupabase and passing it our request and response.
[4:17] We need to remember to use the JSON helper which comes in from @remix-run/node to send back our messages and then also send those headers from our response. When we go back to the browser and refresh, we can force the user to go through that full GitHub authentication flow again by going over to our OAuth application settings in GitHub and revoking all user tokens.
[4:38] We click "I understand, revoke all user tokens." Back in our Remix application, when we click Login, we'll be sent through that authorization flow as if this is the first time that we signed into the application.
[4:49] If we refresh, we should again see those messages from Supabase. If we open up the console and have a look at our server session, we should see that it now contains our logged in user's session.
[5:00] To recap, supabase-js uses Local Storage to store the user's session. Local Storage only exists within the user's browser. Therefore, our loaders, which are running on the server, don't have access to the user's session. Therefore, we used these Supabase Auth Helpers package for Remix to swap out the storage mechanism to use cookies instead of Local Storage.
[5:20] This means our currently signed in user's session is now available server-side in our loaders and actions, and the same shared session is now available anywhere within our components client-side.