🇰🇷 채널, DM, 스레드, 검색이 지원되는 오픈소스 설치형 협업 채팅 도구
🇺🇸 Open-source self-hosted team chat with channels, DMs, threads, and search
🇪🇸 Chat colaborativo autoalojado de código abierto con canales, MD, hilos y búsqueda
🇫🇷 Outil de chat collaboratif open source auto-hébergé avec salons, MP, fils et recherche
🇯🇵 チャンネル・DM・スレッド・検索対応のオープンソース自己ホスト型チャットツール
🇨🇳 支持频道、私信、话题和搜索的开源自托管协作聊天工具
🇹🇼 支援頻道、私訊、討論串與搜尋的開源自架協作聊天工具
🇭🇰 支援頻道、私信、討論串同搜尋嘅開源自架協作聊天工具
🇻🇳 Công cụ chat cộng tác tự lưu trữ mã nguồn mở với kênh, DM, chuỗi hội thoại và tìm kiếm
🇮🇩 Alat obrolan kolaborasi open source yang dihosting sendiri dengan saluran, DM, utas, dan pencarian
Supported languages: 🇰🇷 한국어 · 🇺🇸 English · 🇪🇸 Español · 🇫🇷 Français · 🇯🇵 日本語 · 🇨🇳 中文(简体)· 🇹🇼 中文(繁體)· 🇭🇰 廣東話 · 🇻🇳 Tiếng Việt · 🇮🇩 Bahasa Indonesia
Demo: tight.coroke.net
**text**), links, image upload, @mentions, #channel links| Layer | Technology |
|---|---|
| Framework | SvelteKit 2 + Svelte 5 (runes) |
| Database | SQLite via better-sqlite3 |
| ORM | Drizzle ORM |
| Auth | Auth.js for SvelteKit (Google OAuth) |
| Real-time | WebSocket (ws) |
| File Storage | Cloudflare R2 (optional) |
| Deployment | Node.js (@sveltejs/adapter-node) |
git clone https://github.com/rainygirl/tight.git
cd tight
npm install
cp .env.example .env
Open .env and fill in the values:
# Auth.js — generate with: openssl rand -base64 32
AUTH_SECRET=your-secret-here
AUTH_TRUST_HOST=true
# Google OAuth — https://console.cloud.google.com/apis/credentials
# Authorized redirect URI: http://localhost:5173/auth/callback/google
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
# Cloudflare R2 (optional — required for image uploads)
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=tight-uploads
R2_PUBLIC_URL=https://pub-xxxx.r2.dev
# WebSocket
VITE_WS_URL=wss://localhost:3001
WS_PORT=3001
PORT=3000
npm run db:generate
npm run db:migrate
Or start from the included sample database:
cp tight.db.sample tight.db
npm run dev
Open http://localhost:5173, sign in with Google, and create your first workspace.
A pre-built sample database is included at tight.db.sample. Copy it to get started instantly:
cp tight.db.sample tight.db
To regenerate the sample data from scratch (also updates tight.db.sample):
npm run db:seed-sample
This resets the entire database and inserts:
Acme Corp)#general, #engineering, #design, #product, #randomNo real user data is included. When you sign in with Google OAuth after seeding, your account is added as a new user alongside the sample data.
Alternatively, to only add sample users to an existing workspace (without resetting):
npx tsx scripts/seed.ts
http://localhost:5173/auth/callback/googlehttps://your-domain.com/auth/callback/google.envR2 is used for avatar and image uploads. Without it, the upload feature will error.
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_URL in .envnpm run build
npm start
npm start runs node --env-file=.env --import tsx/esm server.ts. This starts the custom server (server.ts) which loads the built SvelteKit handler and attaches the WebSocket server — both on the same port. Place your .env in the project root.
Note:
VITE_WS_URLis a Vite variable baked into the client bundle at build time, not at runtime. Set it to your production WebSocket URL before runningnpm run build.
| Mode | HTTP | WebSocket |
|---|---|---|
| Development | Vite on :5173 |
Standalone on WS_PORT (default 3001) |
| Production | Node.js on PORT (default 3000) |
Same port, path /ws |
In production, the WebSocket server attaches to the HTTP server — no separate port is needed. The client connects to /ws on the same origin.
Environment variables that affect ports:
| Variable | Default | Effect |
|---|---|---|
PORT |
3000 |
HTTP server port (production) |
WS_PORT |
3001 |
WebSocket port (development only) |
VITE_WS_URL |
ws://localhost:3001 |
WebSocket URL baked into the client bundle at build time |
For production, set VITE_WS_URL to your public WebSocket URL before building:
VITE_WS_URL=ws://your-domain.com/ws
Auth.js note:
X-Forwarded-ProtoandX-Forwarded-Hostare required for session cookies to work correctly behind a proxy. Without them, Auth.js may construct the wrong callback URL and redirect back to the login screen after OAuth.
npm run db:generate # Generate migration files from schema changes
npm run db:migrate # Apply pending migrations
npm run db:studio # Open Drizzle Studio (visual DB browser)
npm run db:seed-sample # Reset DB and insert fictional sample data
src/
├── lib/
│ ├── components/
│ │ ├── ChatView.svelte # Main chat area (header, message list, input)
│ │ ├── MessageList.svelte # Virtualized message list with date dividers
│ │ ├── MessageItem.svelte # Single message with reactions, thread reply
│ │ ├── MessageInput.svelte # Rich text editor (bold, links, mentions, images)
│ │ ├── ThreadPanel.svelte # Thread side panel
│ │ ├── Sidebar.svelte # Left nav (channels, DMs, search, profile menu)
│ │ ├── SearchModal.svelte # Full-text search modal with keyboard nav
│ │ └── ProfileCard.svelte # User profile popup
│ ├── db/
│ │ ├── schema.ts # Drizzle table definitions
│ │ └── index.ts # DB instance + auto-migrations
│ ├── i18n/
│ │ └── translations.ts # All UI strings for 10 languages
│ ├── stores/
│ │ ├── theme.svelte.ts # Dark/light mode state
│ │ ├── locale.svelte.ts # Language selection state
│ │ ├── socket.svelte.ts # WebSocket connection + event bus
│ │ ├── sidebar.svelte.ts # Mobile sidebar open/close state
│ │ └── flash.svelte.ts # Message flash-highlight after search navigation
│ ├── ws/
│ │ ├── server.ts # WebSocket server (attaches to HTTP)
│ │ ├── handlers.ts # Message routing logic
│ │ ├── dev-server.ts # Standalone WS server for development
│ │ └── types.ts # WS message type definitions
│ ├── auth.ts # Auth.js config (Google provider, avatar sync)
│ ├── r2.ts # Cloudflare R2 upload helpers
│ └── utils/
│ └── hangulSearch.ts # Korean fuzzy search (초성/중성/종성)
├── routes/
│ ├── +layout.svelte # Root layout (CSS variables, dark mode, fonts)
│ ├── +layout.server.ts # Env var check on startup
│ ├── login/ # Login page
│ ├── setup/ # Workspace creation
│ ├── [workspace]/
│ │ ├── +layout.svelte # App shell (Sidebar + main)
│ │ ├── +layout.server.ts # Auth guard, load channels/DMs/members
│ │ ├── [channel]/ # Channel view
│ │ └── dm/[userId]/ # DM view
│ └── api/
│ ├── channels/ # CRUD + member management
│ ├── threads/[messageId]/ # Thread replies
│ ├── profile/ # Update own profile
│ ├── users/[userId]/ # Public profile lookup
│ ├── upload/ # Server-side R2 file proxy (avoids CORS)
│ ├── upload-url/ # Presigned URL generation (legacy)
│ └── search/ # Full-text message search
└── app.html # HTML shell with dark mode FOUC fix
WS_PORT (default 3001), npm run dev starts both concurrentlyserver.ts), served at /wsVITE_WS_URL (dev) or ws(s)://same-origin/ws (prod)Messages are stored as a custom markup format (not raw HTML):
@[userId:displayName]#[channelId:channelName]**text** (auto-converted on input)[text](url) (inserted via link modal)renderBody() in MessageItem.svelte converts this to safe HTML at render time.
When a user uploads a custom avatar, avatarSource is set to 'custom' in the DB. The Google OAuth callback checks this flag before overwriting the avatar URL, so custom avatars are never replaced by the Google profile picture on subsequent logins.
src/app.html contains an inline blocking script in <head> that reads localStorage and applies data-theme="dark" to <html> before the page renders, preventing the white flash on dark mode reload.
src/lib/utils/hangulSearch.ts implements Hangul decomposition search — typing ㅈㅇ matches 정원, 죠원, etc. Used in DM user search and channel member invite.
LANGUAGES in src/lib/stores/locale.svelte.tssrc/lib/i18n/translations.ts (copy from en and translate all values)🇰🇷 기여를 환영합니다. 아이디어가 담긴 코드라면 새 기능, 개선, 버그 수정, 실험 무엇이든 자유롭게 Pull Request를 열어주세요. 별도의 가이드라인은 없습니다.
🇺🇸 Contributions are welcome. Feel free to open a pull request with any idea — new features, improvements, bug fixes, or experiments. There are no strict guidelines; if you have code that embodies an idea, propose it.
🇪🇸 Las contribuciones son bienvenidas. Abre un pull request con cualquier idea — nuevas funciones, mejoras, correcciones o experimentos. No hay pautas estrictas; si tienes código con una idea, proponla.
🇫🇷 Les contributions sont les bienvenues. Ouvrez librement une pull request avec n'importe quelle idée — nouvelles fonctionnalités, améliorations, corrections ou expériences. Aucune règle stricte ; si votre code porte une idée, proposez-la.
🇯🇵 コントリビューションを歓迎します。新機能・改善・バグ修正・実験など、アイデアを持つコードであれば自由にPull Requestを開いてください。厳格なガイドラインはありません。
🇨🇳 欢迎贡献。无论是新功能、改进、修复还是实验,只要代码中包含想法,均可自由提交 Pull Request。没有严格的规范。
🇹🇼 歡迎貢獻。無論是新功能、改進、修復或實驗,只要程式碼中包含想法,均可自由提交 Pull Request。沒有嚴格的規範。
🇭🇰 歡迎貢獻。無論係新功能、改進、修復定係實驗,只要代碼入面有想法,都可以自由提交 Pull Request。冇嚴格規範。
🇻🇳 Chào đón mọi đóng góp. Hãy thoải mái mở pull request với bất kỳ ý tưởng nào — tính năng mới, cải tiến, sửa lỗi hay thử nghiệm. Không có quy tắc nghiêm ngặt; nếu bạn có code mang ý tưởng, hãy đề xuất.
🇮🇩 Kontribusi sangat disambut. Silakan buka pull request dengan ide apa pun — fitur baru, peningkatan, perbaikan bug, atau eksperimen. Tidak ada panduan ketat; jika kode Anda mengandung ide, ajukan saja.
MIT