Skip to content
FunDev
FunDev
vite

Building and Deploying a Serverless Firebase Web Chat with Vite and TypeScript

Building and Deploying a Serverless Firebase Web Chat with Vite and TypeScript
14 views
13 min read
#vite
Table of Contents

Building and Deploying a Web Chat with Vite and TypeScript

Goal: migrate a vanilla chat app to Vite + TypeScript and set up production deployment with Ubuntu + Nginx + PM2 + HTTPS + GITHUB ACTIONS


0) What Is Web Chat?

“Dad, I want to use KakaoTalk too!”

The children were disappointed that they could not message their family because they did not have phones. They had tablets and computers, but messengers such as KakaoTalk require a phone number.

So I built one myself.

  • Works without a phone, using only a web browser on a tablet or computer
  • A private chat room just for the family
  • Assign cute colors to each child's name
  • Receive push notifications for new messages

I initially built it quickly with plain HTML and JavaScript, but the code grew complex as features accumulated. This post records the process of modernizing its structure with Vite + TypeScript.


1) Why Vite + TypeScript?

The reason I did not immediately adopt a framework was simple: I first needed **“a structure with the fundamentals of frontend development—modules, builds, and types—before a framework.”**

  • Vite: a toolchain covering the development server with HMR and optimized production builds
  • TypeScript: a type system that catches errors before runtime, during development and builds
  • The code can be structured without a framework, and the same fundamentals remain useful if it later expands to Vue or React.

2) Vite Basics: A Development Server and Build Tool in One

2.1 The Two Things Vite Does

Vite optimizes development and deployment separately.

(1) Development mode: npm run dev

  • Run a local development server
  • File changes are reflected immediately through HMR (Hot Module Replacement)
  • ES modules make the import/export flow natural

(2) Deployment build: npm run build

  • Optimize the project and output it to dist/
  • Tree shaking (remove unused code), minification, hashed filenames, and more
  • Optional source maps to help debugging

Simply put, it is fast during development and creates optimized output for deployment.

2.2 Why index.html Matters in Vite

In a Vite project, index.html is more than HTML: it is the app's entry point.

<script type="module" src="/src/main.ts"></script>

Starting from this line, Vite:

  1. Loads /src/main.ts
  2. Traces the files imported by main.ts
  3. Builds the full dependency graph for development and builds

The basic Vite structure is therefore index.html → main.ts → 모듈들.

2.3 src/ vs. public/ (Important in Practice)

src/ Contains Files to Bundle

  • Connect TypeScript, JavaScript, and CSS through imports
  • Vite analyzes and optimizes them

public/ Contains Static Files Copied As-Is

  • Copied to the dist root without processing during the build
  • Also available directly as /filename on the development server

Why put the service worker in public/?

  • Service workers are sensitive to paths and scope
  • They often require a particular filename at the root path

→ public/firebase-messaging-sw.js is the most reliable choice

2.4 Why Use an Alias (@)?

Relative paths become messy as the code grows.

import { initFCM } from '../../../firebase/fcm'  // 😵

With an alias:

import { initFCM } from '@/firebase/fcm'        // 🙂

Keep in mind:

  • Vite reads aliases from vite.config.ts
  • TypeScript and the IDE read paths from tsconfig.json

So both must match for the app to run and the editor to remain error-free.


3) TypeScript Basics: Catching Errors Early

3.1 What Is Different About TypeScript?

JavaScript errors often surface only at runtime.

Before the code runs, TypeScript:

  • Checks variables, functions, and data structures
  • Flags suspicious code with compile-time warnings and errors

The key point:

TS does not change the program's runtime result; it is a tool for reducing mistakes during development.

3.2 Does TypeScript Require Node?

  • Browsers run JavaScript, not TypeScript.
  • TS is therefore converted, or transpiled, into JS during the build.
  • Tools such as Vite and tsc normally run in the Node/npm ecosystem, so Node is needed for development and builds.

But Node is not required in production when users run the deployed app.

  • Users receive only the static JS, CSS, and HTML files in dist/.
  • A static server such as Nginx can serve those files.

3.3 Understanding What tsconfig.json Options Mean

{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "moduleResolution": "bundler",
    "lib": ["ES2020", "DOM", "DOM.Iterable"]
  }
}
  • strict: true → aggressively catches common bugs such as possible null values and type mismatches
  • noEmit: true → tsc checks types without generating JS files; Vite handles the actual build
  • moduleResolution: "bundler" → resolves imports for a bundler environment such as Vite
  • lib: ["DOM", ...] → includes browser DOM API types, allowing TypeScript to recognize document, HTMLElement, and others

3.4 Why TS Is Especially Useful for the DOM (Example)

Browser APIs can return null.

const el = document.getElementById('chat');

To TS, the type is HTMLElement | null.

In practice, this is often enforced through a helper:

export function getElement<T extends HTMLElement>(id: string): T {
  const el = document.getElementById(id);
  if (!el) throw new Error(`Missing element: #${id}`);
  return el as T;
}

That keeps the code that follows clean:

const form = getElement<HTMLFormElement>('chatForm');
form.addEventListener('submit', ...);

4) Final Project Structure

kid-chat/
├── src/
│   ├── firebase/
│   │   ├── config.ts      # Firebase 초기화 및 인스턴스 export
│   │   ├── auth.ts        # 인증 관련 함수 (login, logout, signup 등)
│   │   ├── chat.ts        # 채팅 관련 함수 (메시지 전송, 구독, 삭제)
│   │   └── fcm.ts         # FCM 푸시 알림 초기화 및 권한 요청
│   ├── types/
│   │   └── index.ts       # 타입/인터페이스 정의
│   ├── utils/
│   │   └── helpers.ts     # 유틸리티 함수 (formatTime, getNameClass 등)
│   ├── main.ts            # 앱 진입점 (이벤트 핸들러, UI 로직)
│   ├── styles.css
│   └── vite-env.d.ts
├── public/
│   ├── firebase-messaging-sw.js  # 서비스 워커(빌드 시 그대로 복사)
│   └── img/
├── functions/              # Firebase Cloud Functions (푸시 알림)
├── dist/                   # 빌드 결과 (gitignore)
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── firebase.json
├── firestore.rules
└── CLAUDE.md               # 프로젝트 문서

src/ vs. public/: The Important Distinction

  • src/: bundled files (TS/JS/CSS modules)
  • public/: static files served or copied as-is
  • Because of path and scope requirements, public/ is the most reliable place for a service worker.

5) What Is Firebase? Building an App Without Your Own Backend

5.1 What Is Firebase?

Firebase is Google's backend-as-a-service (BaaS) platform. Put simply, it lets you use the following without building a server yourself:

  • Authentication: registration, login, and password changes
  • Database (Firestore): message storage and real-time synchronization
  • Push notifications (FCM): notifications for new messages
  • Serverless functions (Cloud Functions): execute server-side logic

In a word, it is **“a collection of tools for completing an app without developing a server.”**

5.2 What Firebase Does in Kid Chat

┌─────────────────────────────────────────────────────────────────┐
│                        Kid Chat 구조도                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   [브라우저]                         [Firebase]                  │
│                                                                 │
│   ┌──────────┐    로그인 요청    ┌─────────────────┐            │
│   │  사용자   │ ───────────────▶ │  Authentication │            │
│   │  (나윤)   │ ◀─────────────── │  (인증 서비스)    │            │
│   └──────────┘    인증 토큰      └─────────────────┘            │
│        │                                                        │
│        │ 메시지 전송                                             │
│        ▼                                                        │
│   ┌──────────┐    실시간 동기화   ┌─────────────────┐            │
│   │ 채팅 화면 │ ◀───────────────▶ │   Firestore     │            │
│   │          │                   │  (데이터베이스)   │            │
│   └──────────┘                   └─────────────────┘            │
│        │                                │                       │
│        │ 알림 수신                       │ 새 메시지 감지         │
│        ▼                                ▼                       │
│   ┌──────────┐    푸시 알림      ┌─────────────────┐            │
│   │ 알림 표시 │ ◀─────────────── │ Cloud Functions │            │
│   │          │                   │   + FCM         │            │
│   └──────────┘                   └─────────────────┘            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 The Role of Each Firebase Service

ServiceRoleUse in Kid Chat
AuthenticationUser authenticationSign in with an account under each person's name
FirestoreNoSQL databaseStore chat messages and synchronize them in real time
FCMPush notificationsSend a notification when a new message arrives
Cloud FunctionsServerless functionsAutomatically trigger push notifications when a message is saved

5.4 How Real-Time Synchronization Works

One of Firestore's core features is the real-time listener.

// 메시지 컬렉션을 "구독"하면
onSnapshot(query(collection(db, 'messages'), orderBy('createdAt')), (snapshot) => {
  // 다른 사람이 메시지를 보낼 때마다 이 콜백이 자동 실행됨
  snapshot.docChanges().forEach((change) => {
    if (change.type === 'added') {
      // 새 메시지 화면에 추가
    }
  });
});

With this approach:

  • A child sends a message
  • Firebase tells every subscriber that something changed
  • The message appears immediately on the parent's screen

If you built the server yourself, you would need to implement WebSockets, connection management, and reconnection logic. Firestore handles all of that.

5.5 The Push-Notification Flow

1. 아이가 메시지 전송
   ↓
2. Firestore에 메시지 저장됨
   ↓
3. Cloud Functions가 "새 메시지 생성" 이벤트 감지
   ↓
4. FCM을 통해 다른 가족 기기에 푸시 알림 전송
   ↓
5. 부모가 태블릿에 "아이: 안녕!" 알림 표시

Why Cloud Functions matters in this flow:

  • A client browser cannot directly send push notifications to another device
  • Server-side code is needed, and Cloud Functions fills that role

6) Core Configuration Files

6.1 package.json

{
  "name": "kid-chat",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "firebase": "^10.13.1"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "vite": "^6.0.0"
  }
}
  • build: type-check with tsc → create an optimized build with vite build
  • preview: a local/server preview that behaves like static hosting in a production environment

6.2 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "strict": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}
  • strict: true: this is where TypeScript's real value in preventing mistakes comes from
  • noEmit: true: tsc only checks; Vite produces the actual bundle
  • paths: configure TS to understand the @/ alias

6.3 vite.config.ts

import { defineConfig } from 'vite';
import { resolve } from 'path';
 
export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  server: {
    host: true,
    port: 3001,
    open: true,
  },
  preview: {
    host: true,
    port: 3001,
    allowedHosts: ['chat.funq.kr'],
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
  },
});
  • host: true: allow external access; localhost binding is the default
  • allowedHosts: prevent “Blocked request. This host is not allowed”

7) Fix the Model with Type Definitions

src/types/index.ts

export interface ChatUser {
  uid: string;
  name: string;
  email: string;
}
 
export interface Message {
  id: string;
  uid: string;
  name: string;
  text: string;
  createdAt: Date | null;
}
 
export type NameColorClass =
  | 'color-nayoon'
  | 'color-soyoon'
  | 'color-parent1'
  | 'color-parent2'
  | 'color-parent3';

Benefits of defining types first:

  • Clear conversion logic from Firestore to the app's internal model
  • Far fewer mistakes in UI rendering and event handlers

8) Separating the Code into Modules

Firebase Configuration (src/firebase/config.ts)

  • Initialize the Firebase app
  • Export Auth, Firestore, and Messaging instances
  • Manage the VAPID key

Authentication (src/firebase/auth.ts)

FunctionDescription
getCurrentUserName()Return the current user's name
setCurrentUserName(name)Set the user name and save it in localStorage
onAuthChange(callback)Listen for authentication-state changes
signup(name, password)Create an account
login(name, password)Log in
logout()Log out
changePassword(current, new)Change the password

Chat (src/firebase/chat.ts)

FunctionDescription
subscribeToMessages(callback)Subscribe to messages in real time
sendMessage(text)Send a message
deleteMessage(messageId)Delete a message
parseMessageData(docId, data)Parse Firestore data

FCM (src/firebase/fcm.ts)

FunctionDescription
initFCM()Initialize FCM and register the service worker
requestNotificationPermission(userId)Request notification permission and save the token
isFcmTokenSaved()Check whether a token has been saved

Utilities (src/utils/helpers.ts)

FunctionDescription
getNameClass(name)Return the color class for a name
nameToEmail(name)Convert a name to a virtual email address
formatTime(date)Format the time
getElement<T>(id)Get a DOM element with type safety

9) How to Run It

Development Server

npm run dev
# http://localhost:3001

Type Check

npx tsc --noEmit

Production Build

npm run build
# dist/ 생성

Preview the Build Output

npm run preview

10) Security: Firebase's Real Security Is in Its Rules

Client configuration values in firebaseConfig are generally safe to expose, but permissions are controlled by Firestore Security Rules.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /messages/{messageId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow delete: if request.auth != null && request.auth.uid == resource.data.uid;
    }
    match /fcmTokens/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Never expose:

  • serviceAccountKey.json
  • Firebase Admin SDK credentials
  • Secret values in environment variables

11) Notes on Bundle Size

npm run build
FileSizegzip
index.html2.70 KB0.93 KB
assets/index-*.css7.32 KB2.17 KB
assets/index-*.js500.36 KB114.97 KB

The JS bundle is relatively large because it includes the Firebase SDK. If needed, consider code splitting with dynamic imports.


12) Production Deployment: Ubuntu + Nginx + PM2 + HTTPS

12.1 Nginx Reverse Proxy Configuration

/etc/nginx/sites-available/chat.conf

sudo tee /etc/nginx/sites-available/chat.conf << 'EOF'
server {
    listen 80;
    server_name chat.funq.kr;
 
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
EOF

Apply it:

sudo ln -s /etc/nginx/sites-available/chat.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

When using a heredoc, EOF must start at the beginning of the line, with no indentation.

12.2 Running preview with PM2

npm install
npm run build
pm2 start npm --name "kid-chat" -- run preview

Frequently used PM2 commands:

pm2 logs kid-chat
pm2 restart kid-chat
pm2 status
pm2 save
pm2 startup

12.3 Firewall (UFW)

ss -tlnp | grep 3001
sudo ufw allow 3001

12.4 HTTPS (Let's Encrypt)

sudo certbot --nginx -d chat.funq.kr

Certbot automatically:

  • Issues a certificate
  • Adds HTTPS configuration
  • Redirects HTTP to HTTPS
  • Registers an automatic renewal schedule

13) Production Troubleshooting

13.1 "Blocked request. This host is not allowed"

Vite's security policy blocks unapproved hosts. → Add the domain to preview.allowedHosts in vite.config.ts.

13.2 Cannot Connect from Outside

  1. Check the host: true setting
  2. Check the firewall with sudo ufw status
  3. Check the listening port: ss -tlnp | grep 3001

14) CI/CD

I applied the same CI/CD approach described in the post below.

Following that post connects the build, deployment, and PM2 restart into one flow.


15) Wrapping Up

The gain from this work was applying “the fundamentals to establish before adopting a framework” to a real service.

  • Module separation reduces how much code must be understood or changed at once
  • Strict TS catches most mistakes during development
  • Vite handles both fast development with HMR and deployment builds
  • Completing production deployment turns it into a service people can actually use

Most importantly, the children can now send “Dad, I love you!” from their tablets.

Related posts

Comments

Korean and English pages share this conversation.

Write a comment

0 / 5,000
You will need this password to edit or delete this comment.

Loading comments…