aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Bot SQLite Integration with better-sqlite3

A Discord bot SQLite setup is more than enough of a database solution for small and mid-sized bots. Without standing up a separate database server, you can keep all your data in a single file and persist things like leveling systems, economies, warning logs, or settings in seconds. In this guide I'll walk through, step by step, how to add a local database to a Node.js bot using discord.js and better-sqlite3: setting up tables and writing fast, safe queries.

Why SQLite and why better-sqlite3?

SQLite is a serverless, file-based relational database. For a bot, that brings several advantages:

  • Zero setup: you don't need to run a separate service like MySQL or PostgreSQL. The database is a single .sqlite file.
  • Portability: moving the bot to another VPS is just a matter of copying that file.
  • Speed: data is read from disk inside the same process as your app. There's no network latency.

On the Node.js side there are two popular drivers: node:sqlite (the built-in module shipped with Node 22+) and better-sqlite3. I prefer better-sqlite3 here because it has a synchronous API. That may sound backwards, but because SQLite calls complete extremely fast, you get to write direct, readable code without juggling async/await chains. It's a mature, well-tested library that handles prepared statements efficiently.

Installation and project structure

First let's add the dependencies. better-sqlite3 is a native module that compiles on install, so you'll need build tools available on your machine.

npm install discord.js better-sqlite3

A simple, tidy structure might look like this:

my-bot/
  index.js        # the bot's entry point
  db.js           # database connection and schema
  data/
    bot.sqlite    # SQLite file (created automatically)
  .gitignore

Important: always add data/bot.sqlite to your .gitignore. Your database should never end up in version control.

Opening the connection and creating the schema

Opening the connection in one place and exporting it is the cleanest approach, so every file shares the same connection. In db.js:

const Database = require('better-sqlite3');
const path = require('node:path');

const db = new Database(path.join(__dirname, 'data', 'bot.sqlite'));

// Recommended settings for performance and durability
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

// Create the table if it doesn't exist (safe on every startup)
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    user_id   TEXT PRIMARY KEY,
    guild_id  TEXT NOT NULL,
    xp        INTEGER NOT NULL DEFAULT 0,
    level     INTEGER NOT NULL DEFAULT 1,
    coins     INTEGER NOT NULL DEFAULT 0
  );
`);

module.exports = db;

journal_mode = WAL (Write-Ahead Logging) is almost always the right choice for a bot because it handles concurrent reads and writes much better. Thanks to CREATE TABLE IF NOT EXISTS, you can safely prepare the table every time the bot boots.

Reading and writing safely with prepared statements

The correct approach in better-sqlite3 is to use prepared statements. They're both faster and, because you pass values as parameters, they protect you against SQL injection. Never embed user input into a query through string concatenation.

const db = require('./db');

// Prepare queries once and reuse them
const getUser = db.prepare(
  'SELECT * FROM users WHERE user_id = ? AND guild_id = ?'
);

const upsertXp = db.prepare(`
  INSERT INTO users (user_id, guild_id, xp)
  VALUES (@userId, @guildId, @amount)
  ON CONFLICT(user_id) DO UPDATE SET xp = xp + @amount
`);

function addXp(userId, guildId, amount) {
  upsertXp.run({ userId, guildId, amount });
  return getUser.get(userId, guildId);
}

A few important points here:

  • .get() returns a single row, .all() returns every row as an array, and .run() is used for INSERT/UPDATE/DELETE.
  • You can use positional parameters with ? or named parameters with @name.
  • ON CONFLICT ... DO UPDATE (an "upsert") inserts when the record is missing and updates when it exists. Ideal for leveling systems.

Wiring a message event to the database

Now let's connect this to a real event on the discord.js side. Award a little XP whenever a user sends a message:

const { Client, GatewayIntentBits } = require('discord.js');
const { addXp } = require('./xp');

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

client.on('messageCreate', (message) => {
  if (message.author.bot || !message.guild) return;

  const user = addXp(message.author.id, message.guild.id, 5);

  // Simple level-up check
  const needed = user.level * 100;
  if (user.xp >= needed) {
    db.prepare('UPDATE users SET level = level + 1 WHERE user_id = ?')
      .run(message.author.id);
    message.reply(`Congrats, level ${user.level + 1}!`);
  }
});

client.login(process.env.DISCORD_TOKEN);

Note: MessageContent is a privileged intent and you must enable it from the Discord Developer Portal to access message content.

Bulk operations and performance

Performing many writes one at a time hits the disk too often. Wrapping them in a transaction is both much faster and gives you an all-or-nothing guarantee:

const insertMany = db.transaction((rows) => {
  for (const row of rows) upsertXp.run(row);
});

// Hundreds of rows in a single disk operation
insertMany(largeDataList);

A few extra tips: don't open and close the database repeatedly, use a single connection for the entire app's lifetime; add an index with CREATE INDEX on columns you query often; and back up your bot.sqlite file regularly, because all your data lives in that one file.

Frequently Asked Questions

Is SQLite enough for a large bot?

For bots accessed from a single process and serving up to tens of thousands of users, SQLite works comfortably. For massive projects with heavy concurrent writes, or where you need to scale the bot horizontally across multiple processes or servers, moving to a server database like PostgreSQL makes more sense.

better-sqlite3 is synchronous; won't it block the bot?

SQLite queries usually take microseconds, so in practice they don't block the event loop noticeably. Still, batch very large bulk operations inside a transaction, and avoid running thousands of individual queries one by one.

What if the database file gets corrupted?

WAL mode improves durability against sudden shutdowns, but regular backups are still essential. Rather than copying the file while the bot is running, prefer the backup method better-sqlite3 offers, or stop the bot and copy the file.

Need a solid database foundation for your bot? If you want to design a scalable Discord bot for a leveling system, economy, or moderation logs, get in touch with me — let's build the right solution for your project together.

Devamı için