Skip to content

platform-blob-storage

Cloud file storage abstraction supporting pluggable strategies and optional metadata persistence via ports. Ships with Vercel Blob, Azure Blob Storage, AWS S3, local filesystem, and an empty/mock strategy out of the box.

Package: @breadstone/archipel-platform-blob-storage

npm install @breadstone/archipel-platform-blob-storage

Quick Start

typescript
import { Module } from '@nestjs/common';
import { BlobModule } from '@breadstone/archipel-platform-blob-storage';
import { VercelBlobStorageStrategy, VERCEL_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/vercel';

@Module({
  imports: [
    // Minimal setup — Vercel Blob, no metadata tracking
    BlobModule.register({
      storageStrategy: VercelBlobStorageStrategy,
      configEntries: VERCEL_CONFIG_ENTRIES,
    }),

    // Full setup — with metadata tracking
    BlobModule.register({
      storageStrategy: VercelBlobStorageStrategy,
      configEntries: VERCEL_CONFIG_ENTRIES,
      objectPersistence: PrismaBlobObjectAdapter,
      variantPersistence: PrismaBlobVariantAdapter,
      isGlobal: true,
    }),
  ],
})
export class AppModule {}

Module Configuration

IBlobModuleOptions

PropertyTypeRequiredDefaultDescription
storageStrategyType<BlobStorageStrategyBase>YesStorage strategy to use
configEntriesIConfigEntry[]No[]Config entries for the chosen strategy
objectPersistenceType<BlobObjectPersistencePort>NoMetadata tracking for uploaded objects
variantPersistenceType<BlobVariantPersistencePort>NoMetadata tracking for image variants
isGlobalbooleanNotrueRegister as a global module

Strategy Registration

typescript
import { VercelBlobStorageStrategy, VERCEL_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/vercel';
import { AzureBlobStorageStrategy, AZURE_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/azure';
import { AwsS3BlobStorageStrategy, AWS_S3_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/aws-s3';
import { LocalBlobStorageStrategy, LOCAL_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/local';
import { EmptyBlobStorageStrategy } from '@breadstone/archipel-platform-blob-storage/strategies/empty';

// Vercel Blob
BlobModule.register({ storageStrategy: VercelBlobStorageStrategy, configEntries: VERCEL_CONFIG_ENTRIES });

// Azure Blob Storage
BlobModule.register({ storageStrategy: AzureBlobStorageStrategy, configEntries: AZURE_CONFIG_ENTRIES });

// AWS S3
BlobModule.register({ storageStrategy: AwsS3BlobStorageStrategy, configEntries: AWS_S3_CONFIG_ENTRIES });

// Local filesystem
BlobModule.register({ storageStrategy: LocalBlobStorageStrategy, configEntries: LOCAL_CONFIG_ENTRIES });

// Empty/mock strategy (for development and testing — logs operations, returns mock data)
BlobModule.register({ storageStrategy: EmptyBlobStorageStrategy });

// Custom strategy — extend BlobStorageStrategyBase
BlobModule.register({ storageStrategy: MyCustomStrategy });

BlobService API

The main service for all blob operations. Inject it anywhere:

typescript
import { BlobService } from '@breadstone/archipel-platform-blob-storage';

@Injectable()
export class ImageService {
  constructor(private readonly _blob: BlobService) {}
}

Upload

typescript
// Upload from buffer
const result = await this._blob.uploadFile('avatars/user-123.png', imageBuffer, 'image/png');
console.log(result.metadata.url);

// Upload with full request
const result = await this._blob.uploadObject({
  key: 'documents/report.pdf',
  body: pdfBuffer,
  contentType: 'application/pdf',
  bucket: 'my-bucket',
});

Upload Variant

Upload a derived version of an existing blob (e.g., thumbnail):

typescript
const thumbnail = await this._blob.uploadVariant(originalBlobId, 'thumbnail-200x200', {
  key: 'avatars/user-123-thumb.png',
  body: thumbnailBuffer,
  contentType: 'image/png',
});

Download

typescript
// Download as buffer
const result = await this._blob.downloadFile<Buffer>('avatars/user-123.png', 'arraybuffer');
const buffer = result.data;

// Download as stream
const result = await this._blob.downloadFile<ReadableStream>('videos/intro.mp4', 'stream');

// Download as JSON
const result = await this._blob.downloadFile<Record<string, unknown>>('config/settings.json', 'json');

Delete

typescript
await this._blob.deleteObject({
  key: 'avatars/user-123.png',
  bucket: 'my-bucket',
});
// If objectPersistence is registered, metadata is also marked as deleted

Signed URLs

typescript
const signedUrl = await this._blob.createSignedUrl({
  key: 'private/contract.pdf',
  expiresIn: 3600, // 1 hour
});

Ports (Contracts)

BlobObjectPersistencePort

Tracks metadata for uploaded blob objects. Optional — when not provided, uploads still work but metadata is not persisted.

typescript
abstract class BlobObjectPersistencePort {
  abstract createFromMetadata(metadata: IBlobObjectMetadata): Promise<void>;
  abstract markAsDeleted(provider: string, bucket: string, key: string, versionId?: string): Promise<void>;
}

BlobVariantPersistencePort

Tracks metadata for image/document variants.

typescript
abstract class BlobVariantPersistencePort {
  abstract createFromMetadata(originalId: string, label: string, metadata: IBlobObjectMetadata): Promise<void>;
}

Real-World Adapter: Prisma

typescript
@Injectable()
export class PrismaBlobObjectAdapter extends BlobObjectPersistencePort {
  private readonly _prisma: PrismaService;

  constructor(prisma: PrismaService) {
    super();
    this._prisma = prisma;
  }

  public async createFromMetadata(metadata: IBlobObjectMetadata): Promise<void> {
    await this._prisma.blobObject.create({
      data: {
        provider: metadata.provider,
        bucket: metadata.bucket,
        key: metadata.key,
        url: metadata.url,
        size: metadata.size,
        contentType: metadata.contentType,
        versionId: metadata.versionId,
      },
    });
  }

  public async markAsDeleted(provider: string, bucket: string, key: string, versionId?: string): Promise<void> {
    await this._prisma.blobObject.updateMany({
      where: { provider, bucket, key, versionId },
      data: { deletedAt: new Date() },
    });
  }
}

Built-in Strategies

VercelBlobStorageStrategy

The Vercel Blob storage strategy using Vercel's Blob Storage API.

typescript
import { VercelBlobStorageStrategy, VERCEL_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/vercel';

BlobModule.register({
  storageStrategy: VercelBlobStorageStrategy,
  configEntries: VERCEL_CONFIG_ENTRIES,
});

Environment variables: VERCEL_BLOB_READ_WRITE_TOKEN, VERCEL_BLOB_URL, VERCEL_BLOB_PUBLIC_URL, VERCEL_BLOB_BUCKET, VERCEL_BLOB_REGION

AzureBlobStorageStrategy

Microsoft Azure Blob Storage integration using @azure/storage-blob.

typescript
import { AzureBlobStorageStrategy, AZURE_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/azure';

BlobModule.register({
  storageStrategy: AzureBlobStorageStrategy,
  configEntries: AZURE_CONFIG_ENTRIES,
});

Environment variables:

VariableRequiredDescription
AZURE_BLOB_CONNECTION_STRINGNo*Full connection string (takes precedence)
AZURE_BLOB_ACCOUNT_NAMENo*Storage account name for DefaultAzureCredential
AZURE_BLOB_CONTAINER_NAMEYesContainer to operate on

* Provide either connectionString or accountName. When accountName is set without a connection string, the strategy authenticates via DefaultAzureCredential from @azure/identity (supports managed identity, environment credentials, etc.). Install @azure/identity as a peer dependency when using this mode.

The strategy supports SAS token generation for signed URLs and lazy-loads the Azure SDK for tree-shaking.

AwsS3BlobStorageStrategy

Amazon S3 integration using @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner.

typescript
import { AwsS3BlobStorageStrategy, AWS_S3_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/aws-s3';

BlobModule.register({
  storageStrategy: AwsS3BlobStorageStrategy,
  configEntries: AWS_S3_CONFIG_ENTRIES,
});

Environment variables:

VariableRequiredDescription
AWS_S3_REGIONYesAWS region
AWS_S3_BUCKETYesS3 bucket name
AWS_S3_ACCESS_KEY_IDNo*Explicit access key
AWS_S3_SECRET_ACCESS_KEYNo*Explicit secret key
AWS_S3_ENDPOINTNoCustom endpoint (for MinIO, LocalStack, etc.)

* accessKeyId and secretAccessKey must be provided together. Omit both to use the default AWS credential chain (IAM roles, instance profiles, environment variables).

The strategy supports pre-signed URLs and lazy-loads the AWS SDK for tree-shaking. downloadObject() gracefully handles JSON parse errors, returning the raw response body when content cannot be parsed.

LocalBlobStorageStrategy

Local filesystem storage for development and testing.

typescript
import { LocalBlobStorageStrategy, LOCAL_CONFIG_ENTRIES } from '@breadstone/archipel-platform-blob-storage/strategies/local';

BlobModule.register({
  storageStrategy: LocalBlobStorageStrategy,
  configEntries: LOCAL_CONFIG_ENTRIES,
});

Environment variables:

VariableRequiredDefaultDescription
LOCAL_BLOB_BASE_PATHNo./blob-dataBase directory for local file storage
LOCAL_BLOB_BUCKETNodefaultDefault bucket subdirectory

EmptyBlobStorageStrategy

A no-op storage strategy for development and testing. Logs all operations to the NestJS logger and returns mock results. No actual storage occurs.

typescript
import { EmptyBlobStorageStrategy } from '@breadstone/archipel-platform-blob-storage/strategies/empty';

BlobModule.register({
  storageStrategy: EmptyBlobStorageStrategy,
});

No environment variables required.

Custom Strategy

Extend BlobStorageStrategyBase for any other storage backend (GCS, MinIO, etc.):

typescript
@Injectable()
export class GcsBlobStorageStrategy extends BlobStorageStrategyBase {
  public override readonly providerId = 'gcs';
  public override readonly defaultBucket = 'my-gcs-bucket';

  public async uploadObject(request: IBlobUploadRequest): Promise<IBlobUploadResult> {
    // GCS upload implementation
  }

  public async downloadObject<TData>(request: IBlobDownloadRequest): Promise<IBlobDownloadResult<TData>> {
    // GCS download implementation
  }

  public async deleteObject(request: IBlobDeleteRequest): Promise<void> {
    // GCS delete implementation
  }

  public override async createSignedUrl(request: IBlobSignedUrlRequest): Promise<string> {
    // GCS signed URL
  }
}

// Register
BlobModule.register({ storageStrategy: GcsBlobStorageStrategy });

Health Check

The BlobHealthIndicator pings the configured blob storage URL via HTTP. If no URL is configured, it reports disabled: true. Import it from the /health subpath:

typescript
import { BlobHealthIndicator } from '@breadstone/archipel-platform-blob-storage/health';
import { HealthModule } from '@breadstone/archipel-platform-health';

@Module({
  imports: [
    BlobModule.register({ /* ... */ }),
    HealthModule.withIndicators([BlobHealthIndicator]),
  ],
})
export class AppModule {}
KeyCheckDependencies
blobHTTP ping to blob URL (or disabled if unconfigured)ConfigService, HttpHealthIndicator

---

## Sub-path Imports

Each strategy is tree-shakeable via sub-path imports:

| Import Path                                                    | Contents                                                   |
| -------------------------------------------------------------- | ---------------------------------------------------------- |
| `@breadstone/archipel-platform-blob-storage`                   | Core module, service, ports, interfaces, abstract base     |
| `@breadstone/archipel-platform-blob-storage/strategies/vercel` | VercelBlobStorageStrategy, VERCEL_CONFIG_ENTRIES            |
| `@breadstone/archipel-platform-blob-storage/strategies/azure`  | AzureBlobStorageStrategy, AZURE_CONFIG_ENTRIES              |
| `@breadstone/archipel-platform-blob-storage/strategies/aws-s3` | AwsS3BlobStorageStrategy, AWS_S3_CONFIG_ENTRIES             |
| `@breadstone/archipel-platform-blob-storage/strategies/local`  | LocalBlobStorageStrategy, LOCAL_CONFIG_ENTRIES              |
| `@breadstone/archipel-platform-blob-storage/strategies/empty`  | EmptyBlobStorageStrategy (mock/logging)                     |
| `@breadstone/archipel-platform-blob-storage/health`            | BlobHealthIndicator                                        |

---

## Exports Summary

| Export                        | Type          | Description                                            |
| ----------------------------- | ------------- | ------------------------------------------------------ |
| `BlobModule`                  | NestJS Module | Main module with `register()`                          |
| `BlobService`                 | Service       | Upload, download, delete, signed URLs                  |
| `BlobStorageStrategyBase`     | Abstract      | Base class for custom storage strategies               |
| `BlobObjectPersistencePort`   | Port          | Object metadata tracking                               |
| `BlobVariantPersistencePort`  | Port          | Variant metadata tracking                              |
| `VercelBlobStorageStrategy`   | Strategy      | Vercel Blob implementation (sub-path)                  |
| `AzureBlobStorageStrategy`    | Strategy      | Azure Blob Storage implementation (sub-path)           |
| `AwsS3BlobStorageStrategy`    | Strategy      | AWS S3 implementation (sub-path)                       |
| `LocalBlobStorageStrategy`    | Strategy      | Local filesystem implementation (sub-path)             |
| `EmptyBlobStorageStrategy`    | Strategy      | Mock/logging implementation (sub-path)                 |
| `BlobHealthIndicator`         | Health        | Blob storage health check (`/health` subpath)          |
| `BlobDownloadResponseTypes`   | Constant      | `'arraybuffer'` \| `'json'` \| `'stream'` \| `'text'` |

Released under the MIT License.