VAST is a self-hosted instant messaging platform that ships as a single binary β Rust on the backend, React SPA embedded at compile time, and SQLite for zero-config persistence. No containers, no microservices, no cloud dependency. Just one process, one database file, and your own hardware.
I wanted a team chat tool that didn't outsource its database, its auth, or its uptime to someone else's cloud. Something I could drop onto a $5 VPS and immediately have channels, threads, reactions, file sharing, and full-text search β without configuring Redis, without running a separate database server, without wrestling with Docker Compose.
I also wanted an AI bot that actually lives in the channel, not in a separate sidebar. When you @mention a bot in VAST, it sees the same channel context you do β message history, participants, thread structure β and replies inline, in real time.
So I built it. Rust + React + SQLite + WebSockets. One binary, one database file, zero bullshit.
git clone https://github.com/ipconfiger/vast && cd vast
cp .env.example .env
# Generate a real JWT secret in production:
# openssl rand -base64 48
make devBackend starts on http://localhost:3000, frontend dev server on http://localhost:5173 (with API/WS proxy to the backend).
Admin console: http://localhost:5173/admin/login β admin / admin123 in dev mode.
./scripts/build.sh
# Binary: target/release/im-server
# The frontend is embedded β no separate static file serving needed.cp .env target/release/
./target/release/im-server
# With TLS:
TLS_MODE=self-signed ./target/release/im-servercp .env.docker.example .env.docker
# Edit .env.docker β at minimum set JWT_SECRET
docker compose up -d| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:vast.db |
SQLite path |
JWT_SECRET |
dev-secret-change-me |
Change this in production |
INVITE_CODE |
(removed) | Registration invite codes are managed from the admin console (DB-backed); no default code exists |
SERVER_PORT |
3000 |
HTTP listen port |
UPLOAD_MAX_SIZE |
52428800 |
Max upload (bytes), default 50 MiB |
TLS_MODE |
none |
none, self-signed, lets-encrypt |
ADMIN_USERNAME |
admin |
Admin console username |
ADMIN_PASSWORD |
(empty = disabled) | Admin console password |
For bots with a public API endpointοΌOpenAIγGroqγself-hosted LLM with public URLοΌ:
- Open the admin panel β Bots β Create Bot
- Set Connection Mode to
HTTP - Fill in the API URLοΌe.g.
https://api.openai.comοΌand API Key - Assign the bot to a channel
- Users can now
@botnameto trigger replies
For local LLMs behind NAT/firewallοΌOllamaγLM StudioγvLLMγHermes AgentοΌ:
- Open the admin panel β Bots β Create Bot
- Set Connection Mode to
Connector - Copy the Connection Key shown after creationοΌonly shown onceοΌ
- Assign the bot to a channel
- Choose one of the two connector options:
Option A: Hermes Agent PluginοΌrecommended for Hermes usersοΌ
cp tools/hermes-vast-plugin.py hermes-agent/gateway/platforms/vast.pyThen add to your Hermes Agent config.yaml:
platforms:
- type: vast
connection_key: "your-connection-key"
ws_url: "ws://your-vast-server:3000/ws/bot"Launch with hermes gateway β Hermes manages the WebSocket lifecycle, and your bot inherits Hermes' skill system, memory, and user model.
Option B: Standalone ConnectorοΌworks with any OpenAI-compatible APIοΌ
cd tools
pip install websockets httpx pyyaml
# Edit bot-connector.yaml β paste your connection_key and configure the LLM endpoint
python3 bot-connector.py --config bot-connector.yaml# bot-connector.yaml β example with local Ollama
vast:
url: "ws://your-vast-server:3000/ws/bot"
bots:
- connection_key: "your-uuid-from-admin-panel"
llm:
url: "http://localhost:11434/v1" # Ollama
model: "qwen2.5:7b"
api_key: ""
system_prompt: "δ½ ζ―δΈδΈͺεε₯½ηδΈζε©ζ"./scripts/build.sh
sudo ./deploy/install.sh target/release/im-server
sudo nano /opt/im-server/.env # Set JWT_SECRET and ADMIN_PASSWORD
sudo systemctl start im-serverThe systemd unit runs as a dedicated im-server user with hardening:
NoNewPrivileges=true,PrivateTmp=true,ProtectSystem=strict- Write access limited to
/opt/im-serverand/var/log/im-server - File descriptor limit: 65536
The included deploy/nginx.conf provides TLS termination, WebSocket upgrade, security headers, and rate limiting.
sudo cp deploy/nginx.conf /etc/nginx/sites-available/im-server
sudo ln -s /etc/nginx/sites-available/im-server /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx- Channels β public and private, with archive/unarchive + ZIP download for history export
- Threads β nested replies that don't clutter the main channel view
- Direct Messages β one-on-one conversations, private by default
- Reactions β emoji reactions on any message (pick any emoji, not just a fixed set)
- Message quoting β reply with inline quote preview for context
- Typing indicators and presence β see who's online and who's typing, live
- Upload with multipart support, up to 50 MiB by default (configurable)
- Indexed listing with keyset pagination, grid/list views, and infinite scroll
- Soft delete β files are marked deleted but recoverable; clients see 410 Gone
- Access control β files are scoped to the channel they were uploaded to
- Full-text search across all channel messages
- Indexed via SQLite FTS5, so it's fast even with tens of thousands of messages
- Searches within the user's accessible channels only
- Channel-resident bots β add them as virtual channel members that reply inline
- @mention activation β bots only respond when explicitly called, by name or display name
- Full channel context β message history, participants, and thread structure sent to the bot on each @mention
- Two connection modes:
- HTTP β VAST calls your bot's OpenAI-compatible endpoint directly. Requires a public URLοΌideal for cloud APIs like OpenAI, GroqοΌ.
- ConnectorοΌWebSocketοΌ β your bot connects out to VAST. No public IP needed. Two connector options:
- Standalone Connector β a ~200-line Python script that bridges VAST to any OpenAI-compatible APIοΌOllama, vLLM, LM StudioοΌ. One process can manage multiple bots.
- Hermes Agent Plugin β a native gateway plugin for Hermes Agent. Zero extra process; leverages Hermes' skill system, memory, and user model.
- Admin-managed β create, configure, test connectivity, add/remove from channels from the admin console
- JWT authentication β access + refresh token pair (15 min / 7 days), Argon2id password hashing
- Token epoch revocation β disable a user and every JWT they hold is instantly invalid
- Invite codes β admin-managed, with usage limits and enable/disable toggle
- Join requests β for private channels; owners approve or reject
- Invitations β channel owners can invite specific users directly
A full admin panel isolated from the main app β separate JWT domain, separate login, so a compromised user token can't touch admin endpoints.
- Dashboard β user count, channel count, message count at a glance
- User management β disable, force-logout, reset password, delete
- Invite code management β create, toggle, reset usage count, delete
- Audit logging β every admin action logged with timestamp, admin username, action type, and target
- Bot management β CRUD, test connectivity, assign to channels
Browser push notifications via service worker. Subscribe from the app, get notified when someone @mentions you or sends a DM while you're away.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client β
β βββββββββββ ββββββββββββββββββ ββββββββββββββββββββββββ β
β β React β β WebSocket β β REST API calls β β
β β (SPA) β β (real-time) β β (auth, files, ...) β β
β ββββββ¬βββββ βββββββββ¬βββββββββ ββββββββββββ¬ββββββββββββ β
βββββββββΌββββββββββββββββΌβββββββββββββββββββββββΌββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IM Server (Axum) β
β βββββββββββββ βββββββββββββ βββββββββββββ βββββββββββββββ β
β β Auth β β Channels β β Search β β Files β β
β β (JWT) β β + Threads β β (FTS5) β β (50 MiB) β β
β βββββββββββββ€ βββββββββββββ€ βββββββββββββ€ βββββββββββββββ€ β
β β Bots β β DM β β Reactions β β Web Push β β
β β + WS /bot β β β β β β β β
β βββββββββββββ€ βββββββββββββ€ βββββββββββββ€ βββββββββββββββ€ β
β β Admin β β β β β β β β
β β(JWT+audit)β β β β β β β β
β βββββββββββββ΄ββ΄ββββββββββββ΄ββ΄ββββββββββββ΄ββ΄ββββββββββββββ€ β
β β WebSocket Hub (broadcast + presence) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β
β β SQLite (WAL mode, compile-time migrations) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Bot Connector (optional) β
β βββββββββββββββββββ β
β β Connector / β ββ HTTP βββΊ LLM API β
β β Hermes Plugin β (Ollama / vLLM / OpenAI) β
β βββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Rust + Axum β I wanted a language that compiles to a single binary and a framework that's fast without being bloated. Axum's extractor-based middleware and built-in WebSocket support make the code clean and the runtime lean. Memory usage at idle is around 10-15 MB.
React + Vite + Tailwind β The frontend is embedded into the Rust binary via rust-embed. Vite builds it, Cargo bundles it. No separate frontend deployment, no CDN, no build steps on the server.
SQLite β The database is a single file. Back it up with cp. Migrations are embedded at compile time, so the server auto-creates and auto-migrates on first run. WAL mode gives concurrent read performance good enough for a team-sized IM server.
WebSocket β Axum's native WebSocket support means no extra dependency for real-time. Messages, typing, presence, reactions β all go over the same persistent connection. The broadcast hub uses Tokio channels internally.
| Layer | Technology |
|---|---|
| Language | Rust 2024, TypeScript 6.0 |
| Web framework | Axum 0.8 (ws + multipart) |
| Async runtime | Tokio 1.52 |
| Database | SQLite via sqlx 0.8 (WAL mode, FTS5) |
| Auth | jsonwebtoken 10.4, Argon2 0.5 |
| Real-time | WebSocket (Axum built-in), Tokio broadcast |
| TLS | rustls via axum-server |
| Frontend | React 19, Vite 7, Tailwind CSS 4 |
| State management | Zustand 5 (client), TanStack React Query 5 (server) |
| Routing | React Router 7 |
| Push | web-push 0.11 |
| Testing | cargo test, vitest, Playwright (E2E) |
vast/
βββ src/ # Rust backend
β βββ main.rs # Entrypoint, TLS setup, graceful shutdown
β βββ lib.rs # AppState, router, health check
β βββ embed.rs # Frontend SPA embedding (rust-embed)
β βββ auth/ # JWT creation/validation + admin domain
β βββ db/ # Pool init, WAL, compile-time migrations
β βββ ws/ # Connection pool, broadcast, heartbeat, /ws/bot
β βββ bot/ # OpenAI-compatible HTTP client
β βββ push/ # Web Push (VAPID)
β βββ api/ # REST endpoints (18 modules)
β βββ admin/ # Admin console endpoints
βββ frontend/ # React SPA
β βββ src/
β βββ components/ # UI components (MessageBubble, MessageInput, ...)
β βββ pages/ # Route pages + admin/
β βββ hooks/ # useWebSocket, useCursorSync
β βββ stores/ # Zustand stores
βββ tools/ # Bot connectors
β βββ bot-connector.py # Standalone WebSocket connector
β βββ bot-connector.yaml # Example configuration
β βββ bot-connector.service # systemd unit
β βββ hermes-vast-plugin.py # Hermes Agent gateway plugin
βββ db/migrations/ # 12 compile-time SQL migrations
βββ tests/integration/ # 28 Rust integration tests
βββ deploy/ # systemd + nginx deployment files
βββ scripts/ # Build, bench, dev scripts
All endpoints prefixed with /api. Authentication via Authorization: Bearer <jwt_token>.
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/register |
Register new user |
| POST | /api/auth/login |
Login, get JWT |
| Method | Path | Description |
|---|---|---|
| GET | /api/channels |
List channels |
| POST | /api/channels |
Create a channel |
| GET | /api/channels/{id} |
Get channel details |
| PATCH | /api/channels/{id} |
Update channel |
| POST | /api/channels/{id}/archive |
Archive channel |
| POST | /api/channels/{id}/unarchive |
Unarchive channel |
| Method | Path | Description |
|---|---|---|
| GET | /api/channels/{channel_id}/messages |
List messages (paginated) |
| POST | /api/channels/{channel_id}/messages |
Send a message |
| DELETE | /api/messages/{message_id} |
Delete a message |
| GET | /api/channels/{channel_id}/messages/{msg_id}/thread |
Get thread replies |
| Method | Path | Description |
|---|---|---|
| GET | /api/messages/{message_id}/reactions |
Get reactions |
| POST | /api/messages/{message_id}/reactions |
Add reaction |
| DELETE | /api/messages/{message_id}/reactions/{emoji} |
Remove reaction |
| Method | Path | Description |
|---|---|---|
| GET | /api/dm/ |
List DM conversations |
| POST | /api/dm/ |
Create/open a DM |
| Method | Path | Description |
|---|---|---|
| POST | /api/files/upload |
Upload a file |
| GET | /api/files/{id} |
Download a file |
| Method | Path | Description |
|---|---|---|
| GET | /api/search |
Full-text message search |
| Method | Path | Description |
|---|---|---|
| POST | /api/channels/{id}/join-request |
Request to join |
| GET | /api/requests |
List join requests |
| PUT | /api/requests/{id}/approve |
Approve |
| PUT | /api/requests/{id}/reject |
Reject |
| POST | /api/channels/{id}/invitations |
Create invitation |
| GET | /api/invitations |
List invitations |
| PUT | /api/invitations/{id}/accept |
Accept |
| PUT | /api/invitations/{id}/reject |
Reject |
All admin endpoints require a separate admin JWT (/api/admin/login).
| Method | Path | Description |
|---|---|---|
| POST | /api/admin/login |
Admin login |
| GET | /api/admin/dashboard |
Dashboard stats |
| GET | /api/admin/users |
List users |
| PATCH | /api/admin/users/{id} |
Update (disable/enable) |
| DELETE | /api/admin/users/{id} |
Delete user |
| GET | /api/admin/invite-codes |
List invite codes |
| POST | /api/admin/invite-codes |
Create invite code |
| DELETE | /api/admin/invite-codes/{code} |
Delete invite code |
| GET | /api/admin/audit-logs |
Audit log (filterable) |
| Method | Path | Description |
|---|---|---|
| GET | /api/bots |
List active bots (public) |
| POST | /api/admin/bots |
Create bot (admin) |
| GET | /api/admin/bots |
List all bots (admin) |
| PATCH | /api/admin/bots/:id |
Update bot (admin) |
| DELETE | /api/admin/bots/:id |
Delete bot (admin) |
| POST | /api/admin/bots/:id/test |
Test connectivity (admin) |
| POST | /api/channels/:id/bots |
Add bot to channel (owner) |
Connect bot connectors at /ws/bot, authenticating with the connection key in the Authorization: Bearer <connection_key> header (the legacy ?key= query form is still accepted but deprecated β it leaks the credential into access logs).
| Direction | Event | Description |
|---|---|---|
| VAST β Connector | BotMention |
User @mentioned the bot, with channel context |
| Connector β VAST | BotReply |
Connector sends back the LLM response text |
Connect at /ws?token=<jwt_token>. Events: New messages, typing, presence, reactions, message changes.
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Server health check |
| GET | / |
Root health check |
# Run all tests
make test
# Frontend unit tests
cd frontend && bun test
# Backend tests
make test-backend
# E2E tests
make test-e2e
# Lint
make clippy
# Clean
make cleanBenchmarks: scripts/bench.sh β insert throughput, concurrent read latency, WebSocket memory usage.
MIT
Built because someone had to.