Self-host and scale Fortnite lobby bots for Bot Lobbies with the official FNLB Node.js / Bun SDK.
Important
The full documentation is at developers.fnlb.net.
- SDK Introduction: install, authentication, shards, and shutdown
- SDK guides: events, parties, cosmetics, chat, and
fnlb.api - SDK reference: every class and method
- Category config: every
overrideCategoryConfigkey
npm install fnlb@latest
# or
bun install fnlb@latestYou need Node.js 22+ or Bun, and an API token from your FNLB account.
Create a client, then call start() with your token and the category that owns the bot.
import FNLB from 'fnlb';
const fnlb = new FNLB();
await fnlb.start({
token: 'your-api-token', // https://app.fnlb.net/account
categories: ['your-category-id'] // bot page → About this bot → Category ID
});When you're done:
await fnlb.stop();start() accepts token (API Token or OAuth2 access token):
| Credential | Prefix | Where to get it |
|---|---|---|
| API token | FNLB_… |
app.fnlb.net/account |
| OAuth2 access token | FNLBOA2AT_… |
Needs bots.run, categories.read, and bots.read (see OAuth2 docs) |
await fnlb.start({
token: 'FNLBOA2AT_...',
categories: ['your-category-id']
});botsPerShard is the max number of bots that can run inside a single shard (subprocess):
import FNLB from 'fnlb';
const fnlb = new FNLB();
await fnlb.start({
token: 'your-api-token',
categories: ['your-category-id'],
botsPerShard: 10
});Each shard is its own subprocess. Multiply shards × bots-per-shard for your capacity ceiling:
import FNLB from 'fnlb';
const fnlb = new FNLB();
await fnlb.start({
token: 'your-api-token',
categories: ['your-category-id'],
numberOfShards: 2,
botsPerShard: 10 // up to 20 bots total
});Pass one or more category IDs. Find them on app.fnlb.net/bots → select a bot → About this bot → Category ID.
await fnlb.start({
token: 'your-api-token',
categories: ['category-id-1', 'category-id-2'],
numberOfShards: 2,
botsPerShard: 10
});Pass exact bot IDs from the same About this bot panel (FNLB ID):
await fnlb.start({
token: 'your-api-token',
bots: ['bot-id-1', 'bot-id-2'],
botsPerShard: 2
});categories and bots are include lists:
- Categories only - bots in those categories, plus unassigned bots
- Bots only - only the listed IDs
- Both - union of category bots and listed bot IDs
- Neither - your full bot pool
Invalid IDs are ignored. Multiple shards can share the same bots list; the gateway assigns each ID to one shard.
await fnlb.start({
token: 'your-api-token',
categories: ['category-id-1', 'category-id-2'],
bots: ['bot-id-1', 'bot-id-2'],
numberOfShards: 2,
botsPerShard: 10
});Omit categories (or pass []) when you only want specific bot IDs from any category. If categories is set, bots without a category are still included.
After start(), you can change which categories/bots the running shards may use without calling stop().
Optional second argument: a partial category config applied to those ids (same merge rules as overrideCategoryConfig).
import FNLB from 'fnlb';
import { CategoryConfigPartyPrivacy } from '@fnlb-project/shared/types';
const fnlb = new FNLB();
await fnlb.start({
apiToken: 'your-api-token',
categories: ['category-id-1'],
botsPerShard: 10
});
// Expand the include list
fnlb.addCategories(['category-id-2'], {
privacy: CategoryConfigPartyPrivacy.Private
});
fnlb.addBots(['bot-id-1'], {
acceptFriendRequests: false
});
// Replace the include list
fnlb.setCategories(['category-id-2', 'category-id-3']);
fnlb.setBots(['bot-id-1', 'bot-id-2'], {
statusText: ['Custom status']
});
// Clear (back to unrestricted)
fnlb.setCategories();
fnlb.setBots();
// Shrink it (bots that no longer match are stopped; capacity refills from the new pool)
fnlb.removeCategories(['category-id-1']);
fnlb.removeBots(['bot-id-1']);
// Current include lists (`undefined` = unrestricted for that dimension)
fnlb.getCategories();
fnlb.getBots();Rules:
undefinedmeans unrestricted for that dimension.add*on an unrestricted dimension starts an include list (narrows from “all”).add*on an existing list unions and dedupes.set*replaces the list (undefined/ omitted = unrestricted).remove*on unrestricted is a no-op.- Removing until empty returns that dimension to unrestricted.
- Overrides passed to
add*/set*are stored per id and removed withremove*(or whenset*drops that id). - Throws if no shards are running.
Change a few category options for this run without editing the dashboard.
overrideCategoryConfig is scoped. Merge order (later wins): FNLB config → all → matching category → matching bot.
import FNLB from 'fnlb';
import { CategoryConfigPartyPrivacy } from '@fnlb-project/shared/types';
const fnlb = new FNLB();
await fnlb.start({
apiToken: 'your-api-token',
overrideCategoryConfig: {
// every bot
all: {
acceptFriendRequests: false
},
// bots in this category
categories: {
'category-id-1': {
privacy: CategoryConfigPartyPrivacy.Private
}
},
// one specific bot
bots: {
'bot-id-1': {
statusText: ['Special bot']
}
}
}
});Overrides stay applied even if the category config is updated live.
See the category config reference for every supported key and type. How overrides are applied is in the Node.js SDK guide.
Useful when you run more than one host and want clear logs / gateway labels:
import FNLB from 'fnlb';
const fnlb = new FNLB({ clusterName: 'MyAwesomeCluster' });
await fnlb.start({
token: 'your-api-token'
});import FNLB from 'fnlb';
const fnlb = new FNLB();
async function startFNLB() {
await fnlb.start({
token: 'your-api-token',
categories: ['your-category-id'],
numberOfShards: 1,
botsPerShard: 5
});
}
async function restartFNLB() {
console.log('Restarting FNLB...');
await fnlb.stop();
await startFNLB();
}
await startFNLB();
setInterval(restartFNLB, 3_600_000); // every hourPrefer a ready-made env-based launcher? Use the Self-Hosted template, or the self-hosting guide.
FNLB is an event emitter. Shard processes push live bot events over local IPC, and the client emits them in the parent process. Event names, party actions, and cosmetics are covered in the SDK guides.
import FNLB, { Events } from 'fnlb';
const fnlb = new FNLB({ clusterName: 'dev' });
fnlb.on(Events.Ready, (bot) => {
console.log(bot.displayName, 'is in party', bot.party.id);
});
fnlb.on(Events.PartyMemberMessage, async (message) => {
if (message.content === '!ping') await message.reply('pong');
});
await fnlb.start({
token: 'your-api-token',
categories: ['your-category-id']
});fnlb.bots.cache holds the bots running in this process. bot.fetch() refreshes one bot from its shard. fnlb.bots.fetchAll() asks every shard.
const bot = fnlb.bots.cache.get('bot-id') ?? (await fnlb.bots.fetch('bot-id'));
await bot.party.meta.get();
await bot.party.meta.set('Default:CustomMatchKey_s', 'my-key');
await bot.party.meta.get('Default:CustomMatchKey_s');
await bot.runCommand('outfit', ['renegade']);
await bot.party.invite('friend-epic-id');
await bot.party.kick('member-epic-id');
await bot.send('hello party');Party meta get() with no key returns the parsed schema. j keys are objects, b keys are booleans, and U keys are numbers. set() writes one key and sends that patch. Member meta is bot.party.me.meta or member.meta.
Other local calls include bot.friends(), bot.pendingFriends(), bot.acceptFriend(id), bot.declineFriend(id), bot.whisper(id, content), party.promote, party.leave, and party.fetchMembers(). fnlb.fetchShardInfo() returns version, gateway state, and bot count for each shard. fnlb.broadcast(method, params) sends one bridge call to every shard.
Privacy, playlists, cosmetics, friend add/remove, and presence use the same local bridge. See Bots and parties and Chat and commands.
These calls need a FNLB build that speaks the SDK bridge. Older downloaded builds throw a clear error. The REST client still works.
fnlb.api uses the same token passed to start() and calls https://api.fnlb.net/v1. Set apiBaseUrl on the constructor to point somewhere else. OAuth tokens still need the scope each route requires. Route details are in the HTTP client guide.
const accountBots = await fnlb.api.bots.list();
const me = await fnlb.api.user.get({ includeEmail: true });
const categories = await fnlb.api.categories.list();
await fnlb.api.bots.commands.run('bot-id', { command: 'outfit', args: ['renegade'] });
await fnlb.api.bots.chat.send('bot-id', { content: 'hello' });
await fnlb.api.bots.friends.list('bot-id');
await fnlb.api.presets.list();bot.runCommand runs on the local shard. fnlb.api.bots.commands.run runs through the cloud API, including for bots this process did not start.
| Option | Description |
|---|---|
clusterName |
Label for this cluster in logs / gateway |
fnlbPath |
Directory for the .fnlb download cache (default: current working directory) |
channel |
Default release channel: stable | beta | dev |
updateIntervalMs |
How often to check for package updates |
maxDownloadRetries / maxBackoffMs |
Download retry behavior |
onLogMessage / onSubProcessLogMessage |
Log callbacks |
disableLogs / disableErrorLogs |
Mute SDK logs |
disableSubProcessLogs / disableSubProcessErrorLogs |
Mute shard stdout/stderr mirroring |
apiBaseUrl |
REST API origin (default https://api.fnlb.net/v1) |
sdkHelloTimeoutMs |
How long to wait for a shard to announce the local bridge |
| Option | Description |
|---|---|
token |
Auth credential (one required) |
categories |
Category ID include list |
bots |
Bot ID include list |
numberOfShards |
Number of subprocesses (default 1) |
botsPerShard |
Max bots per shard (default 1) |
overrideCategoryConfig |
Scoped partial category config for this run. Keys are in the category config reference. |
channel |
Release channel for this run |
logLevel |
'INFO' or 'DEBUG' (LogLevel enum exported) |
hideUsernames / hideEmails |
Redact PII in shard logs |
extraEnv |
Extra env vars passed into each shard process |
