Building real-time applications has never been easier thanks to modern frameworks and serverless platforms. In this tutorial, you’ll learn how to build a fully functional real-time chat application using Nuxt 4, Firebase, and VueUse — a powerful combination that gives you instant updates, smooth UX, and minimal backend overhead.
What We’re Building
We’ll create a simple but modern chat app where users can:
-
Sign in using Google or Email/Password
-
Join a real-time chat room
-
Send and receive messages instantly
-
See online/offline presence
-
Enjoy conveniences like auto-scroll, dark mode, clipboard copy, and more
Why Nuxt 4?
Nuxt 4 introduces improvements over Nuxt 3, including:
-
Faster DX and improved HMR
-
Enhanced server routing (Nitro v3)
-
Better TypeScript support
-
More stable app directory routing
-
Built-in patterns that simplify full-stack development
It’s the best choice for Vue developers building modern, full-stack web apps.
Why Firebase?
Firebase lets you build real-time systems without the need to manage servers. You get:
-
Firestore for real-time updates
-
Authentication with multiple providers
-
Simple web SDK
-
Global scalability out of the box
Perfect for small and medium-sized chat apps.
Why VueUse?
VueUse is a utility library built on top of Vue’s Composition API, offering:
-
useFirebaseAuth()for quick Firebase auth integration -
useOnline()for offline detection -
useClipboard()for quick copying -
useLocalStorage()/useStorage()for persistent state -
Dozens of high-level helpers that make coding faster
You’ll see how VueUse drastically reduces boilerplate.
Prerequisites
Before we start building the real-time chat application with Nuxt 4, Firebase, and VueUse, make sure you have the following tools and setup ready.
✔ Node.js 20 or Later
Nuxt 4 requires a modern Node environment.
You can check your version with:
node -v
If you need to upgrade, download it from the official Node.js website or use nvm:
nvm install 20
nvm use 20
✔ Basic Knowledge of Vue.js / Composition API
While Nuxt 4 abstracts a lot for you, it helps to understand:
-
Vue components
-
Reactive state
-
Composition API (
ref,reactive, etc.)
✔ Nuxt CLI (optional)
You don’t need to install Nuxt globally, but you can:
npm install -g nuxi
We’ll create the project using npx, so global installation is optional.
✔ A Firebase Account
You’ll need:
-
A Google account
-
Access to the Firebase Console
-
A new Firebase project with:
-
Authentication enabled
-
Firestore enabled
-
A Web App created to get configuration keys
-
We’ll walk through this setup soon.
✔ A Git-capable Environment (optional)
Not required, but recommended for version control:
git --version
✔ A Code Editor (VS Code recommended)
VS Code + Vue extensions will make development easier:
-
Volar (Vue Language Tools)
-
Tailwind CSS IntelliSense (if using Tailwind)
That’s all you need — you’re ready to build!
Creating the Nuxt 4 Project
In this section, we’ll set up a fresh Nuxt 4 project and prepare it for Firebase and VueUse integration. We'll use the latest Nuxt 4 scaffolding setup with an app/ directory structure.
1. Create a New Nuxt 4 Project
Run the following command in your terminal:
npx nuxi init nuxt4-chat-app
This creates a new Nuxt 4 project with the recommended file structure.
Go into the project directory:
cd nuxt4-chat-app
Install dependencies:
npm install
Now run the development server:
npm run dev
Open the browser and visit:
http://localhost:3000

You should see the Nuxt 4 starter page.
2. Install Firebase & VueUse
We need Firebase for authentication + Firestore, and VueUse for convenient utilities.
Install both:
npm install firebase @vueuse/core
3. (Optional) Install Tailwind CSS
We’ll use Tailwind for fast UI styling.
Nuxt 4 has first-class Tailwind integration via a module.
Install Tailwind:
npx nuxi module add tailwindcss
This creates:
-
tailwind.config.ts -
assets/css/tailwind.css
Also adds the module to nuxt.config.ts.
If you’re not using Tailwind, you can skip this part — but the UI examples will assume Tailwind is available.
4. Project Structure Overview (Nuxt 4)
Your project should now look like this:
nuxt4-chat-app/
│
├─ app/
│ ├─ layouts/
│ ├─ pages/
│ ├─ plugins/
│ └─ components/
│
├─ public/
├─ nuxt.config.ts
├─ package.json
├─ tailwind.config.ts (if enabled)
└─ .env (we will add later)
Key folders we will use:
-
app/pages → Chat pages (
/login,/chat) -
app/plugins → Firebase plugin
-
app/middleware → Auth guard
-
components → Chat UI components
Nuxt will automatically load files based on folder names — no need for manual imports for pages, layouts, or middleware.
5. Prepare Environment Variables
Create a .env file in the project root:
touch .env
We will populate this later once Firebase is set up.
The Nuxt 4 project is now fully prepared.
Setting Up Firebase
In this section, we’ll create a Firebase project, enable Authentication and Firestore, and prepare the configuration keys that we’ll later inject into Nuxt 4 via environment variables.
1. Create a Firebase Project
-
Go to the Firebase Console:
https://console.firebase.google.com -
Click Add project
-
Enter a project name (e.g.,
nuxt4-chat-app) -
Disable Google Analytics (optional)
-
Click Create Project
Once Firebase finishes provisioning, click Continue.
2. Add a Web App
We need a Web App to get the Firebase SDK configuration.
Inside your project:
-
Go to Project Overview → Web → “</>”
-
Click Register App
-
Choose a name (e.g.,
nuxt4-web) -
Click Register App
-
Copy the Firebase config object that looks like this:
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "..."
};
You will place these values into .env soon.
3. Enable Firebase Authentication
Go to:
Build → Authentication → Get Started
Enable the providers you need:
Preferred (simplest)
✔ Google Provider
Optional
✔ Email/Password
✔ GitHub
✔ Facebook
We will implement Google Sign-in first.
4. Enable Firestore Database
We’ll use Firestore because:
-
It provides built-in real-time subscriptions using
onSnapshot -
It’s structured and scalable
-
Perfect for chat apps
Steps:
-
Go to Build → Firestore Database
-
Click Create Database
-
Choose Start in production mode
-
Select your region (e.g., asia-southeast1)
-
Click Enable
Firestore will now create your first database instance.
5. Add Firebase Keys to the .env File
Open your Nuxt project and edit .env:
NUXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_auth_domain
NUXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_storage_bucket
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
NUXT_PUBLIC_FIREBASE_APP_ID=your_app_id
Why NUXT_PUBLIC_?
Nuxt 4 requires NUXT_PUBLIC_ prefix for environment variables exposed to frontend code.
6. Add Runtime Config in nuxt.config.ts
Open nuxt.config.ts and add:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
runtimeConfig: {
public: {
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID,
firebaseStorageBucket: process.env.NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID
}
}
})
Now Nuxt exposes Firebase configuration safely and typed.
7. Verify Environment Variables
Restart your dev server to load .env:
npm run dev
Check in the browser console:
useRuntimeConfig().public.firebaseApiKey
If you see the correct key, Firebase config is connected properly.
That completes your Firebase setup.
Creating a Firebase Plugin (Nuxt 4 Style)
With Firebase configured and environment variables ready, the next step is to create a Nuxt 4 plugin that initializes Firebase on the client and makes Firestore + Auth available throughout your app.
Nuxt plugins allow you to run code at app startup and inject reusable instances into the app context.
1. Create Firebase Plugin File
Create a new file:
app/plugins/firebase.client.ts
The .client.ts suffix ensures the plugin only loads on the client, which is required because the Firebase SDK depends on browser APIs.
Add the following:
import { initializeApp, getApp, getApps } from "firebase/app"
import { getAuth } from "firebase/auth"
import { getFirestore } from "firebase/firestore"
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const firebaseConfig = {
apiKey: config.public.firebaseApiKey as string,
authDomain: config.public.firebaseAuthDomain as string,
projectId: config.public.firebaseProjectId as string,
storageBucket: config.public.firebaseStorageBucket as string,
messagingSenderId: config.public.firebaseMessagingSenderId as string,
appId: config.public.firebaseAppId as string,
}
const firebaseApp = getApps().length
? getApp()
: initializeApp(firebaseConfig)
const auth = getAuth(firebaseApp)
const db = getFirestore(firebaseApp)
return {
provide: {
firebase: { app: firebaseApp, auth, db }
}
}
})
2. How to Access Firebase Anywhere in Nuxt
Nuxt automatically injects $firebase into the Nuxt app context:
const { $firebase } = useNuxtApp()
const auth = $firebase.auth
const db = $firebase.db
You'll use this in pages, components, and composables.
3. Optional: TypeScript Support
Add a type declaration file:
types/firebase.d.ts
Add:
import type { FirebaseApp } from "firebase/app"
import type { Auth } from "firebase/auth"
import type { Firestore } from "firebase/firestore"
declare module "#app" {
interface NuxtApp {
$firebase: {
app: FirebaseApp
auth: Auth
db: Firestore
}
}
}
declare module "vue" {
interface ComponentCustomProperties {
$firebase: {
app: FirebaseApp
auth: Auth
db: Firestore
}
}
}
export { }
Open tsconfig.json and add:
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
],
"compilerOptions": {
"types": ["@types/node"],
"paths": {
"#app": ["./.nuxt/types/app"]
}
},
"include": ["types/**/*.d.ts"]
}
Type augmentation only loads at startup.
npm run dev
This gives full intellisense in VS Code.
4. Test the Plugin
In any page (e.g., app/pages/index.vue):
<script setup lang="ts">
const { $firebase } = useNuxtApp()
console.log("Firebase app:", $firebase.app)
console.log("Auth instance:", $firebase.auth)
console.log("Firestore:", $firebase.db)
</script>
<template>
<div>Firebase Test Page</div>
</template>
You should see the Firebase objects printed in the browser console.
5. Why Firebase Plugin is Important
-
Ensures Firebase initializes only once
-
Prevents HMR double-init issues
-
Makes Firebase Auth & Firestore globally accessible
-
Compatible with Nuxt SSR (client-only)
-
Cleaner imports for all pages
Authentication System
In this section, we’ll implement user authentication using Firebase Auth and VueUse, plus Nuxt 4 middleware to protect routes.
We’ll start with Google Sign-In (simplest) but our structure will allow adding Email/Password later if you want.
1. Install VueUse Firebase Support (No extra package required)
@vueuse/core already includes Firebase bindings such as useFirebaseAuth().
We already installed VueUse:
npm install @vueuse/core
2. Create /login Page
Create:
app/pages/login.vue
Add:
<script setup lang="ts">
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"
const { $firebase } = useNuxtApp()
const auth = $firebase.auth
const isLoading = ref(false)
async function loginWithGoogle() {
try {
isLoading.value = true
const provider = new GoogleAuthProvider()
await signInWithPopup(auth, provider)
navigateTo("/chat")
} catch (err) {
console.error("Login failed", err)
} finally {
isLoading.value = false
}
}
</script>
<template>
<div class="flex items-center justify-center min-h-screen bg-gray-100">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-sm">
<h1 class="text-xl font-semibold text-center mb-4">Login</h1>
<button
class="w-full py-3 bg-blue-600 text-white rounded hover:bg-blue-700"
@click="loginWithGoogle"
>
<span v-if="!isLoading">Sign in with Google</span>
<span v-else>Loading...</span>
</button>
</div>
</div>
</template>
This gives you a simple Google Sign-In page.
3. Track Auth State Using VueUse
We use useFirebaseAuth() which automatically tracks:
-
current user
-
loading state
-
real-time auth changes
Create a composable:
app/composables/useUser.ts
Add:
import { useAuth } from "@vueuse/firebase"
export const useUser = () => {
const { $firebase } = useNuxtApp()
const auth = $firebase.auth
const { user } = useAuth(auth)
return { user }
}
Now any component can do:
const { user } = useUser()
4. Create Nuxt 4 Route Middleware for Auth Protection
We want to block unauthorized users from accessing /chat.
Create:
app/middleware/auth.global.ts
Add:
import { useUser } from "~/composables/useUser"
export default defineNuxtRouteMiddleware((to) => {
const { user } = useUser()
if (!user.value && to.path !== "/login") {
return navigateTo("/login")
}
if (user.value && to.path === "/login") {
return navigateTo("/chat")
}
})
This middleware:
✔ Runs globally
✔ Protects all routes
✔ Redirects correctly based on auth state
5. Add a Logout Button
In any component, e.g., layout or chat page:
const { $firebase } = useNuxtApp()
async function logout() {
await $firebase.auth.signOut()
navigateTo("/login")
}
Example button:
<button
class="text-sm text-gray-600 hover:text-black"
@click="logout"
>
Logout
</button>
6. Verify Authentication Works
-
Visit
/login -
Click Sign in with Google
-
You should be redirected to
/chat -
Refresh page — you should stay logged in
-
Try navigating to
/loginagain — it should redirect to/chat -
Logout should bring you back to
/login
Chat Room UI (Nuxt 4 + Tailwind)
In this section, we’ll build the chat room interface using:
-
Nuxt 4 pages
-
Tailwind CSS
-
Reusable components
-
VueUse utilities (auto-scroll, online detection soon)
The UI includes:
-
A top bar (profile + logout)
-
A messages list
-
A message input box
-
Auto-scroll to bottom
Let’s begin.
1. Create the /chat Page
Create a file:
app/pages/chat.vue
Add this starter structure:
<script setup lang="ts">
import { useUser } from "~/composables/useUser"
const { user } = useUser()
const { $firebase } = useNuxtApp()
// Placeholder messages (we'll replace with Firestore later)
const messages = ref([
{ id: 1, text: "Welcome to the chat!", uid: "system" }
])
const newMessage = ref("")
const messageContainer = ref<HTMLElement | null>(null)
function sendMessage() {
if (!newMessage.value.trim()) return
messages.value.push({
id: Date.now(),
text: newMessage.value,
uid: user.value?.uid ?? "unknown"
})
newMessage.value = ""
scrollToBottom()
}
function scrollToBottom() {
nextTick(() => {
messageContainer.value?.scrollTo({
top: messageContainer.value.scrollHeight,
behavior: "smooth"
})
})
}
async function logout() {
await $firebase.auth.signOut()
navigateTo("/login")
}
</script>
<template>
<div class="flex flex-col h-screen bg-gray-100">
<!-- Top Bar -->
<header class="p-4 bg-white shadow flex justify-between items-center">
<h1 class="text-lg font-semibold">Chat Room</h1>
<button @click="logout" class="text-sm text-gray-600 hover:text-black">
Logout
</button>
</header>
<!-- Messages -->
<main
ref="messageContainer"
class="flex-1 overflow-y-auto p-4 space-y-4"
>
<div
v-for="msg in messages"
:key="msg.id"
class="max-w-xs p-3 rounded-lg"
:class="msg.uid === user?.uid
? 'bg-blue-500 text-white self-end ml-auto'
: 'bg-white shadow'"
>
{{ msg.text }}
</div>
</main>
<!-- Message Input -->
<footer class="p-4 bg-white flex gap-2">
<input
v-model="newMessage"
type="text"
class="flex-1 border p-2 rounded"
placeholder="Type a message..."
@keyup.enter="sendMessage"
/>
<button
@click="sendMessage"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Send
</button>
</footer>
</div>
</template>
✔ What This Gives You Now
You now have:
-
A responsive full-screen chat layout
-
Automatic scrolling
-
Message bubbles (your messages in blue)
-
A logout button
-
Input field + Enter key support
2. Add Tailwind Layout Improvements (optional)
Add this inside <main> if you want slightly nicer spacing:
class="flex-1 overflow-y-auto p-4 space-y-4 flex flex-col"
3. Add Auto-scroll on New Messages
We already added this in scrollToBottom():
nextTick(() => {
messageContainer.value?.scrollTo({
top: messageContainer.value.scrollHeight,
behavior: "smooth"
})
})
Later we will replace this with VueUse’s useScroll() for smoother handling.
4. Show User Avatar in Top Bar (Optional)
Inside the header:
<div class="flex items-center gap-3">
<img
v-if="user?.photoURL"
:src="user.photoURL"
class="w-8 h-8 rounded-full"
alt="avatar"
/>
<span>{{ user?.displayName }}</span>
</div>
🎉 The chat UI is now functional — but still using sample messages.
Real-Time Messages with Firestore
Now that your UI and authentication are working, let's connect everything to Firestore so messages sync instantly across all clients.
We’ll implement:
-
A Firestore
messagescollection -
Real-time listener using
onSnapshot -
Sending new messages via
addDoc -
Auto-scroll synced with real-time updates
Let’s go step-by-step.
1. Create Firestore Collection
In Firebase Console → Firestore:
-
Click Start Collection
-
Collection ID: messages
-
Add any dummy field (or click skip)
We'll overwrite it anyway.
2. Update the Chat Page (chat.vue) to Use Firestore
Open:
app/pages/chat.vue
Replace the script section with:
<script setup lang="ts">
import {
collection,
addDoc,
serverTimestamp,
query,
orderBy,
onSnapshot
} from "firebase/firestore"
import { navigateTo, useNuxtApp } from "nuxt/app"
import { nextTick, onMounted, ref } from "vue"
import { useUser } from "../composables/useUser"
const { user } = useUser()
const { $firebase } = useNuxtApp()
const db = $firebase.db
// Local message storage
const messages = ref<any[]>([])
const newMessage = ref("")
const messageContainer = ref<HTMLElement | null>(null)
// Reference to Firestore collection
const messagesRef = collection(db, "messages")
// Real-time listener
onMounted(() => {
const q = query(messagesRef, orderBy("createdAt", "asc"))
onSnapshot(q, (snapshot) => {
messages.value = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}))
scrollToBottom()
})
})
async function sendMessage() {
if (!newMessage.value.trim()) return
if (!user.value) return
await addDoc(messagesRef, {
text: newMessage.value,
uid: user.value.uid,
displayName: user.value.displayName,
photoURL: user.value.photoURL,
createdAt: serverTimestamp()
})
newMessage.value = ""
}
function scrollToBottom() {
nextTick(() => {
messageContainer.value?.scrollTo({
top: messageContainer.value.scrollHeight,
behavior: "smooth"
})
})
}
async function logout() {
await $firebase.auth.signOut()
navigateTo("/login")
}
</script>
3. Update Template for Real-Time Chat
Update your <main> message list to handle sender/receiver bubbles:
<!-- Messages -->
<main
ref="messageContainer"
class="flex-1 overflow-y-auto p-4 space-y-4 flex flex-col"
>
<div
v-for="msg in messages"
:key="msg.id"
class="max-w-xs p-3 rounded-lg"
:class="
msg.uid === user?.uid
? 'bg-blue-500 text-white self-end ml-auto'
: 'bg-white shadow'
"
>
{{ msg.text }}
</div>
</main>
4. How It Works
✔ Real-time Updates
onSnapshot() subscribes to Firestore:
onSnapshot(q, (snapshot) => {
messages.value = snapshot.docs.map(...)
})
Whenever someone sends a message:
-
Firestore triggers the listener
-
UI updates instantly
✔ Sending Messages
addDoc() pushes a new message to Firestore.
✔ Ordering Messages
We use a Firestore query:
query(messagesRef, orderBy("createdAt", "asc"))
✔ Timestamping
serverTimestamp() ensures accurate ordering across devices.
5. Auto-Scroll on New Messages
We scroll to the bottom automatically after each DB update:
scrollToBottom()
This ensures chat behaves like WhatsApp / Messenger.
6. Optional: Show Avatar + Name
Inside each chat bubble:
Enhancing the Experience with VueUse
Now that your real-time chat functionality works, we can make the experience smoother, more user-friendly, and more modern using VueUse, a powerful collection of Vue composition utilities.
In this section, we’ll add:
-
Online/offline indicator using
useOnline() -
Auto-scroll helper using
useScroll() -
Copy-to-clipboard using
useClipboard() -
Theme persistence (dark mode) using
useLocalStorage() -
Typing indicator debounce using
useDebounceFn()(optional)
Let’s implement them step-by-step.
1. Online / Offline Badge (VueUse: useOnline)
Add to your <script setup> in chat.vue:
import { useOnline } from '@vueuse/core'
const isOnline = useOnline()
Add this banner at the top of your template, below the header:
<div
v-if="!isOnline"
class="bg-red-500 text-white text-center py-2 text-sm"
>
You are offline — messages will not sync.
</div>
✔ Works instantly
✔ Updates as the user goes offline/online
✔ No extra code needed
2. Smooth Auto-Scroll with VueUse (useScroll)
Replace your manual scrollToBottom() with VueUse:
import { useScroll } from '@vueuse/core'
const messageContainer = ref<HTMLElement | null>(null)
const { y } = useScroll(messageContainer)
Then modify scrollToBottom:
function scrollToBottom() {
nextTick(() => {
y.value = messageContainer.value?.scrollHeight || 999999
})
}
✔ Simpler
✔ Smoother
✔ Uses reactive scrolling
3. Copy Message to Clipboard (useClipboard)
A nice UX touch: click on any message to copy text.
Add in script:
import { useClipboard } from '@vueuse/core'
const { copy } = useClipboard()
Modify your message bubble:
<div class="flex items-start gap-2" :class="msg.uid === user?.uid ? 'flex-row-reverse' : ''"
v-for="msg in messages" :key="msg.id" @click="copy(msg.text)">
<img
v-if="msg.photoURL"
:src="msg.photoURL"
class="w-8 h-8 rounded-full"
/>
<div>
<div class="text-xs text-gray-500" v-if="msg.displayName">
{{ msg.displayName }}
</div>
<div
class="p-3 rounded-lg max-w-xs"
:class="msg.uid === user?.uid
? 'bg-blue-500 text-white ml-auto'
: 'bg-white shadow'
"
>
{{ msg.text }}
</div>
</div>
</div>
✔ Instant copy
✔ No toast? You can add it if you want later
✔ Very useful for code snippets or long messages
4. Persistent Dark Mode (useLocalStorage)
Add at top of script:
import { useLocalStorage } from '@vueuse/core'
const isDark = useLocalStorage('chat-theme-dark', false)
Add a toggle in the header:
<button
@click="isDark = !isDark"
class="text-sm px-3 py-1 border rounded"
>
{{ isDark ? 'Light Mode' : 'Dark Mode' }}
</button>
Wrap your root container with a dark class toggle:
<div :class="isDark ? 'dark bg-gray-900 text-white' : 'bg-gray-100'">
✔ Theme persists across refresh
✔ Stored in localStorage
✔ Works across SSR/CSR
5. Typing Indicator (Optional)
Use useDebounceFn to detect typing without spamming Firestore.
import { useDebounceFn } from "@vueuse/core"
const isTyping = ref(false)
const handleTyping = useDebounceFn(() => {
isTyping.value = false
}, 500)
watch(newMessage, () => {
isTyping.value = true
handleTyping()
})
Add indicator in UI:
<div v-if="isTyping" class="text-sm text-gray-500 px-4 py-2">
Someone is typing…
</div>
🎉 Summary of Enhancements
| Feature | VueUse Utility | Benefit |
|---|---|---|
| Online/Offline | useOnline |
Instant connectivity awareness |
| Auto-scroll | useScroll |
Smooth message scrolling |
| Copy message | useClipboard |
Quick sharing & reuse |
| Theme persistence | useLocalStorage |
Dark/light mode saved |
| Typing indicator | useDebounceFn |
Better UX, less noise |
Your chat app now feels polished, modern, and friendly.
Deployment (Firebase Hosting or Vercel)
Your Nuxt 4 + Firebase real-time chat app is now fully functional.
The last step is deploying it to production. You have two excellent options:
-
Firebase Hosting (Recommended) — Easy, global CDN, perfect integration with Firebase services.
-
Vercel — Great for Nuxt apps, automatic builds, perfect for hybrid SSR.
Below are full deployment steps for both.
🔥 Option A — Deploy to Firebase Hosting (Recommended)
This is the easiest and most integrated option for Firebase apps.
1. Install Firebase CLI
npm install -g firebase-tools
Log in:
firebase login
2. Initialize Firebase Hosting
Inside your project:
firebase init hosting
Choose:
✔ Use existing project
✔ Select your Firebase project
✔ Public directory: .output/public
✔ Configure as SPA: No
✔ Overwrite existing index.html: No
3. Build the Nuxt App
npm run build
Nuxt 4 outputs into:
.output/
├─ public/ <-- frontend build
├─ server/ <-- Nitro server (not needed for static hosting)
Because Firebase Hosting is static-only, you are deploying the public build.
4. Deploy
firebase deploy
🔥 Your live URL is now available!
⚡ Optional: Use Firebase Hosting + Cloud Run for SSR
If you want full SSR, deploy the .output/server folder to Cloud Run and route traffic via Firebase Hosting.
It requires:
firebase init hosting:github
firebase init functions
But for most chat apps, the static deployment is fine.
▲ Option B — Deploy to Vercel (SSR-Friendly)
Vercel is great if you want:
-
Server-side rendering
-
Edge deployments
-
Auto builds from GitHub
5. Create Vercel Project
Either:
npm i -g vercel
vercel
Or go to: https://vercel.com/new
Import your GitHub repo.
6. Configure Environment Variables
Go to:
Project → Settings → Environment Variables
Add all Firebase public keys:
NUXT_PUBLIC_FIREBASE_API_KEY
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN
NUXT_PUBLIC_FIREBASE_PROJECT_ID
NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
NUXT_PUBLIC_FIREBASE_APP_ID
7. Deploy Automatically
Every push to main:
🚀 Vercel builds + deploys your Nuxt 4 app
🚀 SSR enabled
🚀 Live URL available instantly
Or deploy manually:
vercel --prod
👍 Which Deployment Should You Choose?
| Hosting | Best For | Pros | Cons |
|---|---|---|---|
| Firebase Hosting | Static SPA | Fast, simple, cheap, perfect with Firebase | No SSR |
| Vercel | SSR Nuxt apps | Full SSR support, easy Git integration | Costs more at scale |
For a real-time chat app running on Firestore, Firebase Hosting is usually enough unless you need SSR pages.
🎉 Deployment Complete!
Your Nuxt 4 + Firebase + VueUse realtime chat app is production-ready.
Conclusion
Congratulations! 🎉
You’ve successfully built a fully functional real-time chat application using:
-
Nuxt 4 for modern Vue architecture
-
Firebase Authentication for secure login
-
Firestore for instant real-time messaging
-
VueUse for powerful UX enhancements
-
Tailwind CSS for clean, responsive UI styling
This project combined full-stack concepts—authentication, protected routes, real-time data streams, composables, and UI utilities—into a modern, production-ready chat app.
What You Achieved
✅ Real-time messaging
Firestore listeners update messages instantly across all clients
with zero backend setup.
✅ Secure authentication
Google Sign-In + reactive auth tracking via VueUse.
✅ Protected routes
Nuxt 4 middleware ensures only authenticated users access the chat.
✅ Modern UI
Tailwind-powered layout with avatars, message bubbles, and auto-scroll.
✅ VueUse enhancements
-
Online/offline detection
-
Clipboard support
-
Debounced typing updates
-
Persistent dark mode
-
Smooth auto-scroll
✅ Deployment-ready
You can host it on Firebase Hosting or deploy full SSR on Vercel.
Possible Improvements
Want to take this further? Here are natural next steps:
🔹 Add multiple chat rooms
Use a rooms collection and nested messages.
🔹 Add message reactions and editing
Like Slack or Discord.
🔹 Add file/image upload
Integrate Firebase Storage.
🔹 Typing indicators with Firestore presence
Sync who’s typing in real time.
🔹 Push notifications
Use Firebase Cloud Messaging for mobile-like alerts.
🔹 Server-side rendering (SSR)
Deploy via Cloud Run or Vercel for SEO-friendly pages.
All improvements are fully compatible with your current architecture.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about Nuxt.js, you can take the following cheap course:
- Nuxt 3 & Supabase Mastery: Build 2 Full-Stack Apps
- Build Web Apps with Nuxt.js 3: Master TypeScript & API [New]
- The Nuxt 3 Bootcamp - The Complete Developer Guide
- Complete Nuxt.js Course (EXTRA React)
- The Complete NUXT 3 Bootcamp: Full-Stack Guide
- Nuxt 3 Authentication with Laravel Sanctum:A Practical Guide
- Learn How to Make Your Nuxt 3 App SEO-Friendly
- Headless Prestashop with Nuxt JS
Thanks!
