Toast
Contributor

Overview

How Toast loads pluggable infrastructure drivers for email and storage.

Toast uses a driver architecture for pluggable infrastructure such as email delivery and file storage.

Drivers are npm packages that implement typed interfaces from @toast/drivers, but the app does not let drivers read env directly. Configuration is parsed centrally in the API config layer and injected into the driver loaders.

ADR: See ADR-009: Driver Architecture for the design rationale.

Current loading model

config modules

driver loader config objects

getEmailDriver(...) / getStorageDriver(...)

dynamic import of driver package

cached process-level driver instance

Why config is injected

The loader APIs deliberately take config objects:

  • env parsing stays in config modules only
  • drivers become easier to test
  • app code can construct drivers explicitly at startup

This follows the same dependency-injection rules as the rest of the codebase.

Example loader shape

const emailDriver = await getEmailDriver({
  driverName: config.drivers.emailDriver,
  isProduction: config.environment.nodeEnv === 'production',
  driverConfig: config.drivers.mailgun,
});
const storageDriver = await getStorageDriver({
  driverName: config.drivers.storageDriver,
  driverConfig: config.drivers.s3,
});

Key properties

  • npm-package based — drivers are installed as packages
  • typed interfaces — each driver implements a shared contract
  • dynamic import — the loader resolves the configured package at runtime
  • cached instances — drivers are process-level singletons once created
  • config consistency checks — reinitializing the same driver with different config throws

Writing a custom driver

The current loader expects a default export class whose constructor receives a config object.

import type { EmailDriver, EmailMessage, SendResult } from '@toast/drivers';

interface ResendConfig {
  apiKey: string;
  from?: string;
}

export default class ResendEmailDriver implements EmailDriver {
  readonly name = 'resend';

  constructor(private readonly config: ResendConfig) {}

  async send(message: EmailMessage): Promise<SendResult> {
    // send email using this.config.apiKey
    return { success: true, messageId: 'example' };
  }
}

The important part is that the app supplies the config object — the driver should not reach into process.env itself.

See also

On this page