Managing users and authentication in a web application can be a complex task. In this blog, we will explore how to implement user management using Nuxt 3 and Supabase. Supabase is built on top of PostgreSQL and offers CRUD API, Realtime API, and Auth API, making it an ideal choice for our user management needs. While we will mostly focus on the Supabase part of the implementation and skip most of the UI aspects of the application.
Project Setup
Let's start by setting up a new Nuxt project:
npx create-nuxt-app nuxt-supabase-auth
cd nuxt-supabase-auth
Next, we install the required dependencies, @nuxtjs/supabase
, and @supabase/supabase-js
, to easily integrate Supabase with Nuxt:
npm i
npm install @nuxtjs/supabase --save-dev
npm install @supabase/supabase-js
To configure the Nuxt project with Supabase, we modify the nuxt.config.ts
file:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/supabase'],
})
Before we can proceed, we need to create a Supabase project. Visit https://supabase.com/dashboard/projects, create an account, and start a new project. Once the project is set up, obtain the Project URL
and API Key
. Create a .env
file in the project's root directory and add the credentials:
SUPABASE_URL="https://example.supabase.co"
SUPABASE_KEY="<your_key>"
Replace SUPABASE_URL
with your Project URL
and SUPABASE_KEY
with the corresponding API Key
. These environment variables will be used by the Nuxt Supabase module to connect to the database.
Update app.vue
to use the NuxtPage
component:
// app.vue
<template>
<NuxtPage />
</template>
Next, create a landing page at pages/index.vue
with a profile button (which redirects users to their profile page) and a logout button:
For user logout functionality, we utilize the useSupabaseClient()
composable, composable which gives us access to the Supabase client anywhere in our application.
// pages/index.vue
const client = useSupabaseClient();
const logout = async () => {
const { error } = await client.auth.signOut();
console.log('logout')
if (error) throw error
navigateTo('/login')
}
User Sign-Up
To implement user sign-up, we create a login page at pages/login.vue
and add a login form:
We introduce a ref
named email
, bound to our input tag. Additionally, we create a computed property
, ValidEmail
, using regex to validate the email's format.
// pages/login.vue
const email = ref<string>('');
const ValidEmail = computed<boolean>(() => {
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email.value)) {
return true;
}
return false;
})
For user sign-in, we use auth.signInWithOtp
, which sends a magic link to the provided email. The user is redirected to the /confirm
page upon clicking the link. The loading
and msg
refs are used to update the UI as needed.
// pages/login.vue
const client = useSupabaseClient();
const loading = ref<boolean>(false);
const msg = ref<string>('');
const SignInUser = async () => {
loading.value = true
const { error } = await client.auth.signInWithOtp({
email: email.value,
options: {
emailRedirectTo: `${location.origin}/confirm`,
}
})
if (error) {
msg.value = error.message;
loading.value = false
throw error
} else {
msg.value = "Check your email for the login link!"
}
loading.value = false
}
Supabase gives us access to multiple providers which you can use to sign in users. For this example, we would use signInWithOtp
which logs in an existing user or adds the user to auth.users
and logs them in if they are new.
We use the useSupabaseUser()
hook to get the current user object and watchEffect()
to redirect the user to the homepage upon successful login.
// pages/login.vue
const user = useSupabaseUser()
watchEffect(() => {
if (user.value) {
navigateTo('/')
}
})
Finally, we create the /confirm
route, where users will be redirected when clicking the magic link:
//pages/confirm.vue
<template>
<div class="flex w-full justify-center pt-20 text-3xl">
Redirecting...
</div>
</template>
<script setup>
const user = useSupabaseUser()
watchEffect(() => {
if (user.value) {
navigateTo('/')
}
})
</script>
Remember to configure the Authentication -> URL Configuration -> Redirect URLs
with https://localhost:3000/confirm
(update localhost
with your domain once deployed).
Middleware
To restrict access to certain routes to only logged-in users, we can use Nuxt middleware. Create a middleware
folder in the root directory and add an auth.ts
file:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useSupabaseUser();
if (!user.value) {
return navigateTo("/login");
}
});
You can add this middleware to any page you want to restrict access to by using:
// pages/index.vue
definePageMeta({
middleware: 'auth',
});
Profile Table
While Supabase stores some user data under user.auth
, we may want to store additional user-related data in our own table. Let's create a profile
table in Supabase:
Make the uid
a foreign key connecting to the id
column of the auth.users
table to maintain data integrity.
Creating Triggers
When a user signs up, we want to automatically create a profile for them. We can automate this using PostgreSQL functions and triggers.
- Head over to your Supabase dashboard -> "Database" -> "Functions". Create a new function called
create_profile_for_user
with the return type set as a trigger. Use the following SQL code:
begin
insert into
public.profiles (uid, name, email)
values
(new.id, "Some Name", new.email);
return new;
end;
- Check "Show advanced settings" and select "SECURITY INVOKER" to ensure the function has the required privileges to access the
auth.users
table.
- Now, navigate to "Triggers" and create a new trigger named
create_user_profile_trigger
with the following settings:
Make sure to add the function we created as the Function to Trigger
Profile Page
With everything set up, we can now create a profile page to display a user's data. Let's create a pages/profile.vue
:
// page/profile.vue
definePageMeta({
middleware: 'auth',
});
const client = useSupabaseClient()
const user = useSupabaseUser();
const userName = ref<string>('');
const userEmail = ref<string>('');
const { data } = await client
.from('profiles')
.select(`name, email`)
.eq('uid', user.value?.id)
.single()
if (data) {
userName.value = data.name
userEmail.value = data.email
}
Typesafety (Optional)
Supabase provides us with auto-generated TypeScript types for our database tables. To generate these types, create a types/supabase.ts
file and run:
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
Replace $PROJECT_REF
with the Reference ID found in the general settings of your project dashboard.
To use these types, import Database
type and add it to the client object:
import { Database } from '../types/supabase';
const client = useSupabaseClient<Database>();
Conclusion
In this blog, we explored how to set up user management using Nuxt 3 and Supabase. We covered user sign-up, authentication, restricting access to certain routes, and using middleware for enhanced security. Additionally, we automated profile creation for new users using PostgreSQL triggers. By combining the power of Nuxt 3 and Supabase, we've created a robust user management system with an awesome developer experience.
The source code for this tutorial is available here: https://github.com/AnkushSarkar10/nuxt-supabase-auth
Thank you for reading! 🎉