Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. TypescriptSeparator
  4. Storage

Storage

Use @putnami/storage for object storage with pluggable backends, bucket declarations, presigned URLs, and a local development server. Store and serve files like avatars, documents, and media with type-safe constraints and environment-adaptive configuration.

Getting started

Installation

bunx putnami deps add @putnami/storage

Enable the storage plugin

import { application, http } from '@putnami/application';
import { storagePlugin, storageServer } from '@putnami/storage';

export const app = () =>
  application()
    .use(http({ port: 3000 }))
    .use(storagePlugin())
    .use(storageServer()); // local dev server for signed URLs

Storage configuration

Configure the backend in your environment files:

# .env.local.yaml — local development
storage:
  backend: file
  dataDir: .data/storage

# .env.production.yaml — production
storage:
  backend: remote
  endpoint: https://storage.putnami.cloud
  accessKey: ${STORAGE_ACCESS_KEY}
  secretKey: ${STORAGE_SECRET_KEY}

# .env.test.yaml — tests
storage:
  backend: memory

Configuration options

storage:
  backend: file                # Backend type: 'remote', 'file', or 'memory'
  endpoint: https://storage.putnami.cloud  # Remote backend URL
  accessKey: ''                # Access key for remote auth
  secretKey: ''                # Secret key for remote auth
  dataDir: .data/storage       # Data directory for file backend
  slowOperationThresholdMs: 0  # Warn on slow operations (ms, 0 = disabled)

Declaring buckets

Each project can declare any number of buckets. Buckets register globally when their module is evaluated, like Table() in @putnami/sql.

Basic bucket

import { Bucket } from '@putnami/storage';

export const AvatarsBucket = Bucket('avatars', {
  maxFileSize: '10mb',
  allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
  public: true,
});

Private bucket

import { Bucket } from '@putnami/storage';

export const InvoicesBucket = Bucket('invoices', {
  maxFileSize: '50mb',
  allowedMimeTypes: ['application/pdf'],
  public: false,
});

Bucket options

  • maxFileSize — Maximum file size (e.g., '10mb', '1.5gb'). Validated on upload.
  • allowedMimeTypes — Allowed content types. Validated on upload.
  • public — Whether objects are publicly readable (default: false).
  • storage — Named storage backend for per-bucket config override.

File size format

Supported units: b, kb, mb, gb, tb. Decimals supported (e.g., '1.5gb').

Using the client

Get a StorageClient for a bucket by name. The client wraps the active backend with validation and observability.

Basic operations

import { storage } from '@putnami/storage';

const avatars = await storage('avatars');

// Upload
await avatars.put('user-123/photo.png', file, { contentType: 'image/png' });

// Download
const result = await avatars.get('user-123/photo.png');

// Check existence
const exists = await avatars.exists('user-123/photo.png');

// Copy
await avatars.copy('user-123/photo.png', 'user-123/photo-backup.png');

// List objects
const list = await avatars.list({ prefix: 'user-123/', delimiter: '/' });

// Delete
await avatars.delete('user-123/photo.png');

Public URL

For public buckets, get a direct URL:

const url = avatars.url('user-123/photo.png');
// Production: https://storage.putnami.cloud/avatars/user-123/photo.png
// Local dev:  .data/storage/avatars/user-123/photo.png

Upload metadata

Attach content type, cache headers, and custom metadata:

await avatars.put('user-123/photo.png', file, {
  contentType: 'image/png',
  cacheControl: 'public, max-age=31536000',
  custom: { uploadedBy: 'user-123', originalName: 'selfie.png' },
});

Connection lifecycle

import { storage, closeStorage, closeAllStorage } from '@putnami/storage';

// Get or create client
const avatars = await storage('avatars');

// Close a specific client
await closeStorage('avatars');

// Close all clients (on shutdown — handled by the plugin)
await closeAllStorage();

Signed URLs

Signed URLs let browsers upload to and download from storage directly, bypassing the application server.

Upload flow

Browser  ─── 1. Request signed URL ──▸  App Server
         ◂── 2. Return signed URL ───
         ─── 3. PUT directly ────────▸  storage.putnami.cloud
import { storage } from '@putnami/storage';

// Server-side: generate signed upload URL
const avatars = await storage('avatars');
const { url, headers, expiresAt } = await avatars.signedUploadUrl('user-123/photo.png', {
  expiresIn: '15m',
  contentType: 'image/png',
});

// Return url + headers to the browser
// Client-side: upload directly
await fetch(url, { method: 'PUT', headers, body: file });

Download flow

// Server-side: generate signed download URL
const invoices = await storage('invoices');
const { url } = await invoices.signedDownloadUrl('inv-2025-001.pdf', {
  expiresIn: '1h',
  contentDisposition: 'attachment; filename="invoice.pdf"',
});

// Redirect or return the URL to the browser

Sign options

  • expiresIn — URL validity duration (e.g., '15m', '1h', '7d')
  • maxFileSize — Max upload size
  • allowedMimeTypes — Restrict upload content types
  • contentType — Expected content type for upload
  • contentDisposition — Disposition header for download
  • metadata — Custom metadata to attach to uploaded object

Security

Signed URLs for the file backend are HMAC-SHA256 signed with a per-process random secret. Tokens cannot be forged, reused across processes, or applied to a different bucket/key. Object keys are validated against path traversal attacks (../ sequences, absolute paths, null bytes). Upload size is validated against the actual request body.

Backends

Remote backend

HTTP client for storage.putnami.cloud. Used in production.

storage:
  backend: remote
  endpoint: https://storage.putnami.cloud
  accessKey: ${STORAGE_ACCESS_KEY}
  secretKey: ${STORAGE_SECRET_KEY}

File backend

Stores objects on the local filesystem. Used in local development.

storage:
  backend: file
  dataDir: .data/storage

Directory layout:

.data/storage/
  avatars/
    user-123/
      photo.png           # Object data
      photo.png.meta.json # Metadata sidecar
  invoices/
    inv-2025-001.pdf
    inv-2025-001.pdf.meta.json

Memory backend

In-memory storage with zero I/O. Used in tests.

storage:
  backend: memory
import { MemoryBackend, StorageClient, Bucket } from '@putnami/storage';

const TestBucket = Bucket('test-uploads', {
  maxFileSize: '5mb',
  allowedMimeTypes: ['image/png'],
});

const backend = new MemoryBackend();
const client = new StorageClient(TestBucket, backend);

// Use in tests
await client.put('test.png', new Blob(['data'], { type: 'image/png' }), {
  contentType: 'image/png',
});

// Reset between tests
backend.clear();

Local development server

The storageServer() plugin mounts /_storage/* routes on the application's HTTP server. This handles signed URL uploads and downloads when using the file backend locally.

import { storageServer } from '@putnami/storage';

app.use(storageServer({ dataDir: '.data/storage' }));

In production, signed URLs point to storage.putnami.cloud. In local dev, they point to /_storage/* on localhost, where storageServer() handles them.

Environment Signed URL target
Production https://storage.putnami.cloud/avatars/user-123/photo.png?signature=...
Local dev http://localhost:3000/_storage/avatars/user-123/photo.png?token=...
Tests memory://avatars/user-123/photo.png (not HTTP, direct client use)

Named storage backends

Like database.analytics in @putnami/sql, buckets can use named backends for per-bucket configuration:

const ArchiveBucket = Bucket('archive', {
  storage: 'cold', // Uses 'storage.cold' config path
});
storage:
  backend: remote
  endpoint: https://storage.putnami.cloud

storage.cold:
  backend: remote
  endpoint: https://archive.putnami.cloud

Bucket validation

The StorageClient validates constraints before uploading:

import { bucketHelper } from '@putnami/storage';

const helper = bucketHelper(AvatarsBucket);

// Programmatic validation
const errors = helper.validate({ size: 20_000_000, mimeType: 'image/gif' });
// ['File size 20000000 bytes exceeds maximum 10mb',
//  'MIME type "image/gif" is not allowed. Allowed: image/png, image/jpeg, image/webp']

Validation runs automatically on client.put() — invalid uploads throw StorageValidationError.

Observability

All storage operations emit structured logs and telemetry metrics. When the telemetry() plugin is active, storage metrics are collected alongside HTTP and SQL metrics.

Metrics emitted

Metric Type Description
storage.{operation}.{bucket} Counter Operation count per bucket
storage.{operation}.{bucket}.duration Histogram Operation duration (ms)
storage.operation.duration Histogram Aggregate across all buckets
storage.{operation}.bytes Histogram Bytes transferred
storage.operation.error Counter Aggregate error count
storage.operation.slow Counter Operations exceeding slow threshold
storage.client.created Counter Client creation events
storage.client.closed Counter Client close events
storage.client.count Gauge Current active client count

Structured logging

[storage] put avatars { operation: "put", bucket: "avatars", duration: 45, bytes: 102400, key: "user-123/photo.png" }
[storage] get invoices { operation: "get", bucket: "invoices", duration: 12, key: "inv-2025-001.pdf" }

Slow operation detection

storage:
  slowOperationThresholdMs: 500
[storage] Slow storage operation: put avatars (820ms >= 500ms)

Related guides

  • Persistence
  • Plugins & lifecycle
  • Configuration
  • Telemetry
  • Testing

On this page

  • Storage
  • Getting started
  • Installation
  • Enable the storage plugin
  • Storage configuration
  • Configuration options
  • Declaring buckets
  • Basic bucket
  • Private bucket
  • Bucket options
  • File size format
  • Using the client
  • Basic operations
  • Public URL
  • Upload metadata
  • Connection lifecycle
  • Signed URLs
  • Upload flow
  • Download flow
  • Sign options
  • Security
  • Backends
  • Remote backend
  • File backend
  • Memory backend
  • Local development server
  • Named storage backends
  • Bucket validation
  • Observability
  • Metrics emitted
  • Structured logging
  • Slow operation detection
  • Related guides