Implementing authentication is something that you do on most projects, but still something that you may not remember how to do by memory because of how often you actually do it.
Here is a quick how-to about implementing Supabase Auth with Nuxt v3. In this example, we will be using OTP, but it applies to every case.
You will first want to start your project by going to Supabase's website.
After creating a project in Supabase and starting your project on Nuxt, we want to then install the Supabase Nuxt package by doing:
npx nuxi@latest module add supabase
We will then create our .env file and add the following environment variables:
SUPABASE_URL=<your_supabase_url> SUPABASE_KEY=<your_supabase_key>
You can find these on the Supabase dashboard for your project, under Settings -> API
Afterwards, we can being setting up our project. I have made 2 very basic files so far:
import { defineStore } from "pinia"; export const useAuthStore = defineStore("auth", () => { const supabase = useSupabaseClient(); const sendOtp = async (email: string) => { const { error } = await supabase.auth.signInWithOtp({ email, }); if (error) { throw error; } return true; }; const verifyOtp = async (email: string, otp: string) => { const { error } = await supabase.auth.verifyOtp({ type: "email", token: otp, email, }); if (error) { throw error; } return true; }; return { sendOtp, verifyOtp, }; });
<template> <div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md"> <h2 class="text-3xl font-bold mb-6 text-center text-gray-800">Welcome</h2> <form @submit.prevent="handleSubmit" class="space-y-6"> <div v-if="mode === 'email'"> <label for="email" class="block mb-2 font-medium text-gray-700" >Email</label > <input type="email" id="email" v-model="email" required placeholder="Enter your email" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200" /> </div> <div v-else-if="mode === 'code'"> <p class="mb-2 font-medium text-gray-700"> Enter the 6-digit code sent to {{ email }} </p> <input type="text" v-model="otpCode" required placeholder="Enter 6-digit code" maxlength="6" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200" /> </div> <UButton icon="i-heroicons-paper-airplane" size="lg" color="primary" variant="solid" :label="buttonLabel" :trailing="true" block /> </form> </div> </template> <script setup lang="ts"> import { ref, computed } from "vue"; import { useAuthStore } from "~/stores/auth"; const authStore = useAuthStore(); const email = ref(""); const otpCode = ref(""); const mode = ref("email"); const buttonLabel = computed(() => { return mode.value === "email" ? "Send One-Time Password" : "Verify Code"; }); const handleSubmit = async () => { if (mode.value === "email") { try { await authStore.sendOtp(email.value); mode.value = "code"; } catch (error) { console.log("Error sending OTP: ", error); } } else { try { await authStore.verifyOtp(email.value, otpCode.value); } catch (error) { console.log("Error verifying OTP: ", error); } } }; </script> <style scoped></style>
Note that I am also using NuxtUI, in case you get any errors.
Because by default, the signInWithOtp function sends a magic link, you will have to change the email template on Supabase's dashboard to send a token:
This is found under Authentication -> Email Templates -> Change Confirm Signup and Magic Link templates to use {{ .Token }}
And that is pretty much it, you have a working auth!
If you want to add signout, you can also add a method to your previous file like such:
const signOut = async () => { const { error } = await supabase.auth.signOut(); if (error) { throw error; } return true; };
However, if you want to protect certain routes, we can also do that adding middleware.
Create a folder on the root called middleware (name is key) and a file called auth.ts.
You can then add something like this:
export default defineNuxtRouteMiddleware((to) => { const user = useSupabaseUser(); const protectedRoutes = ["/app"]; if (!user.value && protectedRoutes.includes(to.path)) { return navigateTo("/auth"); } if (user.value && to.path === "/auth") { return navigateTo("/"); } });
This will basically protect from the server your /app route, so if you attempt to go to /app without being signed in, you will be redirected to /auth.
Likewise, if you try to visit /auth while already being signed in, you will be redirected to the home page /.
Now, to use this, you can place it inside the