Lifecycle & Quotas
A file uploaded through File Operations stays until something removes it. Two things can: an explicit DELETE, or the permanent removal of the record that referenced it. Everything else — including a soft-deleted record — leaves the bytes in place.
Files and Their Records
Attachments follow Sovrium's soft-delete-by-default posture, so file presence tracks record presence rather than the delete request:
| What happens to the record | What happens to the file |
|---|---|
| Soft-deleted (the default) | Kept. A restore brings the attachment back intact. |
| Restored | Nothing to do — the bytes never left. |
Purged (?purge=true) |
Removed from storage, then the row is dropped. |
Hard-deleted (?permanent=true) |
Kept. The row goes; the bytes are orphaned. |
| Attachment replaced on update | The previous file is removed. |
Attachment cleared to null |
The previous file is removed. |
| Key referenced by another record | Kept, even during a purge. |
Two rows deserve attention. The last one saves you: before deleting bytes during a purge, Sovrium checks whether any other record still points at the same key — soft-deleted records included — so two records sharing one upload cannot orphan each other.
The ?permanent=true row is the trap. It is the admin-only hard delete, and it drops the row without touching storage. Use ?purge=true when the file must go too; it needs only the ordinary delete permission and it cleans both sides.
Purge is irreversible. DELETE /api/tables/{table}/records/{id}?purge=true drops the row and the bytes together, with no trash step. A plain DELETE is the reversible one; reach for purge when you mean the GDPR sense of erasure, not the tidy-up sense.
Size Caps
Two operator-controlled limits bound uploads. Both are validated at boot: set either to a non-positive or non-numeric value and the server refuses to start rather than silently ignoring your cap.
| Variable | Default | Bounds |
|---|---|---|
STORAGE_MAX_FILE_SIZE |
104857600 (100 MB) |
One upload. A bucket's own maxFileSize overrides it. |
STORAGE_MAX_TOTAL_SIZE |
unlimited | Every stored byte in the application, summed. |
STORAGE_MAX_FILE_SIZE=52428800 # 50 MB per file
STORAGE_MAX_TOTAL_SIZE=10737418240 # 10 GB in totalAn oversized upload answers 413; one that would push the total past the quota answers 507. With no STORAGE_MAX_TOTAL_SIZE set there is no quota check at all, so uploads are bounded only per-file.
Uploads are buffered, not streamed. The request body is read into memory before the size check runs, so a 100 MB cap means a 100 MB allocation on an accepted request. Set STORAGE_MAX_FILE_SIZE — or the bucket's maxFileSize — to the smallest number your app can actually live with, and treat it as a memory budget rather than a policy preference.
Reading Current Usage
curl https://app.example.com/api/admin/buckets/overview \
-H "Cookie: $ADMIN_SESSION"{
"totals": {
"buckets": 1,
"files": 1284,
"totalBytes": 524288000,
"by_provider": { "s3": 1, "local": 0, "bytea": 0 }
},
"series": {
"interval": "1h",
"points": [{ "timestamp": "2026-08-13T09:42:00.000Z", "uploads": 12, "bytes": 4194304 }]
}
}The endpoint is open to the admin and operator roles, and reports what is stored right now, not what is allowed — compare totals.totalBytes against your own STORAGE_MAX_TOTAL_SIZE to know how much headroom is left. Both totals.totalBytes and totals.files are read live from the storage backend rather than tracked in a counter, so deleting files lowers them immediately. totals.buckets counts the buckets your config declares; an app declaring none reports the single virtual default bucket, and one whose storage provider does not resolve reports zero.
Upload History
The series block answers when those bytes landed, which totals cannot. Each point reports the uploads count and bytes sum for one interval, read from the storage catalog — every upload path and every backend writes there, so the series summed across the window equals totals.
?period picks the window and defaults to 24h: 24h returns 24 hourly points, 7d seven daily ones, 30d thirty. Each point is labelled by the instant its interval opens, and the window is measured back from the moment you call rather than snapped to clock hours or midnight. period never affects totals. The catalog holds only files that still exist, so deleting one lowers the interval it was uploaded into.
/api/admin/buckets/quota is gone. The older endpoint returned a flat { totalBytes, fileCount } and now answers 404. Both numbers survive the move unchanged — read them as totals.totalBytes and totals.files.
Automation Temp Files
Automations that write scratch files put them under tmp/automations/. Those are reclaimed opportunistically — the next temp write sweeps anything older than the threshold. Nothing is scheduled, so the value is an age, not an interval.
| Variable | Default | Meaning |
|---|---|---|
STORAGE_TEMP_CLEANUP_AFTER |
86400000 (24 h) |
Age in milliseconds at which a temp file becomes sweepable. |
Set it to 0 to disable app-level sweeping entirely — the right choice when an S3 lifecycle rule already reclaims the prefix. Any other malformed value falls back to 24 hours rather than disabling the sweep, because silently unbounded growth is the worse failure.
Related Pages
- File Operations — uploading and deleting files directly.
- Upload Security — the validations that run before a byte is stored.
- Attachment Fields & Storage — the record side of this lifecycle.
- Records CRUD — the delete endpoint and its query flags.
- Environment Variables: Services — the full
STORAGE_*reference.
Last updated September 1, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.