File Storage — Overview
The File Storage service is the shared binary storage layer for all
modules. Modules never write directly to S3 — they call kernel.files()
and the service handles upload, virus scanning, image processing,
access control, and lifecycle management.
Technical Stack
| Component | Technology | Role |
|---|---|---|
| Go runtime | aws-sdk-go-v2 | All S3 operations |
| Object storage | SeaweedFS (self-hosted) or AWS S3 | Binary storage backend |
| Upload strategy | Presigned URLs | Direct client-to-S3 upload, zero gateway bandwidth cost |
| AV scanning | Two-bucket staging pipeline | Every upload scanned before becoming accessible |
| Image processing | bimg (libvips, Go binding) | Crop, resize, convert, compress (used by Netflix, AWS) |
| Metadata store | PostgreSQL | File records, status, ownership, soft-delete state |
Staging + AV Scan Pipeline
Every file goes through a two-bucket pipeline before it is accessible. The staging bucket is never exposed publicly.
Client uploads:
Presigned POST → S3 staging bucket: {tenantId}/staging/{fileId}
Status: pending_scan
S3 Event Notification → File Service → AV scanner
Clean: move to main bucket → {tenantId}/{bucket}/{fileId}
Status: available
Infected: delete from staging
Status: rejected
Notify tenant admin
A file with status pending_scan is not accessible via
GET /files/:id. Any attempt returns 404 Not Found until the
AV scan completes and the file moves to available.
Staging Overflow Protection
| Parameter | Value | Env variable |
|---|---|---|
| Staging file TTL | 24 hours | FILES_STAGING_TTL_HOURS=24 |
| Max pending files per tenant | 100 | FILES_STAGING_MAX_PER_TENANT=100 |
| TTL exceeded | File auto-rejected + uploader notified | — |
| Max pending exceeded | 503 Service Unavailable | — |
| AV scan queue backlog > 1 000 | Priority inversion: newest files first | — |
Bucket Layout
All files for all tenants reside in a single S3 bucket, partitioned
by {tenantId}/{bucket}/:
| Logical bucket | S3 path prefix | Contents |
|---|---|---|
avatars | {tenantId}/avatars/ | User and org avatars |
assets | {tenantId}/assets/ | PWA assets, icons, branding |
documents | {tenantId}/documents/ | Reports, contracts, exports |
exports | {tenantId}/exports/ | Data export archives |
modules | {tenantId}/modules/ | Module-specific file attachments |
One S3 bucket per deployment, not per tenant. This keeps S3 bucket counts manageable (AWS S3: 100-bucket soft limit without quota request) while maintaining strict tenant isolation via prefix and JWT-level access control.
Tenant Isolation
File access is enforced at two levels:
- S3 key prefix — every file path is prefixed with
{tenantId}/. Files from different tenants physically reside under different prefixes. - JWT check — the File Service verifies that the
tenantIdfrom the JWT matches thetenantIdin the file record before serving any request. Cross-tenant file access is structurally impossible.
REST API
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/files/upload | Multipart upload (server-proxied) |
POST | /api/v1/files/presign | Get presigned upload URL for direct S3 upload |
GET | /api/v1/files/:id | Download file (binary stream) |
GET | /api/v1/files/:id/url | Get presigned download URL |
GET | /api/v1/files | List files (paginated, filterable) |
DELETE | /api/v1/files/:id | Soft delete (moves to trash, 30-day restore window) |
DELETE | /api/v1/files/:id/permanent | Immediate permanent delete (irreversible) |
POST | /api/v1/files/:id/restore | Restore from trash (within 30 days) |
POST | /api/v1/files/:id/process | Process image (resize, crop, convert) |
GET | /api/v1/files/:id/thumbnail/:preset | Get thumbnail by preset name |
File Status Lifecycle
┌─────────────┐
│ pending_scan │ (in staging bucket, not accessible)
└──────┬──────┘
AV scan │
┌───────────┴───────────┐
│ clean │ infected
▼ ▼
┌───────────┐ ┌─────────────┐
│ available │ │ rejected │
└─────┬─────┘ └─────────────┘
│ DELETE /files/:id
▼
┌─────────────┐
│ deleted │ (soft delete, in trash, restorable 30 days)
└─────┬───────┘
│ 30 days elapsed OR DELETE permanent
▼
┌─────────────┐
│ destroyed │ (physically removed from S3, irreversible)
└─────────────┘
Related Pages
- Uploading Files — multipart upload, presigned URL, staging pipeline
- Downloading Files — direct download, presigned download URL, tenant isolation
- Image Processing — bimg/libvips pipeline, crop, resize, WebP/AVIF
- Thumbnails — 5 presets, auto-generation, on-demand
- Retention & Deletion — soft delete, orphan scan, bucket layout