One bot, many servers

The same bot in twenty servers needs twenty different welcome channels, prefixes, and settings. Store per-guild config in SQLite so each server gets its own — and it survives restarts.

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 UPDATE is 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."
  • getGuild falls back to defaults with ?? { ... }, so a server that's never been configured reads sensible defaults instead of undefined. Every command can call getGuild without 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 @name parameters 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.db from 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 through getGuild (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-sqlite3 is synchronous, which avoids most of this; if you see it, you're likely opening several Database connections — 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

Was this guide helpful?