Music that
never stops.
Raya is the Lavalink v4 client for Node.js that survives bot restarts, dead nodes and network blips, and still fits in a few lines of code.
npm install raya.jsimport { Raya, Connectors } from 'raya.js';
const raya = new Raya({
nodes: [{ host: 'localhost', password: 'youshallnotpass' }],
connector: new Connectors.DiscordJS(client),
playerDefaults: { autoplay: true },
voiceStatus: true,
});
const player = await raya.join({ guildId, voiceChannelId });
await player.enqueue(await player.search('neon tide'));Play with a Raya player
A simulated player that follows Raya's real queue rules. Each click shows the method you'd call, the request Raya sends, and the events your bot receives.
The queue is empty. With autoplay on, Raya keeps going anyway.
Built for bots that people actually listen to
The hard parts of Lavalink, handled for you: sessions, failover, voice, queues and state across restarts.
Restart-proof players
raya.shutdown() saves every player. The next process resumes the same Lavalink session, and the music never pauses.
Learn moreAutomatic failover
A node dies? Players move to a healthy node at the same position, with the same filters and voice connection.
Learn moreRequest batching
setVolume(), pause() and filters.nightcore() in the same tick become one PATCH, always sent in order.
Learn moreSmart autoplay
YouTube mixes, LavaSrc recommendations, then more from the same artist. Works with Spotify even without Extended quota.
Learn moreVoice channel status
Show "Now playing" on the voice channel with your own template, with no stale status after fast skips.
Learn moreAuto-leave that never guesses
Leave or pause when the channel empties. Raya only acts on complete voice data, so it never leaves too early.
Learn moreAny Discord library
Connectors for discord.js, Eris and Oceanic, or pass a send function and feed raw packets from anything.
Learn moreTyped from end to end
Typed events, a typed requester through declaration merging, and RayaError with stable codes you can switch on.
Learn morePlugins built in
LavaLyrics with live lines, SponsorBlock, LavaSearch, route planner and plugin filters, no extra glue needed.
Learn moreBreak things. The music keeps going.
Each dot is a guild's player. Crash a Lavalink node, cut its network, or restart your bot, and watch what Raya does, with the same events your code gets.
Four calls. One request.
Most clients send a request per method call. Raya collects what you change in the same tick and sends it together, in order, so bots with hundreds of guilds stay light on their Lavalink nodes.
- Same-tick batchingChanges made together go out as one PATCH, still in order.
- Local track decodingdecodeTrack() reads the binary track format without calling Lavalink.
- Search cache and de-dupeRepeated or simultaneous searches reuse one request.
- No per-player pollingPosition is calculated when you read it, with no timer per guild.
player.setVolume(80)player.pause()player.filters.nightcore()player.filters.bassBoost('low')PATCH /v4/sessions/:id/players/:guild
{
"volume": 80,
"paused": true,
"filters": { "timescale": {β¦}, "equalizer": [ β¦ ] }
}Up and running in a few lines
Pick your Discord library. The connector handles raw gateway events, voice updates and startup for you.
const { Client, GatewayIntentBits } = require('discord.js');
const { Raya, Connectors } = require('raya.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
});
const raya = new Raya({
nodes: [{ name: 'main', host: 'localhost', password: 'youshallnotpass' }],
connector: new Connectors.DiscordJS(client),
});
raya.on('trackStart', (player, track) => {
client.channels.cache.get(player.textChannelId)?.send(`Now playing **${track.info.title}**`);
});
client.login(process.env.DISCORD_TOKEN);const Eris = require('eris');
const { Raya, Connectors } = require('raya.js');
const client = new Eris(`Bot ${process.env.DISCORD_TOKEN}`, {
intents: ['guilds', 'guildVoiceStates'],
});
const raya = new Raya({
nodes: [{ name: 'main', host: 'localhost', password: 'youshallnotpass' }],
connector: new Connectors.Eris(client),
});
client.connect();import { Client } from 'oceanic.js';
import { Raya, Connectors } from 'raya.js';
const client = new Client({
auth: `Bot ${process.env.DISCORD_TOKEN}`,
gateway: { intents: ['GUILDS', 'GUILD_VOICE_STATES'] },
});
const raya = new Raya({
nodes: [{ name: 'main', host: 'localhost', password: 'youshallnotpass' }],
connector: new Connectors.Oceanic(client),
});
client.connect();const player = await raya.join({
guildId: interaction.guildId,
voiceChannelId: interaction.member.voice.channelId,
textChannelId: interaction.channelId,
});
const result = await player.search(query, { requester: interaction.user });
const { added } = await player.enqueue(result); // starts playing when idle
await interaction.reply(
result.type === 'playlist'
? `Queued ${added.length} tracks from ${result.playlist.name}`
: `Queued ${added[0].info.title}`,
);Rebuilt, not just renamed
Raya started from LavaFlow's code and fixed what held it back.