Skip to main content

Retention & Deletion

The File Storage service provides a two-phase deletion model: soft delete (recoverable) and permanent delete (irreversible), plus automatic orphan detection for files that lose all references.


Soft Delete

Soft delete moves a file to the trash. The file is no longer listed in normal queries but can be restored within 30 days.

DELETE https://api.septemcore.com/v1/files/01j9paf1l000000000000000
Authorization: Bearer <access_token>

Response 204 No Content. The file record in PostgreSQL is updated:

{ "status": "deleted", "deletedAt": "2026-04-15T10:30:00.000Z" }

Effects of soft delete:

  • GET /files/:id returns 410 Gone
  • File is excluded from GET /files list by default
  • Storage bytes continue to count against the tenant quota
  • File is recoverable via POST /files/:id/restore for 30 days
await kernel.files().delete('01j9paf1l000000000000000');
// file.status → 'deleted'

Restore from Trash

POST https://api.septemcore.com/v1/files/01j9paf1l000000000000000/restore
Authorization: Bearer <access_token>

Response 200 OK:

{
"fileId": "01j9paf1l000000000000000",
"status": "available",
"message": "File restored successfully."
}
await kernel.files().restore('01j9paf1l000000000000000');
// file.status → 'available'

Restore is only possible within 30 days of soft deletion. After that, automatic permanent deletion removes the file from S3.


Permanent Delete

Permanent delete removes the file from S3 immediately and irreversibly. Storage quota is updated within seconds.

DELETE https://api.septemcore.com/v1/files/01j9paf1l000000000000000/permanent
Authorization: Bearer <access_token>

Response 204 No Content. The file binary is removed from S3. The PostgreSQL record is retained for audit purposes (status: destroyed) but the URL returns 410 Gone permanently.

await kernel.files().deletePermanent('01j9paf1l000000000000000');
// file removed from S3, quota updated
ActionStorage freedBilling recalculatedRecoverable
Soft deleteNo — file still in S3NoYes — 30 days
Auto-delete after 30 daysYesYesNo
Permanent deleteYes — immediatelyYes — immediatelyNo

Env override: FILES_SOFT_DELETE_RETENTION_DAYS=30 overrides the 30-day window. Billing recalculates storage_bytes after physical removal.


List Deleted Files

To see files in trash:

GET https://api.septemcore.com/v1/files?status=deleted&limit=20
Authorization: Bearer <access_token>
{
"data": [
{
"fileId": "01j9paf1l000000000000000",
"filename": "old-contract.pdf",
"status": "deleted",
"deletedAt": "2026-04-10T08:00:00.000Z",
"restorableUntil": "2026-05-10T08:00:00.000Z"
}
],
"pagination": { "hasMore": false }
}

restorableUntil is deletedAt + FILES_SOFT_DELETE_RETENTION_DAYS.


Orphan Detection

An orphaned file is a file that has no foreign-key references from any table — for example, an avatar replaced by a new upload, a file whose parent record was deleted, or a module that was uninstalled.

Without orphan cleanup, these files occupy S3 storage indefinitely.

Detection Rules

RuleValue
Scan intervalDaily (FILES_ORPHAN_SCAN_INTERVAL_HOURS=24)
Grace period24 hours (FILES_ORPHAN_MIN_AGE_HOURS=24)
Delete after detection30 days
Audit recordfiles.orphan.deleted written to Audit Service

Grace period rationale: A file is only eligible for orphan detection if it has zero FK references and created_at < NOW() - 24h. The 24-hour grace prevents a false positive where the file was uploaded but the module has not yet written the FK reference (e.g. the user uploaded an avatar but the profile update transaction has not committed).

Detection Workflow

Daily background job:
Scan S3 object metadata
→ Check each fileId against PostgreSQL FK tables
→ No FK references found AND created_at < NOW() - 24h?
→ Mark file status: 'orphaned', orphanedAt: NOW()
→ 30 days later: DELETE from S3
WRITE audit record: files.orphan.deleted

Orphan vs Soft-Delete Retention

CategoryHot storageCold storagePhysical delete after
Orphaned (no FK references)30 daysNone30 days from detection
Soft-deleted (in trash)30 daysNone30 days from deletion
Failed scan (rejected)Until tenant removesNoneManual only

No cold storage tier for either category. 30 days is the enterprise standard (AWS, GCP: 30-day lifecycle policy for orphaned objects).


Bucket Layout

All tenant files are stored in a single S3 bucket, organized by {tenantId}/{bucket}/ prefix. This maintains strict tenant isolation while keeping S3 bucket counts manageable.

Logical bucketS3 prefixContents
avatars{tenantId}/avatars/User and org avatars
assets{tenantId}/assets/PWA assets, icons, branding
documents{tenantId}/documents/Reports, contracts, invoices
exports{tenantId}/exports/Data export archives (CSV, ZIP)
modules{tenantId}/modules/Module-specific file attachments
staging{tenantId}/staging/AV scan staging area (internal, not accessible)

Tenant Prefix Example

S3 bucket: platform-files

Tenant A: platform-files/01j9p3kz5f.../avatars/01j9paf1l...
platform-files/01j9p3kz5f.../documents/01j9paf2m...

Tenant B: platform-files/01j9p4mk7g.../avatars/01j9paf3n...
platform-files/01j9p4mk7g.../exports/01j9paf4p...

Files from different tenants reside under separate prefixes. The File Service's S3 IAM role policy grants access only to {tenantId}/* where tenantId comes from the validated JWT — cross-tenant access is architecturally prevented at the IAM level, not only at the application level.


Error Reference

ScenarioHTTPCode
File not found404not-found
File already permanently deleted410FILE_DELETED
Restore window expired (> 30 days)409RESTORE_WINDOW_EXPIRED
File belongs to different tenant403forbidden
Restore on non-deleted file409FILE_NOT_DELETED