This content originally appeared on DEV Community and was authored by Aqib Shoaib
Written by Aqib Shoaib
As a full-stack developer working on projects that demand both flexibility and control, I found myself needing something Strapi doesn’t offer out of the box: a one-click, on-demand PostgreSQL database backup, directly from the admin panel or a secure API.
Today, I decided to build it myself — and let me tell you, it wasn’t just a coding task. It was an adventure.
Here’s how it went.
What I Set Out to Build
A custom Strapi plugin called db-backup that lets me:
Trigger a backup via API
Dump the PostgreSQL DB using pg_dump
Save .sql files in public/backups
(Eventually) send backups via email, zip them, etc.
And while I was at it, I also experimented with the Strapi Import/Export plugin, planning for production deployments and safe data migrations between environments.
Plugin Setup: The Skeleton
The plugin lives under:
In strapi-server.js, I kept it minimal to ensure loading worked:
module.exports = () => ({
register() {},
bootstrap() {
console.log(‘ Plugin bootstrapped’);
},
});
What Went Wrong (And How I Fixed It)
Route returns 404
Initially, my route looked like this:
{
method: ‘GET’,
path: ‘/backup’,
handler: ‘index.backupDatabase’,
}
But hitting /api/db-backup/backup returned 404. Turns out: plugin routes are not prefixed with /api. The correct URL was:
GET http://localhost:1337/db-backup/backup
Lesson: Plugin routes skip the /api prefix entirely.
“Cannot read properties of undefined (reading ‘backupDatabase’)”
This classic error means Strapi can’t find your handler. In my case, I had:
File named index.js in controllers
Handler string: ‘index.backupDatabase’
The fix: Make sure the file is named index.js, and that you’re exporting properly from it:
module.exports = {
async backupDatabase(ctx) {
ctx.send({ message: ‘Backup works!’ });
}
};
‘pg_dump’ is not recognized as an internal or external command
Windows users, brace yourself.
This error showed up when trying to run:
const command = pg_dump ...
;
exec(command);
Turns out:
pg_dump isn’t globally available on Windows unless PostgreSQL is added to PATH
PGPASSWORD=… inline syntax doesn’t work on Windows
My approach:
Ensure PostgreSQL is installed and added to PATH
Move away from using inline PGPASSWORD
Use native .env vars and rely on PostgreSQL authentication mechanisms (or .pgpass in VPS)
A Note on Neon DB (PostgreSQL as a service)
While testing, I was using Neon.tech — a cloud-hosted Postgres service.
What I discovered:
pg_dump works technically, but:
Neon only allows secure SSL connections, so your VPS must support this
Backup performance may lag for large datasets over network
In production, I’m moving to a VPS-hosted PostgreSQL instance where both the Strapi backend and DB are colocated — making pg_dump blazing fast and predictable.
Import/Export Plugin: The Data Lifeline
Aside from building the custom plugin, I also tinkered with the strapi-plugin-import-export-content to move data between local and production environments.
It was smooth to install and use:
npm install strapi-plugin-import-export-content
I could export entire collections into .csv and re-import them wherever I needed.
Just be careful with deeply relational data — some post-processing might be required.
Final Controller Code That Worked
const { exec } = require(‘child_process’);
const path = require(‘path’);
const fs = require(‘fs’);
module.exports = {
async backupDatabase(ctx) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, ‘-‘);
const fileName = backup-${timestamp}.sql
;
const backupDir = path.join(strapi.dirs.static.public, ‘backups’);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const filePath = path.join(backupDir, fileName);
const command = `pg_dump -U ${process.env.DATABASE_USERNAME} -h ${process.env.DATABASE_HOST} -p ${process.env.DATABASE_PORT || 5432} -F c -b -v -f "${filePath}" ${process.env.DATABASE_NAME}`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error('❌ Backup failed:', error.message);
return ctx.internalServerError('Backup failed');
}
ctx.send({ message: '✅ Backup created', file: `/backups/${fileName}` });
});
} catch (err) {
console.error('💥 Error during backup:', err);
ctx.internalServerError('Backup failed');
}
},
};
What’s Next?
Add UI support in the Admin panel
Zip backup files
Add email notifications
Auto-delete old backups
Secure route with auth policies
Final Words
This wasn’t just plugin development — it was Strapi internals, Linux/Windows command-line quirks, and real-world dev experience all rolled into one. I hit 404s, obscure errors, and environment-related gotchas… but I pushed through, and it works.
If you’re building serious apps on Strapi, I strongly encourage you to write tools that serve your exact workflow. No plugin? No problem. Build it.
If you found this useful or want the full plugin code, feel free to connect or ping me!
– Aqib Shoaib
This content originally appeared on DEV Community and was authored by Aqib Shoaib