Adapter Cookbook
End-to-end recipes for plugging djs-commands into a real database.
Each entry walks through wiring a specific backing store to djs-commands' Storage adapter interface, soup-to-nuts. All four first-party adapters implement the same contract, so the only thing that changes between them is the constructor call and the schema/migration step.
const handler = createCommandHandler({
client,
commands: [...],
storage, // ← swap this line
});The framework's three built-in models — guild_prefix, disabled_commands, channel_locks — are read/written automatically by the dispatcher. You don't write any code for them; you just need the table/collection to exist.
Drizzle (Postgres)
The Drizzle adapter is recommended for new projects. It ships a ready-made schema you import directly.
bun add @djs-commands/adapter-drizzle drizzle-orm pg
bun add -d drizzle-kitimport { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool);import type { Config } from "drizzle-kit";
export default {
schema: ["./node_modules/@djs-commands/adapter-drizzle/dist/schema.js"],
out: "./drizzle",
dialect: "postgresql",
dbCredentials: { url: process.env.DATABASE_URL! },
} satisfies Config;Generate and apply migrations:
bunx drizzle-kit generate
bunx drizzle-kit migrateWire into the handler:
import { drizzleStorage } from "@djs-commands/adapter-drizzle";
import { db } from "./db";
createCommandHandler({
client,
commands: [...],
storage: drizzleStorage(db),
});To use your own table objects (renamed columns, extra fields), pass them via tables:
import { drizzleStorage, schema } from "@djs-commands/adapter-drizzle";
import { myGuildPrefixTable } from "./schema";
drizzleStorage(db, {
tables: { guildPrefix: myGuildPrefixTable },
});Prisma
bun add @djs-commands/adapter-prisma @prisma/client
bun add -d prismaThe adapter ships schema fragments as exported template strings. Copy them into your schema.prisma:
import {
GUILD_PREFIX_PRISMA_MODEL,
DISABLED_COMMANDS_PRISMA_MODEL,
CHANNEL_LOCKS_PRISMA_MODEL,
} from "@djs-commands/adapter-prisma/schema";
console.log(GUILD_PREFIX_PRISMA_MODEL);
// model GuildPrefix { ... }Or copy the models manually:
model GuildPrefix {
guildId String @id @map("guild_id")
prefix String
@@map("guild_prefix")
}
model DisabledCommand {
guildId String @map("guild_id")
commandName String @map("command_name")
@@id([guildId, commandName])
@@map("disabled_commands")
}
model ChannelLock {
guildId String @map("guild_id")
commandName String @map("command_name")
channelId String @map("channel_id")
@@id([guildId, commandName, channelId])
@@map("channel_locks")
}Then:
bunx prisma migrate dev --name add-djs-commandsimport { PrismaClient } from "@prisma/client";
import { prismaStorage } from "@djs-commands/adapter-prisma";
const prisma = new PrismaClient();
createCommandHandler({
client,
commands: [...],
storage: prismaStorage(prisma),
});If you renamed the Prisma models in your schema (e.g. you have a different naming convention), pass the delegates explicitly:
prismaStorage(prisma, {
delegates: { guildPrefix: prisma.myGuildPrefix },
});Mongoose (MongoDB)
The Mongoose adapter is the v1 continuity path — your existing v1 connection works as-is.
bun add @djs-commands/adapter-mongoose mongooseimport mongoose from "mongoose";
import { mongooseStorage } from "@djs-commands/adapter-mongoose";
await mongoose.connect(process.env.MONGODB_URI!);
createCommandHandler({
client,
commands: [...],
storage: mongooseStorage(mongoose.connection),
});The adapter creates the three framework models automatically (guildPrefix, disabledCommand, channelLock). To reuse models from your own application — e.g. you already have a Guild collection — pass them via models:
import { createGuildPrefixModel } from "@djs-commands/adapter-mongoose";
mongooseStorage(connection, {
models: {
guildPrefix: createGuildPrefixModel(connection, "MyGuildPrefix"),
},
});Redis (cache adapter, not storage)
Redis isn't a Storage adapter — it's a CacheAdapter for cooldowns. Use it alongside one of the storage adapters above.
bun add @djs-commands/adapter-redis ioredisimport Redis from "ioredis";
import { redisCacheAdapter } from "@djs-commands/adapter-redis";
import { drizzleStorage } from "@djs-commands/adapter-drizzle";
import { db } from "./db";
const redis = new Redis(process.env.REDIS_URL!);
createCommandHandler({
client,
commands: [...],
storage: drizzleStorage(db), // persistent state (prefixes, locks)
cacheAdapter: redisCacheAdapter(redis), // hot state (cooldowns)
});The Redis adapter uses SET key expiresAt PX ttl so Redis handles expiration server-side — you never accumulate dead keys.
To prefix Redis keys (e.g. multi-tenant Redis), pass keyPrefix:
redisCacheAdapter(redis, { keyPrefix: "bot:prod:" });Default prefix is djs-commands:.
In-memory (the default)
If you don't pass a storage, only non-persistent features work — slash commands run, validators run, but guild_prefix, disabled_commands, and channel_locks lookups are skipped.
If you don't pass a cacheAdapter, cooldowns are stored in a process-local Map. That's fine for development and single-process bots.
For production, register both a storage adapter (persistent, durable) and a cache adapter (fast, ephemeral).
Writing your own adapter
Storage is six methods. Implement them, run the conformance suite, ship.
import type { Storage } from "@djs-commands/core";
import { runStorageConformance } from "@djs-commands/core";
export function myStorage(): Storage {
return {
async create(model, data) { /* ... */ },
async findOne(model, where) { /* ... */ },
async findMany(model, opts) { /* ... */ },
async update(model, where, data) { /* ... */ },
async delete(model, where) { /* ... */ },
async count(model, where) { /* ... */ },
};
}
// Validate against the framework's contract:
runStorageConformance("my-adapter", () => myStorage());The conformance suite covers every code path the framework relies on — pass it and your adapter will work for every shipped feature.