Build a reminder bot

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.

Add the package

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.

The whole bot

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".

How it works

  • One table. Each reminder is a row: who set it, which channel to post in, the text, and remind_at (a Unix timestamp in milliseconds). AUTOINCREMENT gives each one an id; addReminder.run(...).lastInsertRowid hands that ID straight to the timer.
  • Fire, then delete. When the timer runs out, fire reads the row, deletes it (so it never fires twice), and posts the ping in the original channel.
  • Restarts are survivable. On 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 a catch, since users can block DMs from server bots.

⚠️ Heads up: setTimeout maxes out at about 24.8 days, so scheduling remindme 60d in 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.

Where the data lives

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.

Verify it works

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.

Troubleshooting

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.

Next steps

Was this guide helpful?