A room is a realtime channel that several browsers join from an invite your app shares, and every message in it is sealed before it leaves the tab. This page covers what a room is, how an invite creates and joins one, how messages travel, the permission model, what survives a reconnect, and the identity a member carries.
Three functions and one object. available, create and join are the whole entry, and everything you do afterwards is a method on the Room they resolve. The rest of the block is types.
Import from @fkn/lib/rooms, or use the rooms namespace on the root entry. The examples on this page live in app.ts, the page of the media library the other guides build, and each block picks up where the previous one left off.
A room is three values. The id is a v4 uuid the platform assigns, and it encodes nothing: not where the room runs, not when it was made. The key is 32 random bytes minted in your browser by the FKN broker, and the platform never holds it. The invite is the two joined by a dot, 80 characters, and it is the only thing another browser needs.
Whether this realm can join a room: false in Node, false in a worker nothing bridged, false against a shell older than rooms. Answers rather than rejecting.
id and key as one string, the thing to put in a link
invite// a fragment, so the invite reaches no server log
}
The invite is a capability. Anyone holding it can attempt a join, subject to blocks, so share it the way you would share a private link. Put it in a URL fragment, as the block does, and it never reaches a server log: browsers keep everything after # out of the request.
The key never leaves the browsers that hold it. Messages are sealed under a key derived from it, and the platform verifies that a joiner holds the right key without learning what it is, so a wrong invite is refused before a single frame is relayed.
Anyone can create a room, with an FKN account or without one. The creator is the room’s first member and its owner for as long as the room lives. A join needs both halves of the invite, and an invite with no key is refused before any request is made.
app.ts
const
constopen: () =>Promise<rooms.Room>
open= ():
interfacePromise<T>
Represents the completion of an asynchronous operation
@param ― start The index to the beginning of the specified portion of stringObj.
@param ― end The index to the end of the specified portion of stringObj. The substring includes the characters up to, but not including, the character indicated by end.
If this value is not specified, the substring continues to the end of stringObj.
create takes an optional members cap, clamped to 2 to 64, and optional defaults for the two permissions every joiner starts with. join takes the invite as one string. Both resolve once the room is open, and both reject with a RoomsError whose code says why.
The room lives while one member remains in it. When the last member leaves, the platform deletes the room with every permission and every block, and the invite then answers rooms: no such room.
A message is text of at most 4,096 bytes. Your browser seals it under a key derived from the room key before it leaves the tab, and the platform relays ciphertext it cannot read to every member who may receive. The browser on the other end unseals it, so your listener sees plain text and never a byte of ciphertext.
app.ts
const
constoff: () =>void
off=await
constroom:rooms.Room
room.
on: (listener: (event:rooms.RoomEvent) =>void) =>Promise<() =>void>
Await the returned unsubscribe in cleanup, the account.onChange shape.
at most 4,096 bytes of UTF-8, sealed before it leaves the browser. Rejects too-large, never truncates.
send('the catalog moved to library/catalog.json') // resolves once the platform took it
await
constoff: () =>void
off() // await the unsubscribe in your cleanup
Every message carries a seq, a counter the room increments once per delivered message. It is a total order: every member sees the same messages in the same order, and you see your own message with the seq it was delivered under, so there is nothing to reconcile with a local echo. A gap in seq means your own connection missed something, never that the room reordered.
There is no history. A member who joins sees nothing that was sent before it arrived, and the platform holds no ciphertext it could replay. An app that wants a transcript keeps its own, and it has the plain text to do so.
Permissions come in two layers. A room carries defaults for send and receive, and a member carries overrides that the owner grants and revokes. Where a member has an override it wins, and where it does not the default applies, so the four permissions and their defaults are:
remove and block have no room default. They are false until the owner grants them, because the member on the other end cannot undo them. grant, revoke and setDefault belong to the owner alone. A member holding remove may remove and may not grant it to anyone else, a member holding block may block and unblock, and nobody may remove or block the owner.
Changing a default moves every member who has no override for it, joined already or joining later, and each member it moved receives a permissions event so room.self.permissions and members() stay current. An override outlives any later change of the default. Removing a member ends their membership, and they may join again with the same invite. Blocking removes them and refuses their return.
A block holds for the room’s life: from any device, any app and any network for a member with an account, and from the same tab and the same network for a guest. It dies with the room, along with every other permission, so a new room starts clean.
Reconnecting is the broker’s job, never yours. When a connection drops, the FKN broker re-dials on your behalf and rejoins as the same member, and your Room object keeps working. What you see depends on what happened:
The hold is 20 seconds, and it is what turns a Wi-Fi handover, a phone locking, the shell’s Update button and a platform deploy into a gap in seq rather than a departure. The broker re-dials at 500 ms, then 1, 2, 4 and 8 seconds, and gives up when the hold runs out. A room the platform snapshots on a deploy comes back on the first rejoin, with its members, permissions and blocks intact.
app.ts
const
constend:rooms.RoomEnd
end=await
constroom:rooms.Room
room.
closed: Promise<rooms.RoomEnd>
Settles once, when the room ends for this app. Never rejects.
reason==='ended'?'the room ended':`you left the room (${
constend:rooms.RoomEnd
end.
reason: "left"|"removed"|"blocked"|"unavailable"
reason})`)
A member id never changes silently. If a rejoin would seat you as a different member, because the hold ran out or a guest’s tab lost its seed, the broker reports the room closed instead of continuing as someone else. Call join again with the invite for a new Room with a new self.
Every member gets a fresh id in every room. The platform derives it from who you are and from a value your browser derives from the room key, so it survives your reconnects and is unrelated to your id in any other room. With an FKN account it is also the same in your other tabs and on your other devices, and a guest carries one id per tab. No app, and no room owner, can recognise the same person across two rooms from the ids alone.
app.ts
constroom:rooms.Room
room.
self: rooms.RoomMember
this app's member record, as this room sees it. The id is fresh in every room.
self.
id: string
id// this member, in this room, and nowhere else
const
constmembers:rooms.RoomMember[]
members=await
constroom:rooms.Room
room.
members: () =>Promise<rooms.RoomMember[]>
members() // the ids you address in grant, revoke, remove and block
Ids are display values. Use them to address a member in grant, revoke, remove and block, and to tell members apart on screen, and give them nothing more: a room has no names, no avatars and no account details, and a member with an FKN account looks exactly like one without.
What the platform can see is who is in a room, who sent each message, when, and how large it was. What it cannot see is a byte of content. The room key is never on its side, and the value it derives member ids from is zeroed when the room ends, so a room that is over holds nothing that maps an id back to a person. Timing, writing style and any nickname your app asks for are outside that guarantee, since they are yours and not the platform’s.
Some of what a room does not do is a decision and some is a limit of today’s platform. Either way, plan around these:
A hard restart. A platform deploy snapshots every room and brings it back on the first rejoin. A hard restart of the platform, a crash or a node reboot, writes no snapshot, and every room it held ends.
One region. The service runs in one region. A member far from it pays that round trip on every message, and a room does not follow its members around the world.
History. A joiner sees nothing sent before it arrived, and there is nothing to replay: the platform holds no ciphertext once it has been relayed.
A durable block on a guest. A guest is blocked by the tab and by the network. Close the tab and change network, and they are a new visitor. The network half also reaches a bystander on the same address.
A guest owner who closes the tab. A guest owner’s identity lives in the tab. Close it and nobody can grant or revoke in that room again, although delegated moderators keep working. Alone in the room, a guest owner ends it by leaving, and the app creates a new one.
A lost invite. There is no list of your rooms. Lose the invite and the room is unreachable, and an account keeps that room’s slot against its cap until the room empties.
Moderation after the room ends. The platform cannot read a message and keeps no record of a room once it ends. Moderation is the owner’s, and it happens live or not at all.
A local counterpart. Every other capability can run against your own machine instead of the cloud. A room is a rendezvous between strangers, and it is cloud only.
The room’s send default is off and this member has no override, or the owner revoked it. Ask the owner for grant, or read room.self.permissions before showing a composer.