Supabase supports a collection of auth strategies - Email and password, passwordless and OAuth. In this lesson, we look at implementing OAuth with GitHub.
In order to do this, we create a new GitHub OAuth app, and configure Supabase to use its Client ID and Secret. Additionally, we create a <Login />
component that uses the Supabase client to sign users in and out.
This identifies a problem that we don't have access to the environment variables required to create a Supabase client outside of loaders or actions. Therefore, we pipe through our SUPABASE_URL
and SUPABASE_ANON_KEY
from the Loader function, and create a singleton Supabase client, to use across our components.
Lastly, we look at sharing this single instance of Supabase through the Outlet Context and declare types to ensure we have TypeScript helping us out throughout the application.
Expose environment variables from the server
export const loader = async ({}: LoaderArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
};
return json({ env });
};
Access env
in component
const { env } = useLoaderData<typeof loader>();
Create a singleton Supabase client
const [supabase] = useState(() =>
createClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
);
Share global variables with Outlet Context
<Outlet context={{ supabase }} />
Consuming Outlet Context
const { supabase } = useOutletContext<SupabaseOutletContext>();
Logging in with GitHub
await supabase.auth.signInWithOAuth({
provider: "github",
});
Logging out
await supabase.auth.signOut();
Entire root component
import { json, LoaderArgs, MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "@remix-run/react";
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { useState } from "react";
import type { Database } from "db_types";
type TypedSupabaseClient = SupabaseClient<Database>;
export type SupabaseOutletContext = {
supabase: TypedSupabaseClient;
};
export const meta: MetaFunction = () => ({
charset: "utf-8",
title: "New Remix App",
viewport: "width=device-width,initial-scale=1",
});
export const loader = async ({}: LoaderArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!,
};
return json({ env });
};
export default function App() {
const { env } = useLoaderData<typeof loader>();
const [supabase] = useState(() =>
createClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
);
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet context={{ supabase }} />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Instructor: [0:00] Let's add authentication to our Remix app. We're going to do this with GitHub. Let's head over to GitHub and click sign in, and then click your little avatar and go down to settings. At the bottom of this sidebar, you'll find developer settings and then OAuth apps and click new OAuth app. [0:18] The name of this one is going to be chatter. Our home page URL for development is going to be http://localhost:3000 and you can find your authorization callback URL in your Supabase dashboard under the authentication menu, and then providers. We're going to scroll down this list to find GitHub.
[0:38] Here we can copy our redirect URL and go back over to GitHub and paste it in here as our authorization callback URL, and then click register application. This will give us the two values that we need to tell Supabase about which is our client ID and our client secret.
[0:54] We can copy our client ID here and paste it in under these authentication settings for GitHub. For our client secret, we need to click Generate a new client secret. We'll see this secret value once. Make sure you copy it before it's gone.
[1:09] Go back to our authentication settings in Supabase. Paste it in as our client secret value. We can enable GitHub here. Click Save. Our users will now be able to sign in to our application using GitHub or an email and password, which is on by default.
[1:26] Back in our application, let's create a new Login component. Let's create a new folder again at the root level of our application called components. In there, we want a login.tsx file. This component is going to simply render out two buttons, one for Logout and one for Login.
[1:45] When we click the Login button, it's going to call this handleLogin function, which we can declare here. This is just going to use our supabase client that we've imported and called .auth.signInWithOAuth. This function allows us to pass in which providers we would like use, in this case just GitHub.
[2:02] If there's an error during that sign-in process, we just want to console.log it out. We also need to declare our handleLogout function. We'll do that here. This one is very similar. We're just using supabase to say .auth.signOut. Again, if there's an error, we'll console.log it out so we can do some debugging.
[2:19] Let's render our new Login component in our index.tsx file. That's under app/routes/index.tsx. Rather than just rendering out our pre tag, we want to render out a fragment. We can put our pre tag back in there. We also want to render out our Login component, which is going to be imported from components/login. We need to get rid of that semicolon.
[2:44] Now, if we head back over to the browser and refresh, everything looks OK, but if we click this login button, nothing is going to happen. If we have a look in the console, we'll see this error saying uncaught reference error, process is not defined.
[2:58] This is because our Superbase utility, which we're using to create a Superbase client for us relies on these two environment variables that only exist when we're running in a server context, so when we're running in a loader or an action.
[3:13] Our login component which is being triggered when we click one of these buttons, which is happening when our application is loaded in our user's browser. In the browser, we don't have those environment variables exposed because that would be a security risk. Hence, the error that, process is not defined.
[3:28] This means our Superbase.ts file should really be called Superbase.server.ts, and I'm just going to let VS Code update the inputs for me. Now, the server part of this file name is a convention that tells Remix we don't want any of the code that's in this file to end up in the users browser.
[3:47] This utility file will work fine for our index.tsx file, when we're creating a Superbase client because this is happening in a loader, which is being run on the server and so, therefore has access to our environment variables.
[4:00] If we want to make those environment variables available to our client side components, we need to pipe them through from the server and the best place to do that is in our root.tsx file, which wraps around our entire application.
[4:13] If we scroll down to what we're actually returning from our route.tsx, this outlet component is a special component that basically gets swapped out for whichever route we have active from our routes directory. If I was to put a paragraph here that said, "Hi," and then we went back to the browser, and we can close the console now. We'll see that text "Hi" appearing above our index.tsx component.
[4:38] If we were to instead move this paragraph tag to below our outlet, and if we go back to the browser we'll see that it's rendering below. This root.tsx file is rendered on every page of our application and is a great place to do global stuff for our application, like pipe through these environment variables for our Superbase client.
[4:56] Let's start by exporting out a loader function. Again, we can input our type for loader ags from remix-run/node. Let's construct a new object of environment variables that we want to be able to expose from the server side. This will be our Superbase URL, which we can get from process.MV.Superbase URL and we'll put our exclamation point there and then we also want our Supabase anon key. We can then return this env object using the JSON helper and that one comes in from remix-run/node.
[5:33] Now, in our component, we can destructure that env object from what we get back by calling use loader data, which comes in from remix-run/react. We can give it the type of loader to infer our TypeScript types.
[5:48] We can now create a new Supabase client by calling useState, which comes in from React and passing it a function, which calls create clients which comes in from @supabase/supabase-js. Again, this one needs a Supabase URL and a Supabase anon key. We can get those from this env object that got returned from our loader.
[6:12] This will be env.supabase_URL and env.supabase anon key. If you haven't seen this funky syntax before of calling useState and passing it a function, this just ensures that we're only creating one Supabase client that we can then share across our entire application. A new state will never be called again.
[6:31] It'll be the same singleton instance forever, which is important when we're using Supabase Client side. Now, to make this Supabase Client available to any component across our entire application, we want to use the context prop on our Outlook component.
[6:44] We can then give it our Supabase Client and now back over in our logging component instead of importing our Supabase Client, we're going to get that single instance by calling useOutlet context which comes in from remix-run/react.
[7:01] Now we're getting some red squigglies under Supabase and that's because this is now implicitly any. We don't have those nice type declarations that came along with our server instance of our Supabase client, but we can do the same thing in route.tsx by importing the type for database.
[7:18] Then when we're calling create client in our component, we can pass across those types for our database. Now, if we have a look at the type for our Supabase client here, we'll see that is properly typed. However, our login component is still red. And so, if we have a look, this is still implicitly any.
[7:34] That's because we need to tell our useOutlook context which types to expect. We can declare that up in our route.tsx file. We can create a new type for a type Supabase Client, which can be equal to a Supabase client, which is a type that comes in from @supabase/supabase-js and then we just need to pass along those database types.
[7:57] We now have our own custom type for a type Supabase Client. We can then export a new type for Supabase Outlet context. This can be equal to an object that has the key Supabase, which is a type Supabase Client. We can then declare the type of our useOutlet context here to be Supabase Outlet context, which we can import from our route.
[8:25] Again, I'm just going to add the type keyword here and add an extra line for consistency. We can now see that our Supabase client is now correctly typed as a type Supabase Client. If we head back to the browser and refresh, we can now click this login button and it's going to take us through the proper GitHub OAuth flow.
[8:44] I'm going to authorize this application. This is then going to redirect us back to our Remix application. If we open up the console and go across to storage, open up local storage, which is how Supabase JS stores our access token, we'll see that we are successfully signed in.
[9:00] We can also validate that we have a new user in Supabase by going over to our dashboard and coming across to authentication and users and seeing that we now have that new user who has signed in through GitHub.
@jon-meyers, would you give an answer to the previous question, please?
Wow - all those steps. And it worked! I'm a little behind on my typescript. Can you explain this:
type TypedSupabaseClient = SupabaseClient<Database>
Is this assigning or adding the <Database> type to the supabaseClient object?
Is this supabase syntax or typescript syntax? If it is typescript syntax can you point me to some documentation where it is described? Thanks.