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