Migrating to 2.0

Breaking changes for the Fluxer.js 1.x → 2.0 rewrite. For 2.x → 3.0, see Upgrading to 3.0.

Already on 2.x and moving to 3.0? Use Upgrading to 3.0 instead.

New to Fluxer? Follow InstallationBasic botPrefix commandsErrorsCaching. Coming from discord.js? See From discord.js.

Migrating to 2.0

2.0 is a full rewrite of the non-voice packages (types, util, collection, rest, ws, builders, core). Expect breaking changes. This guide covers 1.x → 2.0.

Related reading: Events, Errors, Caching, Multi-instance, Voice.

Upgrade checklist

  1. Set @fluxerjs/core (and any other direct @fluxerjs/* deps) to ^2.0.0 in package.json, then pnpm install.
  2. Fix TypeScript errors first. Most breaks show up as missing methods or camelCase options.
  3. Search for removed aliases: sendToChannel, fetchMessage, bulkDeleteMessages, addRole / removeRole on members, UsersManager, snake_case mutation options.
  4. Update event handlers: reactions are one DTO; role delete/update are objects; many payloads are structures or camelCase DTOs.
  5. Wrap guilds.fetch / channels.fetch in try/catch (FluxerError, not null).
  6. Drop Discord interaction / slash-command code. It was never in Fluxer's OpenAPI.
  7. Re-test voice separately. @fluxerjs/voice was not rewritten and is being reworked.
  8. When you are ready for current APIs, continue with Upgrading to 3.0.
json
{
  "dependencies": {
    "@fluxerjs/core": "^2.0.0"
  }
}
bash
pnpm install

Bump builders, rest, ws, and types the same way if they are direct dependencies. Voice is optional: add @fluxerjs/voice only if you need it.

Mental model

Then (1.x habits)Now (2.0)
Raw gateway JSON in many handlersStructures (Message, Role, …) or camelCase DTOs
Snake_case public optionsCamelCase SDK options; snake_case only on the wire
RangeError / ad-hoc throwsFluxerError + ErrorCodes for user-facing validation
Dual aliases and Discord stubsOne path; stubs deleted without deprecation warnings
Unbounded caches by defaultBounded DEFAULT_CACHE_LIMITS (0 / Infinity = unbounded; messages: false = off)
toJSON() at send sitesPass builders MessagePayload / options directly

Voice gateway events stay wire-shaped on purpose so @fluxerjs/voice keeps working. See Voice.

CamelCase options everywhere

Public mutation options are camelCase. Wire JSON stays snake_case inside REST.

javascript
// Before
await guild.edit({ system_channel_id: id, afk_timeout: 300 });
await guild.ban(userId, { delete_message_days: 1 });
await channel.edit({ parent_id: catId, rate_limit_per_user: 5 });
await channel.createInvite({ max_uses: 1, max_age: 3600 });
await member.edit({ accent_color: 0xff0000 });
await webhook.send({ content: 'hi', avatar_url: 'https://example.com/a.png' });

// After
await guild.edit({ systemChannelId: id, afkTimeout: 300 });
await guild.ban(userId, { deleteMessageDays: 1 });
await channel.edit({ parentId: catId, rateLimitPerUser: 5 });
await channel.createInvite({ maxUses: 1, maxAge: 3600 });
await member.edit({ accentColor: 0xff0000 });
await webhook.send({ content: 'hi', avatarUrl: 'https://example.com/a.png' });

Same rule for roles, member search, discovery, sudo, and attachments:

javascript
await guild.createRole({ name: 'Mod', color: 0x99aab5 });
await role.edit({ hoist: true, mentionable: true });
const roles = await guild.setRolePositions([{ id: role.id, position: 2 }]);

await client.preloadMessages(['123']);
await guild.members.search({ roleIds: [roleId], limit: 25 });

APIGuildMemberSearchRequest and APIBulkMessageFetchRequestItem are not re-exported from core. Use the camelCase SDK options.

Messages and builders

Send path

One path: channel.send / client.channels.send / message.reply. Accepts string, options, or builders MessagePayload. Do not call .toJSON() at the call site.

javascript
import { MessagePayload, EmbedBuilder } from '@fluxerjs/core';

await channel.send('hi');
await channel.send({ content: 'hi', embeds: [new EmbedBuilder().setTitle('Hello')] });
await channel.send(new MessagePayload().setContent('hi'));

Empty or invalid payloads throw FluxerError (EMPTY_MESSAGE, etc.), not bare RangeError.

Reply ping

ping: false and defaultReplyPing: false only set allowed_mentions.replied_user: false. They do not set MessageFlags.SuppressNotifications (v1.4.0 tied those incorrectly).

javascript
new Client({ defaultReplyPing: false });
await message.reply('hi', { ping: false });

defaultAllowedMentions applies to every send path (channel.send, client.channels.send, message.send / reply), not only replies. See Allowed mentions.

Embeds

EmbedBuilder.setVideo() / setAudio() are removed. Those fields are response-only (unfurler). Builders emit RESTPostAPIEmbed; reading messages still uses APIEmbed.

EmbedBuilder.clearFields() is removed. Clear with setFields() or setFields([]).

javascript
// Before (never worked on send)
new EmbedBuilder().setTitle('x').setVideo('https://cdn.example/v.mp4');

// After
new EmbedBuilder().setTitle('x').setDescription('…').setImage('https://cdn.example/thumb.png');
embed.setFields();

Embeds accept attachment://filename.png URLs (Fluxer AttachmentURLType), not only http(s):. See Embeds and Embed media.

Bulk delete

bulkDeleteMessages is gone. Use bulkDelete only.

javascript
await channel.bulkDelete(50);           // fetch last 50, then delete (1–100)
await channel.bulkDelete([id1, id2]);   // explicit IDs
// single ID uses DELETE rather than the bulk route

Message edit / delete / bulkDelete update the message cache when caching is enabled.

Preload (bots)

Multi-channel history for bots uses preload, not bulk fetch (user-account only):

javascript
const latest = await client.preloadMessages(['123', '456']);

Presigned attachments

Large files: plan → upload → send with upload filenames (not only multipart files):

javascript
const uploaded = await channel.uploadAttachmentsForSend([
  { id: 0, filename: 'big.bin', data: buf, contentType: 'application/octet-stream' },
]);
await channel.send({ content: 'here', uploadedAttachments: uploaded });

Forward a message:

javascript
await channel.send({
  forward: { channelId: 'c1', messageId: 'm1', attachmentIds: ['a1'] },
});

See File attachments.

Removed aliases and Discord stubs

No deprecation warnings. Deleted means deleted. Short list:

RemovedUse instead
client.fetchMessage(channelId, messageId)channel.messages.fetch(messageId) or client.channels.fetchMessage(channelId, messageId)
client.sendToChannel(channelId, payload)client.channels.send(channelId, payload)
channel.fetchMessage(messageId)channel.messages.fetch(messageId)
channel.bulkDeleteMessages(...)channel.bulkDelete(...)
member.addRole / removeRolemember.roles.add / remove / set
guild.addRoleToMember / removeRoleFromMembermember.roles.add / remove
UsersManagerUserManager (client.users)
Interactions / slash commandsPrefix commands or webhooks
Phantom guild togglesNever existed in OpenAPI
GET /instanceclient.fetchInstance() / Client.fromDiscovery()
javascript
await member.roles.add(roleId);
await member.roles.remove(roleId);
await member.roles.set([roleId1, roleId2]);
member.roles.cache.has(roleId);

See the table above and the Upgrade checklist.

Also removed as Discord-compat stubs: GUILD_INTEGRATIONS_UPDATE, GUILD_SCHEDULED_EVENT_*, and phantom routes (text-channel-flexible-names, detached-banner, disallow-unclaimed-accounts).

Channel types

Before: ChannelType.GuildLink === 5, ChannelType.GuildLinkExtended === 998
After: ChannelType.GuildLink === 998 (value 5 and GuildLinkExtended removed)

Link channels require url:

javascript
await guild.createChannel({
  type: ChannelType.GuildLink,
  name: 'docs',
  url: 'https://example.com',
});

Also: ChannelType.DMPersonalNotes = 999, MessageType.ClientSystem = 99.

Fetch, errors, and managers

client.guilds.fetch(id) throws FluxerError with GUILD_NOT_FOUND. It no longer returns null (same pattern as channels.fetch).

javascript
import { FluxerError, ErrorCodes } from '@fluxerjs/core';

try {
  const guild = await client.guilds.fetch(id);
} catch (err) {
  if (err instanceof FluxerError && err.code === ErrorCodes.GuildNotFound) {
    // missing
  }
}
  • Out-of-range fetch/bulk limits → FluxerError (INVALID_FETCH_LIMIT), not RangeError.
  • guild.members.fetchMe() uses the same @me route as guild.fetchMe().
  • client.users is a UserManager.
  • Position helpers like setRolePositions return Role[], not raw APIRole[].
  • Many audit / vanity / discovery / profile returns are camelCase DTOs, not raw API* on public methods.

See Errors.

Events and cache patching

Gateway updates patch in place when the entity type/id is unchanged. *Update handlers get an old snapshot (clone). If nothing was cached, old is null (never the same ref as new).

GUILD_UPDATE patches the existing guild and preserves members / channels / roles caches.

Reactions

Was six positional args. Now one MessageReactionPayload:

javascript
client.on(Events.MessageReactionAdd, ({ reaction, user, emoji, userId, messageId, channelId }) => {
  // …
});

message.fetchReactionUsers() still returns User[] (reactions v2 /users path). Use fetchReactionUsersPage() for items / has_more / next_after.

Roles

javascript
client.on(Events.GuildRoleCreate, (role) => { /* Role */ });
client.on(Events.GuildRoleUpdate, (oldRole, role) => { /* … */ });
client.on(Events.GuildRoleDelete, (role, guildId, roleId) => {
  // role is cached Role or null
});

Common DTO shapes

EventPayload
MessageDeleteBulk{ ids, channelId, guildId }
InviteDelete{ code, guildId, channelId }
TypingStart{ channelId, guildId, userId, timestamp }
GuildEmojisUpdate / GuildStickersUpdate{ guildId, emojis | stickers }
MessageReactionRemoveAll / RemoveEmoji / AddManycamelCase DTOs
PresenceUpdate / WebhooksUpdate / pins / chunks / counts / auditcamelCase DTOs
GuildRoleUpdate(oldRole, role)
MessageReactionAdd / Remove{ reaction, user, messageId, channelId, emoji, userId }
GuildRoleDelete(role, guildId, roleId)

Events.VoiceStatesSync is fully typed with voice state fields (guild_id, channel_id, user_id, session_id, mute/deaf flags, etc.). Voice dispatch events stay wire Gateway* shapes.

Deep dive: Events.

Cache defaults

Clients default to bounded caches (DEFAULT_CACHE_LIMITS). Opt out per numeric field with 0 or Infinity (unbounded). Disable message caching with messages: false. Note: messages: 0 is unbounded, not off (a warning is emitted if you pass 0).

javascript
new Client({
  cache: {
    guilds: 0,
    users: 0,
    channels: 0,
    members: 0,
    messages: false, // was: messages: 0 meaning "off" in older docs
  },
});

Sweep via client.cache.sweepMessages() / sweepMembers() / sweepUsers() / sweepChannels() / sweepGuilds(), or the legacy aliases client.sweepMessages(), client.sweepMembers(), client.users.sweep(), guild.members.sweep(). See Caching.

Presence

Initial presence: new Client({ presence: { status, activities } }) (wire-shaped gateway data still OK for the constructor).

Runtime updates use camelCase:

javascript
client.user.setPresence({
  status: 'online',
  activities: [{ name: 'help', type: 0 }],
  customStatus: { text: 'v2' },
});

Gateway opcode 3 on all shards. See Presence.

Invites

APIInvite is a discriminated union (guild / group DM). Do not assume guild and channel always exist. Pack invite types (InviteType.EmojiPack / StickerPack) last through 2.2 and are removed in 3.0.

javascript
const invite = await Invite.fetch(client, code);
if (invite.isGuild()) {
  console.log(invite.guildSnapshot?.name, invite.channelSnapshot?.name);
  const guild = await invite.resolveGuild();
} else if (invite.isGroupDM()) {
  console.log(invite.channelSnapshot?.name);
}

See Invites.

Emojis and stickers

Bulk create returns { success, failed }, not a bare array:

javascript
const { success, failed } = await guild.createEmojisBulk([{ name: 'ok', image: buf }]);

Also available: createEmoji / cloneEmoji / createSticker / cloneSticker. See Emojis & Stickers.

Bitfields and snowflakes (@fluxerjs/util)

javascript
// Before
PermissionFlags.ManageEmojisAndStickers;
bitField.valueOf(); // string

// After
PermissionFlags.ManageExpressions;
bitField.valueOf(); // bigint
SnowflakeUtil.parse(id); // { timestamp, workerId, processId, increment }
  • PermissionsBitField.has() treats Administrator as implying all permissions.
  • Snowflake strings reject leading zeros.
  • Instance defaultBit removed → use BitField.DefaultBit.
  • PermissionFlags.ViewChannelMembers added (bit 54).
  • MessageFlagsBitField is re-exported from @fluxerjs/core.
  • emitDeprecationWarning removed (majors delete shims instead of warning).

See Permissions.

Self-hosting and multi-instance

One Client = one Fluxer instance. Each instance needs its own bot token.

javascript
const main = new Client();
const self = await Client.fromDiscovery('https://api.my.instance');
await Promise.all([
  main.login(process.env.FLUXER_BOT_TOKEN),
  self.login(process.env.SELFHOST_BOT_TOKEN),
]);

Prefer ClientOptions.instance or fromDiscovery so avatar/invite URLs match the instance. rest.api alone still works (legacy). If both instance.api and rest.api are set and disagree, construction throws CONFLICTING_INSTANCE_CONFIG.

APIInstance.api_code_version is a number; discovery expects a full endpoint map (WellKnownFluxerResponse).

ClientCluster (beta)

Supervises multiple clients in one process; add / remove / restart runtimes without process restart. Also available as @fluxerjs/core/cluster.

javascript
import { ClientCluster, Events } from '@fluxerjs/core';

const cluster = new ClientCluster({
  configure(rt) {
    rt.client.on(Events.MessageCreate, async (m) => {
      if (m.content === '!ping') await m.reply(`Pong from ${rt.id}`);
    });
  },
});

await cluster.add({ id: 'hosted', token: process.env.FLUXER_BOT_TOKEN });
await cluster.add({
  id: 'self',
  token: process.env.SELFHOST_BOT_TOKEN,
  discovery: process.env.SELFHOST_API,
});
await cluster.restart('self', { token: process.env.SELFHOST_BOT_TOKEN });
await cluster.remove('self');

Full guide: Self-hosting & multi-instance. Example: multi-instance-bot.

Voice

@fluxerjs/voice was not rewritten. It must still build against the new core. Fluxer is also updating how voice works, so treat current voice APIs as temporary. Details: Voice.

Notable new wrappers

Useful 2.0 surface area that did not exist (or was incomplete) before:

Repo tooling

Root tooling now uses Biome (replaces Prettier/ESLint at the monorepo root) and packageManager: pnpm@11.6.0. See biome.json.

Breaking changes reference

Quick index of every known break. Details and code live in the sections above.

ChangePackageNotes
ChannelType.GuildLink is 998; 5 / GuildLinkExtended removed@fluxerjs/types, @fluxerjs/coreLink create requires url
EmbedBuilder.setVideo / setAudio removed@fluxerjs/buildersResponse-only on APIEmbed
EmbedBuilder.clearFields removed@fluxerjs/buildersUse setFields() / setFields([])
Request embed type is RESTPostAPIEmbed@fluxerjs/types, buildersResponse remains APIEmbed
Default cache limits are bounded@fluxerjs/coreDEFAULT_CACHE_LIMITS; 0/Infinity = unbounded; messages: false = off
channel.bulkDelete(n | ids)@fluxerjs/coreNumber fetches last N; bulkDeleteMessages removed
ping: false / defaultReplyPing: false only set replied_user: false@fluxerjs/coreNot SuppressNotifications
client.user.setPresence({ status, activities?, customStatus? })@fluxerjs/coreOpcode 3 on all shards
preloadMessages / search / webhook / discovery options camelCase@fluxerjs/coreWire at REST edge
createEmojisBulk / createStickersBulk return { success, failed }@fluxerjs/coreWas bare array
VoiceStatesSync fully typed@fluxerjs/core, @fluxerjs/typesFull APIVoiceState[]
Reaction events emit MessageReactionPayload@fluxerjs/coreWas 6 positional args
fetchReactionUsers uses reactions v2 /users@fluxerjs/corePrefer list_reaction_users_v2
REST.delete accepts optional JSON body@fluxerjs/restSudo/MFA deletes
PermissionFlags.ManageEmojisAndStickers removed@fluxerjs/utilUse ManageExpressions
BitField.valueOf() returns bigint@fluxerjs/utilWas string
Instance defaultBit removed@fluxerjs/utilUse BitField.DefaultBit
Stricter snowflake string validation@fluxerjs/utilNo leading zeros
PermissionFlags.ViewChannelMembers added@fluxerjs/utilBit 54
APIInvite is guild / group-DM union@fluxerjs/types, @fluxerjs/coreisGuild() / isGroupDM()
ChannelType.DMPersonalNotes = 999@fluxerjs/typesPersonal notes channel
MessageType.ClientSystem = 99@fluxerjs/typesFluxer message type
APIUserPartial.public_flags removed@fluxerjs/typesUse flags only
Interactions / slash commands removed@fluxerjs/types, @fluxerjs/coreNot in OpenAPI
Phantom guild toggle routes removed@fluxerjs/types, @fluxerjs/coreNever existed
GET /instance removed@fluxerjs/typesUse discovery / fetchInstance
Prefer ClientOptions.instance for hosts@fluxerjs/coreConflicting instance.api + rest.api throws
APIInstance.api_code_version is number@fluxerjs/typesFull discovery map required
ClientCluster is beta@fluxerjs/coreOne token per runtime
Client.fetchMessage / sendToChannel / Channel.fetchMessage removed@fluxerjs/coreUse managers
GuildMember.addRole / removeRole removed@fluxerjs/coreUse member.roles.*
Channel.bulkDeleteMessages removed@fluxerjs/coreUse bulkDelete
Guild.addRoleToMember / removeRoleFromMember removed@fluxerjs/coreUse member.roles.*
client.guilds.fetch throws GUILD_NOT_FOUND@fluxerjs/coreNo longer returns null
guild.members.fetchMe() uses @me@fluxerjs/coreSame as guild.fetchMe()
Guild/channel/member/webhook/invite options camelCase@fluxerjs/coreWire still snake_case
UploadFileForSend.contentType (was content_type)@fluxerjs/corePresigned uploadAttachmentsForSend
Role create is name, color, permissions@fluxerjs/corehoist / mentionable on edit; unicodeEmoji read-only
setRolePositions / hoist helpers return Role[]@fluxerjs/coreWas APIRole[]
UserManager (was UsersManager)@fluxerjs/coreAlias removed
Fetch/bulk limit errors are FluxerError@fluxerjs/coreINVALID_FETCH_LIMIT
Empty message options throw FluxerError@fluxerjs/coreWas RangeError
channel.send accepts builders MessagePayload@fluxerjs/corePrefer over toJSON()
defaultAllowedMentions applies to all sends@fluxerjs/coreNot only replies
Message edit/delete/bulkDelete update cache@fluxerjs/coreWhen caching enabled
Member/channel/role updates patch in place@fluxerjs/core*Update emits old snapshot
Most non-voice events emit structures / camelCase DTOs@fluxerjs/coreSee Events guide
GuildRoleUpdate emits (oldRole, role)@fluxerjs/coreWas payload object / bare Role
GuildRoleDelete emits (role, guildId, roleId)@fluxerjs/corerole is cached Role or null
GuildMemberUpdate old is null when uncached@fluxerjs/coreNever equals new
Audit/vanity/discovery returns camelCase@fluxerjs/coreNot raw API* where wrapped
Wire request types not re-exported for search/bulk@fluxerjs/coreUse SDK options
Voice events stay wire-shaped@fluxerjs/coreFor @fluxerjs/voice
emitDeprecationWarning removed@fluxerjs/utilMajors delete shims
DEFAULT_USER_AGENT unified with @fluxerjs/rest@fluxerjs/coreIncludes GitHub URL
MessageFlagsBitField re-exported from core@fluxerjs/coreSame pattern as permissions
Biome + pnpm@11 at repo rootrepoSee biome.json

Still stuck?

Questions?Join the Fluxer community for help with the SDK.Join Fluxer