
# Lifecycle & Quotas

A file uploaded through [File Operations](/en/docs/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](/en/docs/tables-overview) 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.

:::callout
**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.          |

```bash
STORAGE_MAX_FILE_SIZE=52428800      # 50 MB per file
STORAGE_MAX_TOTAL_SIZE=10737418240  # 10 GB in total
```

An 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.

:::callout
**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

```bash
curl https://app.example.com/api/admin/buckets/quota \
  -H "Cookie: $ADMIN_SESSION"
```

```json
{ "totalBytes": 524288000, "fileCount": 1284 }
```

The endpoint is admin-only and reports what is stored right now, not what is allowed — compare it against your own `STORAGE_MAX_TOTAL_SIZE` to know how much headroom is left. Deleting files lowers `totalBytes` immediately, since it is summed from storage rather than tracked in a counter.

## 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](/en/docs/file-operations) — uploading and deleting files directly.
- [Upload Security](/en/docs/file-security) — the validations that run before a byte is stored.
- [Attachment Fields & Storage](/en/docs/attachment-storage) — the record side of this lifecycle.
- [Records CRUD](/en/docs/records-crud) — the delete endpoint and its query flags.
- [Environment Variables: Services](/en/docs/env-vars-services) — the full `STORAGE_*` reference.
