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.
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
const { SyntxDB } = require("syntx.js");
const db = new SyntxDB({
path: "./data",
});Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | — | Folder where the database stores its files. Created automatically if it doesn't exist. |
cache | boolean | No | true | Keeps recently used values in memory so repeated reads don't hit the disk. |
cacheMax | number | No | 0 | Max entries kept in the cache before the least recently used one is dropped. 0 means unlimited. |
cacheTTL | number | No | 0 | Milliseconds a cached value stays valid before the next read goes back to disk. 0 means it never expires from cache on its own. |
backup | boolean | No | true | Keeps a .bak copy of every file, so a crash mid-write doesn't corrupt your data. |
pretty | boolean | No | false | Pretty-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.
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.
| Scope | Constant | Tied to | Typical use |
|---|---|---|---|
"user" | Scope.USER | one member, inside one specific server | server warnings, local rank, per-server opt-ins |
"guild" | Scope.GUILD | one server, shared by every member | prefix overrides, welcome channel, server settings |
"global" | Scope.GLOBAL | one user, shared across every server | global 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)
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | The variable's name, e.g. "balance" or "prefix". Lowercased and sanitized automatically. |
options.scope | "user" | "guild" | "global" | Yes | — | One of Scope.USER, Scope.GUILD, or Scope.GLOBAL. |
options.default | any storable value | No | null | What get() and reset() return until something is actually set. |
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.
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.
const handle = db.guild(message.guild.id);db.global(userId)
A handle scoped to one user, shared across every server the bot is in.
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.
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.
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.
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.
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.
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.
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.
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 ===.
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.
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.
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.
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.
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.
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.
await db.remove("oldFeatureFlag");db.stats()
Returns how many variables exist and how much disk space the whole database is using.
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.
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.
| Class | Thrown when |
|---|---|
SyntxDBError | Base class for everything below; also thrown directly for failures like an unwritable path or a value that can't be serialized to JSON. |
ScopeError | A variable is accessed with a different scope than the one it was created with. |
CorruptionError | A stored JSON file is unreadable and its .bak backup is missing or also unreadable. |
ValidationError | A name, value, or argument doesn't follow the rules above: a non-finite number, a non-array passed to push(), and so on. |
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:
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
databaseoption and theclient.db/client.databaseshortcuts. - Bringing your own database instead of
SyntxDB? Pass any object exposing your own methods asdatabaseand reach it throughclient.dbthe same way.