Drivers

Built-in storage providers and guide to creating custom drivers

Storage providers (also called drivers) are responsible for storing and retrieving data from various backends. Vercube includes built-in providers and makes it easy to create custom ones for any storage backend.

Built-in Providers

MemoryStorage

The MemoryStorage provider stores data in memory. It's fast and ideal for caching and temporary data storage.

Usage:

import { StorageManager } from '@vercube/storage';
import { MemoryStorage } from '@vercube/storage/drivers/MemoryStorage';

container.bind(StorageManager);

const storageManager = container.get(StorageManager);
storageManager.mount({
  name: 'cache',
  storage: MemoryStorage
});

// Store data
await storageManager.setItem({
  storage: 'cache',
  key: 'mykey',
  value: { foo: 'bar' }
});

// Retrieve data
const value = await storageManager.getItem({
  storage: 'cache',
  key: 'mykey'
});
console.log(value); // { foo: 'bar' }

Characteristics:

FeatureDescription
PersistenceNone - data is lost on restart
SpeedExtremely fast
Memory UsageGrows with stored data
Use CasesCaching, temporary data, development

Best Practices:

  • Use for caching frequently accessed data
  • Ideal for development and testing
  • Clear periodically to prevent memory leaks
  • Consider size limits for production use

S3Storage

The S3Storage provider stores data in AWS S3 (or S3-compatible services like MinIO, DigitalOcean Spaces, etc.). It's ideal for distributed applications, serverless environments, and persistent cloud storage.

Installation:

You need to install the AWS SDK for S3:

$ pnpm add @aws-sdk/client-s3

Basic Usage:

import { StorageManager } from '@vercube/storage';
import { S3Storage } from '@vercube/storage/drivers/S3Storage';

container.bind(StorageManager);

const storageManager = container.get(StorageManager);

// Recommended: Use IAM roles (no credentials needed)
storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-app-bucket',
    region: 'us-east-1'
    // Credentials are optional - IAM role is used if available
  }
});

// Alternative: Use explicit credentials for local development
// storageManager.mount({
//   name: 's3',
//   storage: S3Storage,
//   initOptions: {
//     bucket: 'my-app-bucket',
//     region: 'us-east-1',
//     credentials: {
//       accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
//       secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
//     }
//   }
// });

// Store data
await storageManager.setItem({
  storage: 's3',
  key: 'user:123',
  value: { id: '123', name: 'John' }
});

// Retrieve data
const user = await storageManager.getItem({
  storage: 's3',
  key: 'user:123'
});

Configuration Options:

The initOptions extends AWS S3ClientConfig with an additional bucket property:

OptionTypeRequiredDescription
bucketstringYesS3 bucket name
regionstringYesAWS region (e.g., 'us-east-1')
credentialsobjectNoAWS credentials (accessKeyId, secretAccessKey, sessionToken). If omitted, AWS SDK uses the default credential provider chain (IAM roles, environment variables, config files)
endpointstringNoCustom endpoint for S3-compatible services
forcePathStylebooleanNoUse path-style URLs (required for some S3-compatible services like MinIO)

Note: When credentials is omitted, the AWS SDK automatically attempts to load credentials from:

  1. IAM roles (Lambda execution role, EC2 instance profile, ECS task role)
  2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  3. Shared credentials file (~/.aws/credentials)
  4. ECS container credentials
  5. EC2 instance metadata service

Using with S3-Compatible Services:

// MinIO
storageManager.mount({
  name: 'minio',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-bucket',
    region: 'us-east-1',
    endpoint: 'http://localhost:9000',
    credentials: {
      accessKeyId: 'minioadmin',
      secretAccessKey: 'minioadmin'
    },
    forcePathStyle: true // Required for MinIO
  }
});

// DigitalOcean Spaces
storageManager.mount({
  name: 'spaces',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-space',
    region: 'nyc3',
    endpoint: 'https://nyc3.digitaloceanspaces.com',
    credentials: {
      accessKeyId: process.env.DO_SPACES_KEY,
      secretAccessKey: process.env.DO_SPACES_SECRET
    }
  }
});

// Cloudflare R2
storageManager.mount({
  name: 'r2',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-bucket',
    region: 'auto',
    endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY_ID,
      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
    }
  }
});

Characteristics:

FeatureDescription
PersistenceYes - data persists across restarts
SpeedNetwork-dependent (slower than memory)
ScalabilityHighly scalable, distributed
Use CasesProduction storage, file storage, distributed apps

Authentication Methods:

The S3Storage driver supports multiple authentication methods, with different security characteristics suitable for various environments.

When running in AWS environments (Lambda, EC2, ECS, etc.), IAM roles provide the most secure authentication method. Credentials are automatically managed by AWS without manual handling.

// AWS Lambda - No credentials needed!
storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-app-bucket',
    region: 'us-east-1'
    // No credentials field - IAM role is used automatically
  }
});

How it works:

  • AWS SDK automatically uses the IAM role attached to your Lambda function, EC2 instance, or ECS task
  • Credentials are temporary and automatically rotated by AWS
  • No secrets to manage or risk exposing in code

Setup:

  1. Attach an IAM role to your AWS resource (Lambda function, EC2 instance, etc.)
  2. Grant the role S3 permissions (e.g., s3:GetObject, s3:PutObject)
  3. Omit the credentials field in initOptions

For applications that require explicit credentials, use AWS Secrets Manager to store and retrieve them securely.

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

async function getS3Credentials() {
  const client = new SecretsManagerClient({ region: 'us-east-1' });
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: 'my-app/s3-credentials' })
  );
  
  if (!response.SecretString) {
    throw new Error('Secret not found or uses SecretBinary');
  }
  
  return JSON.parse(response.SecretString);
}

// In your container setup
const credentials = await getS3Credentials();

storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-app-bucket',
    region: 'us-east-1',
    credentials: {
      accessKeyId: credentials.accessKeyId,
      secretAccessKey: credentials.secretAccessKey
    }
  }
});

Benefits:

  • Credentials are encrypted at rest and in transit
  • Centralized secret management
  • Automatic rotation capabilities
  • Audit logging of secret access
3. STS Temporary Credentials (For Cross-Account or Federated Access)

Use AWS Security Token Service (STS) to assume roles and obtain temporary credentials.

import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts';

async function assumeRole() {
  const stsClient = new STSClient({ region: 'us-east-1' });
  const response = await stsClient.send(
    new AssumeRoleCommand({
      RoleArn: 'arn:aws:iam::123456789012:role/S3AccessRole',
      RoleSessionName: 'vercube-s3-session',
      DurationSeconds: 3600 // 1 hour
    })
  );
  
  const { Credentials } = response;
  if (!Credentials?.AccessKeyId || !Credentials?.SecretAccessKey || !Credentials?.SessionToken) {
    throw new Error('Failed to assume role - incomplete credentials returned');
  }
  
  return {
    accessKeyId: Credentials.AccessKeyId,
    secretAccessKey: Credentials.SecretAccessKey,
    sessionToken: Credentials.SessionToken
  };
}

// Get temporary credentials
const credentials = await assumeRole();

storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-app-bucket',
    region: 'us-east-1',
    credentials
  }
});

Use cases:

  • Cross-account S3 access
  • Federated user access
  • Time-limited access requirements
  • Enhanced security through temporary credentials
4. Environment Variables (For Development)

For local development, you can use environment variables. However, this method should be avoided in production.

// .env file
// AWS_ACCESS_KEY_ID=your-access-key
// AWS_SECRET_ACCESS_KEY=your-secret-key

storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: 'my-app-bucket',
    region: 'us-east-1',
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
    }
  }
});

Important: Never commit credentials to source control. Use .gitignore to exclude .env files.

Environment-Specific Configuration:

Configure different authentication methods based on your environment:

import { StorageManager } from '@vercube/storage';
import { S3Storage } from '@vercube/storage/drivers/S3Storage';

const isProduction = process.env.NODE_ENV === 'production';
const isLambda = !!process.env.AWS_LAMBDA_FUNCTION_NAME;

const storageManager = container.get(StorageManager);

if (isLambda || isProduction) {
  // Production: Use IAM roles (no credentials needed)
  storageManager.mount({
    name: 's3',
    storage: S3Storage,
    initOptions: {
      bucket: process.env.S3_BUCKET!,
      region: process.env.AWS_REGION || 'us-east-1'
      // IAM role provides credentials automatically
    }
  });
} else {
  // Development: Use local credentials
  storageManager.mount({
    name: 's3',
    storage: S3Storage,
    initOptions: {
      bucket: process.env.S3_BUCKET || 'dev-bucket',
      region: process.env.AWS_REGION || 'us-east-1',
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
      }
    }
  });
}

Best Practices:

  • Use IAM roles in production - Most secure method for AWS environments
  • Never hardcode credentials - Always use environment variables, Secrets Manager, or IAM roles
  • Use Secrets Manager - For storing credentials that can't use IAM roles
  • Rotate credentials regularly - If using static credentials, rotate them periodically
  • Apply least privilege - Grant only the S3 permissions your application needs
  • Enable S3 bucket encryption - Encrypt data at rest using S3-managed or KMS keys
  • Use VPC endpoints - For enhanced security when accessing S3 from within AWS VPC
  • Implement caching layer - Use MemoryStorage for frequently accessed data to reduce S3 calls
  • Use appropriate S3 bucket policies - Restrict access to authorized principals only
  • Consider S3 storage classes - Optimize costs based on access patterns
  • Monitor access logs - Enable S3 access logging for security auditing

Creating Custom Providers

Creating a custom provider allows you to store data in any backend: databases, cloud storage, external services, etc.

Basic Provider Template

import { Storage } from '@vercube/storage';
import type { StorageTypes } from '@vercube/storage';

export class CustomStorage extends Storage {
  private client: any; // Your storage client
  
  /**
   * Initialize the storage with options
   */
  public async initialize(options?: StorageTypes.Options): Promise<void> {
    // Setup code: connect to service, initialize client, etc.
    this.client = await connectToService(options);
  }
  
  /**
   * Retrieve a value by key
   */
  public async getItem<T>(key: string): Promise<T | null> {
    const data = await this.client.get(key);
    return data ? JSON.parse(data) : null;
  }
  
  /**
   * Store a value with a key
   */
  public async setItem<T>(key: string, value: T): Promise<void> {
    await this.client.set(key, JSON.stringify(value));
  }
  
  /**
   * Check if a key exists
   */
  public async hasItem(key: string): Promise<boolean> {
    return this.client.has(key);
  }
  
  /**
   * Remove a value by key
   */
  public async deleteItem(key: string): Promise<void> {
    await this.client.delete(key);
  }
  
  /**
   * Get all keys
   */
  public async getKeys(): Promise<string[]> {
    return this.client.keys();
  }
  
  /**
   * Remove all items
   */
  public async clear(): Promise<void> {
    await this.client.clear();
  }
  
  /**
   * Get the number of stored items
   */
  public async size(): Promise<number> {
    return this.client.size();
  }
}

Required Methods

Every custom storage provider must implement these methods:

MethodDescription
initialize(options)Initialize the storage with configuration options
getItem<T>(key)Retrieve a value by key
setItem<T, U>(key, value, options?)Store a value with a key and optional options
hasItem(key)Check if a key exists
deleteItem(key)Remove a value by key
getKeys()Get all keys
clear()Remove all items
size()Get the number of stored items

Example: Redis Storage

A complete example of a Redis storage provider:

import { Storage } from '@vercube/storage';
import type { StorageTypes } from '@vercube/storage';
import Redis from 'ioredis';

interface RedisStorageOptions {
  host: string;
  port: number;
  password?: string;
  db?: number;
  keyPrefix?: string;
}

export class RedisStorage extends Storage {
  private redis!: Redis;
  private prefix: string = '';
  
  public async initialize(options: RedisStorageOptions): Promise<void> {
    this.redis = new Redis({
      host: options.host,
      port: options.port,
      password: options.password,
      db: options.db || 0
    });
    
    this.prefix = options.keyPrefix || '';
  }
  
  private getKey(key: string): string {
    return this.prefix + key;
  }
  
  public async getItem<T>(key: string): Promise<T | null> {
    const data = await this.redis.get(this.getKey(key));
    return data ? JSON.parse(data) : null;
  }
  
  public async setItem<T>(key: string, value: T): Promise<void> {
    await this.redis.set(this.getKey(key), JSON.stringify(value));
  }
  
  public async hasItem(key: string): Promise<boolean> {
    const exists = await this.redis.exists(this.getKey(key));
    return exists === 1;
  }
  
  public async deleteItem(key: string): Promise<void> {
    await this.redis.del(this.getKey(key));
  }
  
  public async getKeys(): Promise<string[]> {
    const keys = await this.redis.keys(this.prefix + '*');
    return keys.map(k => k.replace(this.prefix, ''));
  }
  
  public async clear(): Promise<void> {
    const keys = await this.redis.keys(this.prefix + '*');
    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
  
  public async size(): Promise<number> {
    const keys = await this.redis.keys(this.prefix + '*');
    return keys.length;
  }
}

Usage:

import { StorageManager } from '@vercube/storage';
import { RedisStorage } from './storages/RedisStorage';

// Register custom storage in container
container.bind(RedisStorage);

const storageManager = container.get(StorageManager);

// Mount custom storage
storageManager.mount({
  name: 'redis',
  storage: RedisStorage,
  initOptions: {
    host: 'localhost',
    port: 6379,
    keyPrefix: 'myapp:'
  }
});

// Use it like any other storage
await storageManager.setItem({
  storage: 'redis',
  key: 'user:123',
  value: { name: 'John' }
});

Example: File System Storage

A storage provider that persists data to the file system:

import { Storage } from '@vercube/storage';
import type { StorageTypes } from '@vercube/storage';
import * as fs from 'fs/promises';
import * as path from 'path';

interface FileStorageOptions {
  directory: string;
}

export class FileStorage extends Storage {
  private directory!: string;
  
  public async initialize(options: FileStorageOptions): Promise<void> {
    this.directory = options.directory;
    
    // Ensure directory exists
    await fs.mkdir(this.directory, { recursive: true });
  }
  
  private getFilePath(key: string): string {
    // Sanitize key for use as filename
    const safeKey = key.replace(/[^a-zA-Z0-9-_:]/g, '_');
    return path.join(this.directory, `${safeKey}.json`);
  }
  
  public async getItem<T>(key: string): Promise<T | null> {
    try {
      const filePath = this.getFilePath(key);
      const data = await fs.readFile(filePath, 'utf-8');
      return JSON.parse(data);
    } catch (error) {
      return null;
    }
  }
  
  public async setItem<T>(key: string, value: T): Promise<void> {
    const filePath = this.getFilePath(key);
    await fs.writeFile(filePath, JSON.stringify(value, null, 2));
  }
  
  public async hasItem(key: string): Promise<boolean> {
    try {
      await fs.access(this.getFilePath(key));
      return true;
    } catch {
      return false;
    }
  }
  
  public async deleteItem(key: string): Promise<void> {
    try {
      await fs.unlink(this.getFilePath(key));
    } catch {
      // Ignore if file doesn't exist
    }
  }
  
  public async getKeys(): Promise<string[]> {
    const files = await fs.readdir(this.directory);
    return files
      .filter(f => f.endsWith('.json'))
      .map(f => f.replace('.json', ''));
  }
  
  public async clear(): Promise<void> {
    const files = await fs.readdir(this.directory);
    for (const file of files) {
      if (file.endsWith('.json')) {
        await fs.unlink(path.join(this.directory, file));
      }
    }
  }
  
  public async size(): Promise<number> {
    const keys = await this.getKeys();
    return keys.length;
  }
}

Usage:

storageManager.mount({
  name: 'files',
  storage: FileStorage,
  initOptions: {
    directory: './data/storage'
  }
});

await storageManager.setItem({
  storage: 'files',
  key: 'config',
  value: { theme: 'dark' }
});

Provider Configuration

Register Provider in Container

If your provider has dependencies, register it in the container:

import { Container } from '@vercube/di';
import { CustomStorage } from './storages/CustomStorage';

export function setupContainer(container: Container): void {
  // Register provider
  container.bind(CustomStorage);
  
  // Configure storage manager to use it
  const storageManager = container.get(StorageManager);
  storageManager.mount({
    name: 'custom',
    storage: CustomStorage,
    initOptions: { ... }
  });
}

Provider with Dependencies

If your provider needs other services:

import { Inject } from '@vercube/di';
import { Storage } from '@vercube/storage';
import { Logger } from '@vercube/logger';

export class LoggingStorage extends Storage {
  @Inject(Logger)
  private logger!: Logger;
  
  private innerStorage!: Storage;
  
  public async initialize(options: { storage: Storage }): Promise<void> {
    this.innerStorage = options.storage;
  }
  
  public async getItem<T>(key: string): Promise<T | null> {
    this.logger.debug(`Getting item: ${key}`);
    const value = await this.innerStorage.getItem<T>(key);
    this.logger.debug(`Got item: ${key}`, { found: value !== null });
    return value;
  }
  
  public async setItem<T>(key: string, value: T): Promise<void> {
    this.logger.debug(`Setting item: ${key}`);
    await this.innerStorage.setItem(key, value);
    this.logger.info(`Item set: ${key}`);
  }
  
  // ... implement other methods with logging
}

Async Initialization

If your provider needs async initialization:

export class AsyncStorage extends Storage {
  private connection: Connection;
  
  public async initialize(options: ConnectionOptions): Promise<void> {
    // Async initialization is fully supported
    this.connection = await connectToService(options);
    
    // Wait for connection to be ready
    await this.connection.ready();
  }
  
  // ... implement other methods
}

Using Multiple Providers

Configure multiple providers for different use cases:

import { StorageManager } from '@vercube/storage';
import { MemoryStorage } from '@vercube/storage/drivers/MemoryStorage';
import { S3Storage } from '@vercube/storage/drivers/S3Storage';
import { RedisStorage } from './storages/RedisStorage';

const storageManager = container.get(StorageManager);

// Memory for fast local cache
storageManager.mount({
  name: 'cache',
  storage: MemoryStorage
});

// S3 for persistent cloud storage
storageManager.mount({
  name: 's3',
  storage: S3Storage,
  initOptions: {
    bucket: process.env.S3_BUCKET,
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    }
  }
});

// Redis for distributed cache
storageManager.mount({
  name: 'distributed',
  storage: RedisStorage,
  initOptions: {
    host: process.env.REDIS_HOST,
    port: 6379,
    keyPrefix: 'app:'
  }
});

Troubleshooting

Provider Not Storing Data

Problem: setItem() works but getItem() returns null

Solutions:

  1. Check serialization/deserialization:
// Make sure data can be serialized
await storage.setItem('key', {
  date: new Date().toISOString(), // ✅ Serialize dates as strings
  // date: new Date(), // ❌ May not deserialize correctly
});
  1. Verify key format is consistent:
// Use consistent key formatting
await storage.setItem('user:123', data);
const value = await storage.getItem('user:123'); // ✅ Same key

// Not this:
await storage.setItem('user:123', data);
const value = await storage.getItem('user-123'); // ❌ Different key

Provider Initialization Error

Problem: "Failed to initialize storage provider"

Solutions:

  1. Check initialize() method doesn't throw:
public async initialize(options: any): Promise<void> {
  try {
    // initialization code
  } catch (error) {
    console.error('Storage init failed:', error);
    // Don't throw - handle gracefully
  }
}
  1. Verify provider is bound in container:
container.bind(CustomStorage);
  1. Check provider initOptions:
storageManager.mount({
  name: 'custom',
  storage: CustomStorage,
  initOptions: {
    // Make sure all required options are provided
    requiredOption: 'value'
  }
});

Provider Missing Dependency

Problem: Injected dependency is undefined

Solution: Make sure dependency is registered before storage configuration:

// Register dependencies first
container.bind(Logger);
container.bind(CustomStorage);

// Then configure storage
container.bind(StorageManager);
const storageManager = container.get(StorageManager);
storageManager.mount({
  name: 'custom',
  storage: CustomStorage
});

Async Provider Not Working

Problem: Async initialize() doesn't complete before use

Solution: StorageManager handles async initialization automatically. Make sure you're using await when needed:

// Initialize is called automatically when mounting
storageManager.mount({
  name: 'async',
  storage: AsyncStorage,
  initOptions: { ... }
});

// Storage is ready to use after mount
await storageManager.setItem({
  storage: 'async',
  key: 'mykey',
  value: 'myvalue'
});
Previous

API

Complete API reference for Storage module

Next