Easy User Management with Nuxt and Supabase

Easy User Management with Nuxt and Supabase

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.

  1. 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;
  1. Check "Show advanced settings" and select "SECURITY INVOKER" to ensure the function has the required privileges to access the auth.users table.

  1. 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! 🎉