Try it live: the full demo is running at luxdb.dev/demo on a single Lux instance.
Most chat apps need at least three services: a relational database for users and messages, Redis for caching and pub/sub, and some kind of WebSocket server for real-time delivery. Maybe a fourth for rate limiting. A fifth for presence tracking.
I built one with a single database connection. Here's how.
The architecture
Lux is a Redis-compatible database that also has built-in tables, pub/sub, and key-value storage. So instead of splitting data across Postgres + Redis + a pub/sub broker, everything runs through one Lux connection:
import { Lux } from "@luxdb/sdk";
const lux = new Lux("lux://localhost:6379");Here's what each chat feature maps to:
| Feature | Traditional Stack | Lux |
|---|---|---|
| Users & messages | Postgres | TCREATE, TINSERT, TQUERY |
| Real-time delivery | Redis pub/sub + WS | PUBLISH (built-in) |
| Presence | Redis SET + TTL | SET with EX (same API) |
| Rate limiting | Redis INCR + EXPIRE | INCR + EXPIRE (same API) |
| Typing indicators | Redis pub/sub | PUBLISH |
A single connection string replacing what would normally be three separate services.
Schema
Lux has built-in relational tables with typed fields, foreign keys, and queries. The entire schema for the chat app is three commands:
await lux.call("TCREATE", "users",
"username:str:unique", "color:str", "last_seen:int");
await lux.call("TCREATE", "channels",
"name:str:unique", "topic:str");
await lux.call("TCREATE", "messages",
"channel_id:ref(channels)", "user_id:ref(users)",
"content:str", "timestamp:int");ref(users) is a foreign key. Lux enforces referential integrity without needing an ORM, migrations, or schema files.
Sending a message
When a user sends a message through the WebSocket, the server does four things in sequence:
// 1. Rate limit (5 messages per 10 seconds)
const count = await lux.incr(`ratelimit:msg:${userId}`);
if (count === 1) await lux.expire(rateKey, 10);
if (count > 5) return ws.send("slow down");
// 2. Filter profanity
content = filter.clean(content);
// 3. Store in table
const id = await lux.call("TINSERT", "messages",
"channel_id", channelId,
"user_id", userId,
"content", content,
"timestamp", String(Date.now()));
// 4. Broadcast to channel
broadcast(channelId, { type: "message", id, content, ... });Rate limiting, storage, and broadcast all happen through the same connection. No inter-service latency, no serialization overhead, no distributed transaction coordination.
Presence
Online/offline presence uses key-value TTLs. When a user connects or sends a heartbeat:
await lux.call("SET", `presence:${userId}`, "online", "EX", "60");The key automatically expires after 60 seconds. To check who's online, read the presence keys for each user. If the key exists, they're online. If it expired, they're offline. No cleanup jobs, no zombie connections.
Loading messages
Message history uses table queries with joins resolved via pipeline:
// Get last 50 messages
const rows = await lux.call("TQUERY", "messages",
"WHERE", "channel_id", "=", channelId,
"ORDER", "BY", "timestamp", "DESC",
"LIMIT", "50");
// Batch-fetch usernames
const pipeline = lux.pipeline();
for (const uid of uniqueUserIds) {
pipeline.call("TGET", "users", uid);
}
const users = await pipeline.exec();The pipeline batches all user lookups into a single round trip. 50 messages with usernames resolved in two round trips total.
What's NOT in the server
Things I didn't have to set up:
- No Postgres, no connection pooling, no migrations, no pgAdmin
- No separate Redis for caching or pub/sub
- No message queue service, rate limiting is just INCR + EXPIRE
- No presence service, TTL keys handle it
- No Docker Compose with four containers, just one binary
The entire database is a single Lux instance running on a $10/mo Lux Cloud project.
How it runs
The demo at luxdb.dev/demo runs on a single Lux Cloud instance with 512MB of RAM ($10/mo). Messages are persisted via tiered storage, which automatically evicts cold data to disk when memory fills up. Rate limiting and presence are handled with standard key-value operations that add no meaningful overhead.
Try it
The full demo is running live at luxdb.dev/demo.
If you want to build something similar:
docker run -p 6379:6379 ghcr.io/lux-db/lux:latest
# or use Lux Cloud
# luxdb.devConnect with any Redis client. The tables, vectors, time series, and pub/sub commands are all available alongside the standard Redis commands you already know.
That's the whole thing: a real-time chat app backed by a single database with a single connection string.