Only show guides for your app
A /remindme command that pings you later — saved to SQLite so reminders survive a restart, and re-armed automatically when the bot boots back up.
"Remind me in two hours to take the pizza out." A reminder bot does exactly that: /remindme 2h check the oven, and two hours later it pings you. The catch is the same as any timer bot — if the process restarts before the reminder fires, an in-memory timer is gone. This recipe stores reminders in SQLite, so a restart doesn't lose them: the bot re-arms every pending reminder when it boots.
| At a glance | |
|---|---|
| You need | A working bot from Host a discord.js bot |
| Plan | Free or premium — no premium features required |
| Time | About twenty-five minutes |
New here? Build the base bot with Host a discord.js bot first.
This recipe uses better-sqlite3 — a single-file database that installs cleanly on the Node.js application (it ships prebuilt binaries). Open the Packages page in your server menu, install better-sqlite3, and restart.
Here's the complete index.js.
require('dotenv').config();
const Database = require('better-sqlite3');
const { Client, GatewayIntentBits, Events, SlashCommandBuilder } = require('discord.js');
const db = new Database('reminders.db');
db.pragma('journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
text TEXT NOT NULL,
remind_at INTEGER NOT NULL
);
`);
const addReminder = db.prepare('INSERT INTO reminders (user_id, channel_id, text, remind_at) VALUES (?, ?, ?, ?)');
const getReminder = db.prepare('SELECT * FROM reminders WHERE id = ?');
const deleteReminder = db.prepare('DELETE FROM reminders WHERE id = ?');
const allReminders = db.prepare('SELECT * FROM reminders');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
function parseDuration(text) {
const m = /^(\d+)(s|m|h|d)$/.exec(text.trim());
if (!m) return null;
return Number(m[1]) * { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
}
async function fire(id) {
const row = getReminder.get(id);
if (!row) return;
deleteReminder.run(id);
const channel = await client.channels.fetch(row.channel_id).catch(() => null);
if (channel) await channel.send(`⏰ <@${row.user_id}>, you asked me to remind you: ${row.text}`);
}
function schedule(id, remindAt) {
setTimeout(() => fire(id), Math.max(0, remindAt - Date.now()));
}
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set([
new SlashCommandBuilder()
.setName('remindme')
.setDescription('Set a reminder')
.addStringOption((o) => o.setName('when').setDescription('e.g. 10m, 2h, 1d').setRequired(true))
.addStringOption((o) => o.setName('text').setDescription('What to remind you about').setRequired(true))
.toJSON(),
]);
// Restore reminders saved before the last restart.
for (const row of allReminders.all()) schedule(row.id, row.remind_at);
console.log(`Listening as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== 'remindme') return;
const ms = parseDuration(interaction.options.getString('when'));
if (!ms) {
await interaction.reply({ content: 'Time must look like `10m`, `2h`, or `1d`.', ephemeral: true });
return;
}
const remindAt = Date.now() + ms;
const info = addReminder.run(interaction.user.id, interaction.channelId, interaction.options.getString('text'), remindAt);
schedule(info.lastInsertRowid, remindAt);
await interaction.reply({ content: `Okay! I'll remind you <t:${Math.floor(remindAt / 1000)}:R>.`, ephemeral: true });
});
client.login(process.env.DISCORD_TOKEN);
Restart and try /remindme when:30s text:"say hi".
remind_at (a Unix timestamp in milliseconds). AUTOINCREMENT gives each one an id; addReminder.run(...).lastInsertRowid hands that ID straight to the timer.fire reads the row, deletes it (so it never fires twice), and posts the ping in the original channel.ClientReady, the bot reads every row and re-schedules it. A reminder whose time already passed while the bot was down fires immediately, because the timer delay clamps to 0.💡 Tip: Want the reminder in the user's DMs instead of the channel? Replace the channel fetch with
client.users.fetch(row.user_id)and send there — wrap it in acatch, since users can block DMs from server bots.
⚠️ Heads up:
setTimeoutmaxes out at about 24.8 days, so schedulingremindme 60din one shot won't fire. For far-future reminders, run a periodic sweep that fires anything past due instead — a scheduled task every minute does it.
reminders.db lives in /home/container. It survives restarts, which is the point — but a reinstall or switching the application wipes it, like any server file. Keep it out of Git (add it to .gitignore). For data that must outlive a reinstall, use a managed database.
Set a 30-second reminder and wait for the ping. Then the real test: set a two-minute reminder, restart the server, and confirm it still fires. If it does, the SQLite persistence is working.
| Symptom | Fix |
|---|---|
Cannot find module 'better-sqlite3' |
Install it from the Packages page and restart. |
| Reminders lost after a restart | Confirm the restore loop runs on ClientReady, and that reminders.db still exists (a reinstall wipes it). |
| Bot doesn't ping in the channel | It needs permission to send messages there. Reminders in a deleted channel are silently skipped. |
| "Time must look like…" | Use a number plus s, m, h, or d — e.g. 10m, 2h, 1d. |