host

The host object, a global inside every app.

Example

const { app, me } = await host.ready()
console.log(app.slug, 'opened by', me?.name ?? 'someone signed out')

const trips = (await host.data.get('trips')) ?? []

Properties

PropertyModifierTypeDescription
datareadonlyStoreThe user's data in this app.
friendsreadonlyAudienceStoreWhat the user shows their friends who use this app, and the friends who show them theirs.
publicreadonlyAudienceStoreWhat the user shows everyone who uses this app, and everyone who shows them theirs.
notificationsreadonlyNotificationsNotifications Tinylib sends on the app's behalf.
roomsreadonlyRoomsThe user's rooms in this app.
invitesreadonlyInvitesThe invites waiting on the user in this app from people it can name to them.

host.ready()

ready(): Promise<Context>;

Resolves with the Context. It waits a moment for the latest copy of the user's data to arrive from Tinylib, and resolves right away if Tinylib can't be reached. Call it before anything else. Calling it again returns the same promise.

Opened for a room, it resolves with Context.room as follows. With a copy of the room on this device, it resolves at the same moment, and a newer room reaches Room.subscribe when it arrives. Online with no copy, it waits for Tinylib's answer up to limits.calls.timeoutMs, and room is null after an error or that long. Offline with no copy, room is null at once, and the app doesn't reload when the device reconnects. An app never gets null for a room the user is in only because the answer was slow.

Returns

Promise<Context>

Example
const { app, me, room } = await host.ready()

Context

What host.ready() resolves with.

Properties

PropertyTypeDescription
appApp-
mePerson | nullThe user, or null when nobody is signed in. Signed out, they have host.data, host.friends and host.public on this device only, list answers with nobody, and createRoom, rooms(), every call on a room and every call on notifications that needs an account reject with signed_out. After a click or a tap, such a call first asks the user to sign in, and rejects if they don't. Signing in leaves the app and opens it again, so me never changes while it runs. Example const { me } = await host.ready() greeting.textContent = me ? Hello, ${me.name} : 'Hello'
roomRoom | nullThe room the app was opened for: an accepted invite, a link, or a "Your turn" notification. Otherwise it's null, and host.rooms() has the rest. It's null signed out, and for a room the user left, was removed from, or that was deleted. Host.ready says what it is when Tinylib is slow to answer. Example const { room } = await host.ready() if (room) room.subscribe(render)

App

The app that's running.

Properties

PropertyTypeDescription
idstringStable, and the same for every user.
slugstringThe address, as in /app/trip-log.
namestringThe name from host.json.
versionnumberWhich publish is running, counted from 1. A device that already had the app can be one behind for one open after you publish.

Person

Someone as this app sees them: the user in Context.me, a room's members, and the people list answers.

Properties

PropertyTypeDescription
idstringAn id that only this app sees. It's the same on all their devices and different in every other app. When they delete their data in the app, they get a new one.
namestringThe name they chose. That's all an app gets about a person.

host.data

JSON values by key: the user's data in this app. It works without a connection: reads come from the device's copy, writes wait on the device and go to Tinylib when the connection is back, and they reach the user's other devices once they sign in. If two devices write the same key, whichever write reaches Tinylib last wins. Limits: limits.data.

Each value is stored whole. Values that change together go under one key, such as a list with its items, so another device never sees one without the other.

Private apps

In an app with "private": true in host.json, values are encrypted in the browser before they're stored anywhere. The calls don't change. Key names stay readable, so keep anything sensitive out of them: e/9f2c81a4, not entry-2026-09-14-therapy. Tinylib asks for the user's passkey before the app opens, and closes the app again limits.private.unlockMinutes later.

Extended by

host.data.get()

get<T>(key): Promise<T | undefined>;

The value under key, or undefined if there isn't one. In a private app, rejects with unreadable if the value won't decrypt.

Type Parameters
Type ParameterDefault type
Tunknown
Parameters
ParameterType
keystring
Returns

Promise<T | undefined>

Example
const trips = (await host.data.get('trips')) ?? []

host.data.set()

set(key, value): Promise<void>;

Stores value under key, and resolves once the device has it. Rejects with invalid_key, invalid_value, value_too_large, too_many_keys or too_much_data. undefined is refused with invalid_value; use remove.

Parameters
ParameterType
keystring
valueunknown
Returns

Promise<void>

Example
await host.data.set('prefs', { showNotes: true, lastTab: 'month' })

host.data.remove()

remove(key): Promise<void>;

Removes key. Resolves whether the key existed or not.

Parameters
ParameterType
keystring
Returns

Promise<void>

Example
for (const key of Object.keys(await host.data.getAll('draft.'))) await host.data.remove(key)

host.data.getAll()

getAll<T>(prefix?): Promise<Record<string, T>>;

Every value in the store, by key, in one call. With prefix, only the keys that start with it.

Type Parameters
Type ParameterDefault type
Tunknown
Parameters
ParameterType
prefix?string
Returns

Promise<Record<string, T>>

Example
const items = await host.data.getAll('item.')
for (const [key, item] of Object.entries(items)) draw(key, item)

host.data.onChange()

onChange(listener): () => void;

Listens for changes the app didn't make itself: another device's write arrived, the app wrote in another tab, or Tinylib refused a write that was waiting to be sent and the value went back. The listener gets the keys that changed. A change that arrives after ready() resolves but before you add a listener is delivered to the first listener you add. Returns a function that removes the listener.

Parameters
ParameterType
listener(keys) => void
Returns

() => void

Example
host.data.onChange(async (keys) => {
  if (keys.includes('trips')) render(await host.data.get('trips'))
})

host.friends and host.public

What the user shows an audience in this app, and the people in that audience who show theirs: host.friends for their friends who use the app, host.public for everyone who uses it. The five Store calls work as they do on host.data, on the user's own values, and list reads everyone who shares.

The page never asks for consent. The first set to each audience opens Tinylib's sheet asking whether to share with it, with the write when the call follows a tap, and otherwise at the app's next call that does. The write stores the value and resolves whatever the user answers. Once they've answered, a switch on the app's page decides. The sheet and the switch show the audience's sentence from host.json's shows. While they don't share, their values stay theirs, read back on their devices, and appear in nobody's list; list answers sharing: false.

A value is any JSON, as in host.data. Each audience has its own limits, limits.shared, apart from host.data's. In a private app every call rejects with private_app. Values an app writes can be written by any page, so a board built on them isn't proof of a score.

public is a reserved word in strict mode: host.public works, and const { public } = host doesn't.

Example

await host.public.set('best.hard', 4200)
const { people, rank } = await host.public.list({ sort: 'best.hard', limit: 10 })

Extends

host.friends.get()

get<T>(key): Promise<T | undefined>;

The value under key, or undefined if there isn't one. In a private app, rejects with unreadable if the value won't decrypt.

Type Parameters
Type ParameterDefault type
Tunknown
Parameters
ParameterType
keystring
Returns

Promise<T | undefined>

Example
const trips = (await host.data.get('trips')) ?? []
Inherited from

Store.get

host.friends.set()

set(key, value): Promise<void>;

Stores value under key, and resolves once the device has it. Rejects with invalid_key, invalid_value, value_too_large, too_many_keys or too_much_data. undefined is refused with invalid_value; use remove.

Parameters
ParameterType
keystring
valueunknown
Returns

Promise<void>

Example
await host.data.set('prefs', { showNotes: true, lastTab: 'month' })
Inherited from

Store.set

host.friends.remove()

remove(key): Promise<void>;

Removes key. Resolves whether the key existed or not.

Parameters
ParameterType
keystring
Returns

Promise<void>

Example
for (const key of Object.keys(await host.data.getAll('draft.'))) await host.data.remove(key)
Inherited from

Store.remove

host.friends.getAll()

getAll<T>(prefix?): Promise<Record<string, T>>;

Every value in the store, by key, in one call. With prefix, only the keys that start with it.

Type Parameters
Type ParameterDefault type
Tunknown
Parameters
ParameterType
prefix?string
Returns

Promise<Record<string, T>>

Example
const items = await host.data.getAll('item.')
for (const [key, item] of Object.entries(items)) draw(key, item)
Inherited from

Store.getAll

host.friends.onChange()

onChange(listener): () => void;

Listens for changes the app didn't make itself: another device's write arrived, the app wrote in another tab, or Tinylib refused a write that was waiting to be sent and the value went back. The listener gets the keys that changed. A change that arrives after ready() resolves but before you add a listener is delivered to the first listener you add. Returns a function that removes the listener.

Parameters
ParameterType
listener(keys) => void
Returns

() => void

Example
host.data.onChange(async (keys) => {
  if (keys.includes('trips')) render(await host.data.get('trips'))
})
Inherited from

Store.onChange

host.friends.list()

list(options?): Promise<ListPage>;

One page of the people who share with this audience, the user included when they share, each with every value they hold in it. With sort, only people whose value under that key is a number, ordered by it; a tie goes to whoever reached the value first. Without sort, whoever changed a value most recently comes first. Needs a connection: rejects with offline without one, and signed out it resolves with nobody.

Parameters
ParameterType
options?ListOptions
Returns

Promise<ListPage>

Example
let page = await host.friends.list()
const everyone = [...page.people]
while (page.next) {
  page = await host.friends.list({ after: page.next })
  everyone.push(...page.people)
}

ListOptions

What list reads.

Properties

PropertyTypeDescription
sort?stringThe key whose number orders the list. Without it, the newest change comes first.
order?"desc" | "asc"With sort, desc puts the largest number first, and asc the smallest. Defaults to desc.
limit?numberHow many people. Defaults to limits.shared.pageDefault, and stops at limits.shared.page.
after?stringThe next of the page before, for the page after it.

ListPage

One page of list.

Properties

PropertyTypeDescription
peoplePerson & object[]Each person under the id this app knows them by, with every value they hold in the store.
nextstring | nullWhat after takes for the page after this one, or null for the last page.
ranknumber | nullThe user's place under sort, where tied people share a place, whatever page they're on. null without sort, when they don't share, or when they have no number there.
sharingbooleanWhether the user shares with this audience.

host.notifications

Notifications Tinylib sends to the user on the app's behalf, at a time the app picks, whether or not the app is open then. Each person answers once per app whether it may notify them, on every device at once; the switch on the app's page changes the answer. Each notification goes to every device of theirs that receives, and is shown on its own. Tapping one opens the app. Limits: limits.notifications.

host.notifications.schedule()

schedule(request): Promise<{
  id: string;
}>;

Schedules a notification, and resolves with its id once Tinylib holds it. The first call asks the user, unless they already answered when a game asked, so make it from a click or a tap: without one it rejects with needs_tap. It rejects with needs_tap too when the user closes Tinylib's sheet without answering, and the next tapped call asks again. It rejects with denied when the user said no, signed_out when nobody is signed in, and with invalid_notification, too_many_notifications, too_many_waiting or offline. It resolves on a device that can't show notifications, and they arrive on the user's other devices.

Parameters
ParameterType
requestNotificationRequest
Returns

Promise<{ id: string; }>

Example
startButton.addEventListener('click', async () => {
  const { id } = await host.notifications.schedule({
    title: 'Time to stretch',
    body: 'Two minutes are up.',
    at: Date.now() + 2 * 60 * 1000,
  })
})

host.notifications.cancel()

cancel(id): Promise<void>;

Cancels a scheduled notification, whether or not it was still waiting. Rejects with signed_out or offline.

Parameters
ParameterType
idstring
Returns

Promise<void>

host.notifications.list()

list(): Promise<ScheduledNotification[]>;

Everything scheduled for this user that isn't due yet, soonest first. Rejects with signed_out or offline.

Returns

Promise<ScheduledNotification[]>

NotificationRequest

Properties

PropertyTypeDescription
atstring | number | DateWhen to send it: a Date, an ISO 8601 string, or milliseconds since the epoch. A time in the past sends it now.
titlestring-
body?string-

ScheduledNotification

A notification this app scheduled that isn't due yet.

Properties

PropertyTypeDescription
idstringThe id schedule() gave it.
atstringWhen Tinylib sends it, as an ISO 8601 string, rounded to the second.
titlestring-
bodystringEmpty if you didn't give one.

host.createRoom()

createRoom(options?): Promise<Room>;

Makes a room with the user as its first member, and resolves with the room. A shared room runs server.js's setup now; a game runs it when it starts. options is anything JSON can hold, up to limits.rooms.optionsBytes, and server.js reads it as ctx.options. Rejects with too_many_rooms when the user is in limits.rooms.perPerson rooms of this app that haven't ended, too_big when options is over its limit, invalid_value when JSON can't carry it, failed when a shared room's setup throws, and signed_out, offline or timeout. After a timeout the room may exist, and host.rooms() lists it.

Where the user hasn't answered for the app, their first game in a run, created or opened into, opens Tinylib's notifications sheet at the app's next call that follows a click or a tap. When that call is room.invite() with no ids, the sheet opens once the invite sheet closes. "Your turn" and "Game over" follow that answer. The call doesn't wait on the sheet.

Parameters
ParameterType
options?unknown
Returns

Promise<Room>

Example
newGame.addEventListener('click', async () => {
  room = await host.createRoom()
  room.subscribe(render)
})

host.rooms()

host.rooms is called as host.rooms().

Rooms(): Promise<Room[]>;

Resolves with the rooms the user is in, in this app: every one that hasn't ended, then the ended ones, each group newest first, at most limits.rooms.perPerson in all. An ended room stays for limits.rooms.endedDays, and a room the user left is gone. Each is the copy Tinylib last stored, which can be one change behind the room; Room.subscribe brings the newest. Offline, it answers the rooms this device last received, and rejects with offline when it has none. Rejects with signed_out, timeout or failed too.

Returns

Promise<Room[]>

Example

const [latest] = await host.rooms()
if (latest && !latest.ended) latest.subscribe(render)

Room

One room as the user sees it after one change. Its fields never change: a newer room reaches Room.subscribe. server.js decides what happens in it, and Tinylib runs server.js where no page can read it.

Properties

PropertyTypeDescription
idstring-
kind"shared" | "game"Whether server.js exports game or shared.
sizenumberThe most people in the room at once: a shared room's size, or a game's players.max.
minnumberThe fewest players a game starts with, players.min. It's 1 in a shared room.
membersPerson & object[]Everyone who has been in the room, in the order they joined, under the ids this app knows them by. An entry is never deleted: someone who left has left: true. At most limits.rooms.memberEntries.
turnstring[]Who Tinylib accepts an action from now, by id. Empty means nobody, and it's always empty before a game starts and once the room has ended. Without a turn export in server.js, it names every player still in.
startedbooleanWhether actions run: a game once it has started, and a shared room always. A game starts when players.max have joined, or at Room.start.
outcome| { winners: string[]; } | nullWhat a game's outcome returned once it ended the game, else null. A game that ends any other way has none: when fewer than players.min are left, when turn names nobody, when it idles out, when its app is unpublished, or as if it never started. Always null in a shared room.
viewunknownWhat server.js's view returned for the user, or the whole state when server.js exports no view. null before a game starts, since it has no state yet.
endedbooleanWhether the room has ended. Nothing changes an ended room, and it stays readable for limits.rooms.endedDays.
invitesobject[]The user's own invites to this room that are still open, newest first: when each was sent, in milliseconds since the epoch, and who it went to when the app can name them to the user, as Host.invites says, else null. An invite stays open until its invitee joins or declines, the room fills or ends, or the user leaves. It lists no link and no invite another member sent. A change to it reaches Room.subscribe and Host.onRooms. Example const [latest] = room.invites status.textContent = latest ? Invite sent to ${latest.to?.name ?? 'your friend'}. : 'Invite someone to start.'

room.act()

act(action, args?): Promise<void>;

Runs action in server.js with args, anything JSON can hold up to limits.rooms.argsBytes. It resolves once this page's listener has the new room, and has no timeout: the room answers or refuses every action. Rejects with refused, whose message is the sentence server.js passed to ctx.refuse, or with not_your_turn, not_started, not_member, ended, unknown_action, not_removable, too_big, too_many_calls, invalid_value, signed_out, offline or failed. too_big means args is over its limit, or the result would take the state over limits.rooms.stateBytes or, when server.js exports view, a view over limits.rooms.viewBytes; the state is unchanged. failed means server.js threw or took too long. After offline the action may have stored, and the next room shows whether it did.

Parameters
ParameterType
actionstring
args?unknown
Returns

Promise<void>

Example
room.act('move', { from: 'e2', to: 'e4' }).catch((error) => show(error.message))

room.start()

start(): Promise<void>;

Starts a game before players.max have joined: the players still in take their seats, setup runs with them, and nobody joins after. Any player can call it once players.min are in. It resolves once this page's listener has the started room, and on a game that has already started. Rejects with too_few, not_member, ended, too_many_calls, signed_out, offline or failed, and with unknown_action in a shared room.

Returns

Promise<void>

Example
dealButton.hidden = room.started || room.members.filter((member) => !member.left).length < room.min
dealButton.onclick = () => room.start()

room.subscribe()

subscribe(listener): () => void;

Calls listener with the newest copy of this room the page holds, then with each newer room: after an action, a join, a leave, and the end. Returns a function that stops it. A page listens to one room at a time: subscribing to another room stops every listener on this one. A room older than one the page already has never reaches a listener.

Parameters
ParameterType
listener(room) => void
Returns

() => void

Example
room.subscribe((newer) => {
  room = newer
  render()
})

room.invite()

invite(ids?): Promise<void>;

With no ids, opens Tinylib's invite sheet, where the user picks friends to invite, at most as many as there are free places, or makes a link that anyone signed in can join by. Each member has one link at a time, and turns it off in the same sheet; it stops when they leave. It resolves when the sheet closes, and has no timeout.

With ids, invites those people, by the ids this app knows them by, with no sheet: each one the app can name to the user, as Host.invites says. An id the app can't name, or of someone a block keeps out, is skipped without a word; Room.invites shows what went out.

Everyone invited sees the invite live, in this app when it can name the user to them and in Tinylib, and joins with their own tap. Call it from a click or a tap: without one it rejects with needs_tap. Rejects with full for more people than free places, which a game that has started also answers, too_many_invites once the user would have more than limits.rooms.openInvites open in this app, ended, not_member, signed_out or offline.

Parameters
ParameterType
ids?string[]
Returns

Promise<void>

Example
rematch.onclick = async () => {
  const next = await host.createRoom()
  await next.invite(last.members.filter((member) => member.id !== me.id && !member.left).map((member) => member.id))
}

room.leave()

leave(): Promise<void>;

Marks the user left, and runs server.js's left unless the room has ended or is a game that hasn't started. The listener gets one last room showing them left, and nothing after it. Their place is free for someone new, except in a game that has started, and only a fresh invite brings them back. It has no timeout: the room answers or refuses it. Rejects with not_member, signed_out or offline.

Returns

Promise<void>

host.invites()

host.invites is called as host.invites().

Invites(): Promise<Invite[]>;

Resolves with the invites waiting on the user in this app, newest first, from people the app can name to them: a friend when both of them let the app share with friends, or someone they share a room of the app with. Tinylib shows every other invite itself. Offline, it answers the invites this page last received, none before the first, and rejects with offline when this device holds none of the user's rooms in the app. Rejects with signed_out, timeout or failed too.

Returns

Promise<Invite[]>

Example

for (const invite of await host.invites()) show(`${invite.from.name} invited you`)

Invite

An invite waiting on the user, to a room of this app.

Properties

PropertyTypeDescription
roomstringThe id of the room it's to.
fromPersonWho sent it.
atnumberWhen it was sent, in milliseconds since the epoch.

invite.join()

join(): Promise<Room>;

Joins the room, and resolves with it. Call it from a click or a tap: without one it rejects with needs_tap. Rejects with no_invite once the invite stopped, full, ended, not_joinable, too_many_rooms, signed_out or offline.

Returns

Promise<Room>

Example
accept.onclick = async () => show(await invite.join())

invite.decline()

decline(): Promise<void>;

Declines the invite. Its sender isn't told, and it leaves their Room.invites. Call it from a click or a tap: without one it rejects with needs_tap. Rejects with signed_out or offline too.

Returns

Promise<void>

host.onRooms()

onRooms(listener): () => void;

Calls listener with the user's rooms and invites in this app, as host.rooms() and host.invites() answer them, once it has them and again after each change: the user makes, joins or leaves a room, on any device; someone joins or leaves one of their rooms; a room ends or fills; an invite reaches them; or one of their own invites is sent or answered. Returns a function that stops it. It's never called while nobody is signed in.

While the app listens, Tinylib shows no invite of its own over it for an invite in invites: the app shows those.

Parameters
ParameterType
listener(rooms, invites) => void
Returns

() => void

Example
host.onRooms((rooms, invites) => {
  games = rooms
  offers = invites
  render()
})

host.onOpen()

onOpen(listener): () => void;

Calls listener with a room of this app that the user opened from Tinylib while the app runs: they tapped Join on Tinylib's invite, or tapped a notification about the room. Returns a function that stops it. With no listener, Tinylib opens the app again with that room as ready().room.

Parameters
ParameterType
listener(room) => void
Returns

() => void

Example
host.onOpen((room) => show(room))

As an import

const host: Host;

The host global for bundled code that imports it. Outside Tinylib, the first use throws.