@platform/sdk-files
@platform/sdk-files provides type-safe access to the Platform File
Storage. All files go through an antivirus staging-bucket before
becoming available. Image files are automatically processed (resize,
crop, compression, WebP conversion) using bimg (libvips).
Installation
pnpm add @platform/sdk-files
upload()
Upload a file directly via multipart. The file first lands in the
antivirus staging bucket and becomes available only after a clean scan:
import { kernel } from '@platform/sdk-core';
const file = await kernel.files().upload({
file: fileBlob, // File | Blob | Buffer
fileName: 'product-catalog.pdf',
mimeType: 'application/pdf',
bucket: 'documents', // 'avatars' | 'assets' | 'documents' | 'exports' | 'modules'
metadata: { sourceForm: 'product-upload' },
onProgress: (percent) => {
setUploadProgress(percent); // 0–100
},
});
// file.status: 'pending_scan' → 'available' | 'rejected'
// file: { id, fileName, mimeType, sizeBytes, url, bucket, createdAt, uploadedBy }
Antivirus flow: File lands in
{tenantId}/staging/{fileId}and is not accessible viagetUrl()until the scan completes.upload()resolves as soon as the file is queued for scanning. PollgetMetadata(fileId)to checkstatus: 'available'before presenting the URL to end users.
Staging Limits
| Parameter | Value |
|---|---|
| Staging file TTL | 24 hours (FILES_STAGING_TTL_HOURS). After 24h → auto-rejected + notify uploader |
| Max files in staging per tenant | 100 (FILES_STAGING_MAX_PER_TENANT). Exceeded → 503 Service Unavailable |
| AV scan queue | FIFO by default. When backlog > 1,000 → newest files first (priority inversion) |
Bucket Reference
| Bucket | S3 path prefix | Purpose |
|---|---|---|
avatars | {tenantId}/avatars/ | User avatars |
assets | {tenantId}/assets/ | PWA assets, icons, logos |
documents | {tenantId}/documents/ | Documents, reports |
exports | {tenantId}/exports/ | Exported datasets |
modules | {tenantId}/modules/ | Module-specific files |
All buckets are physically one S3 bucket with prefix-based tenant
isolation. Cross-tenant access is impossible — the Gateway enforces
tenantId from the caller's JWT.
download()
Download a file and return it as a Blob (client-side) or Buffer (server-side):
const blob = await kernel.files().download('01j9pfil0000000000000001');
// Returns Blob (browser) or Buffer (Node.js / server module)
// Save to disk in a server-side module:
import { writeFileSync } from 'node:fs';
writeFileSync('/tmp/export.pdf', Buffer.from(await blob.arrayBuffer()));
getUrl()
Get a time-limited presigned download URL (S3 presigned URL):
const presigned = await kernel.files().getUrl('01j9pfil0000000000000001', {
expiresIn: 3600, // seconds — default: 3600 (1 hour), max: 604800 (7 days)
});
// presigned.url = 'https://storage.platform.io/...?X-Amz-Expires=3600...'
// presigned.expiresAt = '2026-04-22T05:00:00Z'
Presigned URLs bypass the API Gateway — the client fetches directly from S3-compatible storage. No auth header needed once the URL is issued.
Presigned Upload URL
For large files (>100 MB) or direct browser-to-S3 uploads, get a presigned upload URL instead of passing the file through the API:
POST https://api.septemcore.com/v1/files/presign
Authorization: Bearer <access_token>
Content-Type: application/json
{ "fileName": "large-export.csv", "mimeType": "text/csv", "bucket": "exports" }
Response:
{
"fileId": "01j9pfil0000000000000002",
"uploadUrl": "https://storage.platform.io/...?X-Amz-Signature=...",
"expiresAt": "2026-04-22T03:15:00Z",
"maxSizeBytes": 104857600
}
Client then PUTs the file body directly to uploadUrl. The file still
goes through the antivirus staging-bucket flow.
uploadImage()
Upload an image with optional interactive crop and automatic processing. Returns URLs for all generated thumbnail presets:
const result = await kernel.files().uploadImage({
file: imageFile,
bucket: 'avatars',
crop: {
x: 125,
y: 80,
width: 300,
height: 300,
zoom: 1.2,
rotate: 0,
},
outputFormat: 'webp', // 'webp' | 'avif' | 'png' | 'jpeg'. Default: 'webp'
quality: 85, // 1–100. Default: 85
onProgress: (pct) => setProgress(pct),
});
// result.original: { id, url, sizeBytes }
// result.processed: { url, sizeBytes, compressionRatio: 0.72 }
// result.thumbnails: {
// icon_32: { url, width: 32, height: 32 },
// card_300: { url, width: 300, height: 200 },
// }
Processing timeout:
bimg/libvips has a 30-second processing timeout (FILES_IMAGE_PROCESSING_TIMEOUT_SEC). On timeout → the original is saved, thumbnails arependingand retried in the background.Max image size: 10 MB (
FILES_MAX_IMAGE_SIZE_MB). Exceeded →413 Payload Too Large.
processImage()
Apply processing to an already-uploaded image:
const processed = await kernel.files().processImage('01j9pfil0000000000000001', {
resize: { width: 800, height: 600, fit: 'cover' },
outputFormat: 'avif',
quality: 90,
});
// processed: { url, sizeBytes, format: 'avif' }
// Original file is preserved in S3. A new derived file is created.
Supported operations: resize, crop, rotate, flip, sharpen,
blur, format conversion. Backend: bimg (libvips C library — used
by Netflix, AWS).
getThumbnail()
Retrieve the URL of a pre-generated thumbnail by preset name:
const thumb = await kernel.files().getThumbnail('01j9pfil0000000000000001', 'card_300');
// thumb: { url, width: 300, height: 200, format: 'webp' }
Thumbnail Presets
| Preset | Size | Auto-generated on upload? | Use case |
|---|---|---|---|
icon_32 | 32 × 32 | ✅ Yes | Icons, favicon, sidebar logo |
avatar_64 | 64 × 64 | ❌ On request | User avatars in lists |
card_300 | 300 × 200 | ✅ Yes | Card previews, table rows |
preview_600 | 600 × 400 | ❌ On request | Modal previews |
full_1200 | 1200 × auto | ❌ On request | Full-size view |
icon_32 and card_300 are generated automatically for every image
upload. All other presets are generated on first request and cached.
Deletion and Retention
// Soft delete — file moves to trash, recoverable for 30 days
await kernel.files().delete('01j9pfil0000000000000001');
// Restore from trash (within 30 days)
await kernel.files().restore('01j9pfil0000000000000001');
// Permanent delete — immediate physical removal from S3, irreversible
await kernel.files().deletePermanent('01j9pfil0000000000000001');
| Operation | Effect | Storage billing |
|---|---|---|
delete() | Soft delete — file in trash, restorable 30 days | Tenant still billed |
restore() | Restore from trash | Tenant billed |
deletePermanent() | Physical S3 deletion, irreversible | Space freed immediately |
| Auto (30 days) | Trash items older than 30 days → physical delete | Space freed |
Retention window is configurable via FILES_SOFT_DELETE_RETENTION_DAYS.
REST API Reference
For layout brevity, the /api/v1 base path prefix is omitted
from the endpoint table below.
| Method | Endpoint | Description |
|---|---|---|
POST | /files/upload | Upload file (multipart/form-data) |
POST | /files/presign | Get presigned upload URL |
GET | /files/:id | Download file |
GET | /files/:id/url | Get presigned download URL |
GET | /files | List files (paginated, filterable) |
DELETE | /files/:id | Soft delete |
DELETE | /files/:id/permanent | Permanent delete (irreversible) |
POST | /files/:id/restore | Restore from trash |
POST | /files/:id/process | Process image (resize, crop, convert) |
GET | /files/:id/thumbnail/:preset | Get thumbnail by preset name |