Build a Real-Time Chat App with Vue 3 and Firebase Realtime Database

by Didin J. on Jun 09, 2025 Build a Real-Time Chat App with Vue 3 and Firebase Realtime Database

Learn how to build a real-time chat app using Vue 3 and Firebase Realtime Database with modern tools like Composition API and the Firebase modular SDK.

In this tutorial, you’ll learn how to build a fully functional real-time chat web application using Vue 3 and Firebase Realtime Database. Leveraging Vue’s Composition API and the modern modular Firebase SDK, this tutorial walks you through creating an interactive and scalable messaging app from scratch.

Whether you're a beginner looking to understand real-time data handling or an experienced developer exploring Vue 3’s capabilities, this guide will help you integrate Firebase into your Vue app efficiently using best practices.

Why Vue 3 and Firebase?

Vue 3 is a progressive JavaScript framework that offers reactive components and a simplified development experience through its Composition API. Combined with Firebase, a powerful Backend-as-a-Service (BaaS), you get real-time data sync, authentication, and hosting—all without managing a backend server.

Firebase’s Realtime Database is particularly suited for chat apps as it supports live updates, allowing users to see new messages instantly without refreshing the page.

What You’ll Build

By the end of this tutorial, you’ll have created a Vue 3 web chat application with the following features:

  • User authentication using Firebase Auth

  • Sending and receiving messages in real time

  • Automatically updating the chat UI with new messages

  • Responsive design using Vue and CSS

 

The flow of this Vue.js Firebase chat web app is very simple.

  1. When the user goes to the Vue.js app URL, it will redirect to the login page, which only has a nickname as a login field.
  2. After login, redirect to the select chat room page, which has a list of existing chat rooms and a button to add a new chat room.
  3. If you click on the add button, it will be redirected to the add-room page with a single room name field and a submit button.
  4. If you click on an item in the chat room list, it will be redirected to the chat page and start chatting with other users who have joined or entered the chat room.
  5. Chat room contains a list of messages from all users or nicknames and a button to log out of the chat room.

For more clearer explanation, you can refer to this sequence diagram.

vue.js firebase realtime chat - sequence diagram

The following tools, framework, and module are required for this tutorial:

We assume that you have already installed Node.js. Make sure Node.js command line is working (on Windows) or is runnable in Linux/OS X terminal.

You can watch the video tutorial on our YouTube channel.


Step 1: Set up Google Firebase Database

Next, we will set up or create a new Google Firebase project that can use the Realtime Database. Just open your browser, then go to Google Firebase Console, and you will be taken to this page.

Vue.js Firebase Realtime Chat - Console

From that page, click the "+" add project button to create a Google Firebase project then it will be redirected to this page.

Vue.js Firebase Realtime Chat - Start Project

After filling the project name text field, whose project name is "vue-chat-app", then click the continue button, and it will be redirected to this page.

Vue.js Firebase Realtime Chat - Skip Google Analytics

This time, choose not to add Firebase analytics for now, then click the Create Project button.

Vue.js Firebase Realtime Chat - not right now

Now, you have a Google Firebase Project ready to use.

Vue.js Firebase Realtime Chat - project is ready

After clicking the Continue button, it will be redirected to this page.

Vue.js Firebase Realtime Chat - Database Dashboard

While developing the menu expanded in the left menu, choose "Create Database", then it will open this dialog.

Vue.js Firebase Realtime Chat - Create Database

Select "Start in test mode", then click next, and it will go to the next dialog.

Vue.js Firebase Realtime Chat - Cloud Firestore Location

Select the Firebase database server location (better near your Angular server location), then click the Done button. Don't forget to select or change Cloud Firestore to Realtime Database in Develop -> Database dashboard. Next, go to the Rules tab, and you will see these rule values.

{
  /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
  "rules": {
    ".read": false,
    ".write": false
  }
}

Change it to readable and writeable from everywhere for this tutorial only.

{
  /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
  "rules": {
    ".read": "auth === null",
    ".write": "auth === null"
  }
}

Click the publish button to update or save the changes. Now, the Google Firebase database is ready to use with your Vue.js chat web app.


Step 2: Create a Vue 3 Project

Option A: Vue CLI

npm install -g @vue/cli
vue create vue3-firebase-chat
# Choose “Vue 3 + Babel + Router”

Option B: Vite (recommended for speed)

npm create vite@latest vue3-firebase-chat -- --template vue
cd vue3-firebase-chat
npm install
npm install vue-router@4

To make sure that the created Vue 3 project is working, type this command to run the Vue.js application.

npm run dev

You will see this page when you open http://localhost:5173/ in the browser.

Build a Real-Time Chat App with Vue 3 and Firebase Realtime Database - Vire Vue Home


Step 3: Update Dependencies

We will use the Firebase JavaScript SDK that is available as a Node module. For that, type this command to install the Firebase module after stopping the running Vue.js app (press Ctrl + c).

npm install firebase@9 vue-router@4


Step 4: Modular Firebase Integration

In src/firebase.js Convert to v9 modular style:

import { initializeApp } from 'firebase/app';
import { getDatabase } from 'firebase/database';

const firebaseConfig = { /* your config */ };
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);

export { db };


Step 5: Rewrite Routing Using vue-router@4

Create src/router/index.js:

import { createRouter, createWebHistory } from 'vue-router';
import Login from '../components/Login.vue';
import RoomList from '../components/Room.vue';
import Chat from '../components/Chat.vue';

const routes = [
  { path: '/', component: Login },
  { path: '/rooms', component: RoomList },
  { path: '/chat/:roomId', component: Chat, props: true },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Update main.js:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './firebase'; // initialize

createApp(App).use(router).mount('#app');


Step 6: Rewrite Components with Composition API

Example: src/components/Chat.vue

<template>
  <div>
    <h2>Room: {{ roomId }}</h2>
    <div v-for="msg in messages" :key="msg.id">
      <strong>{{ msg.sender }}:</strong> {{ msg.text }}
    </div>
    <input v-model="newMessage" @keyup.enter="send" />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { db } from '../firebase';
import { ref as dbRef, push, onValue } from 'firebase/database';

const route = useRoute();
const roomId = route.params.roomId;

const messages = ref([]);
const newMessage = ref('');

onMounted(() => {
  const r = dbRef(db, `rooms/${roomId}/messages`);
  onValue(r, snapshot => {
    messages.value = [];
    snapshot.forEach(child => {
      messages.value.push({ id: child.key, ...child.val() });
    });
  });
});

function send() {
  if (!newMessage.value) return;
  push(dbRef(db, `rooms/${roomId}/messages`), {
    sender: 'Anonymous',
    text: newMessage.value,
    timestamp: Date.now(),
  });
  newMessage.value = '';
}
</script>

Example: src/components/Login.vue

<template>
  <div>
    <input v-model="email" placeholder="Email" />
    <input v-model="password" type="password" placeholder="Password" />
    <button @click="login">Login</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'

// Refs
const email = ref('')
const password = ref('')
const router = useRouter()

// Login function
const login = async () => {
  try {
    const auth = getAuth()
    await signInWithEmailAndPassword(auth, email.value, password.value)
    router.push('/chat')
  } catch (err) {
    alert(err.message)
  }
}
</script>

Example: src/components/RoomList.vue

<template>
  <div>
    <ul>
      <li v-for="room in rooms" :key="room.id" @click="goToRoom(room)">
        {{ room.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getDatabase, ref as dbRef, onValue } from 'firebase/database'

// Reactive state
const rooms = ref([])
const router = useRouter()

// Fetch room list on mount
onMounted(() => {
  const db = getDatabase()
  const roomsRef = dbRef(db, 'rooms')

  onValue(roomsRef, snapshot => {
    const data = snapshot.val()
    rooms.value = Object.keys(data || {}).map(id => ({
      id,
      ...data[id],
    }))
  })
})

// Navigate to selected room
const goToRoom = room => {
  router.push(`/chat/${room.id}`)
}
</script>

Example: src/components/ChatRoom.vue

<template>
  <div>
    <h2>Chat Room: {{ roomName }}</h2>
    <ul>
      <li v-for="msg in messages" :key="msg.timestamp">
        <strong>{{ msg.user }}:</strong> {{ msg.text }}
      </li>
    </ul>
    <input
      v-model="newMessage"
      @keyup.enter="sendMessage"
      placeholder="Type a message..."
    />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { getDatabase, ref as dbRef, push, onChildAdded } from 'firebase/database'
import { getAuth } from 'firebase/auth'

const route = useRoute()
const roomName = ref(route.params.roomId)
const messages = ref([])
const newMessage = ref('')

onMounted(() => {
  const db = getDatabase()
  const messagesRef = dbRef(db, `messages/${roomName.value}`)

  onChildAdded(messagesRef, snapshot => {
    messages.value.push(snapshot.val())
  })
})

const sendMessage = () => {
  const text = newMessage.value.trim()
  if (!text) return

  const auth = getAuth()
  const user = auth.currentUser
  const message = {
    text,
    user: user ? user.email : 'Anonymous',
    timestamp: Date.now(),
  }

  const db = getDatabase()
  const messagesRef = dbRef(db, `messages/${roomName.value}`)
  push(messagesRef, message)

  newMessage.value = ''
}
</script>

Example: src/components/AddRoom.vue

<template>
  <div>
    <h2>Create Chat Room</h2>
    <input v-model="roomName" placeholder="Room name" />
    <button @click="createRoom">Create</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getDatabase, ref as dbRef, push } from 'firebase/database'

const router = useRouter()
const roomName = ref('')

const createRoom = () => {
  const name = roomName.value.trim()
  if (!name) return

  const db = getDatabase()
  const roomsRef = dbRef(db, 'rooms')
  push(roomsRef, { name })

  roomName.value = ''
  router.push('/rooms')
}
</script>


Step 7: Styling the converted Vue 3 components

Login.vue Styling

<template>
  <div class="login">
    <h2>Login</h2>
    <input v-model="username" type="text" placeholder="Enter your username" />
    <button @click="login">Login</button>
  </div>
</template>

<style scoped>
.login {
  max-width: 400px;
  margin: 80px auto;
  padding: 2rem;
  border: 1px solid #ccc;
  border-radius: 12px;
  text-align: center;
}

.login h2 {
  margin-bottom: 1.5rem;
}

.login input {
  width: 100%;
  padding: 10px;
  margin-bottom: 1rem;
  font-size: 16px;
  border: 1px solid #aaa;
  border-radius: 8px;
}

.login button {
  padding: 10px 20px;
  font-size: 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}
</style>

RoomList.vue Styling

<template>
  <div class="room-list">
    <h2>Chat Rooms</h2>
    <ul>
      <li v-for="room in rooms" :key="room.id" @click="goToRoom(room.id)">
        {{ room.name }}
      </li>
    </ul>
    <router-link to="/add-room" class="add-room-link">+ Add Room</router-link>
  </div>
</template>

<style scoped>
.room-list {
  max-width: 600px;
  margin: 40px auto;
  padding: 1rem;
}

.room-list h2 {
  text-align: center;
  margin-bottom: 1rem;
}

.room-list ul {
  list-style: none;
  padding: 0;
}

.room-list li {
  padding: 12px;
  border: 1px solid #ddd;
  margin-bottom: 10px;
  cursor: pointer;
  border-radius: 6px;
  transition: background 0.2s;
}

.room-list li:hover {
  background-color: #f5f5f5;
}

.add-room-link {
  display: inline-block;
  margin-top: 20px;
  text-align: center;
  color: #42b983;
  text-decoration: none;
}
</style>

AddRoom.vue Styling

<template>
  <div class="add-room">
    <h2>Add New Room</h2>
    <input v-model="roomName" type="text" placeholder="Room name" />
    <button @click="createRoom">Create Room</button>
  </div>
</template>

<style scoped>
.add-room {
  max-width: 400px;
  margin: 80px auto;
  padding: 2rem;
  border: 1px solid #ccc;
  border-radius: 12px;
  text-align: center;
}

.add-room h2 {
  margin-bottom: 1.5rem;
}

.add-room input {
  width: 100%;
  padding: 10px;
  margin-bottom: 1rem;
  font-size: 16px;
  border: 1px solid #aaa;
  border-radius: 8px;
}

.add-room button {
  padding: 10px 20px;
  font-size: 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}
</style>

Chat.vue Styling

<template>
  <div class="chat">
    <h2>{{ roomName }}</h2>
    <div class="messages">
      <div
        class="message"
        v-for="message in messages"
        :key="message.id"
      >
        <strong>{{ message.user }}:</strong> {{ message.text }}
      </div>
    </div>
    <div class="send-box">
      <input
        v-model="newMessage"
        type="text"
        placeholder="Type a message"
      />
      <button @click="sendMessage">Send</button>
    </div>
  </div>
</template>

<style scoped>
.chat {
  max-width: 700px;
  margin: 40px auto;
  padding: 1rem;
}

.chat h2 {
  text-align: center;
  margin-bottom: 1rem;
}

.messages {
  max-height: 400px;
  overflow-y: auto;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin-bottom: 1rem;
  background-color: #fafafa;
}

.message {
  padding: 6px 0;
  border-bottom: 1px solid #eee;
}

.send-box {
  display: flex;
  gap: 10px;
}

.send-box input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
  border: 1px solid #aaa;
  border-radius: 8px;
}

.send-box button {
  padding: 10px 20px;
  font-size: 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}
</style>


Step 8: Run the Vue 3 Firebase Realtime Chat Web App

Running the Vue.js and Firebase real-time chat web app is very simple. Just type this command in your terminal.

npm run dev

And here it is, you can test the real-time chat web app on multiple machines or browsers. You can watch this demo to find out how it works with multiple browsers.

That's the Vue.js Firebase Realtime Chat Web App tutorial. You can check the full source code on our GitHub.

That's just the basics. If you need more deep learning about MEVN Stack, Vue.js, or related, you can take the following cheap course:

Thanks!