Introduction
Authentication is an important feature in web applications today, but many developers have difficulties setting it up in their applications. Thankfully, there are services and libraries out there that help lift this heavy burden off our hands.
Today we’ll go over how to use Supabase to handle user authentication in our Vue applications; specifically, how to build a Vue application with authentication. Supabase will serve as the backend for authentication in an app with sign in and sign up functionality, along with a private route that can only be accessed with valid credentials.
What is Supabase?
Supabase is often best described as an open source alternative to Firebase. It offers some of the key features of Firebase, one of which includes user authentication and management.
Supabase provides support for different external auth providers such as passwords, phone numbers, and identity providers such as Google, Twitter, Facebook, and Github.
Setting up Vue
To get started, we’ll be using the Vue CLI to quickly scaffold a new Vue project. The CLI can be installed globally by running the following command:
npm install -g @vue/cli
# OR
yarn global add @vue/cli
Next, run the following command to create a Vue project:
vue create supabase-auth
You’ll be prompted to pick a preset; pick the option to manually select features. Once there, select Router and Vuex and click E**nter, then choose Vue version 3.x, as we’ll be using the new composition API. Finally, click E**nter on all other selections to get your Vue app ready.
Setting up Supabase
To get started, first you’ll have to create an account by visiting the Supabase login page and proceed to sign in using your Github account.
After signing in to the dashboard, click on the new project button to create your first project. You should see the following modal pop up:
Choose a name for your project, a database password, and a region close to you. It will take some time for the project to be fully created. After it's done, go to Settings, then API, and copy the URL and anonymous public API key:
Create a .env.local
file in the root of your project and save the credentials in it as such:
VUE_APP_SUPABASE_URL=YOUR_SUPABSE_URL
VUE_APP_SUPABASE_PUBLIC_KEY=YOUR_SUPABSE_PUBLIC_KEY
Setting up the Supabase client library
Run the following command to install the Supabase client library:
yarn add @supabase/supabase-js
Next we’ll have to initialize Supabase by creating a supabase.js
file in our src
directory and pasting in the following code:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.VUE_APP_SUPABASE_URL
const supabaseAnonKey = process.env.VUE_APP_SUPABASE_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Creating our pages
Now let’s create some Vue component pages to handle the signup and login functionalities of our project, along with a dashboard page.
In this tutorial, we won’t go into styling our application in order to avoid clouding up our html markup, but you can always choose to style yours however you’d like.
Here’s the markup for our SignIn
page:
<!-- src/views/SignIn.vue -->
<template>
<div>
<h1>Login Page</h1>
<form @submit.prevent="signIn">
<input
class="inputField"
type="email"
placeholder="Your email"
v-model="form.email"
/>
<input
class="inputField"
type="password"
placeholder="Your Password"
v-model="form.password"
/>
<button type="submit">Sign In</button>
</form>
<p>
Don't have an account?
<router-link to="/sign-up">Sign Up</router-link>
</p>
</div>
</template>
Now let’s do the markup for our SignUp
page:
<!-- src/views/SignUp.vue -->
<template>
<div>
<h1>SignUp Page</h1>
<form @submit.prevent="signUp">
<input
class="inputField"
type="email"
placeholder="Your email"
v-model="form.email"
/>
<input
class="inputField"
type="password"
placeholder="Your Password"
v-model="form.password"
/>
<button type="submit">Sign Up</button>
</form>
<p>
Already have an account?
<router-link to="/sign-in">Log in</router-link>
</p>
</div>
</template>
And finally, our Dashboard
page:
<!-- src/views/Dashboard.vue -->
<template>
<div>
<h1>Welcome to our Dashboard Page</h1>
<button @click.prevent="signOut">Sign out</button>
<p>Welcome: {{ userEmail }}</p>
</div>
</template>
Setting up routes with Vue Router
Now that we’ve created our pages, we need to set up routes so that we can move between them. For this, we will be using Vue Router.
Let’s declare routes for our different pages in our router file as such:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
function loadPage(view) {
return () =>
import(
/* webpackChunkName: "view-[request]" */ `@/views/${view}.vue`
);
}
const routes = [
{
path: '/',
name: 'Dashboard',
component: loadPage("Dashboard"),
meta: {
requiresAuth: true,
}
},
{
path: '/sign-up',
name: 'SignUp',
component: loadPage("SignUp")
},
{
path: '/sign-in',
name: 'SignIn',
component: loadPage("SignIn")
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
// get current user info
const currentUser = supabase.auth.user();
const requiresAuth = to.matched.some
(record => record.meta.requiresAuth);
if(requiresAuth && !currentUser) next('sign-in');
else if(!requiresAuth && currentUser) next("/");
else next();
})
export default router
The meta
object in our first route is used to hold extra information about that route. It has a property named requiresAuth
which is set to true
, and we’re going to use this property to guard this route against unauthenticated users.
From lines 34-42, we’re setting up what is known as a Navigation Guard.
What’s happening in the code is a check to determine whether a certain route requires authentication, and if a user is currently logged in. If the route requires authentication and no one is logged in, the user is redirected to the sign-in
route. But if the route requires authentication and there is a user logged in, then the user is redirected to the dashboard private route.
Setting up Vuex
Vuex is a tool available in Vue applications that is used to store data accessible by all components in our application. It has its own set of rules that ensure the stored data can be changed and updated accordingly.
We are going to store all of the logic for our components here in Vuex.
One caveat to using Vuex is that once the page is reloaded, all stored data resets. To solve this problem we’ll use vuex-persistedstate. This package helps save data stored in Vuex even after the page reloads.
Enter the following in your terminal to install vuex-persistedstate:
yarn add vuex-persistedstate
#OR
npm install --save vuex-persistedstate
Configuring our Vuex store
Here, we are configuring vuex-persistedstate
, then importing Supabase and Vue Router. We’ll be needing them to create our Vuex store actions:
import { createStore } from 'vuex'
import createPersistedState from "vuex-persistedstate";
import router from '../router';
import { supabase } from "../supabase";
// Create store
export default createStore({
state:{},
mutations:{},
actions:{},
plugins: [createPersistedState()]
});
Storing data in State
The state
object in our Vuex store is what actually stores the data. Here we can define the default values of our data:
state: {
user:null
};
In our state
object, we set the default value of user
to null
, as this is the value that it takes on when the user is not signed in to our application.
Changing the state with mutations
Mutations are the only way we can change the state
object in our Vuex store.
A mutation takes in the state
and a value from the action committing it like so:
mutations: {
setUser(state, payload) {
state.user = payload;
},
},
When this mutation is committed, it changes the default value of our user
state to whatever value is being passed to it.
Using Actions
to commit mutations
The actions
object contains functions that can be used to commit mutations in order to change the state in our application. Actions can also dispatch other actions. For our example app, we will be using three different actions: sign up, sign in, and sign out.
Sign up action
Our signUpAction
action takes in form data, then calls on the Supabase signup function. This function takes in the collected form data, validates it, and creates a new user if all the requirements are met:
async signUpAction({dispatch}, form) {
try {
const { error } = await supabase.auth.signUp({
email: form.email,
password: form.password,
});
if (error) throw error;
alert("You've been registered successfully");
await dispatch("signInAction", form)
} catch (error) {
alert(error.error_description || error.message);
}
},
Once the user has been created, an alert pops up with a success message, then dispatches the signInAction
action. The singInAction
takes in our form data and logs our newly registered user so they can access the private dashboard route. If at any point it fails, an error alert pops up.
Sign in action
The signInAction
action also takes in form data filled by the user. It passes this data on to our Supabase signIn
function, which validates this data against our user table to check if such user exists. If so, the user is logged in and redirected to the private dashboard route.
Next, we commit the setUser
mutation, which sets the value of our user
state to the email of the user currently logged in:
async signInAction({ commit }, form) {
try {
const { error, user } = await supabase.auth.signIn({
email: form.email,
password: form.password,
});
if (error) throw error;
alert("You've Signed In successfully");
await router.push('/')
commit('setUser', user.email)
} catch (error) {
alert(error.error_description || error.message);
}
},
Sign out action
Our signOutAction
action invokes the Supabase signOut
function, resets the value of our user
state back to null, then redirects the user back to the sign in page:
async signOutAction({ commit }) {
try {
const { error } = await supabase.auth.signOut();
if (error) throw error;
commit('setUser', null)
alert("You've been logged Out successfully");
await router.push("/sign-in");
} catch (error) {
alert(error.error_description || error.message);
}
},
At the end, this is what your Vuex store should look like:
// src/store/index.js
import { createStore } from 'vuex'
import createPersistedState from "vuex-persistedstate";
import router from '../router';
import { supabase } from "../supabase";
export default createStore({
state: {
user: null,
},
mutations: {
setUser(state, payload) {
state.user = payload;
},
},
actions: {
async signInAction({ commit }, form) {
try {
const { error, user } = await supabase.auth.signIn({
email: form.email,
password: form.password,
});
if (error) throw error;
alert("You've Signed In successfully");
await router.push('/')
commit('setUser', user.email)
} catch (error) {
alert(error.error_description || error.message);
}
},
async signUpAction({dispatch}, form) {
try {
const { error} = await supabase.auth.signUp({
email: form.email,
password: form.password,
});
if (error) throw error;
alert("You've been registered successfully");
await dispatch("signInAction", form)
} catch (error) {
alert(error.error_description || error.message);
}
},
async signOutAction({ commit }) {
try {
const { error } = await supabase.auth.signOut();
if (error) throw error;
commit('setUser', null)
alert("You've been logged Out successfully");
await router.push("/sign-in");
} catch (error) {
alert(error.error_description || error.message);
}
},
},
modules: {
},
plugins: [createPersistedState()],
})
Adding logic to components
It’s time for us to rewind a bit and make the components we created a while ago to fully functional by adding some logic.
Let’s start with our SignUp
component:
<!-- src/views/SignUp.vue -->
<template>
<div>
<!-- Our markup goes here -->
</div>
</template>
<script>
import { reactive } from "vue";
import { useStore } from "vuex";
export default {
setup() {
// wrap data gotten from form input in vue's reactive object
const form = reactive({
email: "",
password: "",
});
//create new store instance
const store = useStore();
const signUp = () => {
// dispatch the signup action to register new user
store.dispatch("signUpAction", form);
};
return {
form,
signUp,
};
},
};
</script>
Now, let’s add logic to our SignIn
component. The SignIn
and SignUp
components are similar; the only difference is in calling the signIn
function instead of the signUp
function:
<!-- src/views/SignIn.vue -->
<template>
<div>
<!-- Our markup goes here -->
</div>
</template>
<script>
import { reactive } from "vue";
import { useStore } from "vuex";
export default {
setup() {
// wrap data gotten from form input in vue's reactive object
const form = reactive({
email: "",
password: "",
});
//create new store instance
const store = useStore();
const signUp = () => {
// dispatch the sign in action to Log in the user
store.dispatch("signInAction", form);
};
return {
form,
signIn,
};
},
};
</script>
Let’s also add logic to the Dashboard
component so our logged-in user can log out when they want:
<!-- src/views/Dashboard.vue -->
<template>
<div>
<h1>Welcome to our Dashboard Page</h1>
<button @click.prevent="signOut">Sign out</button>
<p>Welocome: {{ userEmail }}</p>
</div>
</template>
<script>
import { useStore } from "vuex";
import { computed } from "vue";
export default {
setup() {
//create store instance
const store = useStore();
// Fetches email of logged in user from state
const userEmail = computed(() => store.state.user);
const signOut = () => {
// dispatch the sign out action to log user out
store.dispatch("signOutAction");
};
return {
signOut,
userEmail,
};
},
};
</script>
That wraps up all the logic we need to get our components up and running.
Conclusion
In this tutorial, we reviewed how we can perform user authentication using Supabase and Vue. We also learned how to use Vuex and Vue Router in our Vue apps with the new composition API.
If you want to hit the ground running, the complete source code for this tutorial can be found here.