Real-Time Chat App with Nuxt 4, Firebase, and VueUse

by Didin J. on Nov 13, 2025 Real-Time Chat App with Nuxt 4, Firebase, and VueUse

Build a real-time chat app using Nuxt 4, Firebase, and VueUse. Learn authentication, Firestore messaging, UI enhancements, and deployment step-by-step.

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:

  1. A Google account

  2. Access to the Firebase Console

  3. 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

Real-Time Chat App with Nuxt 4, Firebase, and VueUse - npm run dev

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

  1. Go to the Firebase Console:
    https://console.firebase.google.com

  2. Click Add project

  3. Enter a project name (e.g., nuxt4-chat-app)

  4. Disable Google Analytics (optional)

  5. 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:

  1. Go to Project Overview → Web → “</>”

  2. Click Register App

  3. Choose a name (e.g., nuxt4-web)

  4. Click Register App

  5. 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:

  1. Go to Build → Firestore Database

  2. Click Create Database

  3. Choose Start in production mode

  4. Select your region (e.g., asia-southeast1)

  5. 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

  1. Visit /login

  2. Click Sign in with Google

  3. You should be redirected to /chat

  4. Refresh page — you should stay logged in

  5. Try navigating to /login again — it should redirect to /chat

  6. 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:

  1. A Firestore messages collection

  2. Real-time listener using onSnapshot

  3. Sending new messages via addDoc

  4. 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:

  1. Online/offline indicator using useOnline()

  2. Auto-scroll helper using useScroll()

  3. Copy-to-clipboard using useClipboard()

  4. Theme persistence (dark mode) using useLocalStorage()

  5. 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:

Thanks!