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

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_KEY is part of your backup. Integration credentials in the database are AES-256-GCM encrypted with a key derived from SECRET_KEY. Restore the database with a different SECRET_KEY and those credentials become unreadable. Always keep .env (especially SECRET_KEY and JWT_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.
TS
// 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.

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

    Terminal
    neonctl branches create --name backup-pre-upgrade --parent main
    
  • Point-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 .env are 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.

Terminal
# 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):

Terminal
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):

Terminal
# 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:

Terminal
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 .env together, and take a fresh backup of the live database before any destructive restore.

Backup Integrity

  • Use pg_dump --format=custom for 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 least SECRET_KEY and JWT_SECRET) with your backups, stored securely.

© CreativeCape Solutions · creative-cape.com · support@creative-cape.com