Building a Modern Desktop Chat App with Wails: Go + Vue Made Easy
This article walks through creating a lightweight, cross‑platform desktop chat application using Wails, demonstrating project structure, Go‑to‑frontend API generation, real‑time event communication, SQLite storage, UI implementation with Vue3, image handling, a one‑click dev script, and packaging size advantages.
Project Structure
Wails uses a clear directory layout: Go source files reside in backend/, the Vue3 UI in frontend/, the entry point is main.go, and project configuration is stored in wails.json.
chat-app/
├─ backend/ # Go backend
│ ├─ chat.go
│ ├─ chat_ws.go
│ ├─ repository.go
│ ├─ file.go
│ └─ models.go
├─ frontend/ # Vue3 frontend
│ └─ src/
│ ├─ views/Chat.vue
│ ├─ components/MessageBubble.vue
│ ├─ store/chat.ts
│ └─ styles/
├─ wails.json
└─ main.goAutomatic Frontend API Generation
Wails generates a TypeScript SDK that exposes Go methods directly to the frontend, eliminating the need for bridge code.
func (c *ChatService) SendMessage(roomID string, msg string) error {
// store message
return nil
} import { SendMessage } from '@/wailsjs/go/chat/ChatService';
SendMessage("room1", "Hello");Bidirectional Event Communication
The backend can emit events that the frontend listens to, providing a simple real‑time update mechanism.
runtime.EventsEmit(c.ctx, "new_message", msg) EventsOn("new_message", (msg: Message) => {
chatStore.addMsg(msg);
});Local SQLite Storage
Messages are persisted in a local SQLite database, enabling instant loading of chat history on startup.
func (r *Repository) SaveMessage(m *Message) error {
_, err := r.db.Exec(`
INSERT INTO messages (room_id, sender, type, content, created_at)
VALUES (?, ?, ?, ?, ?)
`, m.RoomID, m.Sender, m.Type, m.Content, m.CreatedAt)
return err
} func (r *Repository) GetMessages(roomID string) ([]Message, error) {
rows, err := r.db.Query(`
SELECT sender, type, content, created_at
FROM messages WHERE room_id = ?
ORDER BY created_at ASC
`, roomID)
// process rows ...
return msgs, err
}Frontend loads the history with a promise call:
LoadHistory(roomID).then(msgs => store.setHistory(roomID, msgs));UI Implementation (Vue3 + TailwindCSS + Pinia)
The chat UI is built with Vue components and Tailwind for styling. The following snippet shows the message list rendering logic, handling text, emoji, image, and recall message types.
<div ref="msgContainer" class="flex-1 overflow-auto p-6 bg-gradient-to-b from-white to-gray-50">
<div v-for="m in messages" :key="m.id" class="mb-4 flex" :class="m.username===username ? 'justify-end' : 'justify-start'">
<div :class="['max-w-[60%] p-4 rounded-2xl shadow-md', m.username===username ? 'bg-gradient-to-r from-indigo-500 to-indigo-600 text-white' : 'bg-white border border-gray-200 shadow-sm']">
<div class="flex items-center justify-between">
<div class="text-sm font-medium">{{ m.username }}</div>
<div class="text-xs text-gray-200" v-if="m.username===username">{{ formatTime(m.timestamp) }}</div>
</div>
<div class="mt-2">
<template v-if="m.type==='text'">
<div class="whitespace-pre-wrap">{{ m.content }}</div>
</template>
<template v-else-if="m.type==='emoji'">
<div class="text-2xl">{{ m.content }}</div>
</template>
<template v-else-if="m.type==='image'">
<img :src="m.content" class="w-48 h-48 object-cover rounded cursor-pointer" @click="viewImage(m.content)" />
</template>
<template v-else-if="m.type==='recall'">
<div class="italic text-sm text-gray-400">Message recalled</div>
</template>
</div>
<div class="text-xs text-gray-400 mt-1" v-if="m.username!==username">{{ formatTime(m.timestamp) }}</div>
</div>
</div>
</div>File Handling (Image & Emoji)
Frontend selects a local file and uploads it; the backend writes the file to disk.
function selectImage() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = () => {
const file = input.files[0];
UploadImage(file).then(url => SendMessage(room, url));
};
input.click();
} func (c *ChatService) UploadImage(roomID string, data []byte) (string, error) {
filename := fmt.Sprintf("img_%d.png", time.Now().Unix())
path := filepath.Join(c.DataDir, filename)
os.WriteFile(path, data, 0644)
return filename, nil
}Development Script
A minimal Bash script sets up Go modules and launches the Wails development server.
#!/bin/bash
go mod tidy
wails devBinary Size
The final packaged binary is only a few dozen megabytes, considerably smaller than typical Electron builds that start at 100 MB.
Repository
Source code is available at:
GitHub: https://github.com/louis-xie-programmer/chat-app
Gitee: https://gitee.com/louis_xie/chat-app
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Wrench
Focuses on code debugging, performance optimization, and real-world engineering, sharing efficient development tips and pitfall guides. We break down technical challenges in a down-to-earth style, helping you craft handy tools so every line of code becomes a problem‑solving weapon. 🔧💻
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
