Skip to main content

@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 via getUrl() until the scan completes. upload() resolves as soon as the file is queued for scanning. Poll getMetadata(fileId) to check status: 'available' before presenting the URL to end users.

Staging Limits

ParameterValue
Staging file TTL24 hours (FILES_STAGING_TTL_HOURS). After 24h → auto-rejected + notify uploader
Max files in staging per tenant100 (FILES_STAGING_MAX_PER_TENANT). Exceeded → 503 Service Unavailable
AV scan queueFIFO by default. When backlog > 1,000 → newest files first (priority inversion)

Bucket Reference

BucketS3 path prefixPurpose
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 are pending and 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

PresetSizeAuto-generated on upload?Use case
icon_3232 × 32✅ YesIcons, favicon, sidebar logo
avatar_6464 × 64❌ On requestUser avatars in lists
card_300300 × 200✅ YesCard previews, table rows
preview_600600 × 400❌ On requestModal previews
full_12001200 × auto❌ On requestFull-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');
OperationEffectStorage billing
delete()Soft delete — file in trash, restorable 30 daysTenant still billed
restore()Restore from trashTenant billed
deletePermanent()Physical S3 deletion, irreversibleSpace freed immediately
Auto (30 days)Trash items older than 30 days → physical deleteSpace freed

Retention window is configurable via FILES_SOFT_DELETE_RETENTION_DAYS.


REST API Reference

Base Path

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint table below.

MethodEndpointDescription
POST/files/uploadUpload file (multipart/form-data)
POST/files/presignGet presigned upload URL
GET/files/:idDownload file
GET/files/:id/urlGet presigned download URL
GET/filesList files (paginated, filterable)
DELETE/files/:idSoft delete
DELETE/files/:id/permanentPermanent delete (irreversible)
POST/files/:id/restoreRestore from trash
POST/files/:id/processProcess image (resize, crop, convert)
GET/files/:id/thumbnail/:presetGet thumbnail by preset name