Vue Firebase Chat System
April 13, 2025 · AI Conversations
—
### **1. Firebase Setup**
Enable:
– Firebase Authentication (Email/Password or Anonymous)
– Realtime Database
Create a database structure like:
"chats": {
"userUid1": {
"messages": {
"msgId1": {
"sender": "user", // or "admin"
"text": "Hello",
"timestamp": 1689782321
},
...
}
}
}
—
### **2. Vue Project Setup**
Install:
yarn add firebase pinia vue-router
—
### **3. Firebase Config (src/firebase.js)**
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getDatabase } from 'firebase/database'
const firebaseConfig = {
apiKey: '...',
authDomain: '...',
databaseURL: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...'
}
const app = initializeApp(firebaseConfig)
export const auth = getAuth(app)
export const db = getDatabase(app)
—
### **4. Chat Component (UserChat.vue)**
<template>
<div>
<div v-for="msg in messages" :key="msg.id" :class="msg.sender">
<p>{{ msg.text }}</p>
</div>
<input v-model="newMessage" @keyup.enter="sendMessage" placeholder="Type your message..." />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { db, auth } from '@/firebase'
import { onValue, push, ref as dbRef, serverTimestamp } from 'firebase/database'
import { signInAnonymously } from 'firebase/auth'
const messages = ref([])
const newMessage = ref('')
let uid = ''
onMounted(async () => {
const { user } = await signInAnonymously(auth)
uid = user.uid
const chatRef = dbRef(db, `chats/${uid}/messages`)
onValue(chatRef, (snapshot) => {
messages.value = []
snapshot.forEach((child) => {
messages.value.push({ id: child.key, ...child.val() })
})
})
})
const sendMessage = () => {
if (!newMessage.value.trim()) return
const chatRef = dbRef(db, `chats/${uid}/messages`)
push(chatRef, {
sender: 'user',
text: newMessage.value,
timestamp: serverTimestamp(),
})
newMessage.value = ''
}
</script>
<style>
.user { text-align: right; }
.admin { text-align: left; }
</style>
—
### **5. Admin Panel (AdminChat.vue)**
Basic idea:
<script setup>
import { db } from '@/firebase'
import { ref as dbRef, onValue, push, serverTimestamp } from 'firebase/database'
import { ref } from 'vue'
const userChats = ref({})
onValue(dbRef(db, 'chats'), (snapshot) => {
userChats.value = snapshot.val()
})
const sendToUser = (uid, message) => {
const chatRef = dbRef(db, `chats/${uid}/messages`)
push(chatRef, {
sender: 'admin',
text: message,
timestamp: serverTimestamp()
})
}
</script>
—
### **6. Security Rules (Firebase Realtime DB)**
{
"rules": {
"chats": {
"$uid": {
".read": "$uid === auth.uid || auth.token.admin === true",
".write": "$uid === auth.uid || auth.token.admin === true"
}
}
}
}
—
Want me to help integrate it into your current Vue setup with Vuetify or Pinia?
—
## **1. Project Setup**
Install dependencies:
yarn add firebase pinia vue-router vuetify
Enable Firebase:
– Authentication (Anonymous or Email/Password)
– Realtime Database
—
## **2. Firebase Init (src/firebase.ts)**
import { initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getDatabase } from 'firebase/database'
const firebaseConfig = {
apiKey: '...',
authDomain: '...',
databaseURL: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...'
}
const app = initializeApp(firebaseConfig)
export const auth = getAuth(app)
export const db = getDatabase(app)
—
## **3. Pinia Store (src/stores/chatStore.ts)**
import { defineStore } from 'pinia'
import { db } from '@/firebase'
import {
ref as dbRef,
onValue,
push,
serverTimestamp
} from 'firebase/database'
export const useChatStore = defineStore('chat', {
state: () => ({
uid: '',
messages: [] as Array<{ id: string, text: string, sender: string }>
}),
actions: {
init(uid: string) {
this.uid = uid
const chatRef = dbRef(db, `chats/${uid}/messages`)
onValue(chatRef, snapshot => {
const msgs: any[] = []
snapshot.forEach(child => {
msgs.push({ id: child.key, ...child.val() })
})
this.messages = msgs
})
},
sendMessage(text: string, sender = 'user') {
const chatRef = dbRef(db, `chats/${this.uid}/messages`)
push(chatRef, {
sender,
text,
timestamp: serverTimestamp()
})
}
}
})
—
## **4. Vuetify Chat UI (components/ChatBox.vue)**
<template>
<v-container>
<v-list max-height="400" class="overflow-y-auto">
<v-list-item
v-for="msg in chat.messages"
:key="msg.id"
:class="msg.sender === 'admin' ? 'text-left' : 'text-right'"
>
<v-chip :color="msg.sender === 'admin' ? 'blue' : 'green'" text-color="white">
{{ msg.text }}
</v-chip>
</v-list-item>
</v-list>
<v-text-field
v-model="input"
label="Type your message..."
@keyup.enter="send"
append-icon="mdi-send"
@click:append="send"
/>
</v-container>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { signInAnonymously } from 'firebase/auth'
import { auth } from '@/firebase'
import { useChatStore } from '@/stores/chatStore'
const chat = useChatStore()
const input = ref('')
onMounted(async () => {
const { user } = await signInAnonymously(auth)
chat.init(user.uid)
})
const send = () => {
if (!input.value.trim()) return
chat.sendMessage(input.value.trim())
input.value = ''
}
</script>
—
## **5. Admin View (Optional Example)**
If you want the admin to view all users and respond, I can give a separate component.
—
## **6. Firebase Rules (Realtime DB)**
{
"rules": {
"chats": {
"$uid": {
".read": "$uid === auth.uid || auth.token.admin === true",
".write": "$uid === auth.uid || auth.token.admin === true"
}
}
}
}
—
Would you like the **admin panel version** next? Where admin can select a user and reply?
—
## **1. Firebase Rules Update**
Allow admin (custom claim or role) to read all chats:
{
"rules": {
"chats": {
"$uid": {
".read": "auth.token.admin === true || auth.uid === $uid",
".write": "auth.token.admin === true || auth.uid === $uid"
}
}
}
}
> If you're not using custom claims yet, during development you can temporarily bypass with `.read: true`, but it’s **not safe for production**.
—
## **2. Pinia Admin Store (src/stores/adminChatStore.ts)**
import { defineStore } from 'pinia'
import { db } from '@/firebase'
import {
ref as dbRef,
onValue,
push,
serverTimestamp
} from 'firebase/database'
export const useAdminChatStore = defineStore('adminChat', {
state: () => ({
allChats: {} as Record<string, { messages: any }>
}),
actions: {
loadChats() {
const chatListRef = dbRef(db, 'chats')
onValue(chatListRef, snapshot => {
const chats: Record<string, any> = {}
snapshot.forEach(child => {
chats[child.key!] = child.val()
})
this.allChats = chats
})
},
sendToUser(uid: string, text: string) {
const userMsgRef = dbRef(db, `chats/${uid}/messages`)
push(userMsgRef, {
sender: 'admin',
text,
timestamp: serverTimestamp()
})
}
}
})
—
## **3. Vuetify Admin Panel (components/AdminChatPanel.vue)**
<template>
<v-container>
<v-row>
<v-col cols="4">
<v-list>
<v-list-item
v-for="(chat, uid) in adminChat.allChats"
:key="uid"
@click="selectUser(uid)"
:active="uid === selectedUid"
>
<v-list-item-title>{{ uid }}</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
<v-col cols="8">
<v-list max-height="400" class="overflow-y-auto">
<v-list-item
v-for="(msg, i) in messages"
:key="i"
:class="msg.sender === 'admin' ? 'text-left' : 'text-right'"
>
<v-chip :color="msg.sender === 'admin' ? 'blue' : 'green'" text-color="white">
{{ msg.text }}
</v-chip>
</v-list-item>
</v-list>
<v-text-field
v-model="input"
label="Reply..."
append-icon="mdi-send"
@keyup.enter="send"
@click:append="send"
/>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useAdminChatStore } from '@/stores/adminChatStore'
const adminChat = useAdminChatStore()
adminChat.loadChats()
const selectedUid = ref('')
const input = ref('')
const messages = computed(() => {
return selectedUid.value && adminChat.allChats[selectedUid.value]?.messages
? Object.values(adminChat.allChats[selectedUid.value].messages)
: []
})
const selectUser = (uid: string) => {
selectedUid.value = uid
}
const send = () => {
if (!input.value.trim() || !selectedUid.value) return
adminChat.sendToUser(selectedUid.value, input.value.trim())
input.value = ''
}
</script>
—
## **4. Bonus: Admin Auth**
If needed, you can use a static `admin@example.com` account for testing, or implement [custom claims](https://firebase.google.com/docs/auth/admin/custom-claims) to identify admin users securely.
—
Would you like to:
– Add real-time unread message indicators?
– Filter messages by date?
– Enable file/image sharing?
Let me know and I can guide further!
—
## **1. Admin Selects a User**
In the `AdminChatPanel.vue`, when the admin clicks on a user from the left column (`v-list`), we store their UID in `selectedUid`.
const selectedUid = ref('')
const selectUser = (uid: string) => {
selectedUid.value = uid
}
—
## **2. Admin Types a Message and Sends It**
The message is typed in a `v-text-field` and sent using:
const send = () => {
if (!input.value.trim() || !selectedUid.value) return
adminChat.sendToUser(selectedUid.value, input.value.trim())
input.value = ''
}
—
## **3. Behind the Scenes: `sendToUser()` in Store**
sendToUser(uid: string, text: string) {
const userMsgRef = dbRef(db, `chats/${uid}/messages`)
push(userMsgRef, {
sender: 'admin',
text,
timestamp: serverTimestamp()
})
}
This writes a new message under that user's `messages` node in Firebase Realtime Database.
Example path:
chats/user_uid_abc123/messages/msg_xyz
And message format:
{
"sender": "admin",
"text": "Thanks for reaching out!",
"timestamp": 1710000000
}
—
## **4. It Instantly Appears on User Side**
Because the user chat component uses Firebase's `onValue()` listener on their own `uid`, new admin messages appear in real-time.
—
### **In Summary:**
– Admin clicks a user
– Types a message
– `sendToUser(uid, text)` saves it to that user’s chat node
– User sees the message instantly via Firebase listener
—
Let me know if you want:
– Timestamp formatting
– Typing indicators
– Notification sounds on new messages
– Vuex-compatible version (if not using Pinia)
—
## **1. Typing Indicator (for Admin to see when User is typing)**
### **A. Database Path**
Store typing status like:
chats/{uid}/typing: true or false
—
### **B. User Side – Set Typing Status**
In `UserChat.vue`, update `v-text-field`:
<v-text-field
v-model="newMessage"
@input="updateTyping"
@blur="setTyping(false)"
...
/>
Script logic:
import { set, ref as dbRef } from 'firebase/database'
let typingTimer: any = null
const updateTyping = () => {
setTyping(true)
clearTimeout(typingTimer)
typingTimer = setTimeout(() => setTyping(false), 2000)
}
const setTyping = (isTyping: boolean) => {
if (uid)
set(dbRef(db, `chats/${uid}/typing`), isTyping)
}
—
### **C. Admin Side – Listen to Typing**
In `AdminChatPanel.vue`, add:
import { onValue, ref as dbRef } from 'firebase/database'
const isTyping = ref(false)
watch(selectedUid, (uid) => {
if (!uid) return
const typingRef = dbRef(db, `chats/${uid}/typing`)
onValue(typingRef, snapshot => {
isTyping.value = snapshot.val() === true
})
})
Then show this in the UI:
<v-chip v-if="isTyping" color="grey">User is typing...</v-chip>
—
## **2. Notification Sound on New Message**
### **A. Add Sound File**
Put a `ping.mp3` file in your `public` folder:
/public/sounds/ping.mp3
### **B. In Chat Component (UserChat.vue or AdminChat.vue)**
const audio = new Audio('/sounds/ping.mp3')
const playSound = () => {
audio.currentTime = 0
audio.play()
}
### **C. Trigger on New Message**
In your `onValue()` Firebase listener, play sound **only for messages from the other side**:
Example in user chat:
onValue(chatRef, (snapshot) => {
const msgs: any[] = []
snapshot.forEach((child) => {
const msg = { id: child.key, ...child.val() }
msgs.push(msg)
if (msg.sender === 'admin') playSound()
})
chat.messages = msgs
})
Do similarly on the **admin side**, triggering only when `msg.sender === 'user'`.
—
Would you also like a **"user is online" indicator** or to show when the **last message was seen**?
To enter name and email
– **Anonymous auth**
– **Ask name and email before starting chat**
– **Typing indicator**
– **Notification sound**
—
## **1. Ask Name + Email Before Chat Starts**
### **A. Form Component (UserLoginForm.vue)**
<template>
<v-container>
<v-card class="pa-4" max-width="400" elevation="3">
<v-text-field v-model="name" label="Your Name" required />
<v-text-field v-model="email" label="Email" type="email" required />
<v-btn @click="startChat" color="primary" :disabled="!name || !email">Start Chat</v-btn>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { signInAnonymously } from 'firebase/auth'
import { db, auth } from '@/firebase'
import { ref as dbRef, set } from 'firebase/database'
const name = ref('')
const email = ref('')
const router = useRouter()
const startChat = async () => {
const { user } = await signInAnonymously(auth)
await set(dbRef(db, `chats/${user.uid}/userInfo`), {
name: name.value,
email: email.value,
created_at: new Date().toISOString()
})
router.push({ name: 'chat' })
}
</script>
—
## **2. Store User Info in Firebase**
Stored at:
chats/{uid}/userInfo = { name, email, created_at }
—
## **3. Admin Panel – Show Name + Email**
In `AdminChatPanel.vue` (or wherever you list users):
<v-list-item-title>{{ chat.userInfo?.name || uid }}</v-list-item-title>
<v-list-item-subtitle>{{ chat.userInfo?.email }}</v-list-item-subtitle>
—
## **4. Chat View for User (ChatBox.vue)**
Same as before, with the typing indicator and sound features added (see previous response).
—
## **5. Full Routing Example**
In your `router/index.ts`:
{
path: '/',
name: 'login',
component: () => import('@/components/UserLoginForm.vue')
},
{
path: '/chat',
name: 'chat',
component: () => import('@/components/ChatBox.vue')
}
—
## **6. Add Global Typing + Sound Logic**
As shown earlier:
– Add `typing` node updates on user side
– Admin listens for it and shows "User is typing…"
– Add sound file to `/public/sounds/ping.mp3`
– Play `new Audio('/sounds/ping.mp3')` when message received from the opposite side
—
Let me know if you'd like:
– Message read receipts
– Online/offline status
– Export chat history as PDF
Want a GitHub-ready version of this codebase?
—
## **1. Create Admin Account in Firebase**
### **A. Enable Email/Password Auth**
– Go to Firebase Console > **Authentication** > **Sign-in Method**
– Enable **Email/Password**
### **B. Create Admin User**
– Go to Firebase Console > **Authentication** > **Users**
– Click **Add User**
– Email: `admin@example.com`
– Password: `admin123` (use secure password in production)
### **C. (Optional but Recommended) Add Custom Claims for Admin**
If you're using Firebase Admin SDK (Node.js backend or Cloud Functions), set custom claims:
// Example in Node.js
admin.auth().setCustomUserClaims(uid, { admin: true })
But for now, you can skip this if you're just using email to identify the admin.
—
## **2. Admin Presence Status with Realtime DB**
### **A. Database Path**
Use:
presence/admin/status = "online" / "offline"
—
## **3. Set Admin Online Status**
### **AdminLogin.vue** (basic login + status update)
<script setup lang="ts">
import { ref } from 'vue'
import { signInWithEmailAndPassword, onAuthStateChanged } from 'firebase/auth'
import { auth, db } from '@/firebase'
import { ref as dbRef, set, onDisconnect } from 'firebase/database'
import { useRouter } from 'vue-router'
const email = ref('')
const password = ref('')
const router = useRouter()
const login = async () => {
const { user } = await signInWithEmailAndPassword(auth, email.value, password.value)
const statusRef = dbRef(db, `presence/admin`)
await set(statusRef, { status: 'online', last_active: new Date().toISOString() })
onDisconnect(statusRef).set({ status: 'offline', last_active: new Date().toISOString() })
router.push({ name: 'admin-panel' })
}
</script>
—
## **4. Show Admin Status to Users**
### **UserChat.vue**
import { db } from '@/firebase'
import { ref as dbRef, onValue } from 'firebase/database'
import { ref, onMounted } from 'vue'
const adminStatus = ref('offline')
onMounted(() => {
const statusRef = dbRef(db, 'presence/admin')
onValue(statusRef, (snapshot) => {
const data = snapshot.val()
adminStatus.value = data?.status || 'offline'
})
})
### **In Template**
<v-alert
:type="adminStatus === 'online' ? 'success' : 'warning'"
dense
class="mb-2"
>
Admin is {{ adminStatus }}
</v-alert>
—
## **5. Optional Enhancements**
– Track admin `last_active`
– Use `onDisconnect()` to auto-update status on close
– Add `typing` and `isOnline` together under `chats/{uid}/status`
—
Would you like a **global Vue store (Pinia)** to manage admin status across components?
—
## ✅ 1. **Create Pinia Store for Admin Status**
### **`src/stores/adminStatusStore.ts`**
import { defineStore } from 'pinia'
import { db } from '@/firebase'
import { ref as dbRef, onValue } from 'firebase/database'
import { ref } from 'vue'
export const useAdminStatusStore = defineStore('adminStatus', () => {
const status = ref<'online' | 'offline'>('offline')
const lastActive = ref<string | null>(null)
const listenToAdminStatus = () => {
const adminStatusRef = dbRef(db, 'presence/admin')
onValue(adminStatusRef, (snapshot) => {
const data = snapshot.val()
status.value = data?.status || 'offline'
lastActive.value = data?.last_active || null
})
}
return {
status,
lastActive,
listenToAdminStatus
}
})
—
## ✅ 2. **Initialize in User Components**
### **In `UserChat.vue` or main layout component**
import { useAdminStatusStore } from '@/stores/adminStatusStore'
import { onMounted } from 'vue'
const adminStatusStore = useAdminStatusStore()
onMounted(() => {
adminStatusStore.listenToAdminStatus()
})
—
## ✅ 3. **Use in Template Anywhere**
<v-alert
:type="adminStatusStore.status === 'online' ? 'success' : 'warning'"
icon="mdi-account"
class="mb-3"
>
Admin is currently <strong>{{ adminStatusStore.status }}</strong>
</v-alert>
You can also show **last seen**:
<v-chip v-if="adminStatusStore.status === 'offline'" class="ml-2" size="small" color="grey">
Last seen: {{ new Date(adminStatusStore.lastActive).toLocaleString() }}
</v-chip>
—
## ✅ 4. **Bonus: Display in Header or Nav**
Anywhere in your layout or header:
<v-badge :content="adminStatusStore.status === 'online' ? '●' : ''" color="green">
<span>Admin</span>
</v-badge>
—
Would you like me to combine **admin login, presence handling**, and **this Pinia store** into a single working GitHub example repo or zip?
1. **List of online users**
2. **Button to open a chat**
3. **Realtime reply/chat UI on the right**
All built using **Vue 3 + Vuetify + Firebase + Pinia**.
—
## ✅ 1. Firebase Realtime DB Structure
We'll track online users under:
chats/{uid}/status: {
online: true,
last_active: timestamp,
name: 'User Name',
email: 'user@email.com'
}
And store chat messages under:
chats/{uid}/messages
—
## ✅ 2. Track User Online Status
In `UserChat.vue`, after anonymous login:
import { db } from '@/firebase'
import { ref as dbRef, set, onDisconnect } from 'firebase/database'
await set(dbRef(db, `chats/${uid}/status`), {
online: true,
last_active: new Date().toISOString(),
name: 'User Name',
email: 'email@example.com'
})
onDisconnect(dbRef(db, `chats/${uid}/status`)).set({
online: false,
last_active: new Date().toISOString()
})
—
## ✅ 3. Pinia Store for Admin Panel Users
### `src/stores/onlineUsersStore.ts`
import { defineStore } from 'pinia'
import { db } from '@/firebase'
import { ref as dbRef, onValue } from 'firebase/database'
import { ref } from 'vue'
export const useOnlineUsersStore = defineStore('onlineUsers', () => {
const users = ref<Record<string, any>>({})
const loadOnlineUsers = () => {
const chatRef = dbRef(db, 'chats')
onValue(chatRef, (snapshot) => {
const data: Record<string, any> = {}
snapshot.forEach((child) => {
const status = child.val()?.status
if (status?.online) {
data[child.key!] = {
uid: child.key!,
...status
}
}
})
users.value = data
})
}
return {
users,
loadOnlineUsers
}
})
—
## ✅ 4. Admin Panel Layout (`AdminPanel.vue`)
<template>
<v-container fluid>
<v-row>
<!-- Sidebar - Online Users -->
<v-col cols="4">
<v-card>
<v-card-title>Online Users</v-card-title>
<v-divider></v-divider>
<v-list>
<v-list-item
v-for="(user, uid) in userStore.users"
:key="uid"
@click="selectUser(uid)"
:active="uid === selectedUid"
>
<v-list-item-title>{{ user.name }}</v-list-item-title>
<v-list-item-subtitle>{{ user.email }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card>
</v-col>
<!-- Chat Panel -->
<v-col cols="8" v-if="selectedUid">
<v-card>
<v-card-title>Chat with {{ userStore.users[selectedUid].name }}</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-list max-height="400" class="overflow-y-auto">
<v-list-item
v-for="msg in chatMessages"
:key="msg.id"
:class="msg.sender === 'admin' ? 'text-left' : 'text-right'"
>
<v-chip :color="msg.sender === 'admin' ? 'blue' : 'green'" text-color="white">
{{ msg.text }}
</v-chip>
</v-list-item>
</v-list>
<v-text-field
v-model="input"
label="Type message"
append-icon="mdi-send"
@click:append="sendMessage"
@keyup.enter="sendMessage"
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { db } from '@/firebase'
import {
ref as dbRef,
onValue,
push,
serverTimestamp
} from 'firebase/database'
import { useOnlineUsersStore } from '@/stores/onlineUsersStore'
const userStore = useOnlineUsersStore()
userStore.loadOnlineUsers()
const selectedUid = ref('')
const input = ref('')
const chatMessages = ref<any[]>([])
watch(selectedUid, (uid) => {
if (!uid) return
const msgRef = dbRef(db, `chats/${uid}/messages`)
onValue(msgRef, (snapshot) => {
const msgs: any[] = []
snapshot.forEach((child) => {
msgs.push({ id: child.key, ...child.val() })
})
chatMessages.value = msgs
})
})
const selectUser = (uid: string) => {
selectedUid.value = uid
}
const sendMessage = () => {
if (!input.value.trim() || !selectedUid.value) return
const refToMsg = dbRef(db, `chats/${selectedUid.value}/messages`)
push(refToMsg, {
sender: 'admin',
text: input.value.trim(),
timestamp: serverTimestamp()
})
input.value = ''
}
</script>
—
## ✅ 5. Optional Enhancements
– Show last message in sidebar
– Add search bar for users
– Load chat history on demand (pagination)
– Add avatar/icon per user
—
Would you like this exported into a GitHub repo or a zip structure to test locally?