For the moments nothing should leave the room

A place to talk
that's actually yours.

Your ideas. Your team's metadata. The half-formed thoughts that turn into a product. Relay keeps them where you say they belong — not training data, not telemetry, not someone else's quarterly earnings. For people who want the important stuff to stay local.

A local LLM joined a chatroom.
One day. One engineer.

Yee is a local-first LLM editor — Ollama-powered, no API keys, your code never leaves your machine. It's a real product in a sibling repo. We pointed it at Relay one evening.

The integration shape is two sentences. Open a WebSocket to wss://…/api/ws/rooms/[slug], listen for {type:"message"} frames, reply with {type:"send", content:"yeeyee: …"}. Three env vars — RELAY_API_URL, RELAY_ROOM_SLUG, RELAY_SESSION_TOKEN — and the bot is live for every client in the room. The yeeyee: prefix is what Relay uses to render it as a bot reply.

92acb6dfeat(relay): render yeeyee:-prefixed messages as Yee bot replies shipped · 1 engineer · <24h end-to-end

Want this for your bot? The same WebSocket is open to you.

yee gateway · ws frames
# 1. Connect (JWT in Authorization header)
WSS /api/ws/rooms/my-private-room

# 2. On connect, relay sends last 20 messages
{ "type": "sync", "messages": […] }

# 3. Incoming message frame
{
  "type": "message",
  "message": { "content": "@yee explain this" }
}

# 4. Yee runs local Ollama, replies with prefix
{
  "type": "send",
  "clientMsgId": "550e8400-…",
  "content": "yeeyee: here's what it does…"
}

✓ ack < 20ms — broadcast to every client in the room

Smaller than Slack
on purpose.

Axis Slack-land Relay
Headcount funding the price tag ~1,700 employees pre-Salesforce — explains the bill small team, yours
Time to first message in production weeks — marketplace approval, enterprise procurement <24h
Servers per workspace their cloud, their rules 1, yours
App-directory dependencies many 0
Cost per integration bundled into per-seat rent $0

100 seats on Slack Pro: $725/mo  ·  Relay: $6–$7.20/mo per server. The same headcount, two orders of magnitude apart.

See the full math →

We are not trying to be bigger than Slack. We are trying to be 1 of everything they have many of.

Built on primitives.
Nothing you don't need.

01 — Compute

Single-tenant server

Your workspace runs on a single server. Pick a starter size and scale vertically as you grow — each workspace gets its own isolated instance. Sizing and provider are part of the onboarding conversation.

One workspace per box
02 — Real-time

Redis Pub/Sub

All live message delivery runs through Redis. Channel subscriptions, presence, typing indicators, and SSE event fanout. Sub-20ms delivery.

Hot path
03 — Persistence

SQLite (hot)

Recent messages, channel metadata, and user state stored in SQLite on the server's local SSD. Blazing-fast local reads. WAL mode for concurrent writes.

Hot storage
04 — Archive

Cold Postgres archive

Messages older than your retention threshold are archived to a managed Postgres database. Cold reads on-demand via the same API — zero data loss. Optional DR add-on lets you bring your own connection string or have us run it for you.

Cold archive
05 — Interface

REST + SSE API

Every feature is a first-class API endpoint. Build your own client, automate workflows, or integrate with anything. OpenAI-compatible tool schemas for AI agents.

API-first
06 — Intelligence

AI-ready Tooling

Relay's API follows OpenAI function-calling conventions. Drop an AI agent into any channel as a participant. Webhooks, tool schemas, and structured responses built in.

OpenAI-compatible

One server per workspace.
Scale when you need to.

Most small teams run comfortably on a single low-end server. As your team grows, you resize the box vertically — your data stays put, no migration. Picking the right starting size, the right provider, and the right region is part of the onboarding conversation. Reach out for a sizing recommendation tailored to your team and traffic profile.

Redis-first streaming.
Events, not polling.

Clients subscribe to an SSE stream. Redis Pub/Sub fans out events across all connected clients in a workspace. No WebSocket complexity — just a persistent HTTP connection.

14:32:01.042 message.created channel=#engineering  author=tyler  text="just deployed v2.1"
14:32:01.089 presence.update user=sarah  status=online  workspace=ws_01HXYZ
14:32:02.310 typing.start channel=#engineering  user=sarah
14:32:04.881 message.created channel=#engineering  author=sarah  text="🎉 nice work"
14:32:04.883 reaction.added message=msg_01HABC  emoji=🚀  user=marcus
14:32:05.001 ai.response bot=relay-bot  channel=#engineering  streaming=true

Hot-cold storage.
Reads always fast. History never lost.

Client Web, mobile, or API consumer sends message
API Layer Validates auth, parses request, assigns IDs
Redis Publishes to channel subscribers instantly
SQLite Persists to local hot store (WAL mode)
Cold archive Older history archived to managed Postgres
Hot Path (Redis)
All active channel messages live in Redis for instant Pub/Sub fanout. TTL-based eviction keeps memory lean. Presence and typing use ephemeral keys.
Warm Store (SQLite)
Local SSD SQLite in WAL mode handles concurrent reads and writes. Recent history (configurable 30–90 days) lives here. Zero network overhead for reads.
Cold Archive
A managed Postgres database archives the full message history. Queries against archived data are served transparently through the same room messages API.

The full API.
Mapped to what's actually shipped.

Every endpoint below is mounted in the running Hono server in apps/agents. Public traffic enters at https://dev3lop.com/relay-api/* and is reverse-proxied to the workspace's chat tier. Programmatic API tokens for non-browser clients are on the roadmap; today the same browser session cookie issued by GitHub OAuth carries every call.

Authentication Cookie: relay_session=<signed>  ·  obtained via GitHub OAuth

Open /auth/github?next=/relay/app/<slug> to start the OAuth dance. The callback at /auth/callback upserts your GitHub profile and sets a 30-day signed session cookie. Every /relay-api/* request reuses that cookie — there is no separate Bearer key today.

Sets a short-lived signed state cookie and 302s the browser to github.com/login/oauth/authorize. After the user approves, GitHub redirects to /auth/callback, which exchanges the code, upserts the GitHub profile in SQLite, and writes a 30-day signed session cookie.

Query ParamTypeRequiredDescription
nextstringoptionalPath to land on after callback. Must be same-origin and not under /auth/; defaults to the lobby room.

Reads the session cookie and returns the authenticated user's profile (id, githubId, login, name, avatarUrl, email). Responds 401 with null body when no valid session is attached.

Removes the signed session cookie. The user record stays in SQLite — the next OAuth round trip will reuse it.

Resolves the seeded default room (the lobby) for the signed-in user. Used by the SPA on first load to know where to send people who haven't picked a room yet.

Returns the room's branding, owner, and settings. Whitelisted rooms reject non-members with 403; the lobby is always public.

Owners can rename the room (collision-checked against reserved slugs), update its display name, description, accent colors, and avatar. Updates are broadcast over Redis pub/sub so every connected member's UI updates live.

Flips the room between open and whitelist-only. When enabled, only allow-listed GitHub logins or verified emails can read/write; everyone else hits the access-request flow.

Returns channels in the room — public ones plus any private/group channels and DMs the caller is a member of. Each row carries id, slug, name, visibility, memberCount, and createdBy.

Owner-only. The lobby has no admin, so its create-channel UI is hidden and this endpoint refuses calls there. visibility is public | private | group | personal; inviteLogins[] seeds co-admin members.

Request
{
  "name": "Engineering",
  "slug": "engineering",
  "visibility": "public",
  "inviteLogins": ["alice", "bob"]
}

Available to anyone with room access. Creates (or reuses) a private DM channel between the caller and the supplied GitHub logins. Returns the channel DTO and broadcasts a direct_created event so the other parties' tabs add it without refresh.

Allowed if the caller owns the room or created the channel. Wipes the channel's messages from SQLite and publishes a deleted event to all subscribers.

Returns every user who has spoken in or been invited to the room. Used by the mention picker and the DM flow.

Reads from SQLite (the hot store). Use channelId=__lobby for the room-wide thread, a real channel id to scope to one channel, or omit to get everything visible. Pagination uses a (beforeCreatedAt, beforeId) keyset so concurrent inserts can't shift pages.

Query ParamTypeRequiredDescription
channelIdstringoptionalChannel id, __lobby, or omit for all
beforeCreatedAtISO 8601optionalUsed with beforeId for keyset pagination
beforeIdstringoptionalTiebreaker for messages with identical timestamps
limitintegeroptionalDefault 20, capped at 100

The HTTP send path mirrors the WebSocket path: it builds the message row, publishes to Redis first (so other tabs see it in one RTT), then persists to SQLite asynchronously. Mentions are processed off the response path. Content is trimmed and capped at 4,000 characters.

Request
{
  "content": "Deploy looks green. ",
  "channelId": "ch_8fd2…"
}

Only the original author may edit. Updated body is published as a message_updated event so connected clients can rerender in place.

Author or room owner. Removes the row from SQLite and publishes a message_deleted event with the channel id so clients can drop it from the relevant view.

The primary live transport. On connect the server replays the last 20 messages as a sync frame, then forwards every Redis pub/sub event for the room. Clients send {"type":"send","clientMsgId","content","channelId"} frames; the server fans out via Redis and acks on the same socket so optimistic UI can promote in place.

Frames
// server → client (initial)
{ "type": "sync", "messages": [...] }

// client → server
{ "type": "send", "clientMsgId": "c_42", "content": "hi", "channelId": null }

// server → client (echo on every connected tab)
{ "type": "message", "clientMsgId": "c_42", "message": { ... } }

// server → sender only
{ "type": "ack", "clientMsgId": "c_42", "serverId": "msg_…", "createdAt": "…" }

Read-only SSE stream for environments where WebSockets are blocked. Sends an initial sync with the last 20 messages, then relays every message, message_updated, message_deleted, and channel/branding event the room publishes. A ping event fires every 20 seconds to keep proxies open.

Event types
event: room
data: {"type":"sync","messages":[ ... ]}

event: room
data: {"type":"message","message":{ ... }}

event: room
data: {"type":"message_updated","message":{ ... }}

event: room
data: {"type":"message_deleted","messageId":"msg_…","channelId":"ch_…"}

event: ping
data:

Returns every login or email allowed into a gated room.

Owner-only. Accepts a GitHub login or an email address and a free-form note.

Owner-only. The user keeps any session they already have until it expires, but new joins are blocked.

Owner-only. Returns the queue of would-be members the room hasn't yet approved or denied.

Any signed-in user can call this against a gated room. The request becomes a notification on the owner's dashboard.

Owner-only. Marks the request approved and inserts the requester into the room's whitelist atomically.

Owner-only. The requester is not notified beyond the existing dashboard state change.

Returns mentions, access requests, and other room-scoped notifications addressed to the caller.

Updates the notification's lifecycle state.

Pin a notification so it shows up in the saved tab regardless of read state.

Used by room owners to mark a notification public or private inside the room.

Idempotent. Returns the existing thread if one already exists, otherwise creates a new one and returns its id.

Returns thread replies in chronological order with author profile data joined in.

Posts a reply on behalf of the caller. Thread replies aren't broadcast on the room socket — clients refetch the thread on focus.

Returns the first board for a room. Creates a default Todo · Doing · Done board the first time it's hit.

Spins up another board with default columns alongside the room's first one.

Returns the board with its columns and cards inlined.

Drops the new card into the board's first column.

Updates the card's column and position. The board owner can move any card; other members can only move their own.

Saved cards float to the top of their column for the caller.

Multipart form upload. Images are converted to WebP under 70 KB before storage. Bind the result to a message or other entity by sending entityType + entityId in the same form.

Streams the stored bytes back with the right content-type. Access is gated by the entity the attachment belongs to.

Backed by Redis TTL keys. Connected clients heartbeat every 30 seconds; stale entries fall off automatically.

Bumps the caller's presence key and broadcasts an online event if they were stale.

Used by the AI add-on's private chat surface. Publishes a short-lived typing event for the named conversation.

Unauthenticated. Pings Redis and returns a small JSON document used by the workspace server's external uptime probe.

Response
{
  "ok": true,
  "ts": "2026-05-04T23:30:00.000Z",
  "checks": { "server": "ok", "redis": "ok" }
}
On the roadmap

Programmatic API tokens (so bots and CI pipelines can auth without a browser session), HMAC-signed outbound webhooks, and a public bot registration endpoint with OpenAI-compatible tool schemas. The architecture already supports them — Redis pub/sub fan-out, mention-driven webhooks, and the AI add-on are all wired up internally — they just don't have stable public routes yet.

No bloat.
Add exactly what you need.

AI makes integrations trivial to build.
We don't charge for them.

In 2026, connecting two services is a 10-minute GPT task. Relay provides the webhook surface and the bot API — you own the integrations.

Want us to build one? We can. Want to build it yourself? The API is everything you need. The architecture is intentionally open-ended: bots are just webhook listeners that speak the Relay message format.

There's no app directory to unlock, no per-seat fee for GitHub notifications, no enterprise tier required to connect your CI pipeline. Register a bot, point it at your server, and write 20 lines of code.

relay-bot (AI) Built-in

GPT-4o-powered assistant in every workspace. @mention to ask questions, summarize threads, draft replies, or run custom tools.

Webhooks Built-in

HMAC-signed outbound webhooks for any event. The foundation for every integration you'll ever build.

GitHub / GitLab Community

Post PR, issue, and deployment events to channels. Open-source template — deploy in minutes on your own infra.

PagerDuty / alerts Community

Route on-call alerts to Relay channels. Acknowledge directly from the API. Template available.

Your integration Custom

Register a bot, point a webhook at your server, use the /bots API. You build it in an afternoon. We're here if you need help.

Yee (local LLM) Sister product

Local LLM as a roommate. WebSocket plug-in, yeeyee: reply prefix. Built in under a day — see the proof above.