Skip to content

Backup & Retention ​

Postmill keeps all application state in PostgreSQL and uploaded media on local disk or object storage. Schema changes are applied through committed Prisma migrations (pnpm run prisma-migrate-deploy, or prisma migrate deploy inside the container — the container does not migrate on boot; see Upgrading). Backups are still essential: rollback is forward-only, and a failed or destructive migration is only recoverable from a snapshot.

What to back up ​

1. PostgreSQL database ​

The primary data store. Contains users, organizations, posts, integrations, tokens, analytics snapshots, comments, and all configuration.

bash
# From the Docker host
docker exec postmill-postgres pg_dump -U postmill-user postmill-db-local > postmill_$(date +%Y%m%d).sql

# Or with a connection string
pg_dump "$DATABASE_URL" > postmill_$(date +%Y%m%d).sql

Schedule this daily. Keep at least 7 days of backups.

2. Upload directory or object storage ​

All uploaded media (images, videos, audio). If using local storage, back up the volume. Docker Compose prefixes volume names with the project name (the directory holding docker-compose.yaml, postmill-app for a default clone) — confirm yours with docker volume ls and adjust the commands below:

bash
# From the Docker host
docker run --rm -v postmill-app_postmill-uploads:/data -v $(pwd):/backup alpine tar czf /backup/uploads_$(date +%Y%m%d).tar.gz -C /data .

If using cloud object storage (R2, S3, B2, IDrive e2, or another S3-compatible provider), enable versioning and/or cross-region replication on the bucket.

3. JWT_SECRET and ENCRYPTION_KEY ​

These secrets encrypt OAuth tokens, AI provider credentials, storage credentials, and other secrets at rest. If you lose them, every encrypted value in the database becomes unrecoverable. Store them:

  • In a password manager or secrets vault
  • In a .env file with restricted permissions, outside the backup bundle
  • Never in the database backup alone — if you restore to a fresh instance with a different JWT_SECRET, all tokens will fail to decrypt

What not to back up ​

  • Redis — cache only; data is rebuilt on restart. AOF/RDB persistence is useful for avoiding cold-cache latency but is not a backup.
  • node_modules or build artifacts.

Automated data retention ​

Postmill prunes and rolls up data through Inngest scheduled functions. You do not need to run manual cleanup queries.

DataDefault retentionMechanismEnv var
Daily AnalyticsSnapshot rows548 days (~18 months)Rolled into weekly rows by the analytics collection functionANALYTICS_DAILY_RETENTION_DAYS
PostAnalyticsSnapshot rows90 daysPruned by the analytics collection functionANALYTICS_POST_RETENTION_DAYS
Social comments90 daysSoft-deleted by the comments collection functionSOCIAL_COMMENT_RETENTION_DAYS
Email log metadata90 daysPruned by the analytics collection functionEMAIL_LOG_RETENTION_DAYS
Errors rows90 daysPruned by the retention-purge functionERRORS_RETENTION_DAYS
Notifications180 daysHard-deleted by the retention-purge functionNOTIFICATIONS_RETENTION_DAYS
Incomplete multipart uploads7 daysHard-deleted by the retention-purge functionMULTIPART_UPLOAD_RETENTION_DAYS
Mastra traces/scorers30 daysHard-deleted by the retention-purge functionMASTRA_TRACE_RETENTION_DAYS
Soft-deleted posts/files/design versions30 daysHard-purged by the retention-purge functionSOFT_DELETE_RETENTION_DAYS
AI Designer chat sessions90 daysHard-deleted by the retention-purge function (which also soft-deletes the session's intermediate asset/reference files not referenced by a delivered design)AI_DESIGNER_SESSION_RETENTION_DAYS
Blank canvases1 daySoft-deleted by the retention-purge function: never-customized Untitled Design rows whose doc is content-free, with their preview filesBLANK_DESIGN_RETENTION_DAYS
Designer version historyNewest 10 automatic + 20 named snapshots per designOlder snapshots trimmed when a new one is savedDESIGN_VERSIONS_KEEP_AUTO / DESIGN_VERSIONS_KEEP_NAMED
User/Session IP and agent90 daysNulled by the retention-purge functionIP_RETENTION_DAYS

See Inngest & Cron for how the functions operate.

Why backups are critical ​

Postmill's schema is managed with committed Prisma migrations:

  • pnpm run prisma-migrate-deploy applies migrations in order and is forward-only.
  • Adding a nullable or defaulted column is safe.
  • Renaming or dropping a column is destructive and should be done as a contract step in an expand/contract plan.
  • The destructive-diff guard (tools/db/schema-destructive-guard.mjs) rejects DROP TABLE/DROP COLUMN and ADD COLUMN … NOT NULL without a default unless ALLOW_DESTRUCTIVE_SCHEMA=true.

prisma db push is for local prototyping only. The tools/db/postmill-migrate.sh helper runs prisma migrate deploy inside a running container (never db push). Always back up before any manual schema operation or contract deploy.

Restore checklist ​

  1. Stop the application — prevent write traffic during restore.
  2. Restore Postgres:
    bash
    docker exec -i postmill-postgres psql -U postmill-user postmill-db-local < postmill_20260609.sql
  3. Restore uploads:
    bash
    docker run --rm -v postmill-app_postmill-uploads:/data -v $(pwd):/backup alpine tar xzf /backup/uploads_20260609.tar.gz -C /data
  4. Verify JWT_SECRET and ENCRYPTION_KEY match the values from the backup:
    • If you changed JWT_SECRET since the backup, all encrypted tokens will fail to decrypt.
    • Test by logging in and checking that connected channels still work.
  5. Start the application and verify:
    • Users can log in.
    • Channels are connected (no auth errors).
    • Uploaded media is accessible.
    • Inngest functions are registered and scheduled runs appear in the dashboard.
  6. Take a fresh post-restore backup.

Backup automation example ​

bash
#!/usr/bin/env bash
# /etc/cron.daily/postmill-backup
set -euo pipefail
BACKUP_DIR="/var/backups/postmill"
DATE=$(date +%Y%m%d-%H%M)
mkdir -p "$BACKUP_DIR"

docker exec postmill-postgres pg_dump -U postmill-user postmill-db-local > "$BACKUP_DIR/db_$DATE.sql"
docker run --rm -v postmill-app_postmill-uploads:/data -v "$BACKUP_DIR":/backup alpine tar czf "/backup/uploads_$DATE.tar.gz" -C /data .

# Keep 7 days
find "$BACKUP_DIR" -name '*.sql' -mtime +7 -delete
find "$BACKUP_DIR" -name '*.tar.gz' -mtime +7 -delete

Verified against v1.3.0

The AI-native social media management platform — postmill.ai