djs-commandsv2 docs
Concepts

Storage

The pluggable persistence layer behind framework features and your own commands.

@djs-commands/core ships without a database. Anything that needs to remember state across restarts — guild prefixes, disabled-command lists, channel locks, your own bot's state — goes through the Storage adapter you register.

The contract is a generic CRUD interface modeled on Better Auth's pattern: adapters implement six methods once, and the framework consumes them for every persistent feature. Adding a new model never requires changes to the adapter contract.

The Storage interface

interface Storage {
	create<T>(model: string, data: T): Promise<T>;
	findOne<T>(model: string, where: StorageWhere): Promise<T | null>;
	findMany<T>(model: string, opts?: StorageFindOpts): Promise<T[]>;
	update<T>(model: string, where: StorageWhere, data: Partial<T>): Promise<T>;
	delete(model: string, where: StorageWhere): Promise<void>;
	count(model: string, where?: StorageWhere): Promise<number>;
}

Models are identified by string names (snake_case). The framework ships three model names you'll see in the type signatures: guild_prefix, disabled_commands, channel_locks.

Built-in adapters

@djs-commands/core is database-agnostic. First-party adapters live in their own packages so you only depend on what you use.

Walk-throughs for each are in the Adapter Cookbook.

Using storage

Pass an adapter at boot:

import { drizzleStorage } from "@djs-commands/adapter-drizzle";
import { db } from "./db";

createCommandHandler({
	client,
	commands: [...],
	storage: drizzleStorage(db),
});

The framework reads/writes its own models automatically. To use storage from your own commands, the adapter is exposed via the handler — most commands won't need it because they can use the helper functions below.

Framework models and helpers

For each first-party model, @djs-commands/core exports typed read/write helpers. Use these instead of calling storage.findOne(...) directly — they keep the model name and column shape in one place.

Guild prefix

Per-guild override for the legacy command prefix.

import { getGuildPrefix, setGuildPrefix, clearGuildPrefix } from "@djs-commands/core";

const prefix = await getGuildPrefix(storage, guildId); // string | null
await setGuildPrefix(storage, guildId, "?");
await clearGuildPrefix(storage, guildId);

Disabled commands

Per-guild allow-list for commands. Useful for letting moderators turn individual commands off without redeploying.

import { isCommandDisabled, disableCommand, enableCommand } from "@djs-commands/core";

if (await isCommandDisabled(storage, guildId, "music-play")) { /* ... */ }
await disableCommand(storage, guildId, "music-play");
await enableCommand(storage, guildId, "music-play");

The dispatcher consults isCommandDisabled automatically — you don't need to wire it into your validators.

Channel locks

Restrict a command to specific channels in a guild. Empty list = no restriction.

import { getChannelLocks, lockCommandToChannel, unlockCommandFromChannel } from "@djs-commands/core";

const locks = await getChannelLocks(storage, guildId, "music-play"); // string[]
await lockCommandToChannel(storage, guildId, "music-play", channelId);
await unlockCommandFromChannel(storage, guildId, "music-play", channelId);

Using storage for your own state

Storage is generic — you can register your own models with no framework changes. Pick a snake_case model name, define the row shape, and call the adapter methods directly.

const USER_SETTINGS = "user_settings";

interface UserSettings {
	user_id: string;
	timezone: string;
}

await storage.create<UserSettings>(USER_SETTINGS, {
	user_id: "123",
	timezone: "America/New_York",
});

const row = await storage.findOne<UserSettings>(USER_SETTINGS, { user_id: "123" });

You're responsible for migrations: every adapter expects the underlying table/collection to exist. The Drizzle and Prisma adapters publish schema definitions you can extend.

Writing a custom adapter

Implement Storage, then verify it against the framework's conformance suite:

import { runStorageConformance } from "@djs-commands/core";

runStorageConformance("my-adapter", () => myStorageFactory());

The conformance suite is exported as a plain function so you can call it from any test runner. It exercises every CRUD path the framework relies on, including upserts, partial updates, and find-with-options.

Adapter authors should not implement framework-specific logic. The shipped helpers (getGuildPrefix, etc.) call the generic CRUD methods — keep your adapter focused on the transport, and the helpers will compose on top.

On this page