One running bot serves every server it's invited to — but those servers want different things. Server A's welcome channel is #lobby; server B's is #gate. One wants a prefix of !, another ?. The answer is per-guild configuration: settings keyed by the guild (server) ID, so every server reads and writes its own. This guide builds that with SQLite, which keeps the config through restarts, and is honest about what breaks if you try to keep it all in memory.
| At a glance | |
|---|---|
| You need | a working bot (see the discord.js guide) on the Node.js application |
| Plan | Free or premium — free restarts more often, which is exactly why persistence matters |
| Time | about twenty-five minutes |
Why in-memory breaks
The tempting first move is a plain object or Map keyed by guild ID:
const settings = new Map(); // guildId -> { welcomeChannel, prefix }
It works, it's fast — and it's wrong the moment the server restarts. A Map lives in memory only, so a restart wipes every server's config back to nothing. On the free plan your session timer stops the server regularly, so "configure once, lose it on the next restart" is a constant, not an edge case. Per-guild settings are exactly the kind of data that has to persist — which means writing it somewhere on disk.
Per-guild config in SQLite
SQLite is the sweet spot here: a real database in a single file, no separate service, and better-sqlite3 installs cleanly on the Node.js application because it ships prebuilt binaries. Add it from the Packages page, then make a table keyed by guild_id:
const Database = require('better-sqlite3');
const db = new Database('bot.db');
db.exec(`
CREATE TABLE IF NOT EXISTS guild_settings (
guild_id TEXT PRIMARY KEY,
welcome_channel TEXT,
prefix TEXT NOT NULL DEFAULT '!'
)
`);
The guild_id is the primary key — one row per server, and looking a server up is instant. Now two prepared statements, one to write and one to read:
const upsert = db.prepare(`
INSERT INTO guild_settings (guild_id, welcome_channel, prefix)
VALUES (@guild_id, @welcome_channel, @prefix)
ON CONFLICT(guild_id) DO UPDATE SET
welcome_channel = excluded.welcome_channel,
prefix = excluded.prefix
`);
const selectStmt = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?');
function setGuild(guildId, welcomeChannel, prefix = '!') {
upsert.run({ guild_id: guildId, welcome_channel: welcomeChannel, prefix });
}
function getGuild(guildId) {
return selectStmt.get(guildId) ?? { guild_id: guildId, welcome_channel: null, prefix: '!' };
}
Two things worth calling out:
ON CONFLICT(guild_id) DO UPDATEis an upsert — insert a new server's row, or update it if it already exists. One statement handles both "first time" and "changing it later."getGuildfalls back to defaults with?? { ... }, so a server that's never been configured reads sensible defaults instead ofundefined. Every command can callgetGuildwithout checking whether the row exists.
Use it in a command
A /setwelcome command lets each server set its own channel — writing only that server's row:
if (interaction.commandName === 'setwelcome') {
const channel = interaction.options.getChannel('channel');
const current = getGuild(interaction.guildId);
setGuild(interaction.guildId, channel.id, current.prefix);
await interaction.reply({ content: `Welcome channel set to ${channel}.`, ephemeral: true });
}
And anywhere you need the setting — say, a member-join handler — you read it back per guild:
const { welcomeChannel } = getGuild(member.guild.id);
if (welcomeChannel) {
const channel = member.guild.channels.cache.get(welcomeChannel);
channel?.send(`Welcome, ${member}! 👋`);
}
Because every read and write goes through interaction.guildId / member.guild.id, server A and server B never touch each other's settings — each has its own row, its own welcome channel, its own prefix.
💡 Tip: Always pass IDs as
?or@nameparameters like this, never by building SQL strings. It's faster (the statement is prepared once) and it's what keeps input safe. The same discipline is in Store data for your bot.
What survives, and what doesn't
bot.db sits in /home/container like any other file. It survives restarts — the whole point — so a free-plan timer stop and restart keeps every server's config. But a reinstall wipes it, and switching the server to a different application (or deploying a template that switches it) counts as a reinstall.
⚠️ Heads up: If your per-guild data absolutely must outlive a reinstall — or you'll ever run the bot from more than one server — put it in a managed database instead of a local file. It runs independently of your server and outlives its lifecycle; the same table and queries apply. See Store data for your bot and Connect your app to a database. Back up
bot.dbfrom the Backups page before risky changes either way.
A word on sharding
You'll read that big bots need sharding. Here's the honest version: Discord only requires it once your bot is in roughly 2,500 servers, at which point discord.js's ShardingManager splits the load across processes. The overwhelming majority of bots never come close, so this is not something to build for on day one. The good news is that keeping per-guild config in a database (not in memory) is exactly the design that makes sharding painless later — shared storage every process can read. Build this way now and future-you is already set up.
Verify it works
Run /setwelcome in two different servers, pointing at two different channels. Then restart the server and check that both settings are still there — that's the whole test. If a member-join message posts to the right channel in each server after a restart, your per-guild storage works.
Troubleshooting
- Settings reset on every restart — you're still storing in a
Map, or writing to the database but reading from memory. Every read must go throughgetGuild(the SQLite query), not an in-memory copy. - One server's change affects another — you're keying by something that isn't unique per server, or a global variable is leaking. Every query must use
interaction.guildId/guild.id. SqliteError: database is locked— two writes collided.better-sqlite3is synchronous, which avoids most of this; if you see it, you're likely opening severalDatabaseconnections — open one and reuse it.- Config gone after a template deploy — that deploy switched the application and reinstalled, wiping
bot.db. Restore a backup, or move the data to a managed database as above.
Next steps
- Store data for your bot
- Schema design for bots
- Evolving your bot's SQLite schema
- The sharding guide lives at discord.js.org