Object Storage (MinIO / Wasabi / S3)¶
AZSuite stores most user-uploaded files (photos, PDFs, scans, flight tracks, engine-monitor dumps) in S3-compatible object storage rather than the filesystem. This page explains how the storage layer works, how to add or swap providers, and what gets stored where.
Supported providers¶
The ObjectStorageManager.php class talks to any S3-compatible API.
Tested in production with:
- MinIO — self-hosted, on-premises
- Wasabi — hot-cloud S3, no egress fees
- AWS S3 — the original
- Backblaze B2 — works but not extensively tested
The class makes its own AWS Signature V4 requests (no SDK dependency), which keeps the deployment lightweight.
Per-purpose configuration¶
Storage is configured per purpose in the object_storage_configs table.
Each row defines:
config_id— primary keyconfig_name— human-readable labelstorage_type—minio/wasabi/s3_compatibleendpoint_url—https://s3.us-central-1.wasabisys.com,http://192.168.1.35:9000, etc.access_key/secret_key— encrypted in the databasebucket_name— the bucket to write tobucket_purpose— which AZSuite feature uses it (see below)region— for SigV4 signing (us-east-1,us-central-1, etc.)use_ssl/use_path_style— flagsis_active— only one row per purpose should be active at a timeis_validated— set after the connection-test passes
Purposes:
| Purpose | What's stored |
|---|---|
logbook_photos |
Scan/OCR uploads, logbook page images, signed entries |
aircraft_documents |
Aircraft manuals, weight & balance reports, registration |
aircraft_photos |
Aircraft photo gallery |
aircraft_monitor |
Engine-monitor dumps awaiting parse |
engine_data |
Parsed engine-monitor data archive |
adsb_tracks |
Flight track files (KML/GPX/CSV) |
ocr_cropped |
Cropped regions from scan_define for OCR |
panel_designs |
Panel designer projects |
experimental |
Experimental builder media |
work_event_archive |
Work order finished-job archives |
custom |
Anything else / future use |
How a feature picks the right bucket¶
Code calls getConfigByPurpose('logbook_photos') and gets back the active
config row. The query is:
So only one config can be active per purpose at a time. This makes provider migrations a one-row UPDATE.
Managing configs (admin UI)¶
Admins can view, edit, and add storage configs through the AZSuite admin UI (typically under settings). The form lets you:
- Add a new config row (filled with defaults)
- Edit endpoint, region, bucket name, keys
- Test the connection — issues a
HEAD bucketagainst the endpoint with signed credentials, returns success/fail - Toggle
is_active(with care — see provider migration below)
Provider migration¶
The typical migration story: replace MinIO with Wasabi for one purpose while keeping production traffic flowing.
The migration tool lives at scripts/migrate_bucket.php:
What it does:
- Lists every object in the source config's bucket (paginated)
- Downloads each to a tempfile
- Uploads to the destination config's bucket at the same key
- Verifies the destination's reported size matches the source
- Logs progress + per-object failures to
/tmp/migrate_bucket_<ts>.log
Flags:
--prefix=path/— only migrate keys with that prefix (good for staged migrations)--dry-run— list what would be copied--skip-existing— resume after partial run; skips destination keys that already exist with matching size--limit=N— stop after N objects (testing)--verbose— log every object instead of every 25th
Source is never modified — non-destructive copy only.
Once migration completes, the cutover is two SQL statements in a transaction:
START TRANSACTION;
UPDATE object_storage_configs SET is_active = 0 WHERE config_id = 3; -- old
UPDATE object_storage_configs SET is_active = 1 WHERE config_id = 11; -- new
COMMIT;
After the cutover, the old config can stay around in is_active=0 state for
historical reference (or be deleted entirely).
Key encoding¶
Object keys can contain spaces, parentheses, plus-signs, etc. — common with
human-named PDFs like "Beechcraft-SB 2147.pdf". The storage layer
URL-encodes each path segment when building the request URL and the
SigV4 canonical URI (both must use the same encoded form, or signature
verification fails).
This was a real bug fixed during the Wasabi migration — early implementations didn't URL-encode, which broke any key with a space.
Security¶
- Secret keys are encrypted at rest in the database (custom encryption with a key derived from app config)
- Validation/connection tests don't log the secret
- The
useSSL=trueflag should always be set in production; only disable for local MinIO over HTTP for dev - For Wasabi (real public CA certs),
CURLOPT_SSL_VERIFYPEERshould be on — there's a planned audit fix to make this conditional per provider
Performance characteristics¶
| Operation | MinIO (LAN) | Wasabi (cloud) |
|---|---|---|
| Single PUT, 1 MB | ~50 ms | ~300 ms |
| Single GET, 1 MB | ~30 ms | ~200 ms |
| LIST 1000 objects | ~50 ms | ~500 ms |
| DELETE | ~30 ms | ~100 ms |
LAN MinIO will always be faster than cloud Wasabi for individual operations — it's the latency of the WAN round-trip. Both are fine for AZSuite's scale.
Costs¶
MinIO is free (you pay for the hardware / VPS). Wasabi is ~$6/TB/month flat with no egress fees and no per-request fees. AWS S3 is variable depending on region and storage class.
Migration recommendations¶
If you're starting fresh, Wasabi is the easiest choice — no infrastructure to maintain, predictable pricing, full S3 compatibility.
MinIO makes sense when:
- You have low-bandwidth uplinks (LAN access is faster)
- You need data sovereignty (everything stays on your premises)
- You already have NAS / server hardware you want to use
A common hybrid: MinIO for the frequently-accessed reference library (read-heavy, on the LAN) + Wasabi for user-generated content (need cloud durability + multi-tenant access).