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 Installation → Basic bot → Prefix commands → Errors → Caching. 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. GuildLinkExtended is removed. Type 5 is ChannelType.GuildAnnouncement.

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.deconstruct(id); // { timestamp, workerId, sequence }
// processId is a deprecated always-0 alias (discord.js migrants)
  • 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; GuildLinkExtended removed@fluxerjs/types, @fluxerjs/coreType 5 is GuildAnnouncement. Link 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