Forge
1.x
Docs/Forge/Migration
Open

Reading4 min
Updated8 Aug 2026
Sourcev1/extensions/streaming/migration.mdx

Read This First01

Two independent things changed, and it is worth separating them:

  1. The docs were wrong. Earlier revisions of these pages described Manager signatures that never existed in any release — CreateRoom(ctx, options) (Room, error), JoinRoom(ctx, roomID, conn, opts), Broadcast(ctx, []byte). Code copied from them never compiled, so there is nothing to migrate; use Functions as the reference.

  2. The extension changed. Real breaking changes are listed below.

The one to check by hand is the typing-indicator argument order, because it is the only change that compiles either way.

Silent Behaviour Changes02

StartTyping / StopTyping argument order

The signature is (ctx, userID, roomID). Earlier docs showed (ctx, roomID, userID). Both are strings, so the wrong order compiles cleanly and records typing against a room ID used as a user ID.

// wrong — compiles, silently misfiles the indicator
mgr.StartTyping(ctx, roomID, userID)

// right
mgr.StartTyping(ctx, userID, roomID)

BroadcastExcept now excludes users, not connections

The parameter has always been named excludeUserIDs on the interface and on both Room implementations; the manager matched it against connection IDs. The ordinary call — "broadcast to the room, except the sender" — therefore excluded nobody and echoed every message back to its author.

// Now behaves as documented: excludes every connection belonging to these users.
mgr.BroadcastExcept(ctx, msg, []string{senderUserID})

SendToConnection no longer returns write errors

Delivery is asynchronous: the call hands the frame to the connection's send queue and a dedicated writer goroutine performs the socket write. A transport failure cannot be reported through the return value; the connection tears itself down instead. Check conn.IsClosed() or observe the disconnect, rather than relying on the error.

Presence and multiple connections

A user is marked offline only when their last connection closes. Previously any disconnect marked them offline, so a user with two tabs went offline when either closed.

API Changes03

ChangeAction
NewExtension / NewExtensionWithConfig return *ExtensionRemove any .(*streaming.Extension) type assertion. Assigning to a forge.Extension variable still works
Manager gained ProcessInboundCustom socket handlers must call it before broadcasting client messages
Manager gained ReplayOptional; needed only for cursor-based reconnect
MessageStore gained GetSinceCustom backends must implement it. Return messages with Sequence > afterSequence, oldest first, capped at limit
Message gained SequenceAssigned by the store on save for room messages
router.Connection gained WriteBinaryCustom transports must implement it
forge.Stream gained SendWithID, SendJSONWithID, LastEventIDCustom SSE transports must implement them

Newly Enforced Behaviour04

These were configurable but inert. Enabling nothing new, they may still change how your deployment behaves, because the settings finally do what they said.

Now enforcedConsequence
Room and message authorizationJoinRoom consults a RoomAuthorizer; sends are checked against room membership, mutes, and bans. Previously any client could join any room and publish to any room ID it named
Rate limiting and validationProcessInbound applies them. Previously constructed, reported in the dashboard, and never called
MaxMessageSizeOversize messages rejected with ErrMessageTooLarge
PingInterval / PongTimeoutA real heartbeat now runs; idle connections are closed
Room and channel membership writesGetRoomMembers and the per-user room/channel limits now reflect reality. Both were permanently empty, so the limits could never trip
Identity stampingmsg.UserID is overwritten from the authenticated connection. A client could previously set it to any value
Warning

If your application relied on clients joining arbitrary rooms, or on the per-user room limit never firing, that behaviour will now stop. Supply a permissive RoomAuthorizer via WithRoomAuthorizer if you genuinely want the old semantics.

Endpoint Changes05

The SSE subscribe and unsubscribe endpoints now require the caller to own the connection named in conn_id. Previously they checked only that such a connection existed, so any caller could subscribe or unsubscribe somebody else's connection. Both "no such connection" and "not yours" return the same 404, so the endpoint cannot be used to probe which connection IDs are live.

New Configuration06

SettingDefaultPurpose
MaxAnonymousConnections1000Caps unauthenticated sockets, which no per-user limit can bound
MaxTotalConnections0 (off)Caps sockets per node regardless of identity
DrainTimeout15sHow long Stop waits for a clean drain

WithSessionResumption and WithLoadBalancer are now exported. Both features existed but could not be enabled through the public options API — only by hand-building a Config.

Web Client07

The Go envelope and the TypeScript client disagreed on which field names a frame. Message.Type carries the transport kind (message, system, join, …) and Message.Event carries the domain name (order.created); the client's default decoder read type first, so every frame decoded as "message", matched no binding, and was dropped.

Use the shipped decoder:

import { forgeStreamingDecoder, StreamBinder } from '@forge-go/client-core';

const binder = new StreamBinder({
  cache,
  streams: manifest.streams,
  manager,
  decode: forgeStreamingDecoder(),
});

Server-side, build domain frames with NewEventMessage so Event is never forgotten — a frame that sets only Type is a transport frame by definition and the client drops it silently.