What you need
- A discord.js bot written in TypeScript. Our Node.js Bot type runs a main file ending in
.ts with tsx, so nothing has to be compiled first: How to run a TypeScript Discord bot.
- A PlayNHost bot plan with the Node.js Bot type. Every plan includes one database.
- Node.js and npm on your own computer, to write the migration once.
1. Create the database and the variables
- Open your bot server in the panel, click Databases, then New database and Create database.
- The new database's page opens (later, click it in Your databases). Keep it open, and under Ready-made config pick Generic URL in Config for: it is the
mysql://... connection string the Prisma command line needs.
- Open the Bot page, pane Environment, click Paste a .env file and paste six lines with your values. For
DATABASE_URL, paste the Generic URL exactly as shown: our database passwords can contain characters such as @ and +, which Prisma needs percent-encoded inside a URL, and the Generic URL is already encoded.
DB_HOST=your-host
DB_PORT=3306
DB_NAME=your-database
DB_USER=your-username
DB_PASSWORD=your-password
DATABASE_URL=mysql://your-username:your-password@your-host:3306/your-database
DB_PASSWORD and DATABASE_URL get a Secret badge: they are saved write-only. Click Import 6 variables.
The bot itself connects with the five DB_ values, handed to the adapter one by one as in Prisma's quickstart. DATABASE_URL is only for the prisma commands that run during the install.
2. The project files
package.json
{
"name": "my-discord-bot",
"private": true,
"type": "module",
"scripts": {
"postinstall": "prisma generate && prisma migrate deploy"
},
"dependencies": {
"@prisma/adapter-mariadb": "^7",
"@prisma/client": "^7",
"discord.js": "^14"
},
"devDependencies": {
"prisma": "^7"
}
}
prisma7.config.ts (Prisma 7.10 and later use this name, so it does not clash with Prisma 8's prisma.config.ts)
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env.DATABASE_URL,
},
});
prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "mysql"
}
model Warning {
id Int @id @default(autoincrement())
guildId String @db.VarChar(20)
userId String @db.VarChar(20)
reason String @db.VarChar(200)
createdAt DateTime @default(now())
@@index([guildId, userId])
}
index.ts
import { Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, REST, Routes, SlashCommandBuilder } from "discord.js";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "./generated/prisma/client";
// One client for the whole bot. The values come from the Environment pane.
const adapter = new PrismaMariaDb({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 5,
});
const prisma = new PrismaClient({ adapter });
const commands = [
new SlashCommandBuilder()
.setName("warn")
.setDescription("Warn a member and count their warnings")
.setContexts(InteractionContextType.Guild)
.addUserOption((o) => o.setName("user").setDescription("Who to warn").setRequired(true))
.addStringOption((o) => o.setName("reason").setDescription("Why").setRequired(true).setMaxLength(200))
.toJSON(),
];
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async (c) => {
await new REST().setToken(process.env.DISCORD_TOKEN!).put(Routes.applicationCommands(c.user.id), { body: commands });
console.log(`Logged in as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "warn" || !interaction.guildId) return;
const user = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason", true);
try {
await prisma.warning.create({ data: { guildId: interaction.guildId, userId: user.id, reason } });
const total = await prisma.warning.count({ where: { guildId: interaction.guildId, userId: user.id } });
await interaction.reply(`${user} has ${total} warning(s).`);
} catch (err) {
console.error(err);
if (!interaction.replied) await interaction.reply({ content: "Could not save the warning. Try again later.", flags: MessageFlags.Ephemeral }).catch(console.error);
}
});
client.login(process.env.DISCORD_TOKEN);
setContexts(InteractionContextType.Guild) and the !interaction.guildId check keep /warn out of direct messages. The try/catch matters: without it, discord.js hands a failed query to its error event, and when nothing listens there Node.js stops the bot.
3. Write the first migration on your computer
In the project folder on your own computer (on Windows, create the prisma\migrations\0_init folders by hand instead of mkdir -p):
npm install --ignore-scripts
mkdir -p prisma/migrations/0_init
npx prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
migrate diff compares an empty database with your schema and writes the SQL, without connecting to any database. --ignore-scripts skips the postinstall, which would try to reach a database. The file starts with -- CreateTable and CREATE TABLE `Warning`.
4. Upload and start
- In Files, upload
package.json, prisma7.config.ts, index.ts and the prisma folder with schema.prisma and migrations. Do not upload node_modules or generated: the server builds both.
- On the Bot page, pane Code, set Entry file to
index.ts.
- Press Start and open Console. The install runs your
postinstall: you see Generated Prisma Client (7.10.1), then All migrations have been successfully applied., then [PlayNHost] Starting your bot and Logged in as ....
The console also shows npm warn install-scripts 2 packages had install scripts blocked for prisma and @prisma/engines. The npm version in our Node.js 22 image blocks install scripts of dependencies unless they are allowed. Your own postinstall still runs, and in our test Prisma generated, migrated and queried without those scripts.
Why not prisma migrate dev?
prisma migrate dev first creates a temporary shadow database to check your migrations. Your bot's database user can work inside its own database but cannot create new ones, so on our servers it stops with:
Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases. Read more about the shadow database (and workarounds) at https://pris.ly/d/migrate-shadow
Original error: Error code: P1010
User was denied access on the database `prisma_migrate_shadow_db_...`
prisma migrate deploy only applies migration files and needs no shadow database, which is why the postinstall uses it. You cannot point migrate dev at the bot's database from your own computer either: the database only accepts connections from PlayNHost servers and phpMyAdmin.
Changing the schema later
- On your computer, run
npx prisma migrate dev --name plus a name for the change, against a MySQL or MariaDB of your own, such as one in Docker: set DATABASE_URL in your terminal to that database, because prisma7.config.ts reads it from the environment. It writes a new folder in prisma/migrations.
- Upload the new migration folder and the changed
schema.prisma.
- The server runs
npm install, and with it your postinstall, only when package.json, package-lock.json or the Node.js version changed, when node_modules is missing, or on the first start more than a day after the last install. To apply the migration now, change package.json (for example its version) or delete the node_modules folder in Files, then restart.
Memory: the install is the heavy part
We installed this project from nothing in a container capped at 512 MB, like our smallest bot plan. The install, with prisma generate and prisma migrate deploy, took 41 seconds and peaked at 386 MB of memory (file cache not counted) in one run and 417 MB in a second run, with no out-of-memory kill in either. It fits the 512 MB plan with under 100 MB to spare, so a bigger project or more packages could go over. The same install without Prisma (discord.js and mysql2 only) peaked at 147 MB.
Limits and errors
- 20 connections at once per database user, across every process that uses the same login. Keep
connectionLimit low; 5 is plenty for a bot.
- A query that runs longer than 120 seconds is stopped, with error 1969
Query execution was interrupted (max_statement_time exceeded).
- 500 MB per database: at 500 MB, writes are stopped until you delete rows. Server backups do not include the database. Export it from phpMyAdmin from time to time.
- Connection problems have the same causes as with mysql2 (wrong password, wrong host or port, a variable missing from Environment, connecting from your own computer), but the messages are different: the MariaDB adapter uses the
mariadb driver, not mysql2, and we did not record its messages. The causes are listed in How to use a MySQL database with a discord.js bot.
Plain SQL with mysql2 or aiomysql instead: discord.js (mysql2), discord.py (aiomysql). The overview: How to use a database with your Discord bot. Need a bot server with a database included? See the bot plans.