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), which is the path used by CI and the production boot sequence. 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:

bash
# From the Docker host
docker run --rm -v 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), 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/files30 daysHard-purged by the retention-purge functionSOFT_DELETE_RETENTION_DAYS
AI Designer chat sessions90 daysHard-deleted by the retention-purge functionAI_DESIGNER_SESSION_RETENTION_DAYS
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 wraps prisma db push for manual, in-place sync against a running container and warns you to back up before using --accept-data-loss. 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-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-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.0.0

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