Chapter 17a Compliance, Storage & Ops Admin: #/protect/overview

Backup & Recovery (nextxch Protect): evidence export, configuration, data, restore and provider change

nextxch Protect groups everything you need for evidence, backup and recovery in the admin under Backup & Recovery: evidence-grade EML export with an Ed25519-signed manifest, configuration backup with versions, encrypted incremental data backup (directory, SMB share or S3 with Object Lock), restore into the mailbox from the GoBD archive, deleted items or a data backup, bare-metal recovery on first start, and pipeline deployment with restore plus live sync for a provider change. Everything is built into the server, without an add-on product and without an external service.

Operational Tip & Architecture Note

Order for getting started: first set up the backup key under Backup & Recovery > Configuration (a passphrase of at least 12 characters or a recovery key, only as a signed-in server admin) and keep it outside the server. Configuration and data backup use the same key; the server stores only the public part and can write backups but never read them. Licence: configuration backup, verification of evidence exports, webmail self-service, restore on first start and pipeline restore of the configuration are included in every tier; data backup, evidence export, restore jobs, the data part of the pipeline restore and live sync from Business.

1. Quick start: key, target, first backup and verification

Targets and schedules are set up under Backup & Recovery > Schedules & targets (server admins only). nextxch-cli signs in with an API key or an admin login.

Reference / Configuration
# 1. Admin > Backup & Recovery > Configuration: set up the backup key
#    (passphrase of 12+ characters or recovery key, keep it safe outside the server)
# 2. Schedules & targets: add a target (directory, SMB share \\nas\backup\nextxch or S3) and "Test"
# 3. Back up the configuration (every tier) and take it along as a file:
nextxch-cli config backup --label "before migration" -o nextxch-config.zip

# 4. Data backup (Business): full first, then incremental by schedule
nextxch-cli backup run --target nas --full
nextxch-cli backup list --target nas

# 5. Verify, with the passphrase including decryption of the sample
export NEXTXCH_BACKUP_PASSPHRASE='...'
nextxch-cli backup verify --target nas --passphrase-env NEXTXCH_BACKUP_PASSPHRASE --limit 200

2. Evidence-grade export: ZIP, manifest, signature and verification

Messages go into the ZIP byte for byte as .eml. manifest.json lists every file with SHA-256, size, archive hash, integrity state, retention and legal hold; manifest.json.sig is an Ed25519 signature over the exact bytes of the manifest, derived from the server secret and identical on all cluster nodes. GET /api/compliance/exports/public-key returns the public key without sign-in. The decrypt option removes only the encryption at rest (ServerAdmin and ComplianceAuditor only, otherwise 403 DECRYPT_NOT_ALLOWED); it is not a general S/MIME or OpenPGP decryption. The single download GET /api/compliance/messages/{id}/raw returns the hashes as HTTP headers X-Nextxch-Sha256, X-Nextxch-Stored-Sha256, X-Nextxch-Integrity and Repr-Digest. Export from Business, verification in every tier.

Reference / Configuration
nextxch-evidence-20261111-0930-1a2b3c4d.zip
  messages/000001-<archiveId>.eml     original message, byte for byte
  messages/000002-<archiveId>.eml
  SHA256SUMS.txt                      verifiable with sha256sum -c
  manifest.json                       format nextxch-evidence-export/1, scope (incl. caseReference), items[]
  manifest.json.sig                   Ed25519: algorithm, keyId, publicKey, signedSha256, signature
  README.txt

# Start an export (202, status under GET /api/compliance/exports/{id}):
curl -X POST https://mail.company.com/api/compliance/exports -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope":"mailbox","account":"anna@company.com","sinceUtc":"2026-01-01T00:00:00Z","caseReference":"Case 2026-0815"}'

# Verify offline, without access to the server (exit code 0 valid, 1 invalid/tampered, 2 usage error):
curl -s https://mail.company.com/api/compliance/exports/public-key > nextxch-evidence-key.json
nextxch-cli export verify nextxch-evidence-20261111-0930-1a2b3c4d.zip --public-key <publicKey from the file>
unzip -d export nextxch-evidence-20261111-0930-1a2b3c4d.zip && cd export && sha256sum -c SHA256SUMS.txt

3. Configuration backup: content, encryption, diff and partial restore

A package holds the configuration as code (config/*.yaml), state files without secrets (server/*), Sieve rules and the encrypted secrets in secrets.enc (AES-256-GCM, data key wrapped for the backup key; a manual backup can additionally get its own passphrase). Sections: config:<area>, dkim, passwords, tls-certificates, api-keys, sieve, server-files, licence; licence, passwords and api-keys are restored only when named explicitly. Restore is a dry run with preview by default. In every tier; REST under /api/v1/config/backups.

Reference / Configuration
nextxch-cli config list                                   # versions (reason: manual, pre-update, after-change, pre-restore, import)
nextxch-cli config diff 20261101T020000Z-a1b2c3 current             # difference between two versions or to the current state
nextxch-cli config backup --label "before update" --passphrase-env BACKUP_PW -o nextxch-config.zip
nextxch-cli config import -f nextxch-config.zip           # take over a package of another server

# Partial restore: preview first, then apply
nextxch-cli config restore 20261101T020000Z-a1b2c3 --sections dkim,config:domains --domain company.com --dry-run
nextxch-cli config restore 20261101T020000Z-a1b2c3 --sections dkim,config:domains --domain company.com --yes

# REST: POST /api/v1/config/backups/{id}/restore
# {"sections":["dkim","config:domains"],"domain":"company.com","prune":false,"dryRun":true}

4. Data backup: targets, format and catalog

Every run produces a self-contained version: database snapshot per table (provider-neutral, so SQLite to PostgreSQL is possible too), state files, the GoBD archive manifest and all referenced blobs. Blobs are addressed by SHA-256 and transferred only once; an incremental run uploads only new hashes. Everything is stored encrypted in the NXB1 format (AES-256-GCM in chunks, bound to the object name). Catalog and blob index are readable JSON without secrets, the catalog is signed with the Ed25519 key of the evidence export. The complete format is in the repository under docs/protect/backup-format.md. From Business; REST under /api/admin/protect/backups (server admins).

Reference / Configuration
<target>/                                 directory, SMB share or s3://bucket/nextxch-backup/
  nextxch-backup.json                       target marker
  keys/backup-key-<keyId>.json              public key + sealed private key
  keys/dk-<dataKeyId>.json                  data key of one run (ECDH wrapped)
  blobs/ab/cd/<sha256>.nxb                  one mail or file content, immutable
  versions/20261111T023000Z-a1b2c3/
    catalog.json  catalog.json.sig          catalog (written last = version complete) + signature
    blobs.ndjson.gz                         blob index of the version
    db/<table>.ndjson.gz.nxb                database snapshot, one file per table
    files/<path>.nxb                        state files of the data directory
    archive-manifest.json.gz.nxb            retention, legal hold, integrity chain

# Settings via REST (PUT /api/admin/protect/backups/settings):
{"verifyEveryHours":24,"verifySampleSize":50,"alertOnFailure":true,"uploadParallelism":4}

# Back up a domain or single mailboxes:
nextxch-cli backup run --target s3-fsn1 --scope domains:company.com,company.at
nextxch-cli backup run --target s3-fsn1 --scope mailboxes:anna@company.com --no-wait

5. Restore into the mailbox: source, point in time, target

Sources: archive (GoBD archive), recoverable (deleted items) and backup (data backup). Choose the point in time with --point (snapshot id, day:yyyy-MM-dd or latest) or --as-of. Targets: original (original folder), restored (default, folder "Wiederhergestellt yyyy-MM-dd", name via --label) or account (another mailbox of the same tenant, for example a successor mailbox). Duplicates are skipped by SHA-256, Message-ID and UID, the sources stay unchanged. Allowed for TenantAdmin and ServerAdmin, up to 250,000 items per job; jobs from Business, policy and webmail self-service in every tier.

Reference / Configuration
nextxch-cli restore points  --account anna@company.com --source backup
nextxch-cli restore browse  --account anna@company.com --source backup --point latest --folder INBOX --filter "invoice"
nextxch-cli restore preview --account anna@company.com --source archive --as-of 2026-10-31T23:59:59Z --folder "Projects/Plant"
nextxch-cli restore start   --account anna@company.com --source archive --as-of 2026-10-31T23:59:59Z \
  --folder "Projects/Plant" --target restored --label "Plant (restored)"

# Whole mailbox into a successor mailbox:
nextxch-cli restore start --account ben@company.com --source backup --point day:2026-11-01 \
  --target account --target-account successor@company.com --passphrase-env NEXTXCH_BACKUP_PASSPHRASE
nextxch-cli restore jobs --account ben@company.com

# REST: /api/compliance/restore (sources, points, browse, preview, jobs, policy)
# Webmail: GET /api/mail/self-restore, POST /api/mail/self-restore {"ids":[...]}

6. Bare-metal recovery: new installation plus backup gives you the old server

On a fresh installation (MSI, Docker or Linux, at most one tenant, one domain, one account and no admin sign-in yet) /setup/ shows "Restore from backup" below the sign-in: upload the package, passphrase, preview, restore, optionally the data backup, finish and restart. Access only with the one-time setup token that the first start writes to the log and to {DataDirectory}/first-run/setup-token.txt (header X-Nextxch-Setup-Token), or from the server itself with Setup:TrustLoopback=true. The first admin sign-in or Finish closes the wizard for good. The old server's backup key is not taken over and has to be set up again afterwards. The SQLite full-text index is not part of the backup: search finds restored mail after a reindex.

Reference / Configuration
# Headless via CLI (same API /api/setup):
export NEXTXCH_SETUP_TOKEN="$(cat /var/lib/nextxch/first-run/setup-token.txt)"
export NEXTXCH_BACKUP_PASSPHRASE='...'
nextxch-cli setup restore --config nextxch-config.zip --data s3://dr-bucket/nextxch/ --prune --dry-run
nextxch-cli setup restore --config nextxch-config.zip --data s3://dr-bucket/nextxch/ --prune --yes

# Unattended on first start (Docker Compose, Kubernetes, MSI):
environment:
  NEXTXCH_RESTORE_FROM: s3://dr-bucket/nextxch/          # .zip, directory, s3://, https:// or config-backup:<id>
  NEXTXCH_RESTORE_PASSPHRASE: ${BACKUP_PASSPHRASE}       # without: configuration without secrets
  NEXTXCH_RESTORE_DATA_FROM: s3://dr-bucket/nextxch/     # optional, location of the data backup
  NEXTXCH_RESTORE_PRUNE: "true"                          # remove bootstrap objects
  NEXTXCH_RESTORE_WITH_CREDENTIALS: "true"               # passwords, API keys, licence (default true)
  NEXTXCH_RESTORE_S3_ENDPOINT: https://fsn1.your-objectstorage.com
  NEXTXCH_RESTORE_S3_ACCESS_KEY: ${S3_KEY}
  NEXTXCH_RESTORE_S3_SECRET_KEY: ${S3_SECRET}
# Result: {DataDirectory}/pipeline-restore/first-start.json ("ok": true = variable is ignored afterwards)
# Helm: restore.from, restore.scope, restore.mapping, restore.passphrase, restore.s3.* (replicaCount: 1)

7. Pipeline deployment with restore (nextxch-cli deploy --restore)

One command or pipeline step rebuilds a server from a backup: apply the configuration, restore the data, run checks (hash sample, health, test mail only to Mailpit or a local mailbox) and optionally instructions for the DNS/MX change; DNS is never changed automatically. Every run is idempotent and resumes at the last finished step after an abort. The mapping file (kind RestoreMapping, schema under GET /api/v1/deploy/mapping-schema) rewrites host names, IP addresses and paths, decides where certificates come from (backup, acme, files) and describes database and storage of the new host; mail domains are never renamed. Sign-in with an integration key with scope deploy.restore. Configuration in every tier, data part from Business (except on the first start of a fresh installation); the data part covers scope all.

Reference / Configuration
# mapping.yaml
apiVersion: nextxch/v1
kind: RestoreMapping
hostnames:
  mail.old-provider.example: mail.company.com
ips:
  203.0.113.10: 198.51.100.20
paths:
  "C:\\Data\\NextxchData": /var/lib/nextxch
certificates:
  mode: acme                      # backup | acme | files
  acme: {contactEmail: hostmaster@company.com}
database:
  provider: Postgres              # must match the new host (Storage__Provider)
  connectionStringEnv: NEXTXCH_PG_CONNECTION
storage:
  blobBackend: S3
  s3: {endpoint: https://fsn1.your-objectstorage.com, bucket: nextxch-mail}

# Plan (dry run, shows every replacement), then restore with checks:
nextxch-cli deploy --restore ./nextxch-config.zip --scope config --map mapping.yaml --dry-run
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --map mapping.yaml --test-mail mailpit --dns-instructions --yes

# REST: POST /api/v1/deploy/restore (dryRun defaults to true)
# {"source":"s3://dr-bucket/nextxch/","scope":"all","mapping":"...","dryRun":false,"testMail":"mailpit","hashSample":20}
# Local package: POST /api/v1/deploy/restore/upload (application/zip) -> source "upload:<sha256>"
# Runs: GET /api/v1/deploy/restore/runs

8. CI templates: GitHub Actions, GitLab CI, Azure DevOps and Ansible

The templates are in the repository under deploy/ci: restore with a plan stage and approval on the production environment, plus a nightly DR probe that restores into a throwaway environment and writes a report with duration (RTO) and state (RPO). Secrets belong in masked, protected CI variables; test mails go to a Mailpit container next to the job and never leave it.

Reference / Configuration
deploy/ci/github-actions/nextxch-restore.yml        plan on a throwaway instance, container with restore on first start, checks, DNS step
deploy/ci/github-actions/nextxch-dr-probe.yml       nightly DR probe with RTO/RPO report
deploy/ci/gitlab/.gitlab-ci.yml                     restore (RESTORE_SOURCE, RESTORE_SCOPE, PLAN_ONLY ...) + DR probe (NEXTXCH_PIPELINE=dr-probe)
deploy/ci/azure-devops/azure-pipelines.yml          restore with parameters, deployment job on environment production
deploy/ci/azure-devops/azure-pipelines-dr-probe.yml nightly DR probe, report in the run summary
deploy/ci/ansible/playbook-restore.yml              role nextxch_restore: container (Linux) or MSI (Windows)
deploy/ci/mapping.example.yaml, mapping.schema.json example and JSON Schema of the mapping file

9. Live sync for a provider change: a short freeze instead of a long outage

After the full restore the new server follows the old one with delta runs: the old server keeps writing its data backup (for example hourly to S3), and every run takes over only blobs, table rows and state files that changed since the last version taken over. Progress is in {DataDirectory}/pipeline-restore/live-sync.json; running the same command again changes nothing or resumes. For the cutover the old server is frozen by hand (block mail and client ports at its firewall, keep the service running, last backup with nextxch-cli backup run --full), then --cutover --source-frozen takes over that backup with a full row comparison. Afterwards restart the new server and change DNS. From Business; REST: POST /api/v1/deploy/live-sync, POST /api/v1/deploy/live-sync/cutover, GET /api/v1/deploy/live-sync.

Reference / Configuration
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --scope all --yes                          # 1. full restore (baseline)
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --live-sync --dry-run                      # remaining delta, changes nothing
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --live-sync --interval 5m --until-cutover  # 2. follow the old server
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --cutover                                  # 3. instructions for the freeze
# old server: block ports, then  nextxch-cli backup run --full
nextxch-cli deploy --restore s3://dr-bucket/nextxch/ --cutover --source-frozen                  # 4. last backup, last run

Settings, Default Values & Best Practices

Setting / Location Description Default Value Production Best Practice
Konfigurationssicherung: Versionen (#/protect/config) Automatic versions before every update (AutoBeforeUpdate) and after changes (AutoAfterChanges, bundled after DebounceMinutes), plus manual versions, imports and a safety snapshot before every restore. KeepLast 30 · KeepDailyDays 90 · Debounce 10 min Before larger changes create a labelled manual version and keep the .zip outside the server.
Sicherungsschlüssel (Passphrase / Wiederherstellungsschlüssel) ECDH P-256; the private part exists only sealed (PBKDF2-SHA256 with 600,000 iterations, AES-256-GCM) and travels inside every package. Without a key and without a passphrase a configuration backup contains no secrets. nicht eingerichtet · Passphrase ab 12 Zeichen Print the recovery key and keep it apart from the server; without it data backups cannot be read.
Datensicherung: Aufbewahrung je Ziel The newest KeepLast versions plus the newest version of each of the last KeepHourly hours, KeepDaily days, KeepWeekly ISO weeks and KeepMonthly months (UTC). 3 / 24 / 7 / 4 / 12 Keep at least two versions during a live sync, otherwise the delta cannot be computed (BASELINE_GONE).
S3 Object Lock (ObjectLockMode, ObjectLockDays) Objects are written with mode Governance or Compliance and a retain-until date; reused blobs get an extended date (now plus 1.5 times the lock days) so no version points to an object that becomes deletable too early. Normal · 30 Tage For ransomware protection use Compliance mode on a dedicated bucket with versioning; locked objects are removed only by a later run after their date.
Zeitpläne (Frequency, Type, Scope) Hourly, daily or weekly, incremental or full, per target and scope (server, domains, mailboxes); time in server local time, no cron expression. Täglich 02:30 inkr. · Sonntag 03:30 voll The prepared schedules have no target at first: assign a target and enable them, otherwise no backup runs.
Automatische Prüfung (VerifyEveryHours, VerifySampleSize) Checks stored hashes, envelope headers, key files, the catalog signature and readability of catalog and blob index without the key; with the passphrase the sample and all table files are decrypted as well. alle 24 h · 50 Objekte Keep AlertOnFailure on so failed backups and verification runs reach the alert targets.
Parallele Uploads je Sicherung (UploadParallelism) Number of concurrent uploads to S3 and directory targets. 4 (1 bis 32) For S3 over fast links 8 to 16; for a NAS over slow SMB rather 2 to 4.
SQL Server: ALLOW_SNAPSHOT_ISOLATION nextxch tries to switch snapshot isolation on at startup. If it is off, tables are exported one by one with READ COMMITTED (consistency read-committed) and the run reports a warning. an, wenn erlaubt The overview shows the SQL statement to copy; with snapshot isolation the database snapshot is transactionally consistent.
RecoverableItemsDays (restore-policy.json) How long deleted appointments, contacts, tasks and notes keep their content; afterwards only an empty tombstone stays for sync. Soft-deleted mail stays until it is expunged. 14 Tage (nach Upgrade 30) Note it in the procedure documentation: without a data backup deleted groupware items lose their content after this period.
Selbstbedienung im Webmail (SelfServiceEnabled, SelfServiceDays) Users bring back deleted mail and groupware items themselves under Restore deleted items (up to 500 per run). Adjustable server-wide or per domain; 0 days means no window of its own. an · 0 Tage For domains with strict rules set a window or switch the self-service off there.
Beweisexport: Rollen, Umfang, Download ComplianceAuditor, TenantAdmin (own tenant only) and ServerAdmin may export; scope search, mailbox, range or legalHold, up to 100,000 messages (at most 250,000). The download link is valid for 24 hours. includeSoftDeleted true · 24 h Always give a case reference (caseReference): it is in the signed manifest and in the audit log.
Setup:TrustLoopback Allows requests from the server itself (loopback) to the setup wizard without the setup token. By default every request needs the token from first-run/setup-token.txt. false Set it only on hosts without other local users; otherwise pass the token via NEXTXCH_SETUP_TOKEN.
Audit-Log & GoBD Every step is in the audit log: config.backup*, data.backup.*, Compliance.EvidenceExport*, Protect.Restore*, Protect.SelfRestore, Protect.RestorePolicy, setup.* and config.restore; visible under Backup & Recovery > Log. The GoBD archive stays unchanged by every restore. aktiv The data backup holds the archive manifest with retention and legal hold; after a restore every archived hash is checked and the locks are set again.