Skip to content
Real-time chat infrastructure

Embeddable chat your backend already trusts.

Real-time messaging, presence, typing, and channels in one self-contained service. Your backend signs the tokens, your services own the events — emb.chat just moves the messages.

# engineering
+3
Shipping the presence work — webhooks now carry offline_members.
So push knows who's offline without a callback. 🎯
Message #engineering…

One stack. No external services.

FastifySocket.IOPostgreSQL 16Redis 7Docker
Why emb.chat

Chat is four products pretending to be one.

Messaging, presence, push, and analytics usually arrive as four integrations with four failure modes. emb.chat collapses them into one service that hands your systems everything they need to do the rest.

The usual sprawl

  • A chat vendor that owns your users
  • A separate push pipeline
  • Presence bolted on after the fact
  • Analytics scraping it all back

With emb.chat

  • + You keep identity & sign tokens
  • + Webhooks feed your push service
  • + Presence & typing are built in
  • + Every event lands on your stack
Capabilities

Everything real-time chat needs — and nothing it shouldn't own.

Real-time messaging

Typed message events over WebSocket — new, edited, deleted — with cursor-paginated history and soft deletes. Send and await an ack, every time.

Presence & typing

Live online/offline presence on a 15s heartbeat, last-seen tracking, and 3-second auto-stop typing indicators — broadcast over the socket and to your webhooks.

Role-based channels

Direct, group, and admin-created channels with owner / admin / member roles, a last-owner guard, and idempotent direct channels between any two users.

Three group modes

Pick external (host-owned), internal (client-driven), or hybrid (locked host rooms + ad-hoc client rooms) per app. Lock individual channels when you need to.

Auth you own

Your backend signs chat tokens with RS256 or EdDSA. emb.chat verifies against a registered public key — it can verify, but it can never mint. Rotate keys with one call.

Signed webhooks

HMAC-SHA256 signed deliveries with a 7-attempt retry ladder, per-event delivery logs, and offline-member enrichment so your push service knows exactly who to notify.

Auth delegation

emb.chat can verify identities. It can never mint them.

Register a public key once. Your backend signs chat tokens locally with the matching private key — RS256 or EdDSA — and emb.chat verifies them with zero round-trips. Identity ownership stays exactly where it belongs.

  • Asymmetric by design

    emb.chat only ever holds your public key. It verifies signatures; it cannot forge them.

  • Zero-latency sessions

    No token-validation callback to emb.chat on connect — your signature is the proof.

  • Rotate with a kid

    Multiple active keys per app. Roll a new key, sign with its kid, retire the old one with one call.

  • Claims mirror your user

    name, avatar, and meta from the token sync into the chat profile on connect — host stays the source of truth.

Your backend
signs with private key
emb.chat
verifies with public key
import { ChatAuth } from '@emb-chat/server-sdk';

// Your backend signs locally — zero round-trips to emb.chat.
const token = await ChatAuth.createToken(privateKey, {
  sub: user.id,          // your user id
  aud: 'app_demo',       // your app id
  kid: 'key_01',         // selects the registered public key
  name: user.displayName,
  meta: { role: user.role },
  expiresIn: '1h',
});
Group modes

One config flag reshapes who controls your channels.

Set it per app, lock individual channels when you need to. The same server runs host-owned, client-driven, or a deliberate mix.

external Host-owned

Your API key manages every channel. Token clients read and post, but never create or administer. The original, locked-down stance.

internal Client-driven

Token clients create, rename, and delete their own channels and manage membership through role gates. Perfect for mobile-style social apps.

hybrid Best of both

Pre-create authoritative locked rooms with your API key, and let clients spin up their own ad-hoc unlocked channels alongside them.

Delivery envelope

HMAC-SHA256
X-Emb-Event message.created
X-Emb-Event-Id 8f2a1c84-… (stable across retries)
X-Emb-Signature sha256=2c8e…
X-Emb-Timestamp 1777920123

Retry ladder

now 5s 30s 2m 15m 1h 4h

7 attempts, 5s timeout each. After 50 consecutive failures the endpoint auto-disables — every delivery stays in the audit log.

Webhook-first

Every event, signed and delivered to your services.

message, member, presence, channel, and webhook events stream to your endpoints — HMAC-signed, retried on an exponential ladder, and enriched with the offline members who need a push.

message.createdmessage.editedmessage.deletedmember.addedmember.removedmember.rolepresence.onlinepresence.offlinechannel.updated message.*
Deploy

Four services. One compose file. No external dependencies.

Server, webhook worker, PostgreSQL, and Redis come up together. Scale the worker independently so a slow endpoint never stalls a message. An operator dashboard ships in the box.

1
compose file
0
external services
2
SDKs + a CLI
docker compose up
services:
  server:        # Fastify REST + Socket.IO gateway
  webhook-worker:# signed delivery, retries, auto-disable
  postgres:      # messages, channels, members, keys
  redis:         # presence, typing, pub/sub, job queue
For developers

Three SDKs, one mental model.

Sign on the server, connect on the client, verify on your services. Here's the whole loop.

server.ts — your backend
import { readFileSync } from 'node:fs';
import { ChatAuth } from '@emb-chat/server-sdk';

const privateKey = await ChatAuth.loadPrivateKey(
  readFileSync('./emb-private.pem', 'utf8'),
);

const token = await ChatAuth.createToken(privateKey, {
  sub: user.id,
  aud: 'app_demo',
  kid: 'key_01',
  name: user.displayName,
  expiresIn: '1h',
});

Ship real-time chat this week.

Clone it, run one compose command, sign your first token. The MVP is fully shipped.