u/1gr14

▲ 3 r/bun+1 crossposts

Realtime on Bun with end-to-end types: a whole chat (rooms, auth, history) in one file — no event-name strings, no generated types (I'm the author)

I'm the author of Point0, a fullstack TypeScript framework on Bun, and I just shipped its realtime layer. Sharing it here because the whole thing rides on Bun: `Bun.serve`'s WebSocket server underneath, and Bun's built-in Redis client is the one-line option when you run more than one process.

The idea: instead of a second stack next to your app (event-name strings, `any` payloads, rooms as string concatenation, your own bookkeeping of users and sockets), realtime is four more declarations of the same kind the framework already uses for pages, queries and mutations. One WebSocket per client, everything else rides it.

- a **channel** is the connection, and its connector turns the HTTP handshake into the connection's identity, stored server-side
- a **space** is a family of rooms of one shape; a room is an object, not a string, and it's the pub/sub address
- a **server handler** is client → server, a **client handler** is server → client, both typed by their schemas

Here's a whole chat: rooms per conversation, auth, persisted history, live updates. One file, and the compiler strips the server half out of the client bundle and the client half out of the server one.

// the channel: one connection per client. The connector runs on a normal HTTP
// request (cookies, headers, middleware), so your existing auth just works
export const appChannel = root.lets
  .channel()
  .connector(async ({ request }) => {
    const user = await getUserFromRequest(request) // your app's auth, unchanged
    // whatever you return IS this connection's identity: server-side, never sent
    // to the client, readable in every callback below. No type declared anywhere
    return user
      ? { authorized: true as const, id: user.id }
      : { authorized: false as const, id: null }
  })
  .channel()


// a space: a family of rooms of one shape. Here, one room per chat
export const chatSpace = appChannel.lets
  .space<{ chatId: string }>() // the room shape, declared like component props
  .input(z.object({ chatId: z.string() })) // what the client passes to join
  .joiner(({ input, identity }) => {
    // entering the room IS the read gate, and it runs on the server
    if (!identity.authorized) throw new AppError('Sign in first', { status: 401 })
    return { chatId: input.chatId } // type-checked against the room shape above
  })
  .space()


// client → server: persist, then fan out to the room
export const messageSendHandler = chatSpace.lets
  .serverHandler()
  .clientSend(z.object({ text: z.string().min(1).max(1000) }))
  .serverReply(async ({ input, identity, room }) => {
    if (!identity.authorized) throw new AppError('Sign in first', { status: 401 })
    const message = await prisma.message.create({
      data: { text: input.text, chatId: room.chatId, authorId: identity.id },
    })
    // one publish into the room's topic — not a walk over connections
    void messageAddedHandler.sendToClient(message, { room })
    return message // what the sender gets back from its own send
  })
  .serverHandler()


// server → client
export const messageAddedHandler = chatSpace.lets
  .clientHandler()
  .serverSend(messageSchema)
  .clientHandler()


// ...and the component, in the same file
const Chat = ({ chatId }: { chatId: string }) => {
  const membership = chatSpace.useMembership({ chatId }) // in the room while mounted
  const { data } = messagesQuery.useQuery({ chatId })    // history: an ordinary HTTP query
  const [text, setText] = useState('')


  // `message` is typed by .serverSend, `room` by the space's generic
  messageAddedHandler(membership).useOnMessageFromServer(({ message }) => {
    messagesQuery.setQueryData({ chatId }, (old) => ({
      messages: [...(old?.messages ?? []), message],
    }))
  })


  return (
    <>
      <ul>{data?.messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
      <form onSubmit={(e) => {
        e.preventDefault()
        setText('')
        void messageSendHandler(membership).sendToServer({ text })
      }}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
      </form>
    </>
  )
}

Nothing is generated and nothing is annotated: the identity type comes from the connector's return, the room type from the space's generic, the payload types from the schemas, and they reach every callback on both sides.

A few design decisions worth stating plainly, because they're the part people argue with:

- **A push is a signal, not storage.** Delivery is at-most-once by default: the truth lives in a query, the push only says it went stale. There's an opt-in resume buffer for short drops, and it tells the client honestly whether the gap was covered, so the catch-up refetch is one condition.
- **A room is an object, and its serialization is its address.** `{ members: [a, b].sort() }` is a DM room. No prefix conventions to typo.
- **Two ways in.** `.joiner` is the client asking in and able to leave. `.enroller` is the server putting a connection into a room at connect time, which the client cannot leave — that's what makes a personal push room something you can rely on.
- **Multi-process is config, not rewriting.** Default is process memory; a Redis URL, Postgres LISTEN/NOTIFY, or five functions of your own turn it into a backplane.

More examples (a live board, DMs, and a site where every request travels the socket instead of HTTP): https://1gr14.dev/blog/point0-socket

Socket docs: https://1gr14.dev/point0/latest/socket
The example app: https://github.com/1gr14/point0/tree/main/examples/socket
The framework: https://github.com/1gr14/point0

It's the newest part of the framework and I say so in the docs: the API design has settled, what's underneath still needs a refactor. Happy to take the uncomfortable questions — especially from anyone who has run Socket.IO at scale and sees where this breaks.zc

reddit.com
u/1gr14 — 8 days ago
▲ 1 r/crewai

/party — the skill that lets your agent sessions talk to each other

Any agent that reads skills can be in the channel: Claude Code, Cursor, Codex, Grok. They can all sit on your laptop, or on machines in different countries, and it is the same channel either way.

I was debugging one project on a Mac and a Windows box at the same time. Fix it on Windows, do not break the Mac. I spent that day carrying messages between the two sessions by hand, so I gave them a channel instead. MIT, written for myself.

You type \`/party\` in one session. It creates the channel and prints an invite. Paste that invite into your other sessions, on the same machine or another one, and they join. They install nothing.

Under the hood the agent runs a CLI. Most of it is this:

npm i -g agents-party@latest
agents-party create --title win-vs-mac --as mac
agents-party invite '<ref>'
agents-party send '<ref>' --as mac "fix is in, run the suite"
agents-party listen '<ref>' --as mac

By default a channel is local: a SQLite file on your machine, nothing leaving the disk, no account, no cost. If your sessions sit on different machines, ask the agent for a remote one. You can run that server yourself for free, or use mine for $5 a month. Either way the messages are encrypted before they leave your machine and the key never reaches the server, so I cannot read them, by design.

\`listen\` is the part I care about. It returns only when someone else writes, so the model burns no tokens while the channel is quiet. It runs as a background task, which means your own chat with that agent stays free. You keep typing to it as usual.

Then a use I did not plan. Four worktrees, each built by its own session, all waiting to be rebased in order. I opened a fifth session as the manager and invited the rest. It sorted the order out with the authors directly instead of me relaying every conflict.

The whole thing is a skill file plus that CLI, with nothing running in the background between uses. What else it is good for, you will work out faster than I will.

[https://github.com/1gr14/agents-party\](https://github.com/1gr14/agents-party)

[](https://www.reddit.com/submit/?source\_id=t3\_1vlln4i&composer\_entry=crosspost\_prompt)

u/1gr14 — 8 days ago