Build A CRUD To-Do App With Authentication Using Vue JS & Firebase
In this Firestore CRUD Vue.js tutorial, I will be guiding you on how to build a real-world To-Do web app with Firebase Authentication.
This is the second part of the Firestore CRUD Vue.js tutorial.
Part #1: Firebase Authentication for Vue.js
Part #2: Build A Secure To-Do App with Vue.js + Firestore (you’re here)
What are we building?

Let’s get started 🚀
- Create a Component and Route for To-Do View
- Structure Firestore Data for To-Do App
- Add User-Specific Data to Cloud Firestore
- Get User-Specific Data with Cloud Firestore
- Update User Data in the Firestore Database
- Delete Data from Cloud Firestore
- Secure To-Do App Using Firestore Rules
If you already have the vue.js project running and added Firebase to your project from the Firebase Authentication for Vue.js tutorial, skip to Create a Component and Route for To-Do View.
Up and Running Vue.js Starter Project
Go ahead and download the starter vue.js project.
CD to the project on your Terminal, and run the following command:
npm install
Once the dependencies installation is complete, go ahead and launch the app by going to the provided localhost URL.
If everything is good, you should have an app running like this:

Configure Firebase to the Vue.js Project
Once the vue project is up and running, the next step will be to add Firebase to your project by going to main.js and replace config code from your Firebase project.
const firebaseConfig = {
apiKey: "****************************",
authDomain: "****************************",
databaseURL: "****************************",
projectId: "****************************",
storageBucket: "****************************",
messagingSenderId: "****************************",
appId: "****************************"
};
If OAuth login buttons such as Facebook and Google do not work, you will have to configure them which are covered in the Firebase Authentication for Vue.js tutorial.
Create A Todo Component and Route
In your vue.js project, go to the src → components → create Todos.vue
file.
Then, add the scaffold code.
<template>
</template>
<script>
export default {
}
</script>
<style>
</style>
Once it’s done, switch to router folder → index.js
file and import the component at the top.
import Todos from '@/components/Todos'
Then, add a new object for todo inside the router array.
{
path: '/todo',
name: 'Todos',
component: Todos,
meta: {
auth: true
}
}
Set auth:true
flag in this todo route object, which will make sure only authenticated users have access to the todo page component.
Switch back to ToDo.vue
file and add HTML code for navigation and heading inside between the starting and ending template tags.
<section>
<navigation></navigation>
<h5 class="center-align">To-Dos</h5>
</section>
I am using Materialize CSS Framework in this project in case you are wondering.
Moving on to the script section.
At the top, a <navigation>
component is added.
<script>
import navigation from "@/components/NavBar.vue";
export default {
components: { navigation }
};
</script>
Then, import NavBar.vue
and add it inside the components object.
It would be nice to have a navigation item at the top of the todos page. Hop over to NavBar.vue
and add the following code inside the ul
element.
<li v-show="user">
<router-link to="/todo">To Do</router-link>
</li>
At this stage, you can access the todo page by going to http://localhost:8080/todo.
The port of your localhost can be different. In my case, it’s 8080.
Before diving into the application, let’s take a moment and think about how to structure / model data for the To-Do application.
Structure Firestore Data for the Todo App
It’s really important to have an idea of how to model or structure your Firestore data before starting to write the code.
This is what I came up with:

Firestore Data Modeling To-Do App
First, create a top-level collection called users and add currently logged in user’s uid as the document ID. You can easily obtain uid using firebase.auth().uid
.
The main reason for using uid instead of an auto-generated ID is to protect each user’s data using Firebase Rules, which I will cover in the Secure To-Do App Using Firestore Rules section.
Inside each user’s document, create a sub-collection called todos which will have multiple todos documents with some fields like title, isCompleted etc.
Pretty straight forward! 🙂
There are multiple ways to structure data for this project. Feel free to let me know if you find a better solution for this app.
Create User-Specific Data To Cloud Firestore
The first step is to add the HTML code snippet under the heading in the ToDo.vue file.
It has a ul
with a single list-item which is the list head.
<ul class="collection with-header">
<li class="collection-header">
<div class="row">
<div class="input-field col s10">
<input id="new_todo" type="text" class="validate" v-model="todo.title" />
</div>
<div class="input-field col s2">
<button class="btn" @click="addTodo">Add</button>
</div>
</div>
</li>
</ul>
As you can see, I have an input field that binds the todo.title
and an add button is bounded to a click event with a call back function called addTodo()
.
Adding the following CSS code will make the ul have a width of 500px and center to the canvas.
<style>
.collection.with-header {
max-width: 500px;
margin: 0 auto;
}
</style>
Next, declare the todo object with a title property.
data() {
return {
todo: {
title: "",
}
};
}
Then, import firebase at the top inside the JavaScript section.
import firebase from "firebase";
Finally, define the addTodo()
callback function inside methods:{}
and make a query to add a new todo data to the Cloud Firestore.
addTodo() {
firebase
.firestore()
.collection("users")
.doc(firebase.auth().currentUser.uid)
.collection("todos")
.add({
title: this.todo.title,
createdAt: new Date(),
isCompleted: false,
})
}
Check out Learn Firestore CRUD Queries in case you want to brush up on it.
Let’s take a look at the Firebase query for adding new data.
Get the reference to the users collection and invoke doc()
method on it by passing currently logged in user’s uid
as an argument.
After that, obtain a reference to a sub-collection called todos and add a document using add()
method by passing a JavaScript object that has three key-value pairs:
- title:string
- createdAt:timestamp
- isCompleted:boolean
At this stage, the app will look like this below.

Let’s add a new todo item.

Nice!
Next, retrieve data from the Cloud Firestore.
Get User-Specific Data with Cloud Firestore
The first step is to define todos
array that will contain todo objects of a currently logged in user.
data() {
return {
todos: [],
todo: {
title: ""
}
};
}
Next, declare getTodos()
function inside methods:{}
In the code below, I am using async await syntax for this asynchronous operation.
This query is very similar to the add()
one. Get a reference to the users collection and invoke doc()
method by passing currently logged in user’s uid
as an argument.
async getTodos() {
var todosRef = await firebase
.firestore()
.collection("users")
.doc(firebase.auth().currentUser.uid)
.collection("todos");
todosRef.onSnapshot(snap => {
this.todos = [];
snap.forEach(doc => {
var todo = doc.data();
todo.id = doc.id;
this.todos.push(todo);
});
});
}
Then, get a reference to the todos sub-collection and store the query object to a variable called todosRef
.
Run onSnapshot()
method on the todoRef query object. I could use get()
but I want to see the real-time change to the view as Cloud Firestore Database changes.
Inside the onSnapshot()
method, iterate through the snap object and set the document data using doc.data()
to a variable called todo
. Also, add an ID property to that object and set it to auto-generated ID using doc.id
.
Finally, push the todo object to the todos array that was created earlier.
Make sure to reset the todos array, which is on the first line inside the onSnapshot()
method to avoid duplicated data.
Now, invoke getTodos()
function inside created() {}
method.
created() {
this.getTodos();
},
The third step is to add HTML code.
Inside the unordered list, after the first header style list-item, add the following:
<li class="collection-item" v-for="todo in todos" :key="todo.id">
{{todo.title}}
</li>
Iterate through the todos array using v-for
and show the title using todo.title
.
Pretty straight forward!

Update User-Specific Data in Cloud Firestore
It would be nice to have a checkbox on the right side of each todo list item to allow users to check when it is done.
To make it happen, add checkbox input to an existing HTML code.
<li class="collection-item" v-for="todo in todos" :key="todo.id" :class="{ fade: todo.isCompleted }">
{{todo.title}}
<span class="secondary-content">
<label>
<input type="checkbox" class="filled-in" :checked="todo.isCompleted"
@change="updateTodoItem(todo.id, $event)" />
<span></span>
</label>
</span>
</li>
There will be a change event attached to each todo list item. The callback function for the change event is updatedTodoItem()
that takes two arguments.
The first one is the auto-generated ID of the todo list item that I can access using todo.id
and the second one is an event
object.
Let’s declare a change event callback function updateTodoItem()
inside methods:{}
object.
updateTodoItem(docId, e) {
var isChecked = e.target.checked;
firebase
.firestore()
.collection("users")
.doc(firebase.auth().currentUser.uid)
.collection("todos")
.doc(docId)
.update({
isCompleted: isChecked
});
},
When a user clicks the checkbox, capture the checked status using e.target.checked
and set it to a variable isChecked
.
As you can see, this query is very similar to the previous one.
Use the update()
method to change the value of isCompleted
by assigning isChecked
value to it.
If you click the checkbox at this stage, you can see the isCompleted
value changes to true
on the Cloud Firestore.
The change will reflect immediately on the view.
The persistence is happening because I have set todo.isCompleted
value to :checked
HTML property.

Nice.
Let’s move on to how users can delete their own todo list items.
Delete User Data from Cloud Firestore
Add a span element inside the list item with a class named deleteIcon.
<li class="collection-item" v-for="todo in todos" :key="todo.id" :class="{ fade: todo.isCompleted }">
<span class="deleteIcon" @click="deleteToDo(todo.id)">✕</span>
{{todo.title}}
<span class="secondary-content">
<label>
<input type="checkbox" class="filled-in" :checked="todo.isCompleted"
@change="completedPressed(todo.id, $event)" />
<span></span>
</label>
</span>
</li>
Attach a click event to the span element with a callback function deleteTodo()
and pass an ID to it as an argument.
Then, create deleteTodo()
function inside methods:{}
.
deleteToDo(docId) {
firebase
.firestore()
.collection("users")
.doc(firebase.auth().currentUser.uid)
.collection("todos")
.doc(docId)
.delete();
}
Delete is pretty straight forward and all you have to do is to invoke delete()
method to an appropriate document and done!
Here is the CSS for the delete section.
.deleteIcon {
margin-right: 10px;
cursor: pointer;
}
.deleteIcon:hover {
opacity: 0.5;
}
At this stage, the todo CRUD app is fully functional.
But…
It’s NOT secure!

Secure Users Data Using Firestore Rules
The REAL security lies on the Firebase Rules.
Let me say it again.
The REAL security lies on the Firebase Rules.
Look at the current security rules by going to the Firebase Console → Authentication → Rules Tab
This security rule will allow anyone to have the ability to read or write to any location to the Cloud Firestore Database which is good for development and testing but definitely NOT for production.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Let’s change the Firebase Rules so that only logged in users can read or write to the Database like below. You can access uid using request.auth.uid and check to see if it exists.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}
This will make sure that only authenticated users can have permission to read or write (create, update and delete) to the Cloud Firestore Database.
But, it can let any user modify any other users data.
For example, let’s say userA is logged in and added some todo items. UserB is logged with a different machine and also added some todos. If userA hacks UserB’s uid from the browser, UserA can get UserB’s data as long as UserA is logged in, which is INSECURE.
To prevent this, the Firebase Rule should only authorize users to modify their own data, not others.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid}/todos/{todoId} {
allow read, write: if request.auth.uid == uid
}
}
}
Now, only authorized users can modify their data in the following database path users/{userId}/todos/{todoId} as long as the userId on the users collection matches to the currently logged in user’s uid.
There you have it! 🙂
Conclusion
In this Firestore CRUD Vue.js tutorial, you have learned how to do CRUD queries with Firestore by building a fully functional To-Do Application.
Then, I showed you how to secure the To-Do app using Firestore Rules.
Download the sample source code on Github.
NEXT → Build A Custom Payment Form using Stripe + Cloud Functions