Backup & Recovery
Database backup, restore, and disaster recovery.
Fortémi Backup Guide
This guide covers backup and restore procedures for Fortémi.
Overview
Fortémi provides multiple backup options:
| Method | Use Case | Format | Includes |
|---|---|---|---|
| JSON Export | App-level backup | JSON | Notes, collections, tags, templates |
| Knowledge Shard | Receipt-scoped data exchange | .shard | Only the components and byte policy of its exact `(schema, profile)` tuple |
| Knowledge Archive | Backup + metadata bundle | .archive | Backup file + metadata.json |
| Database Snapshot | Full database backup | pg_dump | Complete database with embeddings |
| Shell script | Automated scheduled backups | pg_dump | Full database with compression |
Encryption Options
All backup methods support optional PKE encryption:
| Encryption | Format | Use Case |
|---|---|---|
| PKE | .mmpke (MMPKE01) | Multi-recipient wallet-style encryption |
See Encryption Guide for cryptographic details and Shard Exchange Primer for practical sharing workflows.
Choosing a Backup Method
- JSON Export (`/api/v1/backup/export`): Quick export of note content. Best for migration to other systems.
- Knowledge Shard (`/api/v1/backup/knowledge-shard`): Named-profile
interchange. Default `1.2.0/core-v1` exports are reference-only unless `include_blobs=true`; exact `2.0.0/full-v1` requires every declared byte. Treat a shard as portable only for a receipt-backed producer/consumer cell.
- Knowledge Archive (`/api/v1/backup/knowledge-archive`): Bundles any backup with its metadata sidecar. Best for transferring backups between systems.
Shard Versioning
Knowledge shards use semantic versioning (MAJOR.MINOR.PATCH) to ensure compatibility across different Fortémi versions.
Supported versions
- Default export schema: `1.2.0`
- Opt-in reader/export schema: exact `2.0.0` tuples, with advertised
support limited to `2.0.0/full-v1`
- Unadvertised schema-2 profiles: `2.0.0/core-v1` and
`2.0.0/record-v1`
- Default profile: `core-v1`
- Authority: `contracts/knowledge-shard/contract.json` and the immutable
versioned schema roots
Version Compatibility
When importing a shard, Fortémi automatically checks version compatibility:
| Scenario | Behavior |
|---|---|
| Exact registered tuple | Import after schema, profile, integrity, relationship, byte, and limit preflight |
| Historical registered tuple | Validate source, run only its registered migration path, revalidate, then import |
| Exact advertised `2.0.0/full-v1` tuple | Opt-in import/export with direct JSON-key presence semantics |
| Unadvertised `2.0.0` tuple | Do not claim support without its own receipt |
| Unregistered tuple or schema `3.x` | Reject before persistent or blob mutation |
| `min_reader_version` newer than the supported schema reader | Reject before mutation |
Migration Support
The default contract is `1.2.0`. Its registered historical path records any legacy default instead of pretending the source carried that state. Schema `2.0.0` distinguishes absent, JSON `null`, empty, and non-empty values and persists those distinctions transactionally. `min_reader_version` is a schema compatibility floor. Application releases belong in `producer.version` and do not participate in compatibility.
Profiles and evidence boundaries
| Profile | Portable surface |
|---|---|
| `core-v1` | Notes, collections, tags, templates, and links; attachment references with explicitly optional sidecars |
| `record-v1` | The declared reduced RecordStore record set; every out-of-profile or lossy projection requires a machine-readable loss entry |
| `full-v1` | All 33 declared components, 34 counts, required attachment bytes, signatures, embeddings and lineage, SKOS, provenance, graph, and community data |
Fortemi's signed `1.2.0/full-v1` fixture and server self-import/re-export prove only the Fortemi-to-Fortemi boundary. The hardened nine-cell schema `1.2.0` matrix is a set of per-cell claims, not a suite-wide parity statement. Exact `2.0.0/full-v1` is now an advertised opt-in backed by the immutable Fortemi #1084 and fortemi-react #382 receipts. That receipt covers only its named React and AIWG producers, PGlite and Fortemi destinations, and HotM pass-through recovery cell. The separate supported-platform aggregate in Gitea run 6393 binds the declared Fortemi authority-to-React/core-to-HotM surface on Linux x86_64, Linux arm64, and macOS arm64 at exact revisions. Windows is deferred under Fortemi #1096. Full portability, complete backup, and schema parity claims remain blocked. Launched GUI/native-dialog coverage also remains unproven; Fortemi #1081 remains `NO-GO` pending independent audit.
For detailed information about versioning, compatibility, and troubleshooting, see the Shard Migration Guide.
- Database Snapshot (`/api/v1/backup/database/snapshot`): Full pg_dump backup. Best for disaster recovery.
Quick Start
Using REST API
# Export all notes as JSON
curl http://localhost:3000/api/v1/backup/export
# Create the default self-importable core-v1 shard
curl http://localhost:3000/api/v1/backup/knowledge-shard -o backup.shard
# Create shard with specific components
curl "http://localhost:3000/api/v1/backup/knowledge-shard?include=notes,links" -o backup.shard
# Opt into a self-contained core-v1 shard for available stored attachments
curl "http://localhost:3000/api/v1/backup/knowledge-shard?include_blobs=true" -o backup-with-blobs.shard
# Trigger database backup
curl -X POST http://localhost:3000/api/v1/backup/trigger
# Check backup status
curl http://localhost:3000/api/v1/backup/status
Using MCP Tools (Recommended for AI Agents)
// Export all notes to JSON
export_all_notes()
// Create the default core-v1 knowledge shard
knowledge_shard()
// Create shard with specific components
knowledge_shard({ include: "notes,links" })
// Validate/import bytes through the multipart REST boundary or an MCP client
// that explicitly supports local file transfer; local server paths are not a
// portable interchange contract.
// Download backup as portable knowledge archive (.archive)
knowledge_archive_download({ filename: "snapshot_database_20260117.sql.gz" })
// Upload and extract knowledge archive
knowledge_archive_upload({ archive_base64: "..." })
// Trigger database backup
backup_now()
// Check backup status
backup_status()
Using Shell Script
# Run immediate backup
./scripts/backup.sh
# Run with verbose output
./scripts/backup.sh -v
# Dry run (show what would happen)
./scripts/backup.sh -n
# Backup to specific destination
./scripts/backup.sh -d local
Using Systemd (Scheduled)
# Enable daily backups at 2:30 AM
sudo systemctl enable matric-backup.timer
sudo systemctl start matric-backup.timer
# Check timer status
systemctl list-timers matric-backup.timer
Per-Memory Backup
The multi-memory architecture allows schema-level backup and restore operations. Each memory archive maintains independent data in its own PostgreSQL schema.
How X-Fortemi-Memory Header Scopes Backup Operations
- Application-level backups (JSON export, knowledge shards) use the `X-Fortemi-Memory` header to scope operations to a specific memory
- Database-level backups (pg_dump) can target specific schemas or the entire database
- Without the header, all backup operations default to the `default` memory
Per-Schema pg_dump for Memory-Level Backup
# Backup a specific memory schema
docker exec fortemi-matric-1 pg_dump -U matric -d matric -n archive_work_2026 > work-memory-backup.sql
# Backup all memories
for schema in $(docker exec fortemi-matric-1 psql -U matric -d matric -t -c "SELECT schema_name FROM archive_registry"); do
docker exec fortemi-matric-1 pg_dump -U matric -d matric -n "$schema" > "backup_${schema}.sql"
done
# Backup multiple specific memories
docker exec fortemi-matric-1 pg_dump -U matric -d matric \
-n archive_work_2026 \
-n archive_personal_2026 \
> multi-memory-backup.sql
Full Database pg_dump Includes ALL Schemas
A full database backup captures all memory archives plus shared infrastructure:
# Full backup (all memories + shared tables)
docker exec fortemi-matric-1 pg_dump -U matric -d matric > full-backup.sql
# This includes:
# - public schema (shared infrastructure: archive_registry, oauth tables, etc.)
# - All archive_* schemas (per-memory data)
Restore Caveats
Restoring a memory backup requires the target schema to exist:
# Option 1: Create memory first, then restore data
curl -X POST http://localhost:3000/api/v1/memories \
-H "Content-Type: application/json" \
-d '{"name": "work-2026", "description": "Restored work memory"}'
# Then restore the schema backup
docker exec -i fortemi-matric-1 psql -U matric -d matric < work-memory-backup.sql
# Option 2: Restore full database backup (recreates all schemas)
docker exec -i fortemi-matric-1 psql -U matric -d matric < full-backup.sql
Important: Schema-level restores assume the schema structure already exists. If restoring to a fresh instance: 1. Let Fortemi create the memory (which runs migrations) 2. Then restore data into the schema
OR
1. Restore full database backup (includes schema creation)
Knowledge Shard Export Respects Active Memory Context
Knowledge shards export data from the memory specified by `X-Fortemi-Memory`:
# Export work memory as knowledge shard
curl http://localhost:3000/api/v1/backup/knowledge-shard \
-H "X-Fortemi-Memory: work-2026" \
-o work-2026.shard
# Export default memory (no header)
curl http://localhost:3000/api/v1/backup/knowledge-shard \
-o default-memory.shard
# Restore shard to different memory (multipart upload)
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?on_conflict=skip \
-H "X-Fortemi-Memory: work-2026-restored" \
-F "[email protected]"
Memory-Scoped Backups
All backup operations respect the `X-Fortemi-Memory` header. Without it, backups operate on the `default` memory.
Backup Specific Memory
# Export work memory only
curl http://localhost:3000/api/v1/backup/export \
-H "X-Fortemi-Memory: work-notes" \
-o work-notes-backup.json
# Create knowledge shard for specific memory
curl http://localhost:3000/api/v1/backup/knowledge-shard \
-H "X-Fortemi-Memory: work-notes" \
-o work-notes.shard
Full Database Backup (All Memories)
Database backup includes all memories and shared tables:
# Database backup includes all memories and shared tables
curl http://localhost:3000/api/v1/backup/database -o full-backup.sql
Restore to Different Memory
# First, create the target memory
curl -X POST http://localhost:3000/api/v1/memories \
-H "Content-Type: application/json" \
-d '{
"name": "work-notes-restored",
"description": "Restored from backup"
}'
# Restore shard to target memory (multipart upload)
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?on_conflict=skip \
-H "X-Fortemi-Memory: work-notes-restored" \
-F "[email protected]"
Memory-scoped backups are useful for:
- Creating per-project backups before major changes
- Migrating specific memories between instances
- Isolating backup/restore operations by project or team
See the Multi-Memory Guide for comprehensive memory management documentation.
MCP Backup Tools
export_all_notes
Exports all notes as a complete JSON export.
Parameters:
- `filter.starred_only` - Only export starred notes
- `filter.tags` - Only notes with these tags
- `filter.created_after` / `created_before` - Date range
Returns:
{
"manifest": {
"version": "1.2.0",
"format": "matric-backup",
"created_at": "2026-01-17T12:00:00Z",
"counts": {
"notes": 150,
"collections": 5,
"tags": 30,
"templates": 3
}
},
"notes": [...],
"collections": [...],
"tags": [...],
"templates": [...]
}
Use cases:
- Complete knowledge base backup
- Migration to another instance
- Offline analysis
- Snapshots before major changes
backup_now
Triggers an immediate database backup using the backup script.
Parameters:
- `destinations` - Array of: `["local", "s3", "rsync"]`
- `dry_run` - Preview without executing
Returns:
{
"status": "success",
"output": "[2026-01-17 12:00:00] Starting Fortémi backup...\
...",
"timestamp": "2026-01-17T12:00:00Z"
}
backup_status
Check the status of the backup system.
Returns:
{
"backup_directory": "/var/backups/fortemi",
"disk_usage": "1.2G",
"latest_backup": {
"path": "/var/backups/fortemi/matric_backup_20260117_120000.sql.gz",
"size_bytes": 52428800,
"timestamp": "2026-01-17T12:00:00Z"
},
"status": "healthy"
}
knowledge_shard
Create a self-importable `core-v1` knowledge shard.
Parameters:
- `include` - Components to include (comma-separated or array):
- `notes` - All notes with original/revised content
- `collections` - Folder hierarchy
- `tags` - All tags
- `templates` - Note templates
- `links` - Semantic relationships between notes
- Default: `notes,collections,tags,templates,links`
Returns:
{
"success": true,
"filename": "matric-shard-20260117-143800.tar.gz",
"size_bytes": 52428,
"size_human": "51.20 KB",
"content_type": "application/gzip",
"base64_data": "H4sIAAAAAAAA...",
"message": "Archive created. Use base64_data to save the file."
}
Shard contents:
manifest.json # Version, counts, SHA256 checksums
notes.jsonl # Notes in streaming JSONL format
collections.json # Folder hierarchy
tags.json # Tags with timestamps
templates.json # Note templates
links.jsonl # Semantic links between notes
Server-generated shards are reference-only for binary attachments by default: attachment metadata appears in note records and import restores it as digest-deduplicated reference records. With `include_blobs=true`, export adds available verified bytes at `blobs/<digest>` and import materializes present valid referenced sidecars. The format and remaining limits are defined in Binary Attachment Projection.
Import safety limits:
- Compressed shard bytes: 50 MiB maximum, or a smaller
`MATRIC_MAX_UPLOAD_SIZE_BYTES` operator limit.
- Expanded component bytes: four times the effective compressed limit in
total; no component may exceed the effective compressed limit.
- Manifest: 1 MiB maximum.
- Archive inventory: 64 regular-file entries with safe UTF-8 relative names;
duplicate names and all non-regular tar entry types are rejected.
- Component records: 250,000 records per component and 4 MiB per record.
Multipart upload reads the request file incrementally up to the compressed limit. The current shard archive reader then buffers bounded entries for validation and apply; this is not streaming archive processing. Base64 and on-disk swap inputs are bounded before decode or allocation. Dry-run and real import use the same structure, checksum, schema, count, and relationship preflight before normal writes. Ordinary import applies all selected database components in one schema-scoped transaction: any database failure rolls back collections, notes, tags, templates, and links together. Reference attachment records and blob metadata use the same transaction, so they also roll back on a late failure. NLP regeneration is queued only after that transaction commits.
Destructive on-disk swap accepts only `wipe` or `merge`. The `wipe` strategy validates the complete shard before mutation, then deletes the existing core-v1 entity families and applies the validated shard in the same schema-scoped transaction. A late apply failure restores the pre-swap state. This atomic database boundary does not make `core-v1` a complete disaster-recovery profile: attachment bytes and richer profile data remain outside its current preservation contract.
Knowledge Shard import
Upload archive bytes to the multipart `POST /api/v1/backup/knowledge-shard/import/upload` boundary. Validate the exact schema/profile tuple and use `dry_run=true` before a mutating import. Filesystem paths are client-local concerns and are not part of the portable wire contract.
- `skip_embedding_regen` - Don't regenerate embeddings (default: false)
Returns:
{
"status": "success",
"manifest": { "version": "1.2.0", "profile": "core-v1", "counts": {...} },
"imported": { "notes": 4, "collections": 2, "links": 12 },
"skipped": { "notes": 0 },
"errors": [],
"dry_run": false
}
Conflict behavior:
- Same shard twice → `on_conflict` determines behavior
- Different shard → New IDs create new notes (merge)
knowledge_archive_download
Download a backup file bundled with its metadata as a portable `.archive` file.
Parameters:
- `filename` - Backup filename from list_backups (e.g., `snapshot_database_20260117.sql.gz`)
Returns:
{
"success": true,
"filename": "snapshot_database_20260117.archive",
"size_bytes": 1234567,
"base64_data": "..."
}
Archive contents:
snapshot_database_20260117.archive (tar format)
├── snapshot_database_20260117.sql.gz # The backup file
└── metadata.json # Title, description, note_count, etc.
Use case: Transfer backups between systems while preserving metadata context.
knowledge_archive_upload
Upload a `.archive` file and extract both the backup and its metadata.
Parameters:
- `archive_base64` - Base64-encoded .archive file
- `filename` - Original filename (optional, for logging)
Returns:
{
"success": true,
"filename": "upload_snapshot_database_20260117.sql.gz",
"path": "/var/backups/fortemi/upload_snapshot_database_20260117.sql.gz",
"size_bytes": 1234567,
"size_human": "1.23 MB",
"metadata": {
"title": "Pre-migration backup",
"description": "Full backup before schema changes",
"backup_type": "snapshot",
"note_count": 42
}
}
Workflow for transferring backups: 1. Source system: `list_backups` → `knowledge_archive_download` 2. Transfer the .archive file 3. Target system: `knowledge_archive_upload` → `database_restore`
REST API Endpoints
The backup system exposes REST API endpoints that can be called directly or via MCP tools.
GET /api/v1/backup/export
Export all notes as a complete JSON export.
Query Parameters:
- `starred_only` - Only export starred notes (boolean)
- `tags` - Comma-separated list of tags to filter by
- `created_after` - Only notes created after this date (ISO 8601)
- `created_before` - Only notes created before this date (ISO 8601)
Example:
# Export all notes
curl http://localhost:3000/api/v1/backup/export
# Export starred notes created in 2024
curl "http://localhost:3000/api/v1/backup/export?starred_only=true&created_after=2024-01-01T00:00:00Z"
Response:
{
"manifest": {
"version": "1.2.0",
"format": "matric-backup",
"created_at": "2026-01-17T12:00:00Z",
"counts": { "notes": 150, "collections": 5, "tags": 30, "templates": 3 }
},
"notes": [...],
"collections": [...],
"tags": [...],
"templates": [...]
}
POST /api/v1/backup/trigger
Trigger an immediate database backup using the backup script.
Request Body (optional):
{
"destinations": ["local", "s3", "rsync"],
"dry_run": false
}
Example:
# Trigger backup to all destinations
curl -X POST http://localhost:3000/api/v1/backup/trigger
# Dry run (preview only)
curl -X POST http://localhost:3000/api/v1/backup/trigger \
-H "Content-Type: application/json" \
-d '{"dry_run": true}'
Response:
{
"status": "success",
"output": "[2026-01-17 12:00:00] Starting Fortémi backup...\
...",
"timestamp": "2026-01-17T12:00:00Z"
}
GET /api/v1/backup/status
Get the current status of the backup system.
Example:
curl http://localhost:3000/api/v1/backup/status
Response:
{
"backup_directory": "/var/backups/fortemi",
"disk_usage": "1.2G",
"backup_count": 7,
"latest_backup": {
"path": "/var/backups/fortemi/matric_backup_20260117_120000.sql.gz",
"filename": "matric_backup_20260117_120000.sql.gz",
"size_bytes": 52428800,
"modified": "2026-01-17T12:00:00Z"
},
"status": "healthy"
}
Status Values:
- `healthy` - Backups exist and are recent
- `no_backups` - No backup files found (directory auto-created if needed)
- `cannot_create_directory: <error>` - Failed to create backup directory (permission error)
Note: The backup directory is automatically created if it doesn't exist. If creation fails due to permissions, the status will indicate the error.
GET /api/v1/backup/knowledge-shard
Create a comprehensive knowledge shard with selected components.
Query Parameters:
- `include` - Comma-separated components: `notes,collections,tags,templates,links`
- Default: `notes,collections,tags,templates,links`
Example:
# Knowledge shard with all data
curl http://localhost:3000/api/v1/backup/knowledge-shard -o backup.shard
# Shard with specific components
curl "http://localhost:3000/api/v1/backup/knowledge-shard?include=notes,links" -o backup.shard
Response: Binary .shard file (gzipped tar) with `Content-Disposition: attachment` header.
Verify shard:
# List contents (it's a gzipped tar internally)
tar -tzf backup.shard
# View manifest
tar -xzf backup.shard -O manifest.json | jq .
POST /api/v1/backup/knowledge-shard/upload
Import a knowledge shard via multipart file upload. Preferred over the legacy JSON/base64 endpoint.
Query Parameters:
- `on_conflict` - `skip` (default), `replace`, or `merge`
- `dry_run` - `true` or `false` (default)
- `include` - Comma-separated components (default: all)
- `skip_embedding_regen` - `true` or `false` (default)
Example:
# Import shard (multipart upload)
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?on_conflict=skip \
-F "[email protected]"
# Dry run first
curl -X POST http://localhost:3000/api/v1/backup/knowledge-shard/upload?dry_run=true \
-F "[email protected]"
POST /api/v1/backup/knowledge-shard/import (Legacy)
Import a knowledge shard via JSON body with base64-encoded data. Use the `/upload` endpoint above for large shards.
Request Body:
{
"shard_base64": "H4sIAAAAAAAA...",
"include": "notes,collections",
"dry_run": false,
"on_conflict": "skip",
"skip_embedding_regen": false
}
Response:
{
"status": "success",
"manifest": { "version": "1.2.0", "profile": "core-v1", "counts": {...} },
"imported": { "notes": 4, "collections": 2 },
"skipped": { "notes": 0 },
"errors": [],
"dry_run": false
}
Backup Script
The backup script (`scripts/backup.sh`) provides full database backups with:
- Compression: gzip (default), zstd, xz, or none
- Encryption: Optional age encryption
- Multiple destinations: Local, rsync, S3
- Retention policy: Automatic cleanup of old backups
- Verification: File integrity checks
Configuration
Create `/etc/Fortémi/backup.conf`:
# Destinations
BACKUP_DEST=/var/backups/fortemi
[email protected]:/backups/matric
BACKUP_REMOTE_S3=s3://my-bucket/matric-backups
# Retention (days)
BACKUP_RETAIN=7
# Compression: gzip, zstd, xz, none
BACKUP_COMPRESS=gzip
# Encryption (optional - path to age public key)
BACKUP_ENCRYPT=/etc/Fortémi/backup-key.pub
# Database
PGUSER=matric
PGPASSFILE=/run/secrets/fortemi-pgpass
PGHOST=localhost
PGPORT=5432
PGDATABASE=matric
# Logging
LOG_FILE=/var/log/Fortémi/backup.log
`PGPASSFILE` must point to an operator-managed secret file owned by the backup service account with mode `0600`. Its PostgreSQL password-file entry uses `hostname:port:database:username:<POSTGRES_PASSWORD>`; do not place the password in the tracked configuration file, command line, or shell history.
Or use environment variables:
BACKUP_DEST=/custom/path ./scripts/backup.sh
Command Line Options
Usage: backup.sh [options]
Options:
-c, --config FILE Configuration file
-d, --destination STR Specific destination: local, s3, rsync, or all
-n, --dry-run Show what would be done without executing
-v, --verbose Enable verbose output
-q, --quiet Quiet mode (errors only)
-h, --help Show help
Systemd Integration
Service Unit
The service unit (`deploy/matric-backup.service`) runs the backup script with:
- Resource limits (50% CPU, 2GB RAM)
- Security hardening (PrivateTmp, ProtectSystem)
- 1-hour timeout
- Journal logging
Timer Unit
The timer unit (`deploy/matric-backup.timer`) schedules backups:
- Daily at 2:30 AM
- 15-minute randomized delay
- Persistent (catches up after downtime)
- 5-minute delay on first boot
Installation
# Copy unit files
sudo cp deploy/matric-backup.service /etc/systemd/system/
sudo cp deploy/matric-backup.timer /etc/systemd/system/
# Reload systemd
sudo systemctl daemon-reload
# Enable and start timer
sudo systemctl enable matric-backup.timer
sudo systemctl start matric-backup.timer
# Verify
systemctl list-timers matric-backup.timer
Manual Trigger
# Run backup immediately
sudo systemctl start matric-backup.service
# Check status
sudo systemctl status matric-backup.service
# View logs
journalctl -u matric-backup.service -f
Restore Procedures
From JSON Export (export_all_notes)
1. Parse the JSON export 2. Use `create_note` or `bulk_create_notes` MCP tools to recreate notes 3. Use `create_collection` to recreate collections 4. Use `create_template` to recreate templates
From pg_dump Backup
# 1. Stop the API service
sudo systemctl stop matric-api
# Use an operator-managed PostgreSQL password file (mode 0600)
export PGPASSFILE=/run/secrets/fortemi-pgpass
# 2. List available backups
ls -lh /var/backups/fortemi/
# 3. Drop and recreate database
psql -U matric -h localhost -c "DROP DATABASE matric;"
psql -U matric -h localhost -c "CREATE DATABASE matric;"
# 4. Restore from backup
# For .sql files:
psql -U matric -h localhost -d matric -f backup.sql
# For .sql.gz files:
gunzip -c matric_backup_YYYYMMDD.sql.gz | psql -U matric -h localhost -d matric
# For pg_dump custom format (.sql without compression):
pg_restore -U matric -h localhost -d matric backup.sql
# 5. Verify
psql -U matric -h localhost -d matric -c "SELECT COUNT(*) FROM note;"
# 6. Restart API
sudo systemctl start matric-api
# 7. Verify health
curl http://localhost:3000/health
Encryption Setup
Generate Encryption Key
# Install age
sudo apt install age
# Generate key pair
age-keygen -o /etc/Fortémi/backup-key.txt
# Extract public key
age-keygen -y /etc/Fortémi/backup-key.txt > /etc/Fortémi/backup-key.pub
# Secure private key
sudo chmod 600 /etc/Fortémi/backup-key.txt
sudo chown root:root /etc/Fortémi/backup-key.txt
Configure Encryption
Add to backup.conf:
BACKUP_ENCRYPT=/etc/Fortémi/backup-key.pub
Decrypt Backup
age --decrypt -i /etc/Fortémi/backup-key.txt backup.sql.gz.age > backup.sql.gz
Remote Destinations
S3 Setup
# Install AWS CLI
sudo apt install awscli
# Configure credentials
aws configure
# Test access
aws s3 ls s3://your-bucket/
# Configure backup
echo "BACKUP_REMOTE_S3=s3://your-bucket/matric-backups" >> /etc/Fortémi/backup.conf
Rsync Setup
# Generate SSH key
ssh-keygen -t ed25519 -f ~/.ssh/backup_key -N ""
# Copy to remote server
ssh-copy-id -i ~/.ssh/backup_key [email protected]
# Test connection
ssh -i ~/.ssh/backup_key [email protected] "mkdir -p /backups/matric"
# Configure backup
echo "[email protected]:/backups/matric" >> /etc/Fortémi/backup.conf
Pre-Migration Backup
Always create a backup before running database migrations:
# Create backup with special retention
BACKUP_DEST=/var/backups/fortemi/migrations \
BACKUP_RETAIN=30 \
./scripts/backup.sh
# Verify backup was created
ls -lh /var/backups/fortemi/migrations/
# Now run migration
PGPASSFILE=/run/secrets/fortemi-pgpass \
psql -U matric -h localhost -d matric -f migrations/new_migration.sql
Monitoring
Check Backup Health
# Via MCP
backup_status()
# Via command line
ls -lh /var/backups/fortemi/ | tail -5
View Logs
# Script logs
tail -f /var/log/Fortémi/backup.log
# Systemd logs
journalctl -u matric-backup.service -f
Alerting
Configure webhook notifications in backup.conf:
NOTIFY_WEBHOOK=https://hooks.slack.com/services/XXX/YYY/ZZZ
Troubleshooting
Backup fails with "permission denied"
# Fix directory permissions
sudo chown -R fortemi:fortemi /var/backups/fortemi
sudo chmod 755 /var/backups/fortemi
Timer not running
# Check timer status
systemctl status matric-backup.timer
# Enable and start
sudo systemctl enable matric-backup.timer
sudo systemctl start matric-backup.timer
S3 upload fails
# Check AWS credentials
aws sts get-caller-identity
# Test S3 access
aws s3 ls s3://your-bucket/
Backup files too large
# Switch to zstd compression (better ratio)
echo "BACKUP_COMPRESS=zstd" >> /etc/Fortémi/backup.conf
Best Practices
1. Test restores regularly - Backups are only useful if they work 2. Use multiple destinations - Local + remote for redundancy 3. Enable encryption for sensitive data 4. Monitor disk space - Set up alerts for backup directory 5. Keep at least 3 backups - Never reduce below minimum retention 6. Backup before migrations - Always create a restore point 7. Document restore procedures - Keep runbooks up to date
File Locations
| Path | Description |
|---|---|
| `/path/to/fortemi/scripts/backup.sh` | Backup script |
| `/etc/Fortémi/backup.conf` | Configuration file |
| `/var/backups/fortemi/` | Default backup destination |
| `/var/log/Fortémi/backup.log` | Backup logs |
| `/etc/systemd/system/matric-backup.service` | Systemd service |
| `/etc/systemd/system/matric-backup.timer` | Systemd timer |