Skip to main content

Build a ToDo App

In this tutorial, you will learn how to build a basic single-party decentralized ToDo app written using web5.js. It’s composed of a Vue.js app powered by web5.js which adds data storage underpinned by a Decentralized Web Node (DWN).

The ToDo app is a basic single-user, local application that only leverages web5.js for storage purposes, but serves as an important precursor to building larger decentralized apps.

By the end of this tutorial, you’ll have a solid understanding of the basics of creating, reading, and writing to a DWN and will be on your way to creating serverless, decentralized applications.

Getting the Skeleton App

Download a copy of the skeleton ToDo app by running:

git clone
cd web5-tutorials
git checkout tags/0.2.0 -b todo-app-tutorial
cd todo-app

In this tutorial, you’ll work through completing the /todo-app/src/App.vue file to use web5.js.

Finished ToDo App

If you’d like to skip ahead and see the finished version of this tutorial, you can download and run that example by executing:

git clone
cd incubating-web5-labs
git checkout tags/0.2.0 -b incubating-web5-labs-todo-app
cd todo-app
npm install
npm run dev

There is also a hosted example deployed at


Add Web5 to package.json in the dependencies section:

  "@tbd54566975/web5": "0.7.2",

Download the necessary packages by running these commands:

npm install
npm run dev

You should now have the app running on https://localhost:5173; this will be a starter application with a mostly blank page. In this tutorial, we'll build the rest.

App Architecture

There are 3 main components to your ToDo app: HTML, CSS, and Javascript. The way that web5.js changes the layout of your ToDo app compared to the way a traditional web app would be laid out is by replacing the RESTful calls you’d normally make with calls to the DWN.


Think of web5.js as your storage, RESTful API service, and the backing data store.

Because we’ve built this sample using Vue.js, you’ll find both the HTML structure of the app and the JS logic within the App.vue file. Your ToDo app will have a few main responsibilities to implement:

  • Create a new DWN for the user or load their saved DWN for storage
  • Display ToDos
  • Create ToDos
  • Delete ToDos
  • Toggle ToDo status

Initialize Constants and Variables

Open src/App.vue in your editor and add this next to the other imports:

import { Web5 } from '@tbd54566975/web5';

First we'll set up instances to hold data you'll use throughout your application.

Copy the following block and paste under all import statements in src/App.vue:

let web5;
let myDid;
const todos = ref([]);

The web5 object is the single entry point for all Web5 operations. You've also set up a todos array to hold your ToDos, and a variable to remember the app user's decentralized identifier (DID) as myDid.

Create DID and Web5 Instance

The first time a user accesses your ToDo app, you’ll need to handle creating an “account” for them - this means ensuring they have a DID and DWN available to access their app data. Once they come back for subsequent sessions, you’ll want to fetch and load that data for them.

Add to src/App.vue:

onBeforeMount(async () => {
({ web5, did: myDid } = await Web5.connect());

Displaying Todos

You can now make calls to the DWN to fetch, write, and delete data. To load your user’s existing ToDos, call:

onBeforeMount(async () => {
// Populate ToDos from DWN
const { records } = await web5.dwn.records.query({
message: {
filter: {
schema: ''
dateSort: 'createdAscending'

Once you’ve loaded the query data, you can map this data from the DWN into an object for your app to use:

onBeforeMount(async () => {
// Add entry to ToDos array
for (let record of records) {
const data = await;
const todo = { record, data, id: };

Now that your app user's ToDos are stored in their DWN, you can display them by using the todos object. In this example, you can do it by adding the following HTML code to the Vue app below the JS, just as you would in any non-Web5 app.

In the template where you see <!-- ToDos --> add this:

<div v-if="(todos.length > 0)" class="border-gray-200 border-t border-x mt-16 rounded-lg shadow-md sm:max-w-xl sm:mx-auto sm:w-full">
<div v-for="todo in todos" :key="todo" class="border-b border-gray-200 flex items-center p-4">
<div @click="toggleTodoComplete(todo)" class="cursor-pointer">
<CheckCircleIcon class="h-8 text-gray-200 w-8" :class="{ 'text-green-500': }" />
<div class="font-light ml-3 text-gray-500 text-xl">
{{ }}
<!-- Delete ToDo goes here later -->

Note that this UI code fulfills more than just loading the ToDos - it also provides the ability to toggle completion status and delete ToDos, which we’ll get to in a bit.

Adding ToDos

To allow for add functionality, begin by creating the UI needed to add ToDos.

In the template add this code block to <!-- Add ToDos Form -->:

<div class="mt-16">
<form class="flex space-x-4" @submit.prevent="addTodo">
<div class="border-b border-gray-200 sm:w-full">
<label for="add-todo" class="sr-only">Add a todo</label>
rows="1" name="add-todo" id="add-todo" v-model="newTodoDescription"
class="block border-0 border-transparent focus:ring-0 p-0 pb-2 resize-none sm:text-sm w-96"
placeholder="Add a Todo" />
<button type="submit" class="bg-indigo-600 border border-transparent focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 hover:bg-indigo-700 inline-flex items-center p-1 rounded-full shadow-sm text-white">
<PlusIconMini class="h-5 w-5" aria-hidden="true" />

Then, writing a ToDo into storage is done by binding a addTodo() method to the UI and leveraging the newTodoDescription model referenced in the textarea to call the web5 method dwn.records.write:

// Adding ToDos
const newTodoDescription = ref('');

async function addTodo() {
const todoData = {
completed : false,
description : newTodoDescription.value

newTodoDescription.value = '';

// Create the record in DWN
const { record } = await web5.dwn.records.create({
data : todoData,
message : {
schema : '',
dataFormat : 'application/json'

This code uses the myDid object we loaded or created in the onBeforeMount function and uses it to write into the DWN that web5 is using to store your app user's data.

Once we’ve written the data to storage, we’ll use the returned record to create your app's todo object, passing along the :

async function addTodo() {
// add DWeb message recordId as a way to reference the message for further operations
// e.g. updating it or overwriting it
const data = await;
const todo = { record, data, id: };

Deleting ToDos

Deleting ToDos can be done by using the deleteTodo method, passing the todoItem:

// Delete ToDo
async function deleteTodo(todoItem) {
let deletedTodo;
let index = 0;

for (let todo of todos.value) {
if ( === {
deletedTodo = todo;
index ++;

todos.value.splice(index, 1);

// Delete the record in DWN
await web5.dwn.records.delete({
message: {

We’ll connect that to the UI with the following code. This replaces the <!-- Delete ToDo goes here later --> comment we placed from above:

<div class="ml-auto">
<div @click="deleteTodo(todo)" class="cursor-pointer">
<TrashIcon class="h-8 text-gray-200 w-8" :class="'text-red-500'" />

Toggling ToDo Status

To toggle a ToDo’s status, you’ll need to change its status both in your local todos object and in your web5-managed DWN. You can do so by modifying the todos object and then using a record.update call to update the ToDo in your data store:

// Toggling ToDo Status
async function toggleTodoComplete(todoItem) {
let toggledTodo;
let updatedTodoData;

for (let todo of todos.value) {
if ( === {
toggledTodo = todo; = !;
updatedTodoData = { ...toRaw( };

// Get record in DWN
const { record } = await{
message: {

// Update the record in DWN
await record.update({ data: updatedTodoData });

Congratulations, you've just built a decentralized web app! Feel free to check out the finished version of the ToDo app here or see it deployed here.

Was this page helpful?

Connect with us on Discord

Submit feedback: Open a GitHub issue

Edit this page: GitHub Repo

Contribute: Contributing Guide