add supabase auth with remix blog post

nulfrost 0066a7a1 6661b1e3

Changed files
+255
src
+255
src/content/blog/supabase-auth-with-remix-and-vite.md
··· 1 + --- 2 + title: Supabase Auth with Remix + Vite 3 + description: Get up and running with Remix, Vite and the new Supabase SSR package 4 + year: 2024 5 + published_at: 2024-03-08 6 + --- 7 + 8 + With Supabase now fully supporting doing authentication completely server-side, it has never been easier to take advantage of all of the features that it offers. We'll go through how to quickly spin up a Remix project and add supabase with authentication. 9 + 10 + ## Spin up a new Remix project 11 + 12 + This guide should also work for non-vite Remix projects but my preference is to use vite so we'll use a clean vite template. 13 + 14 + ```bash 15 + npx create-remix@latest --template remix-run/remix/templates/vite 16 + ``` 17 + 18 + Run this command in your terminal and follow the prompts, then open your new project in your editor of choice. You'll also need to [create a new project in supabase](https://supabase.com/) so that we can get access to the environment variables for this example. 19 + 20 + Once you've created a supabase project, in the root of your project create a `.env` file and paste in the values for your `SUPABASE_URL` and `SUPABASE_ANON_KEY`. You can find these values by going into your supabase project dashboard, clicking on the on the connect button and selecting Remix from the frameworks list. 21 + 22 + ![Supabase dashboard connect button](https://i.ibb.co/k4GYkG8/Screenshot-2024-03-07-at-6-38-35-PM.png) 23 + 24 + ![Supabase frameworks dropdown list](https://i.ibb.co/HpXVMMW/Screenshot-2024-03-07-at-6-40-13-PM.png) 25 + 26 + Lastly, install the `@supabase/ssr` package. 27 + 28 + ```bash 29 + npm install @supabase/ssr 30 + ``` 31 + 32 + ## Setting up authentication 33 + 34 + There are a bunch of ways you can set up auth in supabase. E-mail and password combo, E-mail magic link, OAuth and so on. For the sake of this blog post we'll set up E-mail and password since it's the simplest one. Though that should be enough to explore the other methods as well if you wish. 35 + 36 + ### Create the necessary files 37 + 38 + In here we are just creating a utility function so that we can re-use this function across all instances where we need to access supabase resources. 39 + 40 + ```ts 41 + // app/utils/supabase.server.ts 42 + 43 + import { createServerClient, serialize, parse } from "@supabase/ssr"; 44 + 45 + export function createClient(request: Request) { 46 + const cookies = parse(request.headers.get("Cookie") ?? ""); 47 + const headers = new Headers(); 48 + 49 + const supabase = createServerClient( 50 + process.env.SUPABASE_URL!, 51 + process.env.SUPABASE_ANON_KEY!, 52 + { 53 + cookies: { 54 + get(key) { 55 + return cookies[key]; 56 + }, 57 + set(key, value, options) { 58 + headers.append("Set-Cookie", serialize(key, value, options)); 59 + }, 60 + remove(key, options) { 61 + headers.append("Set-Cookie", serialize(key, "", options)); 62 + }, 63 + }, 64 + } 65 + ); 66 + 67 + return { 68 + supabase, 69 + headers, 70 + }; 71 + } 72 + ``` 73 + 74 + This file is for when we are signing up for the first time and receive a confirmation e-mail. Clicking the link with log you in automatically but going forward you will log in through the log in page. 75 + 76 + ```ts 77 + // app/routes/auth.callback.tsx 78 + 79 + import { redirect, type LoaderFunctionArgs } from "@remix-run/node"; 80 + import { createClient } from "~/utils/supabase.server"; 81 + 82 + export async function loader({ request }: LoaderFunctionArgs) { 83 + const requestUrl = new URL(request.url); 84 + const code = requestUrl.searchParams.get("code"); 85 + const next = requestUrl.searchParams.get("next") || "/"; 86 + const { supabase, headers } = createClient(request); 87 + 88 + if (code) { 89 + const { error } = await supabase.auth.exchangeCodeForSession(code); 90 + if (!error) { 91 + return redirect(next, { headers }); 92 + } 93 + } 94 + 95 + return redirect("/auth/auth-error-page", { headers }); 96 + } 97 + ``` 98 + 99 + Sign up page, you will recieve a confirmation e-mail for the first time that you sign up. After you click the link in your e-mail you will be signed in. 100 + 101 + ```ts 102 + // app/routes/signup.tsx 103 + 104 + import { ActionFunctionArgs, json } from "@remix-run/node"; 105 + import { Form } from "@remix-run/react"; 106 + import { createClient } from "~/utils/supabase.server"; 107 + 108 + export async function action({ request }: ActionFunctionArgs) { 109 + const formData = await request.formData(); 110 + const email = formData.get("email") as string; 111 + const password = formData.get("password") as string; 112 + const { supabase } = createClient(request); 113 + 114 + const { error } = await supabase.auth.signUp({ 115 + email, 116 + password, 117 + }); 118 + 119 + if (error) { 120 + return json({ message: error.message }, { status: 400 }); 121 + } 122 + return null; 123 + } 124 + 125 + export default function Component() { 126 + return ( 127 + <div> 128 + <h1>Sign up</h1> 129 + <Form method="POST"> 130 + <label htmlFor="email">E-mail</label> 131 + <input type="email" name="email" id="email" /> 132 + <label htmlFor="password">Password</label> 133 + <input type="password" name="password" id="password" /> 134 + <button type="submit">Sign up</button> 135 + </Form> 136 + </div> 137 + ); 138 + } 139 + ``` 140 + 141 + Log in page, if all goes well then you will be redirected to the home page after logging in successfully. 142 + 143 + ```ts 144 + // app/routes/login.tsx 145 + 146 + import { ActionFunctionArgs, json } from "@remix-run/node"; 147 + import { Form } from "@remix-run/react"; 148 + import { redirect } from "react-router"; 149 + import { createClient } from "~/utils/supabase.server"; 150 + 151 + export async function action({ request }: ActionFunctionArgs) { 152 + const formData = await request.formData(); 153 + const email = formData.get("email") as string; 154 + const password = formData.get("password") as string; 155 + const { supabase, headers } = createClient(request); 156 + 157 + const { error } = await supabase.auth.signInWithPassword({ 158 + email, 159 + password, 160 + }); 161 + 162 + if (error) { 163 + return json({ message: error.message }, { status: 400 }); 164 + } 165 + return redirect("/", { headers }); 166 + } 167 + 168 + export default function Component() { 169 + return ( 170 + <div> 171 + <h1>Log in</h1> 172 + <Form method="POST"> 173 + <label htmlFor="email">E-mail</label> 174 + <input type="email" name="email" id="email" /> 175 + <label htmlFor="password">Password</label> 176 + <input type="password" name="password" id="password" /> 177 + <button type="submit">Log in</button> 178 + </Form> 179 + </div> 180 + ); 181 + } 182 + ``` 183 + 184 + To verify that everything is working correctly, you can display the information of the currently logged in user. 185 + 186 + ```ts 187 + // app/_index.tsx 188 + 189 + import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; 190 + import { useLoaderData } from "@remix-run/react"; 191 + import { createClient } from "~/utils/supabase.server"; 192 + 193 + export const meta: MetaFunction = () => { 194 + return [ 195 + { title: "New Remix App" }, 196 + { name: "description", content: "Welcome to Remix!" }, 197 + ]; 198 + }; 199 + 200 + export async function loader({ request }: LoaderFunctionArgs) { 201 + const { supabase } = createClient(request); 202 + 203 + const { data } = await supabase.auth.getSession(); 204 + 205 + return { user: data?.session?.user }; 206 + } 207 + 208 + export default function Index() { 209 + const { user } = useLoaderData<typeof loader>(); 210 + return ( 211 + <div> 212 + <h1>Currently logged in user</h1> 213 + <pre>{JSON.stringify(user, null, 2)}</pre> 214 + </div> 215 + ); 216 + } 217 + ``` 218 + 219 + Finally, add a log out button and resource route so that you can sign the user out. 220 + 221 + ```diff 222 + export default function Index() { 223 + const { user } = useLoaderData<typeof loader>(); 224 + return ( 225 + <div> 226 + <h1>Currently logged in user</h1> 227 + <pre>{JSON.stringify(user, null, 2)}</pre> 228 + + <Form method="POST" action="/logout"> 229 + + <button type="submit">Log out</button> 230 + + </Form> 231 + </div> 232 + ); 233 + } 234 + ``` 235 + 236 + ```ts 237 + // app/logout.tsx 238 + 239 + import { ActionFunctionArgs, redirect } from "@remix-run/node"; 240 + import { createClient } from "~/utils/supabase.server"; 241 + 242 + export async function action({ request }: ActionFunctionArgs) { 243 + const { supabase, headers } = createClient(request); 244 + await supabase.auth.signOut(); 245 + return redirect("/login", { headers }); 246 + } 247 + 248 + export const loader = () => redirect("/"); 249 + ``` 250 + 251 + And with that you have fully functioning authentication! 252 + 253 + ## Conclusion 254 + 255 + This is all that is needed to set up authentication, you can explore the other authentication methods if you choose to do so. I have a repository set up with Github authentication that can be used as a starting point [which can be found here](https://github.com/nulfrost/supabase-remix-ssr).