Bichon Docs

Data Directory Layout

{root}/
├── bichon-indices/          Tantivy full-text indices
│   ├── mail_metadata/         Envelope index (email metadata + body text)
│   └── attachment_metadata/   Attachment index (metadata + extracted text)
├── bichon-storage/          bichon-blob Zstd-compressed blob store
├── memdb/                   Metadata database (accounts, users, roles, config)
├── imap-uid/                IMAP stable-UID store (imap-uid.redb) — derived, excluded from backup
├── tmp/                     Scratch space for in-flight operations
├── exports/                 Runtime export artifacts (1-hour TTL, not backed up)
├── backup-staging/          Staging area for online backup snapshots (Pro)
├── logs/                    Server logs (when BICHON_LOG_TO_FILE=true)
├── audit/                   Audit database (Pro) — audit.db + spill file
├── integrity/               Integrity check reports (Pro) — integrity.db
├── timestamp/               Timestamp anchors (Enterprise) — anchor.db
├── instance-id              Deployment identity a license is bound to (Pro)
├── license.txt              Installed license (Pro)
├── license-state.json       Trial/license ledger (Pro)
├── cert.pem / key.pem       TLS certificate material
└── STORAGE_VERSION          On-disk storage format marker

Backup

Bichon ships a built-in backup engine that writes to one of two targets. It is available in all editions and configured on the WebUI Backup page (/backup) or via the REST API:

  • S3-compatible object store — AWS S3, MinIO, Cloudflare R2, and others.
  • Local directory — any directory on a filesystem the host can see, normally a mounted volume: a second internal disk, a USB disk, or an NFS/SMB share. Use this when the backup should stay on hardware you control.

Both targets share the same engine and the same guarantees:

  • Incremental: the first run takes a baseline; subsequent runs upload only new content
  • Content-addressed, immutable objects: each object is stored by content hash — identical data is never uploaded twice
  • Manifest per restore point: every backup produces a manifest that maps the data root to its objects, so any restore point can be rebuilt independently
  • Retention policies and cron scheduling: old restore points are pruned automatically

Switching targets (or changing the prefix) starts a fresh backup chain: each destination keeps its own restore points, and the page only ever browses the one currently configured. The previous target is left frozen, not migrated and not deleted — its restore points simply stop being pruned until you point at it again.

Every run records the destination it wrote to, the status card shows where backups are currently going, and the history flags runs that belong to a different target — so switching back and forth never leaves you guessing where a given restore point lives.

One directory is deliberately not backed up: imap-uid/imap-uid.redb, the IMAP stable-UID mapping used by the built-in IMAP server (Pro). It is a derived store — the archive itself is the source of truth, and the mapping is rebuilt from the restored index — so including it would only bloat the backup.

Local directory target

The directory must be an absolute path that already exists. Bichon never creates it, and that is deliberate: an unattached mount point normally still exists as an ordinary empty directory on the underlying filesystem, so creating it would silently fill that filesystem instead of the volume you meant. Requiring it to exist turns "the USB disk wasn't plugged in" into a failed backup rather than a full root partition.

The subtler version of the same failure — the directory is there, but its volume is not — is caught on Unix by a volume fingerprint: the target volume is identified when the configuration is saved and re-checked before every run, and a path that now resolves to a different volume is refused with an explanation. Windows has no equivalent fingerprint (the check is skipped there), so verify the mount manually. If you configure the path before mounting the volume, re-save the configuration once the volume is attached.

The backup tree the engine writes to is <local_dir>/<prefix> — nothing else under local_dir is ever touched. That tree must be disjoint from the data root, in both directions: a backup stored inside the data root would die with the data it protects, and a data root sitting inside the backup tree would collide with it. A data root that merely sits beside the backup tree is fine, so local_dir: /srv/archive with the data root at /srv/archive/live works — the backup lands in /srv/archive/bichon-backup, a sibling of the data.

Two more properties worth knowing:

  • Writes are flushed before success is reported. fsync plus an fsync of the parent directory (on Unix) runs on every object, so a backup reported as successful also survives a power cut. This is on unconditionally — the durability of a backup target is not something to leave to a checkbox.
  • Room is checked first. A run whose expected size exceeds the free space on the target is refused before it writes anything. The estimate is the previous run's actual upload, so after a large initial import it errs on the safe side; the message says so.

A local target on the same disk as the data is not a backup. It will not survive that disk failing. Point it at a separate volume — and remember that network filesystems offer weaker durability guarantees than a directly attached disk, however carefully the writes are flushed.

Restoring works exactly as it does for S3: bichon-admin restore reads a small JSON config file naming the directory, and rebuilds the archive into an empty one. See CLI Tools for the config format and the restore-point layout on disk.

API

MethodPathDescription
GET/POST/api/v1/backup/configConfigure the target, schedule, retention
POST/api/v1/backup/runStart a backup
GET/api/v1/backup/statusCurrent run status, including the destination
GET/api/v1/backup/recordsBackup history
GET/api/v1/backup/manifestsRestore-point manifests

GET/POST /api/v1/backup/config carries the target and policy:

FieldDefaultMeaning
enabledfalseEnable scheduled backups
schedule0 0 3 * * *Six-field cron in UTC
prefixbichon-backupObject prefix, or subdirectory of local_dir
target_types3s3 or local
s3_endpoint / s3_bucket / s3_region—S3 target (endpoint may be empty for AWS)
s3_access_key / s3_secret_key—Stored encrypted
local_dir—Absolute path of an existing directory (target_type: local)
retention.keep_last7Always keep the newest N restore points
retention.keep_daily7Keep the newest point per day for N days
retention.keep_weekly4Keep the newest point per ISO week for N weeks
retention.keep_monthly6Keep the newest point per month for N months

The Pro edition adds an audit delta chain, integrity & timestamp-anchor snapshots, restore drills, and an audit replayer for disaster recovery — see Backup & Disaster Recovery (Pro).

Encryption

What is encrypted

Stored credentials and secrets are encrypted with AES-256-GCM via ring: IMAP passwords, OAuth2 tokens, S3 access/secret keys, and the SIEM signing secret. The encryption key is derived from BICHON_ENCRYPT_PASSWORD.

Note: Re-encrypting stored secrets after a password change is not yet supported. Keep the encryption password backed up separately — losing it makes the stored credentials unrecoverable. If re-encryption is required for your use case, please open an issue.

What is not encrypted

Archived content and the backup payload are not encrypted by Bichon:

  • message blobs are Zstd-compressed, not encrypted;
  • Tantivy indexes and the memdb metadata store are plaintext on disk;
  • audit.db, integrity.db, and anchor.db are plaintext SQLite files;
  • exported MBOX files are plaintext;
  • backup objects and manifests are uploaded as-is (gzip/Zstd where applicable) — anyone with read access to the bucket or local backup directory can read the mail.

If the threat model includes someone with access to the storage medium or backup target, put the threat model at the storage layer:

  • use S3 server-side encryption (SSE-S3, SSE-KMS, or SSE-C with a key you control) or an equivalent feature on the object store;
  • keep local backup directories on an encrypted volume (LUKS, BitLocker, or the NAS equivalent);
  • enable full-disk encryption on the host for the data root, index directory, and audit databases;
  • keep the backup target and the live data on different volumes/accounts, and restrict bucket policies so backup credentials cannot browse unrelated data.

A backup is a complete copy of the archive; treat it with the same confidentiality controls as the live data root.

Transport

Backup traffic to S3 uses HTTPS. SMTP journaling refuses to run without TLS. The REST API should be served over HTTPS (BICHON_ENABLE_REST_HTTPS) or behind a TLS-terminating reverse proxy — see reverse proxy.

Account Retention (auto-purge)

Each account can carry a retention window (retention_days, a free feature available in all editions). A background sweep runs every 6 hours and permanently purges messages whose effective date falls outside the window. None or 0 — the default — disables retention for that account: everything is kept forever.

Details worth knowing:

  • The effective date is the older of the message's Date header (RFC 5322) and its INTERNALDATE (IMAP) / import timestamp. This is deliberate: it is the timestamp an auditor would trust, and it is robust against the common case of a spuriously rewritten INTERNALDATE (PST/MBOX/EML import, mailbox migration, MTA redelivery). A message is only purged when both timestamps agree it is out of window.
  • Messages with no usable timestamp at all (both unknown) are never purged.
  • Deletion is permanent — retention purging is irreversible; there is no trash or recycle bin.
  • On Enterprise, an account under a legal hold is exempt: the sweep skips it entirely regardless of its retention_days. See Legal Hold.
  • The IMAP download pipeline derives a SINCE floor from the window, so a full re-sync never re-downloads mail the sweep has already purged.

Configure it per account through the dedicated POST /api/v1/account/:id/retention endpoint (root only): send retention_days to set the window, or clear_retention_days: true to disable it. The general account-update endpoint rejects retention fields so a weakening change cannot bypass the policy gate.

Minimum retention (deletion lock)

retention_days is a maximum age: it says when mail may be purged, not what must be kept. For a statutory retention duty, set min_retention_days as well. While it is non-zero:

  • a message whose effective date is inside the window cannot be deleted — not by a single delete, a batch job, a mailbox deletion or an account deletion;
  • a message with no usable timestamp is treated as protected (there is no proof it is old enough);
  • the retention sweep keeps it, even if retention_days is shorter — the sweep uses the longer of the two windows;
  • the WebUI account form shows the field, but only system:root can set or clear it, and only an Enterprise licence may use it;
  • retention is changed through the dedicated POST /api/v1/account/:id/retention endpoint — the general account-update endpoint rejects retention fields;
  • on Enterprise with dual control, a change that weakens protection (shortens either window, or removes/shortens the lock) can require a second signature (retention.update); enabling or lengthening retention applies directly.

The GDPR erasure workflow is the one deliberate exception: its recorded legal assessment is the basis for erasing despite a retention duty.

Each sweep that removes messages emits a retention.purged audit event with the account, count, cutoff and window; on Pro/Enterprise the event also references a detail file listing the purged envelope ids. Those messages get source-UID tombstones, so a later gap-fill does not re-download them.

Hardware Recommendations

  • Recommended: 4+ CPU cores, 2+ GB RAM (sufficient for 10+ accounts and 200+ GB of archived data)
  • Filesystem: use a mainstream Linux filesystem such as ext4 or XFS for all data directories; avoid network / virtual filesystems (NFS, SMB, VirtIO-FS) there — they can cause index corruption and data loss. A backup target, by contrast, may live on a share; see Local directory target for what that does and does not guarantee.
  • Indices benefit from SSD storage; blob storage can use HDD

On this page