Shiver Framework
High-performance Discord bot framework built on discord.js v14. The same foundation that powers Shiver - built so you can create your own bots with command loading, slash and prefix, middleware, storage, and production-ready defaults.
View on GitHub · Source and README are kept in sync with this page.
Overview
Shiver Framework provides the infrastructure for Discord bots: command loading and dispatch, slash and prefix handling, middleware pipelines, storage, settings, health endpoints, and safe response handling.
It does not ship ready-made commands; you implement your bot logic on top of the framework.
- Minimal abstraction - You work with discord.js types and thin wrappers.
- Explicit over implicit - Commands are plain modules with a well-defined shape.
- Performance-first - Event deduplication, optional Redis coordination, efficient caching.
- Pluggable storage - JSON, Supabase, SQLite, or other backends.
Supported runtimes: Node.js 18+. Primary dependency: discord.js ^14.
Why Shiver Framework
Compared to other Discord frameworks, Shiver keeps a thin layer over discord.js so you keep full control. One file per command with name, data, executeSlash, executePrefix - no class boilerplate.
Slash and prefix are first-class with the same middleware and preconditions. Built-in event and prefix deduplication plus optional Redis lock prevent duplicate responses when multiple processes run.
Multi-instance detection warns you with a suggested pkill so you can stop all and restart once. Central safeRespond and defer handling avoid "interaction already replied" and timeout errors. Pluggable storage (JSON, Supabase, etc.) and a plugin system, event bus, and DI container round out extensibility.
| Aspect | Shiver Framework |
|---|---|
| Abstraction | Thin layer; you keep full control of interactions and messages. |
| Command format | Single file; name, data, executeSlash, executePrefix. No class boilerplate. |
| Deduplication | Built-in; optional Redis lock so only one process handles each message. |
| Response safety | Central safeRespond, defer strategies, safe edit/delete. |
| Storage | Pluggable adapter; settings and migrations on top. |
Download & install
The framework is open source and available on GitHub.
Use the link below and follow the commands for your operating system.
Repository
https://github.com/yuwxd/shiver-framework
Clone or download the repository, then install dependencies and use it in your bot project as shown below.
Linux / macOS (terminal)
$ cd shiver-framework
$ npm install
To use the framework in your bot project (from your bot folder):
Windows (PowerShell)
PS> cd shiver-framework
PS> npm install
From your bot project folder:
Windows (Command Prompt)
C:\> cd shiver-framework
C:\> npm install
Requirements
| Requirement | Version |
|---|---|
| Node.js | ≥ 18 |
| discord.js | ^14.25.1 |
Optional (only if you use the feature): redis (multi-instance, prefix dedup), canvas (images), @discordjs/voice + prism-media (voice), @supabase/supabase-js (Supabase storage), better-sqlite3 (SQLite).
Quick start
Create a client, configure the framework, call init(client), then client.login(process.env.DISCORD_TOKEN).
Commands are loaded from commandsPath; slash and prefix listeners are registered automatically.
const { ShiverFramework, ShiverClient } = require('shiver-framework');
const { GatewayIntentBits } = require('discord.js');
const framework = new ShiverFramework({
commandsPath: './src/commands',
prefix: ',',
ownerIds: ['YOUR_DISCORD_USER_ID']
});
const client = framework.createClient({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
async function main() {
await framework.init(client);
await client.login(process.env.DISCORD_TOKEN);
}
main().catch(console.error);
Creating your first command (step by step)
Step 1: Create the commands directory
Ensure your project has a folder for command files, e.g. src/commands. The framework loads all .js files from commandsPath (recursive; files whose name starts with _ are ignored).
Step 2: Create a command file
Create a new file src/commands/ping.js. Each command file must export a single object with at least name, data (for slash), and one or more of executeSlash, executePrefix.
Step 3: Define slash command data
Use SlashCommandBuilder from discord.js. Set name and description (required by Discord for slash commands).
Step 4: Implement executeSlash
Slash commands are deferred by default, so you must use interaction.editReply(...) to send the response. Do not call interaction.reply() unless you did not defer.
Step 5: Implement executePrefix (optional)
For prefix commands, use message.reply(...). The framework passes args (array of arguments after the command name) and commandName (the alias used).
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
name: 'ping',
aliases: ['p'],
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Check bot latency'),
async executeSlash(interaction, client) {
await interaction.editReply({ content: `Pong - ${client.ws.ping}ms` });
},
async executePrefix(message, args, client, commandName) {
await message.reply(`Pong - ${client.ws.ping}ms`);
}
};
Save the file. When you start the bot, the framework loads it automatically.
Sync slash commands to Discord (on first run or when you change slash definitions) with npx shiver-framework sync or by enabling slash sync in options.
Core concepts
After framework.init(client) you have:
| Property | Description |
|---|---|
framework.commands | Command registry: load, get slash/prefix, sync to Discord. |
framework.container | DI container (e.g. container.set('logger', logger)). |
framework.events | Event bus: CommandRun, CommandError, CommandBlocked, afterReady, etc. |
framework.stats | Counters and metrics. |
framework.health | Lifecycle state and optional HTTP health server. |
framework.reload | Reload commands from disk without restart. |
framework.modal | Helper for modals and parsing submissions. |
framework.assets | Load fonts and images from base dir. |
framework.settings | Guild/user settings backed by storage. |
framework.storage | Pluggable key-value storage adapter. |
framework.ping | Gateway and REST latency helper. |
framework.httpPush | Send JSON to external APIs or your website. |
Client and init flow
ShiverClient extends discord.js Client with sensible defaults: cache limits and sweepers, REST timeouts and retries, gateway compression. Use framework.createClient(overrides) or framework.getClientOptions(overrides).
Init flow: (1) init(client) binds the client and loads commands from commandsPath; (2) Registers interactionCreate and messageCreate listeners (deduplication prevents double-handling); (3) Optional multi-instance detector starts if enabled; (4) On ready, slash sync (if configured), afterReady, and health.markReady() run.
Command system
framework.commands is the command registry.
loadFromDirectory(dirPath)- Load all .js commands from a directory. Returns{ loaded, errors }.getSlash(name)- Get command by slash name.getPrefix(name)- Get command by prefix name or alias.getAllSlash()/getAllPrefix()- List all registered commands.syncToDiscord(client, options)- Register slash commands with Discord. Useoptions.guildIdsfor guild-specific sync ornullfor global.getSourcePath(name),removeBySourcePath(path)- For reload.
Slash and prefix flow
Slash: Normalize options → middleware chain (defer, lockdown, blacklist, TOS, premium, rate limit, cooldown, disabled, permissions, preconditions) → executeSlash. Emit CommandRun or CommandError.
Prefix: Skip bot messages; deduplicate by message.id (optional Redis tryAcquirePrefixMessage); resolve prefix via getPrefix(message, framework); parse args (quoted strings supported); same middleware chain → executePrefix.
Context menu and autocomplete
Use ContextMenuCommandBuilder and executeContextMenu for user/message context menus. For autocomplete, set options with setAutocomplete(true) and implement handleAutocomplete; call interaction.respond(choices).
Component handlers
Commands can expose handleButton, handleSelect, handleModalSubmit, etc. InteractionHandler matches customId prefixes to commands. Use framework.buildCustomId(prefix, command, action, userId) for consistent IDs.
Command file shape (reference)
| Field | Required | Description |
|---|---|---|
name | Yes (prefix) | Primary name for prefix dispatch. |
aliases | No | Array of prefix aliases, e.g. ['p']. |
data | Yes (slash) | SlashCommandBuilder or ContextMenuCommandBuilder. |
executeSlash(interaction, client) | No | Called for slash commands. Use editReply if deferred. |
executePrefix(message, args, client, commandName) | No | Called for prefix commands. |
executeContextMenu(interaction, client) | No | For user/message context menu commands. |
handleAutocomplete(interaction, client) | No | Call interaction.respond(choices). |
handleButton, handleSelect, handleModalSubmit | No | Component handlers; match by customId prefix. |
preconditions | No | Array of precondition names or configs. |
cooldown | No | Number or object for cooldown middleware. |
adminOnly | No | If true, command is not registered for prefix. |
deferStrategy, ephemeral | No | Override framework defaults for this command. |
Subcommands: use data.addSubcommand(sub => sub.setName('view').setDescription('…')). In executeSlash, use interaction.options.getSubcommand() and getSubcommandGroup(false) to branch.
Configuration reference
Options are deep-merged with defaults.
Guards and moderation
moderation.checkRoleHierarchy, checkTOS, buildTosReply, isBlacklisted, checkServerBlacklisted, isUserAllowed.
Storage and settings
storage.backend (json | memory | supabase | sqlite | mongodb), storage.path, settings.defaults.guild, settings.defaults.user.
migrationsPath - Directory with migration files (e.g. 001_add_users.js). When set, framework.migrations.run() runs them to evolve storage schema or data safely. If null or path missing, no migrations run.
Health and lifecycle
health.enabled, health.port, health.host, multiInstance (boolean or { groupId?, processMatchPattern? }).
Slash sync and hooks
slashSync.guildIds (null = global), registration.retryOnRateLimit, registration.maxRetries. Hooks: afterReady, afterSlashSync, onCommandRun, onCommandError, onCommandBlocked.
tryAcquirePrefixMessage - Optional async function (messageId) => boolean. Called before running a prefix command; return true to allow execution, false to skip. Use e.g. Redis SET NX so only one process handles each message when multiple bot instances run.
Middleware
Both slash and prefix flows run through a middleware chain. Order (slash): Defer → Lockdown → ServerBlacklist → Blacklist → TOS → Premium → RateLimit → Cooldown → Disabled → Permissions → preconditions → command. Prefix skips Defer.
Add custom middleware for every command:
framework.use(async (context, next) => {
const { interaction, message, command, client } = context;
await next();
});
Custom middleware
Use framework.use(fn) so the middleware runs for every slash and prefix command. Read or mutate context; call await next() to continue or omit it to stop the chain (and optionally send a reply).
Handlers and response safety
safeRespond(interaction, payload, options) - Central helper for replying. Uses followUp if already replied, editReply if deferred, otherwise reply. Use framework.followUp(interaction, payload) in your code.
Defer strategies: always (defer immediately), whenSlow (defer after threshold if no reply yet), never. Default is whenSlow with 1500 ms threshold.
Event deduplication: the framework ensures each interaction/message is handled only once; optional Redis lock prevents duplicate handling across multiple processes.
Storage and settings
framework.storage - Key-value adapter. Methods: get(namespace, key), set(namespace, key, value, ttl?), delete, has, keys, entries, etc. Backends: json, memory, supabase, sqlite.
framework.settings - Guild and user settings. Methods: getGuild(guildId), setGuild, patchGuild, getUser, setUser, patchUser, getGuildPrefix(guildId, fallback?), setGuildPrefix, resetGuildPrefix.
Dynamic prefix example:
const framework = new ShiverFramework({
commandsPath: './src/commands',
prefix: ',',
async getPrefix(message, fw) {
if (!message.guildId) return ',';
return await fw.settings.getGuildPrefix(message.guildId, null) ?? ',';
}
});
Migrations
framework.migrations runs migration scripts from options.migrationsPath against the storage backend and tracks which have run so you can evolve schema or data safely.
Health and custom API routes
framework.health tracks lifecycle and can run an HTTP server when health.enabled is true. Built-in routes: /health, /ready, /live, /metrics, /status.
Custom routes: Call health.addRoute(method, path, handler) before init.
The handler receives (req, res, url) and can return a value to send as JSON.
framework.health.addRoute('GET', '/api/bot', async (req, res, url) => {
return {
name: 'MyBot',
version: '1.0.0',
commands: framework.commands.getAllSlash().length
};
});
Enable the server with health: { enabled: true, port: 8080 } in framework options.
Assets
framework.assets (AssetLoader) uses options.assets.baseDir (default process.cwd()). Use setBaseDir(path), preload(directory, opts), registerAllFonts(), and getPath(name) for fonts and images in commands or image generation.
Components v2, modals, pagination
Builders (from components/v2/builders.js): textDisplay, separator, mediaGallery, container, buildMessageContainerV2, buildEmbedLikeV2, buildConfirmContainerV2, buildPaginatedContainerV2. Send with flags: MessageFlags.IsComponentsV2. framework.modal helps build and show modals; framework.paginate(interaction, pages, opts) and framework.confirm(interaction, question, opts) for pagination and yes/no flows.
Reload and debug
framework.reload clears require.cache for command files and calls loadFromDirectory(commandsPath) so you can reload without restart. When options.debug === true, framework.debugPanel (LiveDebugPanel) can attach to the client and log command runs and errors. Use syncToDiscord again after reload if slash definitions changed.
Stats and health
framework.stats records metrics (commands run, errors, messages, interactions); use recordCommandRun, recordCommandError, etc. framework.health tracks lifecycle (markStarting, markReady, markShuttingDown) and runs the HTTP server when enabled. Built-in routes: /health, /ready, /live, /metrics, /status.
Lifecycle and multi-instance
Register cleanup with framework.onShutdown(fn). On SIGINT/SIGTERM the framework runs shutdown handlers.
When multiInstance is enabled, a Redis heartbeat runs; if more than one process is detected, the framework logs once with a suggested pkill so you can stop all and restart. Uses REDIS_URL or REDIS_URI.
Voice, code execution, monetization
Voice: framework.voice (VoiceManager) for joining channels and playing audio; options: voice.maxBitrateKbps, maxDurationSeconds. Execute: ExecuteRunner for sandboxed code (e.g. Piston or local); options.execute.backend, timeoutMs, maxCodeLength. Monetization: framework.monetization for entitlements and SKUs when monetization.enabled is true.
Moderation, anti-crash, sharding
framework.moderation provides role hierarchy checks and helpers for kick, ban, timeout. framework.antiCrash listens for uncaughtException and unhandledRejection. When options.sharding.scriptPath is set, framework.sharding (ShardManager) can spawn a child process for the sharding launcher.
Plugins
framework.plugins (PluginManager): plugins.register(name, plugin), plugins.load(nameOrPlugin, options). A plugin has init(framework, options). Built-in plugins include slash-sync, backup-restore, feature-flags, error-reporter, stats-server, scheduled-tasks, i18n, webhook-logger, logger.
Events and container
framework.events emits CommandRun, CommandError, CommandBlocked, afterReady, afterSlashSync, commandReloaded, commandsReloaded. Subscribe with framework.events.on('CommandRun', ...). framework.container is a key-value DI container; the framework sets client, assets, etc.; your bot can container.set('logger', logger) and middleware/commands use container.get('logger').
Security
- Token: Load from
process.env.DISCORD_TOKEN. Never commit or log it. The framework uses log redaction (safeError,redactSecrets) so tokens never appear in console output. - User-facing errors: Show only generic messages (e.g. "This command is currently unavailable"). Log details server-side with
safeError. - HTTP push: Use
framework.httpPush(url, payload, opts)when sending data to external URLs; its error logging is redaction-safe.
Message edit and delete
safeEdit(message, payload, opts?) - Edits a message. Returns the edited message or null. Benign Discord errors (Unknown Message, Cannot edit) are not logged. opts.retryOnce: true retries once on 5xx/429.
safeDelete(message, opts?) - Deletes a message if message.deletable. Returns true/false. opts.reason for audit log.
MessageEditDeleteHelper - For high-frequency updates on the same message, use createMessageEditDeleteHelper({ debounceMs: 120 }). Call helper.edit(message, payload); multiple edits within the window are coalesced into one API call. Use helper.delete(message) to cancel pending edit, helper.flush(key) to send immediately.
CLI
Run from a project that depends on the framework (or from the framework directory):
| Command | Description |
|---|---|
npx shiver-framework validate [commandsDir] | Validate command files (default: src/commands). |
npx shiver-framework sync [token] [commandsDir] | Register slash commands with Discord. Token from args or DISCORD_TOKEN. |
npx shiver-framework generate <type> <name> [dir] | Scaffold command, listener, precondition, or system. type: command, listener, precondition, system. |
Testing
CommandTester (testing/CommandTester.js): run commands in isolation with tester.runSlash(commandName, interactionOverrides) or tester.runPrefix(commandName, messageOverrides, args). Mocks (testing/mocks.js): createMockInteraction, createMockMessage, createMockClient, createMockGuild, etc., plus MockStorage and MockEventBus.
Custom IDs and validation
Use framework.buildCustomId(prefix, command, action, userId) for consistent component customIds; framework.parseCustomId(customId) to parse.
Validation helpers: validateFrameworkConfig, validateCommandDefinition, validateCustomId, validatePayload. Helpers: createGenericErrorPayload, createWarningPayload, createSuccessPayload, createCooldownPayload, createPremiumRequiredPayload, etc.
AI rules and development guidelines
Use async/await everywhere; defer early for slow slash commands. Prefer self-explanatory code; minimal code; single response path per flow.
Generic errors only for users; log details server-side with safeError. Ephemeral for sensitive content; consistent customIds; sensible collector timeouts. Respect Discord limits (25 MB per file, 10 files). Finish work fully; verify before finishing (run the bot and fix startup/runtime errors).
Examples
Ping (gateway and REST latency)
await framework.init(client);
const gatewayMs = framework.ping.getGatewayMs();
const restMs = await framework.ping.getRestMs();
const full = await framework.ping.getFullPing();
HTTP push (send JSON to your site or API)
const result = await framework.httpPush('https://your-site.com/api/stats', {
guilds: client.guilds.cache.size,
commands: framework.commands.getAllSlash().length
}, { method: 'POST', timeoutMs: 5000 });
Safe response (followUp)
const payload = { content: 'Done.', ephemeral: true };
await framework.followUp(interaction, payload);
Module index
| Area | Path |
|---|---|
| Entry, init | src/index.js |
| Command registry | src/core/CommandRegistry.js |
| Handlers | src/handlers/SlashHandler.js, PrefixHandler, InteractionHandler, AutocompleteHandler, ContextMenuHandler |
| safeRespond | src/handlers/safeRespond.js |
| safeEdit, safeDelete, MessageEditDeleteHelper | src/utils/Helpers.js, src/utils/MessageEditDeleteHelper.js |
| Middleware | src/middleware/*.js |
| Storage, settings | src/storage/StorageAdapter.js, src/settings/SettingsManager.js |
| Health (addRoute) | src/lifecycle/Health.js |
| Ping, HTTP push | src/utils/PingHelper.js, src/utils/httpPush.js |
| Security, redaction | src/security/redact.js |
| CLI | src/cli/index.js |
Full reference: docs/DOCS.md on GitHub. This page is the web version of the framework docs; for the repo and README, see github.com/yuwxd/shiver-framework.