Database — v1.0.0

The Database class (exported from syntx.js as SyntxDB) is syntx.js's built-in store for bot data: balances, settings, warnings, anything you'd normally reach for a small database for. It lives in its own package, @erxprojects/syntx-db, but syntx.js already lists it as a dependency, so it installs automatically the moment you run npm install syntx.js; there's nothing extra to add to your project.

JavaScript
const { SyntxDB, Scope } = require("syntx.js");

// equivalent, straight from the package syntx.js depends on:
const { Database, Scope } = require("@erxprojects/syntx-db");

Note

Internally the class is just called Database; syntx.js re-exports it as SyntxDB so it doesn't read like "create a new generic database" in your code. Both names point to the exact same class.

Tip

ERXClient's database option isn't limited to SyntxDB; it accepts any object exposing your own methods, so you can plug in Mongoose, Prisma, or a custom class instead. This page covers SyntxDB specifically, since it's the one syntx.js ships with and recommends for most bots. See Client for the option itself.

Data is stored as plain JSON files on disk, one folder per variable, so there's no external service to run or connect to.

Creating a database

JavaScript
const { SyntxDB } = require("syntx.js");

const db = new SyntxDB({
  path: "./data",
});

Options

OptionTypeRequiredDefaultDescription
pathstringYesFolder where the database stores its files. Created automatically if it doesn't exist.
cachebooleanNotrueKeeps recently used values in memory so repeated reads don't hit the disk.
cacheMaxnumberNo0Max entries kept in the cache before the least recently used one is dropped. 0 means unlimited.
cacheTTLnumberNo0Milliseconds a cached value stays valid before the next read goes back to disk. 0 means it never expires from cache on its own.
backupbooleanNotrueKeeps a .bak copy of every file, so a crash mid-write doesn't corrupt your data.
prettybooleanNofalsePretty-prints the JSON files on disk. Easier to read; slightly larger files.

Warning

path is required. The constructor throws a SyntxDBError immediately if it's missing or not a string, before anything touches the filesystem.

Connecting it to your client

Pass the instance as database when creating your ERXClient. It then becomes reachable everywhere as client.database, or its shorter alias, client.db.

JavaScript
const { ERXClient, SyntxDB, Intents } = require("syntx.js");

const db = new SyntxDB({ path: "./data" });

const client = new ERXClient({
  token: process.env.TOKEN,
  intents: Intents.Fast,
  database: db,
});

Note

Inside a slash command's execute, a button/select menu handler, or a modal's run, syntx.js provides the full client instance as an argument. This means client.db is always available, so there's no need to import your entry file from within commands or handlers. Alternatively, you can export db directly from your index.js file and import it wherever needed for a more convenient experience, allowing you to use methods such as db.user() directly.

Scopes

A variable's scope decides who "owns" each value: a value can belong to a member inside one specific server, to a whole server, or to a user across every server the bot is in.

ScopeConstantTied toTypical use
"user"Scope.USERone member, inside one specific serverserver warnings, local rank, per-server opt-ins
"guild"Scope.GUILDone server, shared by every memberprefix overrides, welcome channel, server settings
"global"Scope.GLOBALone user, shared across every serverglobal balance/level, premium status, ban-from-bot flag

Warning

Despite the name, "global" scope is global to the user, not to the whole bot; there's no single value shared by everyone out of the box.

Tip

Need one value the entire bot shares? db.guild(id) and db.global(id) don't check that the id you pass is a real Discord snowflake, so a fixed string like db.guild("config") works perfectly well as a single shared bucket.

Defining a variable

Before reading or writing anything, create the variable once with a scope and an optional default value. Calling create() is what get(), set(), and the rest of the handle methods check against.

db.create(name, options)

FieldTypeRequiredDefaultDescription
namestringYesThe variable's name, e.g. "balance" or "prefix". Lowercased and sanitized automatically.
options.scope"user" | "guild" | "global"YesOne of Scope.USER, Scope.GUILD, or Scope.GLOBAL.
options.defaultany storable valueNonullWhat get() and reset() return until something is actually set.
JavaScript
await db.create("balance", { scope: "user", default: 0 });
await db.create("prefix", { scope: "guild", default: "!" });
await db.create("level", { scope: "global", default: 1 });

Note

Calling create() again with the same name and the same scope is safe; it just overwrites the stored default, which makes it fine to run on every startup. Calling it again with a different scope throws a ScopeError: once a variable exists, its scope is permanent.

Warning

Reading or writing a variable before it's been created throws a SyntxDBError reminding you to call create() first. Run every create() call once, right after building the database and before the bot starts handling commands; client.ready() is a good place for it.

Getting a scope handle

db.user(), db.guild(), and db.global() don't touch the disk themselves; they just return a small handle bound to the right scope and ids. The actual reads and writes happen when you call a method on that handle.

db.user(userId, guildId)

A handle scoped to one member inside one specific server.

JavaScript
const handle = db.user(message.author.id, message.guild.id);

db.guild(guildId)

A handle scoped to one server, shared by every member in it.

JavaScript
const handle = db.guild(message.guild.id);

db.global(userId)

A handle scoped to one user, shared across every server the bot is in.

JavaScript
const handle = db.global(message.author.id);

Tip

Chaining works fine since these methods are synchronous: await db.user(userId, guildId).get("balance").

Handle methods

Every method below is async and resolves once the change is saved to disk (and to the in-memory cache, if enabled). They all throw a ScopeError if the variable was created with a different scope than the handle you're calling them on.

handle.get(name)

Returns the stored value, or the variable's default if nothing has been set yet.

JavaScript
const balance = await db.user(userId, guildId).get("balance");

handle.set(name, value)

Overwrites the stored value. value must be a number, string, boolean, plain object, array, or null; anything else (functions, undefined, circular references) throws a ValidationError.

JavaScript
await db.user(userId, guildId).set("balance", 250);

Note

set() is a plain overwrite: the last call to finish wins. For counters or lists that several commands might touch at once, prefer add(), push(), or pull() below; they read and write atomically, so two calls racing each other can't silently undo one another.

handle.has(name)

Returns true only if a value has actually been stored for this key, as opposed to still sitting at its default.

JavaScript
const hasClaimedDaily = await db.user(userId, guildId).has("lastDaily");

handle.reset(name)

Deletes the stored value and returns the default value it was reset to.

JavaScript
await db.user(userId, guildId).reset("warnings");

handle.delete(name)

Removes the stored record entirely and returns true. Functionally similar to reset(); the difference is reset() gives back the new (default) value, while delete() gives back a boolean.

JavaScript
await db.user(userId, guildId).delete("warnings");

handle.add(name, amount)

Atomically adds amount to a numeric variable and returns the new total. Use a negative number to subtract.

JavaScript
const newBalance = await db.user(userId, guildId).add("balance", -50);

Warning

Throws a ValidationError if the variable's current value isn't a number, or if amount isn't a finite number.

handle.push(name, item)

Atomically appends item to an array variable and returns the updated array.

JavaScript
await db.guild(guildId).push("mutedRoles", roleId);

handle.pull(name, item)

Atomically removes every occurrence of item from an array variable and returns the updated array. Plain objects are matched by deep equality; everything else is matched with ===.

JavaScript
await db.guild(guildId).pull("mutedRoles", roleId);

Warning

Both push() and pull() throw a ValidationError if the variable's current value isn't an array.

handle.setExpiring(name, value, ttlMs)

Stores value like set(), but it automatically reverts to the variable's default once ttlMs milliseconds have passed; no need to clear it yourself.

JavaScript
const ms = require("ms");
await db.user(userId, guildId).setExpiring("cooldown", true, ms("10s"));

Note

Expiring values bypass the in-memory cache entirely, so expiry is always checked straight from disk: get() and has() will never hand back a stale value just because caching is enabled.

Database-wide methods

These read across the whole variable at once, instead of through a single scope handle.

db.exists(name)

Returns whether a variable with this name has been created at all.

JavaScript
if (!(await db.exists("balance"))) {
  await db.create("balance", { scope: "user", default: 0 });
}

db.info(name)

Returns the variable's metadata: its name, scope, default, and createdAt timestamp.

JavaScript
const info = await db.info("balance");
// { name: "balance", scope: "user", default: 0, createdAt: 1716000000000 }

db.getAll(name)

Returns every stored value for a variable, across every user or guild it's been set for; ideal for building a leaderboard.

JavaScript
const balances = await db.getAll("balance");
// { "u_123-g_456": 250, "u_789-g_456": 80 }

Tip

The keys aren't raw Discord IDs; they follow an internal format: u_<user>-g_<guild> for "user" scope, g_<guild> for "guild" scope, and u_<user> for "global" scope. Parse them if you need the original ids, or just sort by value if you only need the numbers.

db.list()

Returns the names of every variable that has been created so far.

JavaScript
const variables = await db.list();
// ["balance", "prefix", "level"]

db.remove(name)

Permanently deletes a variable's definition and every value ever stored under it. There's no undo; back up first if you're not sure.

JavaScript
await db.remove("oldFeatureFlag");

db.stats()

Returns how many variables exist and how much disk space the whole database is using.

JavaScript
const { variables, size } = await db.stats();
// { variables: 3, size: "12.4 KB" }

db.clearCache()

Clears the in-memory cache, forcing the next read of every variable to come straight from disk.

JavaScript
db.clearCache();

Errors

Every problem SyntxDB raises extends SyntxDBError, so a single catch can handle all of them, or you can branch on the specific subclass.

ClassThrown when
SyntxDBErrorBase class for everything below; also thrown directly for failures like an unwritable path or a value that can't be serialized to JSON.
ScopeErrorA variable is accessed with a different scope than the one it was created with.
CorruptionErrorA stored JSON file is unreadable and its .bak backup is missing or also unreadable.
ValidationErrorA name, value, or argument doesn't follow the rules above: a non-finite number, a non-array passed to push(), and so on.
JavaScript
const { ScopeError, ValidationError } = require("@erxprojects/syntx-db");

try {
  await db.guild(guildId).add("balance", 10);
} catch (error) {
  if (error instanceof ScopeError) {
    // "balance" was created with a different scope than "guild"
  } else if (error instanceof ValidationError) {
    // "balance" isn't a number, or the amount wasn't valid
  }
}

Warning

syntx.js's own SyntxError (the one ERXClient and SlashCommand throw) is a completely separate class from SyntxDBError. syntx.js does not re-export SyntxDBError, ScopeError, CorruptionError, or ValidationError; import them directly from @erxprojects/syntx-db if you need to catch them by type.

Full example

A simple per-server, per-member balance system:

JavaScript
const { ERXClient, SyntxDB, Intents } = require("syntx.js");

const db = new SyntxDB({ path: "./data" });

const client = new ERXClient({
  token: process.env.TOKEN,
  intents: Intents.Fast,
  prefix: "!",
  database: db,
});

client.ready(async () => {
  await db.create("balance", { scope: "user", default: 0 });
  console.log(`Logged in as ${client.bot.user.tag}`);
});

client.command({
  name: "balance",
  content: async (message) => {
    const balance = await db.user(message.author.id, message.guild.id).get("balance");
    message.reply(`Your balance is ${balance} coins.`);
  },
});

client.command({
  name: "daily",
  content: async (message) => {
    const newBalance = await db.user(message.author.id, message.guild.id).add("balance", 100);
    message.reply(`Daily claimed. New balance: ${newBalance} coins.`);
  },
});

client.registerCommands();
client.start();

See also

  • Client for the database option and the client.db / client.database shortcuts.
  • Bringing your own database instead of SyntxDB? Pass any object exposing your own methods as database and reach it through client.db the same way.