ObjectOS
Configure

Storage

Where ObjectOS puts files — local disk, S3, R2, MinIO, Spaces.

Storage

ObjectOS files (attachments, uploads, generated documents) flow through the storage service — a pluggable abstraction with two adapters: local filesystem (default) and S3-compatible (production).

The service is provided by @objectstack/service-storage and is enabled by default in standalone and project boots.

How users interact with it

SurfaceBehavior
Console file/image fieldsBrowser uploads directly to storage via presigned URLs
REST /api/v1/storage/*Programmatic upload/download endpoints
Object attachment / image fieldsRender as upload widgets; metadata persists in sys_file

Files are tracked in the sys_file system object — never as raw paths in your records. That decouples your data model from the storage backend.

Local filesystem (default)

Good for: development, single-node deployments, demos.

// objectstack.config.ts (or wherever you assemble plugins)
import { StorageServicePlugin } from '@objectstack/service-storage';

new StorageServicePlugin({
  adapter: 'local',
  local: {
    rootDir: './uploads',
    baseUrl: 'http://localhost:3000',  // for presigned URLs
    signingSecret: process.env.OS_STORAGE_SIGNING_SECRET, // optional; auto-generated if omitted
  },
  presignedTtl: 3600,   // seconds — TTL for presigned URLs
  sessionTtl: 86400,    // seconds — TTL for chunked upload sessions
});

In standalone mode (os start with no project), the runtime configures local storage automatically under ~/.objectstack/data/uploads/.

Production fit depends on the deployment shape:

  • ✅ Desktop apps, single-node internal tools, edge / on-prem appliances — local storage is fine, as long as the uploads/ directory is included in your filesystem backup (or lives on a user-controlled sync folder for desktop apps).
  • ❌ Multi-node, multi-AZ, or anything that needs cross-region durability — use S3-compatible storage.

S3-compatible (production)

Good for: production, multi-node, durability + lifecycle management.

pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
import { StorageServicePlugin } from '@objectstack/service-storage';

new StorageServicePlugin({
  adapter: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    // omit credentials to use the AWS SDK's default chain
    // (env, ~/.aws, IAM role)
  },
});

The AWS SDK reads credentials from its normal chain:

SourceEnv var
Standard envAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
Session tokenAWS_SESSION_TOKEN
Shared config~/.aws/credentials, AWS_PROFILE
IAM roleAuto on EC2 / ECS / EKS / Lambda — no config

Cloudflare R2

new StorageServicePlugin({
  adapter: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'auto',
    endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
    forcePathStyle: false,
  },
});

Credentials: R2 access key ID + secret, passed via the standard AWS_* env vars or your secret manager.

MinIO (self-hosted)

new StorageServicePlugin({
  adapter: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    endpoint: 'http://minio.internal:9000',
    forcePathStyle: true,
  },
});

DigitalOcean Spaces

new StorageServicePlugin({
  adapter: 's3',
  s3: {
    bucket: 'my-bucket',
    region: 'nyc3',
    endpoint: 'https://nyc3.digitaloceanspaces.com',
    forcePathStyle: false,
  },
});

S3 bucket policy

Storage uses presigned PUT/GET URLs. Recommended bucket policy:

  • Block all public access.
  • CORS: allow PUT/GET from your ObjectOS hostname(s).
  • Lifecycle: expire incomplete multipart uploads after 1–7 days; expire objects tagged temp=true after 24 hours.
  • Versioning + Object Lock: optional, recommended for compliance deployments.

REST surface

@objectstack/client calls these — you usually don't hit them directly:

MethodPathPurpose
POST/api/v1/storage/upload/presignedGet a presigned upload URL
POST/api/v1/storage/upload/completeCommit a finished upload
POST/api/v1/storage/upload/chunkedBegin chunked upload
PUT/api/v1/storage/upload/chunked/:uploadId/chunk/:iUpload a chunk
POST/api/v1/storage/upload/chunked/:uploadId/completeFinish chunked upload
GET/api/v1/storage/upload/chunked/:uploadId/progressPoll progress
GET/api/v1/storage/files/:fileId/urlGet a presigned download URL

Per-file authorization is handled by the security plugin's permission evaluator against the sys_file object — you don't need separate storage-layer ACLs.

Live configuration

When the settings service is enabled (it is by default), an admin can swap storage adapter in Console → Configuration → Storage without restarting:

  • pick adapter, bucket, region, endpoint;
  • paste credentials (encrypted at rest in sys_setting);
  • click Test connection before saving.

The change applies to the next request — no restart needed.

Sizing

ResourceDefaultTunable
Presigned URL TTL1 hourpresignedTtl plugin option
Chunked upload session TTL24 hourssessionTtl plugin option
Max single-part uploadBackend-imposed (S3 = 5 GB)
Max chunked uploadBackend-imposed (S3 = 5 TB)

Where to go next

On this page