Table of Contents
- Prerequisites
- Setting Up the Vue.js Project
- Setting Up Firebase
- Implementing User Authentication
- Building the Chat Interface
- Real-Time Message Handling with Firestore
- Testing the Application
- Deploying the App with Firebase Hosting
- Enhancements and Next Steps
- Conclusion
- References
Prerequisites
Before starting, ensure you have the following tools and accounts:
- Node.js (v14+ recommended) and npm (or Yarn) installed.
- Vue CLI: Install globally with
npm install -g @vue/cli. - A Firebase account: Sign up for free at firebase.google.com.
- Basic knowledge of Vue.js (components, props, reactivity) and JavaScript.
Setting Up the Vue.js Project
First, let’s create a new Vue project. Open your terminal and run:
vue create realtime-chat-app
When prompted, select Manually select features and choose:
- Babel
- Vuex (for state management)
- Vue Router (for navigation)
- CSS Pre-processors (optional, but useful for styling)
Once the project is created, navigate into the directory and install the Firebase SDK (required to interact with Firebase services):
cd realtime-chat-app
npm install firebase
Setting Up Firebase
Step 1: Create a Firebase Project
- Go to the Firebase Console and click Add project.
- Enter a project name (e.g., “realtime-chat-app”) and follow the prompts to create the project.
Step 2: Register Your App
- In the Firebase Console, click the Web icon (</>) to register a web app.
- Enter an app nickname (e.g., “chat-app-client”) and click Register app.
- Copy the Firebase configuration snippet (it looks like this):
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_SENDER_ID",
appId: "YOUR_APP_ID"
};
Step 3: Enable Services
We’ll use two core Firebase services:
- Authentication: To let users register and log in.
- Cloud Firestore: A NoSQL database for real-time message storage.
Enable Authentication
- In the Firebase Console, go to Authentication > Sign-in method.
- Enable Email/Password as a sign-in provider.
Enable Cloud Firestore
- Go to Firestore Database > Create database.
- Select Start in test mode (for development; we’ll secure it later) and click Next.
- Choose a region (e.g., “us-central1”) and click Enable.
Step 4: Initialize Firebase in Vue
Create a Firebase configuration file in your Vue project. Create src/firebase.js and paste the config snippet from Step 2:
// src/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Export services for use in the app
export const auth = getAuth(app);
export const db = getFirestore(app);
Replace the placeholder values with your actual Firebase config.
Implementing User Authentication
We’ll build login and registration pages so users can authenticate.
Step 1: Set Up Routes
Update src/router/index.js to define routes for login, registration, and the chat page:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Login from '../views/Login.vue';
import Register from '../views/Register.vue';
import Chat from '../views/Chat.vue';
import { auth } from '../firebase';
// Protect routes: redirect unauthenticated users to login
const requireAuth = (to, from, next) => {
if (!auth.currentUser) {
next({ name: 'Login' });
} else {
next();
}
};
const routes = [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/chat',
name: 'Chat',
component: Chat,
beforeEnter: requireAuth // Protect this route
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
Step 2: Build Login and Registration Components
Register Component (src/views/Register.vue)
This component lets users create an account with email and password:
<template>
<div class="auth-container">
<h2>Register</h2>
<form @submit.prevent="register">
<div class="input-group">
<label>Email</label>
<input type="email" v-model="email" required>
</div>
<div class="input-group">
<label>Password</label>
<input type="password" v-model="password" required minlength="6">
</div>
<button type="submit">Register</button>
<p>Already have an account? <router-link to="/">Login</router-link></p>
<p v-if="error" class="error">{{ error }}</p>
</form>
</div>
</template>
<script>
import { ref } from 'vue';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../firebase';
import { useRouter } from 'vue-router';
export default {
setup() {
const email = ref('');
const password = ref('');
const error = ref('');
const router = useRouter();
const register = async () => {
try {
await createUserWithEmailAndPassword(auth, email.value, password.value);
router.push('/chat'); // Redirect to chat after registration
} catch (err) {
error.value = err.message;
}
};
return { email, password, error, register };
}
};
</script>
<style scoped>
/* Add basic styling here */
.auth-container { max-width: 400px; margin: 2rem auto; padding: 2rem; border: 1px solid #ddd; border-radius: 8px; }
.input-group { margin-bottom: 1rem; }
input { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
button { width: 100%; padding: 0.5rem; background: #2c3e50; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: red; }
</style>
Login Component (src/views/Login.vue)
Similar to registration, but uses signInWithEmailAndPassword:
<template>
<div class="auth-container">
<h2>Login</h2>
<form @submit.prevent="login">
<div class="input-group">
<label>Email</label>
<input type="email" v-model="email" required>
</div>
<div class="input-group">
<label>Password</label>
<input type="password" v-model="password" required>
</div>
<button type="submit">Login</button>
<p>Don't have an account? <router-link to="/register">Register</router-link></p>
<p v-if="error" class="error">{{ error }}</p>
</form>
</div>
</template>
<script>
import { ref } from 'vue';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../firebase';
import { useRouter } from 'vue-router';
export default {
setup() {
const email = ref('');
const password = ref('');
const error = ref('');
const router = useRouter();
const login = async () => {
try {
await signInWithEmailAndPassword(auth, email.value, password.value);
router.push('/chat'); // Redirect to chat after login
} catch (err) {
error.value = err.message;
}
};
return { email, password, error, login };
}
};
</script>
<style scoped>
/* Reuse auth-container styling from Register.vue */
</style>
Building the Chat Interface
Now, let’s create the chat page where users send and receive messages.
Chat View (src/views/Chat.vue)
This component includes:
- A logout button.
- A list of messages.
- An input field to send new messages.
<template>
<div class="chat-container">
<div class="chat-header">
<h2>Real-Time Chat</h2>
<button @click="logout">Logout</button>
</div>
<div class="messages">
<div v-for="message in messages" :key="message.id" class="message">
<div class="message-sender">{{ message.sender }}</div>
<div class="message-text">{{ message.text }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
<div class="message-input">
<input
type="text"
v-model="newMessage"
@keyup.enter="sendMessage"
placeholder="Type a message..."
>
<button @click="sendMessage">Send</button>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { signOut } from 'firebase/auth';
import { collection, addDoc, serverTimestamp, query, orderBy, onSnapshot } from 'firebase/firestore';
import { auth, db } from '../firebase';
import { useRouter } from 'vue-router';
export default {
setup() {
const newMessage = ref('');
const messages = ref([]);
const router = useRouter();
let unsubscribe; // To stop listening to Firestore updates
// Get current user (from Firebase Auth)
const user = computed(() => auth.currentUser);
// Format timestamp (e.g., "3:45 PM")
const formatTime = (timestamp) => {
return new Date(timestamp?.toDate()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Fetch messages in real-time
const fetchMessages = () => {
const q = query(
collection(db, 'messages'),
orderBy('timestamp', 'asc') // Sort messages by time
);
// Listen for real-time updates
unsubscribe = onSnapshot(q, (snapshot) => {
messages.value = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
});
};
// Send a new message to Firestore
const sendMessage = async () => {
if (newMessage.value.trim()) {
try {
await addDoc(collection(db, 'messages'), {
text: newMessage.value,
sender: user.value.email.split('@')[0], // Use email prefix as display name
userId: user.value.uid,
timestamp: serverTimestamp() // Auto-generate timestamp
});
newMessage.value = ''; // Clear input
} catch (error) {
console.error('Error sending message:', error);
}
}
};
// Logout user
const logout = async () => {
await signOut(auth);
router.push('/');
};
// Start listening to messages when component mounts
onMounted(fetchMessages);
// Stop listening when component unmounts (cleanup)
onUnmounted(() => unsubscribe());
return {
newMessage,
messages,
sendMessage,
logout,
formatTime
};
}
};
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 1rem;
background: #2c3e50;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
background: #f5f5f5;
}
.message {
background: white;
padding: 0.5rem;
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.message-sender {
font-weight: bold;
color: #2c3e50;
}
.message-time {
font-size: 0.8rem;
color: #777;
text-align: right;
}
.message-input {
display: flex;
padding: 1rem;
background: white;
border-top: 1px solid #ddd;
}
.message-input input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 0.5rem;
}
.message-input button {
padding: 0.5rem 1rem;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
Real-Time Message Handling with Firestore
How It Works:
- Sending Messages: When the user clicks “Send” or presses Enter,
sendMessageadds a new document to themessagesFirestore collection withtext,sender,userId, andtimestamp. - Real-Time Updates:
onSnapshotlistens for changes to themessagescollection. When a new message is added, Firestore immediately notifies the app, andmessages.valueupdates, triggering a re-render in Vue.
Firestore Security Rules
By default, Firestore blocks all database access. To allow authenticated users to read/write messages, update your Firestore security rules:
- Go to Firestore Database > Rules in the Firebase Console.
- Replace the default rules with:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /messages/{message} {
// Allow read/write only if user is authenticated
allow read, write: if request.auth != null;
}
}
}
Click Publish to save the rules.
Testing the Application
- Start the Vue development server:
npm run serve
- Open
http://localhost:8080in your browser. - Register a new user (e.g.,
[email protected]with passwordpassword123). - Log in, then open a second browser tab (or incognito window) and register another user (e.g.,
[email protected]). - Send messages from both tabs—you’ll see them update in real-time!
Deploying the App with Firebase Hosting
Firebase Hosting makes it easy to deploy Vue apps. Here’s how:
Step 1: Build the Vue App
First, create a production build of your Vue app:
npm run build
This generates a dist folder with optimized assets.
Step 2: Deploy with Firebase Hosting
- Install the Firebase CLI (if not already installed):
npm install -g firebase-tools
- Log in to Firebase:
firebase login
- Initialize Firebase in your project:
firebase init
- Select Hosting from the list of services.
- Choose your Firebase project (the one you created earlier).
- Set the public directory to
dist(the folder with your build files). - Answer “Yes” to “Configure as a single-page app” (since Vue uses client-side routing).
- Deploy the app:
firebase deploy
Firebase will provide a URL (e.g., https://realtime-chat-app.web.app) where your app is live!
Enhancements and Next Steps
Now that you have a basic chat app, consider adding these features:
- User Avatars: Let users upload profile pictures (use Firebase Storage).
- Online Status: Show when users are online/offline (track with Firestore presence).
- Read Receipts: Mark messages as “read” when seen by the recipient.
- File Attachments: Allow sending images/files (Firebase Storage + Firestore).
- Typing Indicators: Show when a user is typing.
Conclusion
In this tutorial, you built a real-time chat app using Vue.js and Firebase. You learned how to:
- Set up Vue.js with Vuex and Vue Router.
- Integrate Firebase Authentication for user registration/login.
- Use Cloud Firestore for real-time data storage and synchronization.
- Deploy the app with Firebase Hosting.
Firebase’s real-time capabilities and Vue’s reactivity make this stack ideal for building interactive apps with minimal backend code.
References
- Vue.js Documentation
- Firebase Web Documentation
- Cloud Firestore Documentation
- Firebase Authentication Documentation
- Vue Router Documentation
- Firebase Hosting Documentation
For the complete code, check out the GitHub Repository (replace with your repo link).