RFC-0007 Storage System
Defines filesystem storage, upload flow, protected/public files, metadata storage, cleanup policies, and future cloud adapters.
Purpose
Bakend stores uploaded files on the local filesystem with metadata in SQLite. Files can be public (downloadable without auth) or protected (owner or admin only).
Goals
- Local filesystem storage (no S3 required for V1)
- Simple upload/download/delete API
- Public and protected file visibility
- Integration with collection
filefields
Upload Flow
- Client authenticates (Bearer JWT)
- Client POSTs multipart form to
/api/storage/upload - Bakend writes bytes to
{storage}/files/{id} - Bakend inserts metadata into
_files - Bakend emits
storage.uploaded - Client receives file metadata including
id
Access Control
| Operation | Rule |
|---|---|
| Upload | Authenticated user required |
| Download (public) | No auth required |
| Download (protected) | Owner (user_id) or admin role |
| Delete | Owner or admin |
On-Disk Layout
{config.storage}/
files/
{fileId}
Original filename and MIME type are stored in SQLite only (not in the path).
Collection File Fields
Collection file fields store a file id string. Record validation checks that the ID exists in _files. Upload is a separate API call; no inline multipart on collection CRUD in V1.
Events
storage.uploaded— safe metadata payload (no disk path)storage.deleted—{ id, userId }
Future
- S3 / Cloudflare R2 / Backblaze B2 adapters
- Image thumbnailing
- Orphan file cleanup on record delete
- Configurable max file size
- Anonymous public uploads
Implementation (Milestone 8)
Module Layout
src/core/storage/
types.ts
file-store.ts
filesystem.ts
permissions.ts
create-storage-engine.ts
Database Table
Schema version 3. Table _files:
idTEXT PRIMARY KEYfilenameTEXT NOT NULLmime_typeTEXT NOT NULLsizeINTEGER NOT NULLvisibilityTEXT NOT NULL (public|protected)user_idTEXT NOT NULL REFERENCES_users(id)ON DELETE CASCADEcreated_atTEXT NOT NULL
Endpoints
POST /api/storage/upload
GET /api/storage/:id
DELETE /api/storage/:id
Upload Request
multipart/form-data:
file(required) — the file bytesvisibility(optional) —publicorprotected, defaultprotected
Response Shape
{
"id": "uuid",
"filename": "photo.png",
"mimeType": "image/png",
"size": 12345,
"visibility": "protected",
"userId": "user-uuid",
"createdAt": "2026-01-01T00:00:00.000Z"
}
Limits (V1)
- Max file size: 10 MB (hardcoded constant)
- Any MIME type accepted; empty files rejected
Configuration
Uses existing storage path in bakend.json (default ./storage). Env: BAKEND_STORAGE.
Function and Job Context
storage: {
get(id: string): Promise<FileMetadata | null>;
delete(id: string): Promise<boolean>;
}
No upload from background context in V1.
Wiring
createStorageEngine() in start(); HTTP handlers enforce ACL via permissions.ts; createServer() receives storage engine; collections validation receives fileExists lookup.