Backup & Restore
A complete EduNest LMS backup has three parts: the PostgreSQL database (users, courses, enrollments, settings, license, encrypted integration credentials), the object-storage uploads (course images, branding, documents, gallery — Cloudflare R2 by default), and your .env file (the secrets that make the other two readable). This guide covers the built-in admin backup export, manual pg_dump/pg_restore, Neon's managed options, and restoring everything together.
Table of Contents
- What to Back Up
- In-App Backup Export
- Manual Database Backup (pg_dump)
- Managed Backups on Neon
- Object Storage (R2) Backup
- Restore
- Backup Integrity
What to Back Up
| Component | Where it lives | Why it matters |
|---|---|---|
| Database | PostgreSQL (Neon) | All app data: users, roles, courses, lessons, enrollments, orders, settings, license, encrypted integration credentials |
| Uploads | Object storage bucket (R2 / Spaces / S3 / GCS) | Course/branding images, documents, gallery — referenced by public URL from the DB |
.env |
Your host / project | JWT_SECRET, SECRET_KEY, DATABASE_URL, R2_*, etc. |
SECRET_KEYis part of your backup. Integration credentials in the database are AES-256-GCM encrypted with a key derived fromSECRET_KEY. Restore the database with a differentSECRET_KEYand those credentials become unreadable. Always keep.env(especiallySECRET_KEYandJWT_SECRET) safe alongside your dumps.
In-App Backup Export
The admin panel includes a Backup screen at Settings → Backup (src/app/admin/(panel)/settings/backup/page.tsx), backed by POST /api/v1/admin/backup. It produces a downloadable ZIP on demand. Access requires the settings:read permission (requirePermission("read", "settings")).
What the export contains
The ZIP can include two things, selected in the request body ({ database, buckets }):
| Part | Contents |
|---|---|
database-<timestamp>.sql |
A data-only SQL dump — INSERT statements for every base table in the public schema, wrapped in SET session_replication_role = replica; so FK constraints don't block import order. Schema (tables/indexes) is NOT included — it is recreated by the app's idempotent ensure*() functions. |
files/<key> |
The raw objects from each selected storage bucket/folder, fetched from object storage and added under a files/ folder in the ZIP. |
// src/app/api/v1/admin/backup/route.ts
if (database) zip.file(`database-${stamp}.sql`, await buildSqlDump()); // data-only INSERTs
// for each selected folder: listAllObjects(`${folder}/`) → add bytes to zip "files/"
The available folders are listed by GET /api/v1/admin/backup/buckets, which enumerates the top-level prefixes in your storage bucket (with file counts and total bytes), defaulting to the app's known folders (branding, products, product-categories, gallery, hero-slider, blogs, documents) when storage isn't reachable.
The in-app feature is export/download only — there is no "restore" button. To restore, import the SQL dump and re-upload the files as described under Restore. Because the dump is data-only, you import it into a database whose schema already exists (a fresh
npm run db:init, or an existing install).
Manual Database Backup (pg_dump)
For a full, self-contained backup (schema and data), use pg_dump against your DATABASE_URL. This is the recommended approach for disaster recovery because it does not depend on the app to recreate the schema.
pg_dump "$DATABASE_URL" \
--format=custom \
--no-owner \
--file=edunest-$(date +%Y%m%d-%H%M%S).dump
Use --format=custom for a compressed, selectively-restorable archive. For a plain-text dump you can read or pipe through psql, use --format=plain (or --file=...sql).
Managed Backups on Neon
EduNest runs on Neon Postgres, which offers two managed safety nets in addition to pg_dump:
Branching — a copy-on-write clone of your database at a point in time. Ideal as a pre-upgrade snapshot.
neonctl branches create --name backup-pre-upgrade --parent mainPoint-in-time restore (PITR) — Neon retains history, letting you restore the database to a past timestamp from the Neon console. Use it to recover from accidental deletes or bad imports.
Branching and PITR cover the database only. Object-storage uploads and
.envare still your responsibility.
Object Storage (R2) Backup
Uploads live in object storage, separate from the database, so they must be backed up independently. The storage layer is S3-compatible (src/lib/r2.ts supports Cloudflare R2, DigitalOcean Spaces, AWS S3, and Google Cloud Storage), so the AWS CLI or rclone can mirror the bucket. Object storage providers like R2 are highly durable, but durability is not a substitute for an off-account copy.
# Mirror the bucket to a local/backup destination (R2 example)
aws s3 sync s3://your-bucket ./uploads-backup \
--endpoint-url https://<accountid>.r2.cloudflarestorage.com
The in-app export can also bundle selected folders into the ZIP (see above) — convenient for ad-hoc backups, but the AWS CLI / rclone mirror is better for large buckets and scheduled jobs.
Restore
A restore must bring back all three parts. Database rows reference uploads by public URL, so the database and the bucket must be consistent, and SECRET_KEY must match for encrypted credentials to decrypt.
1. Restore the database
From a pg_dump custom-format archive (schema + data):
pg_restore \
--dbname="$DATABASE_URL" \
--clean --if-exists --no-owner \
edunest-20260626-020000.dump
From the in-app data-only SQL dump (schema must already exist — run npm run db:init on a fresh database first):
# 1) create schema on the empty database
npm run db:init
# 2) load the data-only dump
psql "$DATABASE_URL" < database-2026-06-26T02-00-00.sql
From a Neon branch / PITR: point DATABASE_URL at the restored branch endpoint, verify the data, then promote it or dump-and-load it back into main.
2. Restore the uploads
Sync your backup back into the live bucket, preserving the same keys, so every stored public URL resolves:
aws s3 sync ./uploads-backup s3://your-bucket \
--endpoint-url https://<accountid>.r2.cloudflarestorage.com
3. Confirm .env
Ensure the restored environment uses the same SECRET_KEY as when the data was backed up (otherwise encrypted integration credentials won't decrypt and must be re-entered) and a valid DATABASE_URL / storage configuration.
Restoring the database without the matching uploads leaves broken media links, and vice-versa. Always restore the DB, the bucket, and the matching
.envtogether, and take a fresh backup of the live database before any destructive restore.
Backup Integrity
- Use
pg_dump --format=customfor compressed, selectively-restorable archives. - Store dumps off-site (a different region/provider from the live data).
- Periodically test-restore into a throwaway Neon branch or database to confirm a dump is valid.
- Treat dumps and connection strings as secrets — they contain bcrypt password hashes and encrypted credentials.
- Keep a copy of
.env(at leastSECRET_KEYandJWT_SECRET) with your backups, stored securely.
© CreativeCape Solutions · creative-cape.com · support@creative-cape.com