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.
Proof of integration · for those who get it
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.
yeeyee:-prefixed messages as Yee bot replies Want this for your bot? The same WebSocket is open to you.
The KPI is smallness
We are not trying to be bigger than Slack. We are trying to be 1 of everything they have many of.
Infrastructure
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 boxAll live message delivery runs through Redis. Channel subscriptions, presence, typing indicators, and SSE event fanout. Sub-20ms delivery.
Hot pathRecent 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 storageMessages 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 archiveEvery 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-firstRelay'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-compatibleSizing
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.
Real-time Architecture
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.
Data Architecture
API Reference
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.
Cookie: relay_session=<signed> · obtained via GitHub OAuthOpen /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 Param | Type | Required | Description |
|---|---|---|---|
| next | string | optional | Path 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.
{
"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 Param | Type | Required | Description |
|---|---|---|---|
| channelId | string | optional | Channel id, __lobby, or omit for all |
| beforeCreatedAt | ISO 8601 | optional | Used with beforeId for keyset pagination |
| beforeId | string | optional | Tiebreaker for messages with identical timestamps |
| limit | integer | optional | Default 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.
{
"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.
// 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: 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.
{
"ok": true,
"ts": "2026-05-04T23:30:00.000Z",
"checks": { "server": "ok", "redis": "ok" }
}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.
Integrations
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.
GPT-4o-powered assistant in every workspace. @mention to ask questions, summarize threads, draft replies, or run custom tools.
HMAC-signed outbound webhooks for any event. The foundation for every integration you'll ever build.
Post PR, issue, and deployment events to channels. Open-source template — deploy in minutes on your own infra.
Route on-call alerts to Relay channels. Acknowledge directly from the API. Template available.
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.
Local LLM as a roommate. WebSocket plug-in, yeeyee: reply prefix. Built in under a day — see the proof above.