December 17, 2023
Implementation of the server-side passwordless (email magic link) authentication with Remix and Supabase in 5 simple steps.
npm install @supabase/supabase-js @supabase/ssr
Create a Supabase project (if you don't have one already) and get the URL and anon key from the project API settings.
Create .env
file
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Now everything is set up, and we can start adding the code.
app/supabase.server.ts
import { createServerClient, parse, serialize } from '@supabase/ssr'
export const createSupabaseServerClient = (request: Request) => {
const cookies = parse(request.headers.get('Cookie') ?? '')
const headers = new Headers()
const supabaseClient = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
cookies: {
get(key) {
return cookies[key]
},
set(key, value, options) {
headers.append('Set-Cookie', serialize(key, value, options))
},
remove(key, options) {
headers.append('Set-Cookie', serialize(key, '', options))
},
},
},
)
return { supabaseClient, headers }
}
app/routes/sign-in.tsx
import { json } from '@remix-run/node'
import { Form, useActionData } from '@remix-run/react'
import type { ActionFunctionArgs } from '@remix-run/node'
import { createSupabaseServerClient } from '~/supabase.server'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabaseClient, headers } = createSupabaseServerClient(request)
const formData = await request.formData()
const { error } = await supabaseClient.auth.signInWithOtp({
email: formData.get('email') as string,
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})
// just for this example
// if there is no error, we show "Please check you email" message
if (error) {
return json({ success: false }, { headers })
}
return json({ success: true }, { headers })
}
const SignIn = () => {
const actionResponse = useActionData<typeof action>()
return (
<>
{!actionResponse?.success ? (
<Form method="post">
<input type="email" name="email" placeholder="Your Email" required />
<br />
<button type="submit">Sign In</button>
</Form>
) : (
<h3>Please check your email.</h3>
)}
</>
)
}
export default SignIn
When the user clicks the magic link in the email, they will be redirected to the callback route we defined in the sign-in file - emailRedirectTo
. We need to exchange the code (from the magic link) for a session and redirect the user to the dashboard.
app/routes/auth.callback.tsx
import type { ActionFunctionArgs } from '@remix-run/node'
import { redirect } from '@remix-run/node'
import { createSupabaseServerClient } from '~/supabase.server'
export const loader = async ({ request }: ActionFunctionArgs) => {
const url = new URL(request.url)
const code = url.searchParams.get('code')
if (code) {
const { supabaseClient, headers } = createSupabaseServerClient(request)
const { error } = await supabaseClient.auth.exchangeCodeForSession(code)
if (error) {
return redirect('/sign-in')
}
return redirect('/dashboard', {
headers,
})
}
return new Response('Authentication faild', {
status: 400,
})
}
If the sign-in is successful, the user will be redirected to the dashboard route.
On the dashboard page, we want to check if the user is authenticated.
app/routes/dashboard.tsx
import { redirect } from '@remix-run/node'
import type { LoaderFunctionArgs } from '@remix-run/node'
import { Form } from '@remix-run/react'
import { createSupabaseServerClient } from '~/supabase.server'
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { supabaseClient } = createSupabaseServerClient(request)
const {
data: { user },
} = await supabaseClient.auth.getUser()
if (!user) {
return redirect('/')
}
return new Response(null)
}
const Dashboard = () => {
return (
<div>
<h1>Dashboard</h1>
<Form action="/sign-out" method="post">
<button type="submit">Sign Out</button>
</Form>
</div>
)
}
export default Dashboard
You'll notice that we added a sign-out button. Now we need to create a sign-out route.
app/routes/sign-out.tsx
import { redirect } from '@remix-run/node'
import type { ActionFunctionArgs } from '@remix-run/node'
import { createSupabaseServerClient } from '~/supabase.server'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabaseClient, headers } = createSupabaseServerClient(request)
// check if user is logged in
const {
data: { session },
} = await supabaseClient.auth.getSession()
if (!session?.user) {
return redirect('/')
}
// sign out
await supabaseClient.auth.signOut()
return redirect('/', {
headers,
})
}