A room is a realtime channel that several browsers open by name or 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 a name opens one and an invite joins it, how messages travel, what a claim keeps, the permission model, what survives a reconnect, and the identity a member carries.
Four functions and one object. available, open, 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 an id and a key. The id is <scope>/<name>, where the scope is your app’s own, its origin for a website, or global, the namespace every app shares. The key is 32 random bytes the FKN broker mints in your browser unless you pass one, and the platform never holds it. The invite is the key and the id joined by a dot, key first, 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 named rooms. Answers rather than rejecting.
Open a room by name: a name under this app's own scope, or global/<name> for the shared one.
Nobody there means the room comes into being with you as its owner; somebody there means you join
them, and the key has to match theirs. Share room.invite to let anyone else in.
key and id as one string, the thing to put in a link
invite// a fragment, so the invite reaches no server log
}
A name is 1 to 64 characters of a to z, 0 to 9, ., _ and -, starting and ending with a letter or a digit. Capitals fold to lowercase, and fkn and every name starting fkn- are kept for the platform. Prefix a name with global/ to open it in the shared namespace. Without the prefix only your app can open the room by name, and anyone holding its invite can still join it.
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 checks a joiner against the hash of a second derived value, never the key itself, so a wrong key is refused before a single frame is relayed.
Anyone can open a room, with an FKN account or without one. open takes a name, create opens a random name of 24 hexadecimal characters, and join takes an invite. All three do the same thing underneath: when the room does not exist the call brings it into being with you as its owner, and when it does, you join it and the key has to match.
@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.
honoured only by the join that brings the room into being
members: 8 })
}
const
constroom:rooms.Room
room=await
constopen: () =>Promise<rooms.Room>
open() // the room this tab opened, or the one the invite named
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's id, derived for this room
constroom:rooms.Room
room.
owner: string
the owner's member id, or '' while a claimed room's owner has never joined
owner// the member id of whoever brought the room into being
constroom:rooms.Room
room.
defaults: () => rooms.RoomDefaults
defaults() // { send: true, receive: true, maxMessageBytes: 262144 } unless the opener set otherwise
members and defaults shape only the join that brings a room into being, and every later join ignores them. members caps the room, clamped to 2 to 100 and 100 when absent, and defaults sets the send and receive permissions and the message size every joiner starts with. signal leaves the room when it aborts, and key passes a room key instead of minting one. Every call rejects with a RoomsError whose code says why.
open without a key mints a fresh one, so a second tab that opens a name somebody is already in is refused bad-key. To meet by name, derive the key from something the members already share, and pass it to every open:
app.ts
const
constkeyOf: (text:string) =>Promise<string>
keyOf=async (
text: string
text:string) => {
const
constdigest:Uint8Array<ArrayBuffer>
digest=new
var Uint8Array:Uint8ArrayConstructor
new <ArrayBuffer>(buffer:ArrayBuffer, byteOffset?:number, length?:number) =>Uint8Array<ArrayBuffer> (+6 overloads)
The TextEncoder.encode() method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.
Passes a string and
{@linkcode
replaceValue
}
to the [Symbol.replace] method on
{@linkcode
searchValue
}
. This method is expected to implement its own replacement algorithm.
@param ― searchValue An object that supports searching for and replacing matches within a string.
Open a room by name: a name under this app's own scope, or global/<name> for the shared one.
Nobody there means the room comes into being with you as its owner; somebody there means you join
them, and the key has to match theirs. Share room.invite to let anyone else in.
the room key, 32 bytes base64url, minted when absent. Deriving one from something people already share is how a name becomes a rendezvous.
key: await
constkeyOf: (text:string) =>Promise<string>
keyOf(
constphrase:string
phrase) }) // everyone holding the phrase lands in one room
A key is 32 bytes as unpadded base64url, 43 characters, and anything else is refused rooms: the key is not 32 bytes base64url. Pick the shared value so that outsiders cannot guess it, since the room name is not a secret.
A room nobody has claimed lives while one member holds a seat. When the last seat is released, the platform deletes the room with every permission and every block, and the next join on that name, from an old invite or a fresh open, brings a new room into being with the joiner as its owner. A claimed room stays, see claims and the mailbox.
A message is text. 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.
sealed before it leaves the browser. Rejects too-large past self.maxMessageBytes on the wire, 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
The size cap is room.self.maxMessageBytes, measured on the wire: the sealed message, nonce and ciphertext in base64url, about four thirds of the text’s UTF-8 length. The default for every member is 262,144, which carries at most 196,580 bytes of UTF-8 text, and the owner’s own cap is 33,554,432. The broker measures before sending and rejects too-large with nothing sent and nothing trimmed.
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.
A room nobody has claimed keeps no history. A member who joins sees nothing sent before it arrived, and the platform stores no ciphertext it could replay. A claimed room keeps every message sent after the claim, unless the claim keeps none, as the next section shows.
A claim keeps a room for an account: the name stays reserved, and from then on a mailbox stores every message sent in it, unless the claim keeps no messages. Only the owner can claim, signed in on a premium account, and one account holds at most 10 claims, 2 of them under global.
keeps the name for the signed-in premium account, and a mailbox unless asked not to. The only thing
in rooms that premium buys. description says what the room is for, shown to the account in its
fkn.app settings; a permanent claim with an unchanged one on every open is free. Claiming on every
open also claims the room again after the account unclaims it from fkn.app, so claim when the person
wants the room kept, or make it temporary.
temporary makes the claim end by itself, in milliseconds (true is 7 days), that long after its
last claim: a temporary claim goes to the account's ledger every time, which is what restarts its
clock. When it ends the app hears a claim event with claimed: false, exactly as for an Unclaim
from fkn.app, and the stored messages are gone. A broker too old to carry it refuses the call
unavailable (rooms: temporary claims are not available) rather than make the claim permanent.
mailbox: false keeps the name and stores nothing: the room relays as an unclaimed one does. It is
read only when the call starts the claim and ignored on a room the account already holds, so claiming
on every open never undoes the owner's choice.
Who changes a claimed room depends on its scope. A room under an app's scope, which is every room
but a global one, is changed by the claim's account only through the app that claimed it (the
same app once it is verified): from another app of the account setDefault, limit, grant,
revoke, remove, block, unblock, setMailbox, edit and delete are refused denied
(rooms: only the app that claimed the room can change it), the account's own messages included,
and nothing changes. That other app, which reaches the room through its invite, still joins, reads
and sends. A claimed global room is changed by any app of the account, which also describes it,
sets its temporary and releases it. The account changes every room it claimed
from its fkn.app settings, whichever app claimed it, and members of other accounts are answered as
in any room. While the room cannot tell whether two apps are one, such a change is refused
unavailable (rooms: rooms are unavailable) and can be sent again.
Past the account's room limit the call waits while fkn.app shows the person their claimed rooms over
your app, with Unclaim on each: it resolves once one is unclaimed and this claim is made, and rejects
full (rooms: too many claims) when they close the card or when fkn.app cannot show it. Nothing
about the account's other rooms reaches your app.
claim({
description?: string |undefined
What the room is for, in the app's own words. The account reads it beside the room in its fkn.app
settings, where it can clear or unclaim the room, so say what would be lost. One line of plain text:
trimmed, then 1 to 200 characters as String.length counts them, with no control character, line or
paragraph separator, or bidi control; anything else is refused invalid, never cut short. Not sealed:
stored with the claim and shown to the account only. Claiming again with one replaces it: on a room
under an app's scope from the app that claimed the room only (the same app once it is verified), and
from another app it is refused denied (rooms: only the app that claimed the room can describe it);
on a global room from any app of the account. Claiming without one keeps it.
description: 'Chat history for your watch parties' }) // the owner, signed in on a premium account
replays stored messages above after as message events marked replayed, one page at a time
backlog(
let page:rooms.RoomBacklog
page.
last: number
last) // one page at a time
await
constroom:rooms.Room
room.
edit: (seq:number, text:string) =>Promise<void>
replaces a stored message's text: one of your own, or any as the owner. On a room claimed under an
app's scope, from another app of the claim's account, it is refused denied (rooms: only the app that claimed the room can change it), the account's own messages included, as claim says.
edit(
constseq:number
seq, 'the catalog moved to library/index.json') // your own message, or any as the owner
await
constroom:rooms.Room
room.
delete: (from:number, to?:number) =>Promise<void>
drops stored messages: one of your own, or any range as the owner. On a room claimed under an app's
scope, from another app of the claim's account, it is refused denied (rooms: only the app that claimed the room can change it), the account's own messages included, as claim says.
Claiming makes the account the owner, so the owner is whichever member that account joins as, on any device, and in a room under an app’s scope through the app that claimed it, as who changes a claimed room says. Every member gets a claim event when the room is claimed or released, or its mailbox is turned off or on, and the event’s mailbox says whether the room keeps messages now. backlog replays stored messages above a seq as ordinary message events with replayed: true, up to 200 messages or 65,536 bytes a page, and answers the last seq it sent and whether more follow. edit and delete change stored messages, and every member who may receive sees an edited or deleted event.
Pass a description to say what the room is for. The claim records your app as the one that made it, and the account sees the room in the Rooms tab of its fkn.app settings under your app’s name once your app is verified, and under its origin or package id before that. In a room under your app’s scope the description is shown as your app’s words, and in a global/ room, where any app of the account may have written it, the Rooms tab and the account’s data export show it without naming an app. In the Rooms tab the account can also clear the mailbox, stop keeping messages, unclaim the room or set a size limit, so write the description for someone deciding whether the room can go.
A description is one line of plain text: trimmed, then 1 to 200 characters as description.length counts them, with no line break, control character, bidi control or lone surrogate. Anything else is refused whole with rooms: the description is not one a claim can carry, and nothing is cut short. Unlike a message, the description is not sealed: the platform stores it with the claim and shows it only to the account, so keep the room’s content out of it.
Claiming again with a description replaces it, and claiming without one keeps it. There is no way to remove one: an empty description is refused like any other that does not fit, and the text goes only with the claim. In a room under an app’s scope only the app that claimed it replaces it, the same app once it is verified: from another app of the account a claim with a description is refused rooms: only the app that claimed the room can describe it while a bare claim() still resolves, so an app that opens rooms it joined by invite should expect that refusal on a room another of the account’s apps claimed. In a global/ room any app of the account replaces it.
A permanent claim made again with no description or the same one is free: the room answers without asking the platform, so it works through a platform outage too. Claiming on every open also claims again a room the account unclaimed in fkn.app: after an Unclaim the room is an ordinary one your member still owns, so the next claim() makes a fresh claim, with an empty mailbox unless it passes mailbox: false, and the room is back in the Rooms tab. Claim when the person asks for it, and on later opens only while room.claimed is true, so an Unclaim stays done, or make the claim temporary.
A claim past the account’s limit, 10 claims or 2 under global for a name there, does not fail at once. fkn.app shows the person a card over your app that says what a room is and how much of the limit is used, and lists every room the account has claimed with the app that claimed it, the date, its size, its description and an Unclaim. Your claim() call waits while the card is open: once the person unclaims a room that frees the slot this claim needs, fkn.app makes the claim and the call resolves, and a room unclaimed in the Rooms tab meanwhile counts too, once the person is back in your app. When they close the card, or when fkn.app cannot show it, the call rejects with rooms: too many claims, code full, the same error an app got before the card existed.
Your app needs no change for this, and it never sees the card’s list: fkn.app draws it in its own frame and hands your app only the outcome of the claim. Each claim at the limit shows the card again, so after that rejection claim again when the person asks, not in a loop.
The mailbox holds 500,000,000 bytes, or less when the account set a size limit for the room, each message counted at its wire size plus 64, and it refuses a send past that rather than dropping old messages. Those bytes count toward the owner’s storage quota with their files. A room whose owner is over it refuses new messages, and each refused write makes the room ask the platform again within a minute, so once the owner frees space a write retried a minute later goes through. A mailbox nobody has written to for 30 days moves to archive storage, mailbox.archived reads true, and the next write brings it back first.
From the Rooms tab the account can clear the mailbox, set a size limit, turn Messages kept off or on, or unclaim the room, without the room key, whichever app claimed it. Clear deletes every stored message and keeps the claim, the name, seq and the size limit, and every member present who may receive gets one deleted event from 1 to the last seq, or none when nothing was stored. A size limit of 1,000,000 to 500,000,000 bytes drops nothing already stored: past it a send, or an edit that makes a message larger, is refused rooms: the mailbox is full, and no event announces the new limit. After a Clear or a new limit, room.mailbox keeps the figures the room last gave, the old cap included, until usage() reads them again.
The room asks the platform once a day whether its claim still stands. When the account stops being premium the claim holds for 30 days, the owner gets an email 7 days in, and then the room becomes an ordinary one: the mailbox is deleted, whoever is present keeps their seat, and the room ends when the last of them leaves. release does the same at once, and so does Unclaim in fkn.app, which works during those 30 days too, and so does a temporary claim when its time is up. Either way every member present gets a claim event with claimed: false and mailbox: false and a permissions event for each member present, and no deleted event for the messages that went: room.claimed reads false and room.mailbox null, so drop what your app kept from the mailbox on that claim event.
In a room under an app’s scope only the app that claimed it can release it, the same app once it is verified, and from another app of the account release is refused rooms: only the app that claimed the room can release it. Any app of the account releases a global/ room. The account can unclaim any room it claimed from fkn.app, in the Rooms tab or on the room limit card, whichever app claimed it.
Once the claim is gone the name is free to claim again, and an invite still reaches the room while someone is in it, now an ordinary room with nothing stored. When the last of them leaves the room ends, and the next join on that name brings a new room into being, as for any room nobody has claimed. A room your app opened before it was verified is the exception: with the claim gone, its invite leads to a new room under the verified app’s name.
A claimed room under an app’s scope, which is every room but a global/ one, is changed by the claiming account only through the app that claimed it, the same app once it is verified. From another app of the account, setDefault, limit, grant, revoke, remove, block, unblock, setMailbox, edit and delete are refused rooms: only the app that claimed the room can change it, code denied, and nothing in the room changes. That covers the account’s own messages too: a member id belongs to the account and not to the app, so another app cannot edit or delete what the account sent through the claiming app. A bare claim() from that app still resolves, and it still joins, reads and sends as any member may.
The owner’s seat stays with the claiming app as well. Another app of the account that joins under a member id of its own, as a tab with a secret of its own does, joins as an ordinary member, with a member’s permissions and message size rather than the owner’s, and room.owner still names the member the claiming app joins as. An account whose encryption key this browser holds has one member id in every app, so there the other app joins as the owner’s own member: room.self.id equals room.owner and it sends up to the owner’s size, while every change above is still refused. Plan for that refusal in any claimed room your app did not claim itself, such as one it reached by invite, since neither room.owner nor room.self.permissions says which app made the claim.
A global/ room is shared by every app, so once claimed any app of the account changes it, describes it, sets when it expires and releases it, as the app that claimed it does. Whatever the scope, the account changes every room it claimed from the Rooms tab of its fkn.app settings, and members of other accounts are answered as in any room, their own edits and deletes included. A room claimed before the platform recorded which app may change it is changed from any app of the account until that claim ends.
When the room cannot ask the platform whether this app is the one that claimed it, such a change is refused rooms: rooms are unavailable and nothing changes, so it can be sent again later. A connection acts for one app for its whole life, and the room refuses a claim or release naming another app than the one the connection joined as with rooms: malformed frame, which @fkn/lib and the FKN broker never send.
Pass temporary to make the claim end by itself, in milliseconds: true is 7 days, and a number is that many milliseconds, a whole number from 60,000 (1 minute) to 31,536,000,000 (365 days). ROOM_TEMPORARY_MS from @fkn/lib/contract carries those three figures as default, min and max. Any other value is refused rooms: the temporary duration is not one a claim can carry, and false or no value makes the claim permanent.
keeps the name for the signed-in premium account, and a mailbox unless asked not to. The only thing
in rooms that premium buys. description says what the room is for, shown to the account in its
fkn.app settings; a permanent claim with an unchanged one on every open is free. Claiming on every
open also claims the room again after the account unclaims it from fkn.app, so claim when the person
wants the room kept, or make it temporary.
temporary makes the claim end by itself, in milliseconds (true is 7 days), that long after its
last claim: a temporary claim goes to the account's ledger every time, which is what restarts its
clock. When it ends the app hears a claim event with claimed: false, exactly as for an Unclaim
from fkn.app, and the stored messages are gone. A broker too old to carry it refuses the call
unavailable (rooms: temporary claims are not available) rather than make the claim permanent.
mailbox: false keeps the name and stores nothing: the room relays as an unclaimed one does. It is
read only when the call starts the claim and ignored on a room the account already holds, so claiming
on every open never undoes the owner's choice.
Who changes a claimed room depends on its scope. A room under an app's scope, which is every room
but a global one, is changed by the claim's account only through the app that claimed it (the
same app once it is verified): from another app of the account setDefault, limit, grant,
revoke, remove, block, unblock, setMailbox, edit and delete are refused denied
(rooms: only the app that claimed the room can change it), the account's own messages included,
and nothing changes. That other app, which reaches the room through its invite, still joins, reads
and sends. A claimed global room is changed by any app of the account, which also describes it,
sets its temporary and releases it. The account changes every room it claimed
from its fkn.app settings, whichever app claimed it, and members of other accounts are answered as
in any room. While the room cannot tell whether two apps are one, such a change is refused
unavailable (rooms: rooms are unavailable) and can be sent again.
Past the account's room limit the call waits while fkn.app shows the person their claimed rooms over
your app, with Unclaim on each: it resolves once one is unclaimed and this claim is made, and rejects
full (rooms: too many claims) when they close the card or when fkn.app cannot show it. Nothing
about the account's other rooms reaches your app.
claim({
temporary?: number | boolean |undefined
Makes the claim end by itself, in milliseconds: true is 7 days (ROOM_TEMPORARY_MS.default), a
number is that many milliseconds, a whole number from 60,000 (1 minute) to 31,536,000,000 (365 days),
and anything else is refused invalid. false or no value makes the claim permanent.
The clock restarts on every claim, so a temporary room ends that long after its LAST claim: a room
the app keeps claiming on open stays, and an abandoned one goes. Restarting the clock needs the
service: while it cannot be reached, a claim answers unavailable and the room keeps the end it had,
so a room in use can still end if an outage outlasts the time it has left. Pick a duration with margin
over how often the app is opened. When it ends, it ends exactly as an
Unclaim from the account's fkn.app settings does: the name is given back, the stored messages are
deleted, and whoever is present hears a claim event with claimed: false and stays in an ordinary
room. It counts toward the account's room limit while it lasts and frees the slot when it ends.
The latest claim decides: claiming again without it makes the room permanent, and with it makes it
temporary again. On a room under an app's scope only the app that claimed the room (the same app
once it is verified) sets or changes it, and from another app it is refused denied (rooms: only the app that claimed the room can set when it expires); on a global room any app of the account
does. A claim from another app without it leaves the room as it was, on either kind of room.
temporary: true }) // ends 7 days after the last claim
keeps the name for the signed-in premium account, and a mailbox unless asked not to. The only thing
in rooms that premium buys. description says what the room is for, shown to the account in its
fkn.app settings; a permanent claim with an unchanged one on every open is free. Claiming on every
open also claims the room again after the account unclaims it from fkn.app, so claim when the person
wants the room kept, or make it temporary.
temporary makes the claim end by itself, in milliseconds (true is 7 days), that long after its
last claim: a temporary claim goes to the account's ledger every time, which is what restarts its
clock. When it ends the app hears a claim event with claimed: false, exactly as for an Unclaim
from fkn.app, and the stored messages are gone. A broker too old to carry it refuses the call
unavailable (rooms: temporary claims are not available) rather than make the claim permanent.
mailbox: false keeps the name and stores nothing: the room relays as an unclaimed one does. It is
read only when the call starts the claim and ignored on a room the account already holds, so claiming
on every open never undoes the owner's choice.
Who changes a claimed room depends on its scope. A room under an app's scope, which is every room
but a global one, is changed by the claim's account only through the app that claimed it (the
same app once it is verified): from another app of the account setDefault, limit, grant,
revoke, remove, block, unblock, setMailbox, edit and delete are refused denied
(rooms: only the app that claimed the room can change it), the account's own messages included,
and nothing changes. That other app, which reaches the room through its invite, still joins, reads
and sends. A claimed global room is changed by any app of the account, which also describes it,
sets its temporary and releases it. The account changes every room it claimed
from its fkn.app settings, whichever app claimed it, and members of other accounts are answered as
in any room. While the room cannot tell whether two apps are one, such a change is refused
unavailable (rooms: rooms are unavailable) and can be sent again.
Past the account's room limit the call waits while fkn.app shows the person their claimed rooms over
your app, with Unclaim on each: it resolves once one is unclaimed and this claim is made, and rejects
full (rooms: too many claims) when they close the card or when fkn.app cannot show it. Nothing
about the account's other rooms reaches your app.
claim({
description?: string |undefined
What the room is for, in the app's own words. The account reads it beside the room in its fkn.app
settings, where it can clear or unclaim the room, so say what would be lost. One line of plain text:
trimmed, then 1 to 200 characters as String.length counts them, with no control character, line or
paragraph separator, or bidi control; anything else is refused invalid, never cut short. Not sealed:
stored with the claim and shown to the account only. Claiming again with one replaces it: on a room
under an app's scope from the app that claimed the room only (the same app once it is verified), and
from another app it is refused denied (rooms: only the app that claimed the room can describe it);
on a global room from any app of the account. Claiming without one keeps it.
description: 'The watch party tonight',
temporary?: number | boolean |undefined
Makes the claim end by itself, in milliseconds: true is 7 days (ROOM_TEMPORARY_MS.default), a
number is that many milliseconds, a whole number from 60,000 (1 minute) to 31,536,000,000 (365 days),
and anything else is refused invalid. false or no value makes the claim permanent.
The clock restarts on every claim, so a temporary room ends that long after its LAST claim: a room
the app keeps claiming on open stays, and an abandoned one goes. Restarting the clock needs the
service: while it cannot be reached, a claim answers unavailable and the room keeps the end it had,
so a room in use can still end if an outage outlasts the time it has left. Pick a duration with margin
over how often the app is opened. When it ends, it ends exactly as an
Unclaim from the account's fkn.app settings does: the name is given back, the stored messages are
deleted, and whoever is present hears a claim event with claimed: false and stays in an ordinary
room. It counts toward the account's room limit while it lasts and frees the slot when it ends.
The latest claim decides: claiming again without it makes the room permanent, and with it makes it
temporary again. On a room under an app's scope only the app that claimed the room (the same app
once it is verified) sets or changes it, and from another app it is refused denied (rooms: only the app that claimed the room can set when it expires); on a global room any app of the account
does. A claim from another app without it leaves the room as it was, on either kind of room.
temporary: 60*60*1000 }) // ends 1 hour after it
constROOM_TEMPORARY_MS: {
readonlydefault:604800000;
readonlymin:60000;
readonlymax:31536000000;
}
How long a temporary claim may last, in milliseconds: default is what temporary: true means (7
days), and a number outside min (1 minute) to max (365 days) is refused invalid.
The clock restarts on every claim, so a temporary room ends that long after its last claim: a room your app claims on every open stays, and one nobody opens any more goes. A temporary claim, and any claim of a room that is temporary, goes to the platform every time, since that is what restarts the clock, so it is never answered by the room alone as a permanent one is. While the platform cannot be reached such a claim is refused rooms: rooms are unavailable and the room keeps the end it had, so a room in use can still end if an outage outlasts the time it has left. Pick a duration with margin over how often your app is opened.
When the time is up the claim ends exactly as an Unclaim in fkn.app does: the name is given back, the mailbox is deleted, and every member present gets a claim event with claimed: false and stays in an ordinary room. A temporary claim counts toward the account’s limit while it lasts and frees its slot when it ends. The Rooms tab and the room limit card show the account how long each temporary room has left, as in “Expires in 6 days”.
The latest claim decides: claiming again without temporary makes the room permanent, and with it makes it temporary again. In a room under an app’s scope only the app that claimed it, the same app once it is verified, sets or changes it: from another app of the account a claim with temporary is refused rooms: only the app that claimed the room can set when it expires, and a claim from another app without it leaves the room as it was. In a global/ room every app of the account sets or changes it, so the latest claim from any of them decides. A broker older than this option cannot carry it, and claim then rejects rooms: temporary claims are not available rather than make a permanent claim your app did not ask for.
Pass mailbox: false to claim the name and store nothing, and the claim still does everything else a claim does: the name stays reserved for the account and locked to the room key, the account is the owner on any device, and the room keeps its blocks and its members’ overrides when everyone leaves. It still needs premium and counts toward the account’s limit of 10 claims. It keeps no messages, so the room relays as one nobody has claimed: a message reaches every member present who may receive and is stored nowhere, backlog answers an empty page, edit and delete are refused rooms: the room keeps no messages, and room.mailbox and usage() read null. Nothing counts toward the owner’s storage, so neither the mailbox’s size nor the owner’s quota refuses a send there.
keeps the name for the signed-in premium account, and a mailbox unless asked not to. The only thing
in rooms that premium buys. description says what the room is for, shown to the account in its
fkn.app settings; a permanent claim with an unchanged one on every open is free. Claiming on every
open also claims the room again after the account unclaims it from fkn.app, so claim when the person
wants the room kept, or make it temporary.
temporary makes the claim end by itself, in milliseconds (true is 7 days), that long after its
last claim: a temporary claim goes to the account's ledger every time, which is what restarts its
clock. When it ends the app hears a claim event with claimed: false, exactly as for an Unclaim
from fkn.app, and the stored messages are gone. A broker too old to carry it refuses the call
unavailable (rooms: temporary claims are not available) rather than make the claim permanent.
mailbox: false keeps the name and stores nothing: the room relays as an unclaimed one does. It is
read only when the call starts the claim and ignored on a room the account already holds, so claiming
on every open never undoes the owner's choice.
Who changes a claimed room depends on its scope. A room under an app's scope, which is every room
but a global one, is changed by the claim's account only through the app that claimed it (the
same app once it is verified): from another app of the account setDefault, limit, grant,
revoke, remove, block, unblock, setMailbox, edit and delete are refused denied
(rooms: only the app that claimed the room can change it), the account's own messages included,
and nothing changes. That other app, which reaches the room through its invite, still joins, reads
and sends. A claimed global room is changed by any app of the account, which also describes it,
sets its temporary and releases it. The account changes every room it claimed
from its fkn.app settings, whichever app claimed it, and members of other accounts are answered as
in any room. While the room cannot tell whether two apps are one, such a change is refused
unavailable (rooms: rooms are unavailable) and can be sent again.
Past the account's room limit the call waits while fkn.app shows the person their claimed rooms over
your app, with Unclaim on each: it resolves once one is unclaimed and this claim is made, and rejects
full (rooms: too many claims) when they close the card or when fkn.app cannot show it. Nothing
about the account's other rooms reaches your app.
claim({
description?: string |undefined
What the room is for, in the app's own words. The account reads it beside the room in its fkn.app
settings, where it can clear or unclaim the room, so say what would be lost. One line of plain text:
trimmed, then 1 to 200 characters as String.length counts them, with no control character, line or
paragraph separator, or bidi control; anything else is refused invalid, never cut short. Not sealed:
stored with the claim and shown to the account only. Claiming again with one replaces it: on a room
under an app's scope from the app that claimed the room only (the same app once it is verified), and
from another app it is refused denied (rooms: only the app that claimed the room can describe it);
on a global room from any app of the account. Claiming without one keeps it.
description: 'The lobby your watch parties meet in',
mailbox?: boolean |undefined
Whether the room keeps its messages. false claims the name and stores nothing: a message reaches
whoever is present and is kept nowhere, backlog answers an empty page, and edit and delete are
refused invalid (rooms: the room keeps no messages). The claim still reserves the name, locks it
to the key and keeps the owner, the blocks and the overrides when everyone leaves, and it still needs
premium. true or no value keeps a mailbox. Anything else is refused invalid (rooms: malformed frame).
Read only when the call starts the claim: on a room the account already holds it is ignored, so an
app that claims on every open never undoes what the owner chose since. A claim landing after a
temporary one ran out starts a new claim, so it reads it again. A broker too old to carry it refuses
falseunavailable (rooms: the mailbox switch is not available) rather than store what the app
asked it not to.
mailbox: false }) // the name, and no history
constroom:rooms.Room
room.
claimed: boolean
whether a premium account keeps this name. Whether it keeps the messages too is mailbox.
claimed// true
constroom:rooms.Room
room.
mailbox: rooms.RoomMailbox |null
what the mailbox holds, as the room last said. Null while the room keeps no messages: nobody has
claimed it, or its claim keeps none. The account can clear the room, unclaim it, turn its messages
off or on, or set a lower cap from its fkn.app settings, which the app hears as a deleted or a
claim event; usage() reads the figures again.
mailbox// null, since nothing sent here is kept
await
constroom:rooms.Room
room.
setMailbox: (on:boolean) =>Promise<void>
turns a claimed room's mailbox off or on, from the room's owner only, which in a claimed room is the
claim's account. Off deletes every stored message at once, the archive included, and the room then
relays as an unclaimed one does while keeping its claim; whoever is present hears a claim event
with mailbox: false and no deleted, so what an app shows stays. On starts an empty mailbox that
keeps messages from the next seq, and nothing deleted comes back. Turning it on needs premium, as a
claim does (denied, rooms: claiming needs premium); turning it off never does. Setting what it
already is changes nothing. Resolves once the room answered, with mailbox reading the new state,
or a newer one when a release or the account's own switch landed before the answer.
Refused invalid (rooms: the room is not claimed) on an unclaimed room, denied (rooms: not the room owner) from anyone else, denied (rooms: only the app that claimed the room can change it)
from another app of the claim's account on a room claimed under an app's scope, as claim says, and
invalid (rooms: malformed frame) for anything but a boolean. A broker too old to carry it refuses
unavailable (rooms: the mailbox switch is not available). The account can do the same from its
fkn.app settings, whichever app claimed the room.
setMailbox(true) // the owner keeps messages from the next seq on
await
constroom:rooms.Room
room.
setMailbox: (on:boolean) =>Promise<void>
turns a claimed room's mailbox off or on, from the room's owner only, which in a claimed room is the
claim's account. Off deletes every stored message at once, the archive included, and the room then
relays as an unclaimed one does while keeping its claim; whoever is present hears a claim event
with mailbox: false and no deleted, so what an app shows stays. On starts an empty mailbox that
keeps messages from the next seq, and nothing deleted comes back. Turning it on needs premium, as a
claim does (denied, rooms: claiming needs premium); turning it off never does. Setting what it
already is changes nothing. Resolves once the room answered, with mailbox reading the new state,
or a newer one when a release or the account's own switch landed before the answer.
Refused invalid (rooms: the room is not claimed) on an unclaimed room, denied (rooms: not the room owner) from anyone else, denied (rooms: only the app that claimed the room can change it)
from another app of the claim's account on a room claimed under an app's scope, as claim says, and
invalid (rooms: malformed frame) for anything but a boolean. A broker too old to carry it refuses
unavailable (rooms: the mailbox switch is not available). The account can do the same from its
fkn.app settings, whichever app claimed the room.
setMailbox(false) // and stops again, deleting what was kept
true or no value keeps a mailbox, so a bare claim() does what it always did, and a value that is not a boolean is refused rooms: malformed frame before the broker is asked. mailbox is read only by a claim that starts the claim, and on a room the account already holds it is ignored: the call resolves and nothing changes, so an app that claims on every open never undoes what the owner chose since. A claim after an Unclaim, or after a temporary claim ran out, makes a new claim and reads it again. A broker older than this option cannot carry it, and claim with mailbox: false then rejects rooms: the mailbox switch is not available rather than make a claim that stores what your app asked it not to.
After the claim, the owner changes it with setMailbox from the app that claimed the room, or in a global/ room from any app of the account that holds the room key (who changes a claimed room), or the account changes it with the Messages kept switch on the room’s row in the Rooms tab of its fkn.app settings, where a room that keeps no messages shows no counts, no Clear and no size limit:
setMailbox(false) deletes every stored message at once, archived ones included, and the size limit with them. Before the call resolves the room offers the platform 0 bytes, and sends them again within a minute when the platform did not take them, so the owner’s storage stops counting them. The call does not say which happened. The claim, the name and seq stay.
setMailbox(true) starts an empty mailbox that keeps messages from the next seq, with no size limit, and nothing deleted comes back. It needs premium, as a claim does, and is refused rooms: claiming needs premium without it. Turning the mailbox off never needs premium, so an owner whose premium lapsed can still free the space.
Every member present hears either change as a claim event with claimed: true and the new mailbox, and on it room.mailbox reads null for false and the empty { messages: 0, bytes: 0, cap: 500000000, archived: false } for true, as it does on the claim event of a new claim that keeps messages. The room sends that event before it answers, so by the time setMailbox resolves room.mailbox reads the state asked for, or a newer one when a release or the account’s own switch in fkn.app landed first. Turning it off sends no deleted event, so what your app shows stays on screen: drop what it replayed from the mailbox on that claim event if it should not show messages the room no longer keeps.
temporary and mailbox go in one claim and act apart. A temporary claim that keeps no messages ends when its time is up like any other, a renewal is a claim the account already holds and ignores mailbox, and a claim after the end makes a new claim that reads it.
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:
Permission
Room default
The owner
send
true, unless the opener passed defaults: { send: false }
always, and it cannot be revoked
receive
true, unless the opener passed defaults: { receive: false }
always, and it cannot be revoked
remove
none, false until the owner grants it
always
block
none, false until the owner grants it
always
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, setDefault and limit belong to the owner alone, and in a claimed room under an app’s scope to the owner through the app that claimed it. 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.
whether a member with no override may send or receive, from the owner only (denied, rooms: not the room owner). On a room claimed under an app's scope, from another app of the claim's account,
it is refused denied (rooms: only the app that claimed the room can change it), as claim says.
setDefault('send', false) // the owner alone, and every member without an override stops sending
gives a member a permission over the room default, from the owner only (denied, rooms: not the room owner). On a room claimed under an app's scope, from another app of the claim's account, it is
refused denied (rooms: only the app that claimed the room can change it), as claim says.
grant(
constid:string
id, 'send') // an override, which outlives a later change of the default
gives a member a permission over the room default, from the owner only (denied, rooms: not the room owner). On a room claimed under an app's scope, from another app of the claim's account, it is
refused denied (rooms: only the app that claimed the room can change it), as claim says.
grant(
constid:string
id, 'remove') // a delegated moderator, who may remove and may not grant
id) // one member's message cap, which null clears
await
constroom:rooms.Room
room.
remove: (id:string) =>Promise<void>
sends a member out of the room, which they may join again, with the remove permission (denied,
rooms: permission denied). On a room claimed under an app's scope, from another app of the claim's
account, it is refused denied (rooms: only the app that claimed the room can change it), as
claim says.
remove(
constid:string
id) // they leave with reason 'removed', and may join again
await
constroom:rooms.Room
room.
block: (id:string) =>Promise<void>
sends a member out of the room and keeps them out, with the block permission (denied, rooms: permission denied). On a room claimed under an app's scope, from another app of the claim's account,
it is refused denied (rooms: only the app that claimed the room can change it), as claim says.
block(
constid:string
id) // they leave with reason 'blocked', and the rejoin is refused
await
constroom:rooms.Room
room.
unblock: (id:string) =>Promise<void>
lets a blocked member join again, with the block permission (denied, rooms: permission denied).
On a room claimed under an app's scope, from another app of the claim's account, it is refused
denied (rooms: only the app that claimed the room can change it), as claim says.
unblock(
constid:string
id) // the room forgets the block
constroom:rooms.Room
room.
self: rooms.RoomMember
this app's member record, as this room sees it. The id is fresh in every room.
Changing a default moves every member who has no override for it, joined already or joining later, and every member receives a permissions event for each member it moved, so room.self and members() stay current. limit sets the message cap the same two ways, from 1 to 33,554,432: with no id it changes the room default, and with an id it sets that member’s override, which null clears. 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 for a guest from the same tab and from any tab on the same network. In a room nobody has claimed it dies with the room, along with every other permission, so a new room starts clean. A claimed room keeps its blocks, and the overrides of members who have left.
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, a ladder sized to run out inside the hold, and then gives up. It pings every 25 seconds and treats a minute without an answer as a dropped connection. A room keeps its roster, permissions, blocks and mailbox in its own storage, so a deploy loses none of them, and in a claimed room that keeps messages backlog from the last seq you saw fills the gap.
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 secret the id is derived from is gone, as when a guest’s tab loses its storage, 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 an id in every room. The platform derives it from a secret your browser holds and from a value derived from the room key, so it survives your reconnects and is unrelated to your id in a room with another key. With an FKN account whose encryption key this browser holds, the secret comes from that key, so the id is the same in your other tabs, on your other devices and in your other apps, and otherwise it is one per tab. Two rooms opened with one key give such an account the same id in both, so give every room its own key.
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
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, limit, 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, plus the account of a signed-in member, used for claims, blocks and metering, a guest’s network, used for blocks and metering, and the app that made each claim. What it cannot see is a byte of content. A claim’s description is the exception you write yourself: it reaches the platform in the clear, for the account’s own settings page.
The room key is never on its side, a room nobody has claimed deletes everything it stored when it ends, and a claim that keeps no messages stores none. 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:
History without a claim. A room nobody has claimed stores nothing it relays, so a joiner sees nothing sent before it arrived. Claim the room to keep a mailbox.
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 name then opens a new room.
A lost key. The platform never holds a key, so it cannot hand one back. Lose the key of a claimed room, or of a room still in use, and every join without it is refused bad-key for as long as that room exists. For a claimed room the account can still end that: Unclaim in the Rooms tab of its fkn.app settings needs no key, and once nobody holds a seat the room ends and the name opens a new room.
Reading content. The platform cannot read a message, so moderation is the owner’s: live in any room, and with delete in a claimed one that keeps messages.
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 exists and its key is not the one presented: an invite whose key half changed, or an open that minted a fresh key for a name somebody is already in. Share the whole invite, room.invite, or pass the room’s key to open.
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.
The sealed text is over room.self.maxMessageBytes, 262,144 on the wire by default. Nothing was sent and nothing was trimmed, so split it or shorten it.