October 3, 2022

Robotic Notes

All technology News

Implementing authentication in Remix applications with Supabase

14 min read


Authentication is an essential part of many applications as it provides a form of security to users and helps personalise the user experience. Most applications today have authentication access to restrict users to certain parts of an application.

In this article, we will be looking at implementing an authentication system with Remix and Supabase.

Getting started with Remix

Remix is a full stack web application framework created by Ryan Florence and Michael Jackson, the developers behind the popular React library React Router. It is similar to Next.js as they share a similar file-based routing system, data loading techniques, and they allow you to interact with sessions and cookies. However, unlike Next.js, Remix will enable you to manipulate cookies and sessions out of the box.

To get started with Remix you’ll need:

  • Node.js v14 or greater,
  • npm 7 or greater
  • Code editor

Follow the steps below to get a Remix project running on your machine

  1. Open a terminal and run the code below

1npx create-remix@latest

  1. On the next prompt type (“y”) to proceed
  2. Specify the directory to install the remix application
  3. Select Remix App Server on the “Where do you want to deploy” prompt. The deployment target can always be changed
  4. Select “JavaScript” on the next prompt
  5. Enter (“Y”) on the last prompt for the installer to install the packages

After installation, navigate to the project directory and run the code below to start the remix app development server

Open a browser, and navigate to https:localhost:3000 to access the web application

Remix application home page

Getting Started with Supabase

Supabase is an open-source alternative to Firebase and provides a platform that includes Auth, Database, and Storage as a service to developers. At the heart of the Supabase products is a PostgreSQL database, and it powers all the products. For this article, we will focus on Authentication and Database.

Supabase provides various Authentication methods that can be integrated into a web application. They include;

  • Email & password.
  • Magic links (one-click logins).
  • Social providers.
  • Phone logins

To get started with Supabase, you have to create a Supabase project. Follow the steps below to get a Supabase project up and running

  1. Visit Supabase
  2. Create an account
  3. Click on “New project”

You’ll have to wait a bit for the project to be created. After creating the project we need to get API Key and URL to be used in the web application.

  1. Go to the “Settings” section.
  2. Click “API” in the sidebar.
  3. Copy the URL in the Configuration section.
  4. Copy the “service secret” key on “Project API Keys” section.

Keeps these keys safely, as we will be using them in the next section

Adding Supabase to Remix

Supabase has an official library that can be used in JavaScript applications. The library exposes an API that allows us to communicate with our Supabase project. Run the code below to install it in your Remix application.

1npm install @supabase/supabase-js

After installation, create a .env file in the root of the Remix application and paste the code below

1SUPABASE_URL=<SUPABASE_PROJECT_URL>

2SUPABASE_SECRET_KEY=<SUPABASE_SERVICE_SECRET_KEY>

Replace the placeholders with the keys copied earlier from Supabase.

Note: Never commit the .env file to a repository, as it is meant to keep your secret keys away from the public.

The next step is to initialize Supabase in our application. Create a supabase.server.js file in the app directory and paste the code below

1import { createClient } from '@supabase/supabase-js'

2const supabaseUrl = process.env.SUPABASE_URL

3const supabaseSecretKey = process.env.SUPABASE_SECRET_KEY

4export const supabase = createClient(supabaseUrl, supabaseSecretKey)

You might wonder about the .server naming convention. Remix ensures that files that append .server to its name never end up in the browser, so when Remix compiles the files, the supabase.server.js file will be skipped.

Adding Tailwind to Remix

We will be styling our application with Tailwind CSS as it is the most popular way of styling a Remix application due to its inline styling, and it can generate a CSS file for Remix to import.

Run the code below to install the libraries required for using tailwind in Remix

1npm install -D npm-run-all tailwindcss

Next, run the code below in a terminal to generate a Tailwind configuration file

1npx tailwindcss init

This generates a tailwind.config.js file in the root of our Remix application. Open the file and replace the content with the code below

1module.exports = {

2 content: ["./app/**/*.{ts,tsx,jsx,js}"],

3 theme: {

4 extend: {},

5 },

6 plugins: [],

7};

I’ve modified the content property, so tailwind will know which files to generate classes from.

Open the package.json file and add the code below to the scripts property

1{

2

3 scripts: {

4 build: "run-s build:*",

5 "build:css": "npm run generate:css -- --minify",

6 "build:remix": "remix build",

7 dev: "run-p dev:*",

8 "dev:css": "npm run generate:css -- --watch",

9 "dev:remix": "remix dev",

10 "generate:css": "npx tailwindcss -o ./app/tailwind.css",

11 postinstall: "remix setup node",

12 start: "remix-serve build",

13 },

14

15 }

I’ve added scripts to generate tailwind.css stylesheet and watch for changes during development and also a production build.

Now, go to app/root.jsx and add the code below to import the generated stylesheet into the Remix application

1import styles from "./tailwind.css";

2

3export const links: LinksFunction = () => [

4 { rel: "stylesheet", href: styles },

5];

We can now go ahead to use Tailwind classes in our Remix application.

Implementing Sign Up

In this section, we will be implementing the signup page. Remix uses a file-based routing system, so every file created in the routes directory is rendered in the browser. Go to the routes directory create a sign-up.jsx file and paste the code below

1const SignUp = () => {

2 return <div>SignUp</div>;

3};

4export default SignUp;

When a user visits the /sign-up up route the contents of this component will be rendered to the browser.

Before we start implementing the sign-up page, I want us to create a Layout component that other routes can reuse. Run the code below to achieve that

1cd app

2mkdir components

3cd components

4touch layout.jsx

The above code creates a layout.jsx in the app/components. Open the layout.jsx file and paste the code below

1import { Form, useTransition } from "remix";

2const Layout = ({ children, showSignOut }) => {

3 const transition = useTransition();

4 return (

5 <div className="container mx-auto p-6">

6 <header className="flex justify-between items-center">

7 <h1 className="text-2xl font-light">

8 Remix{" "}

9 <strong className="font-bold">

10 Supabase

11 </strong>

12 </h1>

13 {showSignOut && (

14 <Form action="/sign-out" method="post">

15 <button

16 type="submit"

17 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mt-3"

18 aria-live="polite"

19 disabled={

20 transition.state !== "idle"

21 }

22 >

23 {transition.state !== "idle"

24 ? "Loading..."

25 : "Sign out"}

26 </button>

27 </Form>

28 )}

29 </header>

30 <main className="w-full md:w-3/4 lg:w-2/4 mx-auto py-6 my-6">

31 {children}

32 </main>

33 </div>

34 );

35};

36export default Layout;

In this file, we’ve created a page with a header and a place to render content. You can notice we imported a Form component and a useTransition hook from Remix.

The Form is a component that allows us to perform data mutations. When the submit button is triggered, the data from the Form will be posted to the /sign-out route. The action prop is optional. If it isn’t present, the form data is posted to the same route in the form.

The useTransition hook provides information about page transitions. It provides a state property that tells us the stage of the transition. The transition state can be in any of these states (idle, submitting, loading). We can now use these states to customise our UI.

Now that we have our Layout component ready let’s create some helpful utilities that will come in handy when creating the SignUp component.

Run the code below to create an auth.js file and cookie.js file in app/utils/

1cd app

2mkdir utils

3cd utils

4touch auth.js

5touch cookie.js

Open the cookie.js file and paste the code below

1import { createCookie } from "remix";

2const cookieOptions = {

3 httpOnly: true,

4 secure: false,

5 sameSite: "lax",

6 maxAge: 604_800,

7};

8const supabaseToken = createCookie("sb:token", {

9 ...cookieOptions,

10});

11export default supabaseToken;

This is a helper file that helps us create a cookie stored on the user’s browser. The createCookie is a logical container for managing a browser cookie.

Next, open the auth.js file and paste the code below.

1import { supabase } from "~/supabase.server";

2

3export const createUser = async (data) => {

4 const { user, error } =

5 await supabase.auth.signUp({

6 email: data?.email,

7 password: data?.password,

8 });

9 const createProfile = await supabase

10 .from("profiles")

11 .upsert({

12 id: user?.id,

13 first_name: data?.firstName,

14 last_name: data?.lastName,

15 phone_number: data?.phoneNumber,

16 });

17 return { user: createProfile, error };

18};

We’ve created a function to help create a user in this file. Two things happen here:

  • First, we create a new user by calling the supabase.auth.signUp, which accepts an email and password
  • Supabase doesn’t allow us to pass extra data to the signUp method, so we call supabase.from('profiles').upsert() to take the additional data and insert the data into a profiles table if it doesn’t exist or update it if it does using the user id from the successful sign up as a reference.

You might wonder how we have access to a profiles table? We don’t, but we will create one now. Go back to Supabase and follow the steps below to create a profiles table.

  1. Navigate to the Supabase dashboard.
  2. Click on your project to open the project dashboard
  3. Click on “SQL editor” in the side navigation

Sidebar showing SQL Editor

  1. Paste the code below in the editor

1create table profiles (

2 id uuid references auth.users,

3 first_name text,

4 last_name text,

5 phone_number text

6);

  1. Click “Run” to execute the query

We’ve now created the profiles table. You can navigate to the tables editor to view the created table

Supabase SQL Editor

Go back to the sign-up.js file and replace the code with the code below

1import {

2 Form,

3 useActionData,

4 json,

5 useTransition,

6} from "remix";

7import { createUser } from "~/utils/auth";

8import Layout from "~/components/layout";

9export async function action({ request }) {

10 const errors = {};

11 try {

12 const form = await request.formData();

13 const firstName = form.get("firstName");

14 const lastName = form.get("lastName");

15 const email = form.get("email");

16 const password = form.get("password");

17 const phoneNumber = form.get("phoneNumber");

18

19 if (!firstName) {

20 errors.firstName = "First name is required";

21 }

22 if (!lastName) {

23 errors.lastName = "Last name is required";

24 }

25 if (!email || !email.match(/^S+@S+$/)) {

26 errors.email = "Email address is invalid";

27 }

28 if (!password || password.length < 6) {

29 errors.password =

30 "Password must be > 6 characters";

31 }

32 if (

33 !phoneNumber ||

34 !phoneNumber.match(/^D*(dD*){9,14}$/)

35 ) {

36 errors.phoneNumber =

37 "Phone number is invalid";

38 }

39

40 if (Object.keys(errors).length) {

41 return json({ errors }, { status: 422 });

42 }

43 const { user, error } = createUser({

44 email,

45 password,

46 firstName,

47 lastName,

48 phoneNumber,

49 });

50 if (user?.status === 201) {

51 return json({ user }, { status: 200 });

52 }

53 throw error;

54 } catch (error) {

55 console.log("error", error);

56 errors.server = error?.message || error;

57 return json({ errors }, { status: 500 });

58 }

59}

60const SignUp = () => {

61 const data = useActionData();

62 const transition = useTransition();

63 return (

64 <Layout>

65 <h2 className="text-3xl font-light">

66 Sign{" "}

67 <strong className="font-bold">up</strong>

68 </h2>

69 <Form method="post" className="my-3">

70 {data?.user && (

71 <div

72 className="mb-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"

73 role="alert"

74 >

75 <strong className="font-bold">

76 Congrats!{" "}

77 </strong>

78 <span className="block sm:inline">

79 Your account has been registered.

80 Please go to your email for

81 confirmation instructions.

82 </span>

83 </div>

84 )}

85 <div className="mb-2">

86 <label

87 className="text-gray-700 text-sm font-bold mb-2"

88 htmlFor="firstName"

89 >

90 First name

91 </label>

92 <input

93 id="firstName"

94 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

95 type="text"

96 placeholder="Your first name"

97 name="firstName"

98 />

99 {data?.errors?.firstName ? (

100 <p className="text-red-500 text-xs italic">

101 {data?.errors.firstName}

102 </p>

103 ) : null}

104 </div>

105 <div className="mb-2">

106 <label

107 className="text-gray-700 text-sm font-bold mb-2"

108 htmlFor="lastName"

109 >

110 Last name

111 </label>

112 <input

113 id="lastName"

114 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

115 type="text"

116 placeholder="Your last name"

117 name="lastName"

118 />

119 {data?.errors?.lastName ? (

120 <p className="text-red-500 text-xs italic">

121 {data?.errors.lastName}

122 </p>

123 ) : null}

124 </div>

125 <div className="mb-2">

126 <label

127 className="text-gray-700 text-sm font-bold mb-2"

128 htmlFor="email"

129 >

130 Email

131 </label>

132 <input

133 id="email"

134 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

135 type="email"

136 placeholder="Your email"

137 name="email"

138 />

139 {data?.errors?.email ? (

140 <p className="text-red-500 text-xs italic">

141 {data?.errors.email}

142 </p>

143 ) : null}

144 </div>

145 <div className="mb-2">

146 <label

147 className="text-gray-700 text-sm font-bold mb-2"

148 htmlFor="password"

149 >

150 Password

151 </label>

152 <input

153 id="password"

154 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

155 type="password"

156 name="password"

157 placeholder="Your password"

158 />

159 {data?.errors?.password ? (

160 <p className="text-red-500 text-xs italic">

161 {data?.errors.password}

162 </p>

163 ) : null}

164 </div>

165 <div className="mb-2">

166 <label

167 className="text-gray-700 text-sm font-bold mb-2"

168 htmlFor="phoneNumber"

169 >

170 Phone Number

171 </label>

172 <input

173 id="phoneNumber"

174 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

175 type="text"

176 placeholder="Your phone number"

177 name="phoneNumber"

178 />

179 {data?.errors?.phoneNumber ? (

180 <p className="text-red-500 text-xs italic">

181 {data?.errors.phoneNumber}

182 </p>

183 ) : null}

184 </div>

185 <div>

186 <button

187 type="submit"

188 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mt-3"

189 aria-live="polite"

190 >

191 {transition.state !== "idle"

192 ? "Loading..."

193 : "Sign up"}

194 </button>

195 {data?.errors?.server ? (

196 <p className="text-red-500 text-xs italic">

197 {data?.errors.server}

198 </p>

199 ) : null}

200 </div>

201 </Form>

202 </Layout>

203 );

204};

205export default SignUp;

Pardon the length of this file. Most are just the markup for the sign-up form. Let’s walk through it. A user submits the sign-up form in this component, which triggers the action function.

The action function is a server only function that runs when a non GET request is made to the server to handle data mutations and other actions. As I explained earlier in the Layout component, because we don’t provide an action prop to the Form component, the data from the form is submitted to the same route and triggers the action server function.

The request property passed to the action server function is an object that describes the request made to a server. We can get the form data from the request object, which we have done, and then perform validation as needed. If there is an error during validation, we use the json helper function to return a JSON response to the client.

If there aren’t validation errors, we go-ahead to create a new user by calling the helper function createUser. A confirmation email is sent to the user when a new user is created on Supabase. Supabase also returns a success response to the client. If there is an error creating the user, Supabase returns an error.

We use the useActionData hook to get data from the action function that runs on the server. We can use this data to manipulate our UI. We conditionally display errors if the useActionData returns an error. We also show a success alert if the user was created successfully.

Save the file, navigate to the /sign-up route in your browser and test to see if it works

Sign up page

You’ll get a confirmation email with a link after successfully submitting the form, redirecting to localhost:3000. The URL the confirmation link redirects to can be changed by going to the Authentication section of your Project Settings on Supabase.

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.

replayer.png

Happy debugging, for modern frontend teams – Start monitoring your web app for free.

Implementing Sign in

We now have our Sign up page up and running let’s go ahead to implement the sign in page.

Open the auth.js file we created and add the code below

1

2export const signInUser = async ({

3 email,

4 password,

5}) => {

6 const { data, error } =

7 await supabase.auth.signIn({

8 email,

9 password,

10 });

11 return { data, error };

12};

This is a wrapper function for the supabase.auth.signIn which takes an email and password as parameters and attempts to sign in the user. It returns data or error depending on if it executes successfully.

Create a sign-in.jsx file in the routes directory and paste the code below

1import {

2 Form,

3 useActionData,

4 json,

5 redirect,

6 useTransition,

7} from "remix";

8import supabaseToken from "~/utils/cookie";

9import Layout from "~/components/layout";

10import { signInUser } from "~/utils/auth";

11export async function action({ request }) {

12 const errors = {};

13 try {

14 const form = await request.formData();

15 const email = form.get("email");

16 const password = form.get("password");

17

18 if (

19 typeof email !== "string" ||

20 !email.match(/^S+@S+$/)

21 ) {

22 errors.email = "Email address is invalid";

23 }

24 if (

25 typeof password !== "string" ||

26 password.length < 6

27 ) {

28 errors.password =

29 "Password must be > 6 characters";

30 }

31

32 if (Object.keys(errors).length) {

33 return json(errors, { status: 422 });

34 }

35

36 const { data, error } = await signInUser({

37 email,

38 password,

39 });

40 if (data) {

41 return redirect("/", {

42 headers: {

43 "Set-Cookie":

44 await supabaseToken.serialize(

45 data.access_token,

46 {

47 expires: new Date(

48 data?.expires_at

49 ),

50 maxAge: data.expires_in,

51 }

52 ),

53 },

54 });

55 }

56 throw error;

57 } catch (error) {

58 console.log("error", error);

59 errors.server = error?.message || error;

60 return json(errors, { status: 500 });

61 }

62}

63const SignIn = () => {

64 const errors = useActionData();

65 const transition = useTransition();

66 return (

67 <Layout>

68 <h2 className="text-3xl font-light">

69 Sign{" "}

70 <strong className="font-bold">in</strong>

71 </h2>

72 <Form

73 method="post"

74 className="my-3 lg:w-3/4"

75 >

76 <div className="mb-2">

77 <label

78 className="text-gray-700 text-sm font-bold mb-2"

79 htmlFor="email"

80 >

81 Email

82 </label>

83 <input

84 id="email"

85 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

86 type="email"

87 placeholder="Your email"

88 name="email"

89 />

90 {errors?.email ? (

91 <p className="text-red-500 text-xs italic">

92 {errors.email}

93 </p>

94 ) : null}

95 </div>

96 <div className="mb-2">

97 <label

98 className="text-gray-700 text-sm font-bold mb-2"

99 htmlFor="password"

100 >

101 Password

102 </label>

103 <input

104 id="password"

105 className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"

106 type="password"

107 name="password"

108 placeholder="Your password"

109 />

110 {errors?.password ? (

111 <p className="text-red-500 text-xs italic">

112 {errors.password}

113 </p>

114 ) : null}

115 </div>

116 <div>

117 <button

118 type="submit"

119 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mt-3"

120 aria-live="polite"

121 disabled={transition.state !== "idle"}

122 >

123 {transition.state !== "idle"

124 ? "Loading..."

125 : "Sign in"}

126 </button>

127 {errors?.server ? (

128 <p className="text-red-500 text-xs italic">

129 {errors.server}

130 </p>

131 ) : null}

132 </div>

133 </Form>

134 </Layout>

135 );

136};

137export default SignIn;

Like our SignUp component, when a user submits the form, the action server function is called, validating the form and attempting to sign in the user. If the request is successful, we return a redirect response using the redirect helper function. We also use supabaseToken.serialize() to serialize the access_token returned by Supabase as a string and store the cookie on the user browser so we can use it later to authenticate requests made to Supabase.

Sign in page

Implementing the Dashboard

After a user signs in successfully, they are taken to the homepage containing sample data. Let’s change that.

Open the auth.js file in the app/utils directory and add the code below

1

2import supabaseToken from "~/utils/cookie";

3

4const getToken = async (request) => {

5 const cookieHeader =

6 request.headers.get("Cookie");

7 return await supabaseToken.parse(cookieHeader);

8};

9

10const getUserByToken = async (token) => {

11 supabase.auth.setAuth(token);

12 const { user, error } =

13 await supabase.auth.api.getUser(token);

14 return { user, error };

15};

16

17export const isAuthenticated = async (

18 request,

19 validateAndReturnUser = false

20) => {

21 const token = await getToken(request);

22 if (!token && !validateAndReturnUser)

23 return false;

24 if (validateAndReturnUser) {

25 const { user, error } = await getUserByToken(

26 token

27 );

28 if (error) {

29 return false;

30 }

31 return { user };

32 }

33 return true;

34};

35

36export const getUserData = async (userId) => {

37 const { data, error } = await supabase

38 .from("profiles")

39 .select()

40 .eq("id", userId)

41 .single();

42 return { data, error };

43};

44

45

We’ve added four new helper methods to our auth.js file:

  • getToken: This gets the cookie from the request object and parses it to return it’s value
  • getUserByToken: Using the parsed cookie gotten from the getToken function which is an access_token of the user. The getUserToken returns the currently authenticated user.
  • isAuthenticated: This function checks if the stored cookie is valid and if it is either returns a boolean or the user data if the cookie is valid, and the *validateAndReturnUser* is true
  • getUserData : This takes a unique user id and uses it to fetch a record from the profiles table we created on Supabase. We use the id returned from the authentication as a reference on the profiles table. So each authentication record on Supabase is connected to a profile record.

Open the index.jsx component in the routes directory and paste the code below

1import {

2 redirect,

3 useLoaderData,

4 json,

5} from "remix";

6import Layout from "~/components/layout";

7import {

8 getUserData,

9 isAuthenticated,

10} from "~/utils/auth";

11export const loader = async ({ request }) => {

12 let errors = {};

13 try {

14 const userAuthenticated =

15 await isAuthenticated(request, true);

16 if (!userAuthenticated) {

17 return redirect("/sign-in");

18 }

19 const { user } = userAuthenticated;

20 const { data, error } = await getUserData(

21 user?.id

22 );

23 if (data) {

24 console.log("here");

25 return json(

26 { user: { ...data, email: user?.email } },

27 { status: 200 }

28 );

29 }

30 throw error;

31 } catch (error) {

32 console.log("error", error);

33 errors.server = error?.message || error;

34 return json({ errors }, { status: 500 });

35 }

36};

37

38const Index = () => {

39 const data = useLoaderData();

40 return (

41 <Layout showSignOut={true}>

42 <h2 className="text-3xl font-light">

43 Welcome{" "}

44 <strong className="font-bold">

45 {data?.user?.first_name}

46 </strong>

47 ,

48 </h2>

49 <section className="max-w-sm w-full lg:max-w-full my-6">

50 <div className="mb-2">

51 <p className="text-gray-700 text-sm font-bold">

52 Full name

53 </p>

54 <p>{`${data?.user?.first_name} ${data?.user?.last_name}`}</p>

55 </div>

56 <div className="mb-2">

57 <p className="text-gray-700 text-sm font-bold">

58 Email

59 </p>

60 <p>{data?.user?.email}</p>

61 </div>

62 <div className="mb-2">

63 <p className="text-gray-700 text-sm font-bold">

64 Phone Number

65 </p>

66 <p>{data?.user?.phone_number}</p>

67 </div>

68 </section>

69 </Layout>

70 );

71};

72export default Index;

Unlike our SignUp and SignIn component, we are using a loader server function here instead of the action server function. The difference is the loader function is called before the component is rendered, while the action function, as the name implies, needs an action to be performed. Think of the loader function as a useEffect hook set to run once on the server.

We then check if a user is authorised to view the dashboard page using the isAuthenticated helper function we created earlier. If the user isn’t, we redirect them back to the /sign-in page. If the user is authenticated, we get the user data and use it to fetch the profile information, returned by the getUserData function.

Refresh the home page and if the user is signed in, you should see a Dashboard page similar to the screenshot below

Dashboard page

Implementing Sign out

You can see a sign-out button on our dashboard page. You will be taken to a 404 page if you click on the button because we don’t have a /sign-out route. Before we create that go back to the auth.js file and add the code below

1

2export const signOutUser = async (request) => {

3 const token = await getToken(request);

4 return await supabase.auth.api.signOut(token);

5};

So what happens in this function? We get the access_token, parsed from the cookie in the request object, and call the supabase.auth.api.signOut() to invalidate the token.

Now create a sign-out.jsx file in the routes directory and paste the code below

1import { redirect } from "remix";

2import supabaseToken from "~/utils/cookie";

3import { signOutUser } from "~/utils/auth";

4export const action = async ({ request }) => {

5 try {

6 await signOutUser(request);

7 return redirect("/sign-in", {

8 headers: {

9 "Set-Cookie":

10 await supabaseToken.serialize("", {

11 maxAge: 0,

12 }),

13 },

14 });

15 } catch (error) {

16 console.log(error);

17 }

18};

We aren’t rendering any component in the /sign-out route. We just redirect the user back to the sign-out page and remove the cookie we stored in the user browser.

Conclusion

We’ve come to the end of the article. You can now test end to end. A user should be able to

  • Create a new account
  • Sign in to the account
  • View the dashboard, which contains their profile info if authentication was successful
  • Sign out

Remix is attempting to redefine how we build web applications by fixing the pitfalls of React while utilising its best features. When combined with Supabase, we can build complete solutions without worrying about database architecture and implementing our custom authentication systems. Endeavour to read the Remix docs to see what more can be achieved as this article only scratches the surface. You can extend the application we’ve built by adding a reset password feature which I intentionally left out. I’ll love to see what you come up with. The complete code of this article can be accessed here.



Source link