Toast
ContributorDrivers

Driver System Documentation

Driver System Documentation

Toast uses a driver architecture for pluggable infrastructure — email delivery, file storage, and future concerns like queues. Drivers are npm packages that implement typed interfaces, loaded dynamically at runtime.

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

Table of Contents


Architecture Overview

┌─────────────────────────────────────────────┐
│  Application Code                           │
│  import { getEmailDriver }                  │
│    from '@toast/drivers'                    │
└──────────────┬──────────────────────────────┘


┌──────────────────────────────────────────────┐
│  @toast/drivers (packages/drivers)           │
│  ┌────────────┐  ┌────────────────────────┐  │
│  │ Interfaces │  │ Loaders                │  │
│  │ EmailDriver│  │ getEmailDriver()       │  │
│  │ Storage    │  │ getStorageDriver()     │  │
│  │ Driver     │  │                        │  │
│  └────────────┘  └───────────┬────────────┘  │
│                              │               │
│  ┌────────────────┐          │               │
│  │ ConsoleEmail   │◄─────────┤ (built-in)    │
│  │ Driver         │          │               │
│  └────────────────┘          │               │
└──────────────────────────────┼───────────────┘
                               │ (dynamic import)
               ┌───────────────┴─────────────┐
               ▼                             ▼
┌──────────────────────┐  ┌──────────────────────┐
│ toast-driver-email-  │  │ toast-driver-storage- │
│ mailgun              │  │ s3                    │
│ (drivers/email-      │  │ (drivers/storage-s3)  │
│  mailgun)            │  │                       │
└──────────────────────┘  └──────────────────────┘

Key principles:

  • Drivers are npm packages — installed via pnpm add, not file-system loading
  • Typed interfaces — every driver implements a TypeScript interface from @toast/drivers
  • Environment-driven selectionEMAIL_DRIVER and STORAGE_DRIVER env vars control which driver loads
  • Singleton caching — drivers are instantiated once and reused for the process lifetime
  • Race-safe loading — concurrent getDriver() calls share a single loading promise

How Drivers Work

Loading Mechanism

The generic loadDriver() function resolves packages from the application's context (not from @toast/drivers):

import { createRequire } from 'node:module';

const appRequire = createRequire(`${process.cwd()}/package.json`);
const driverPath = appRequire.resolve(driverName);
const module = await import(driverPath);
return module.default; // Driver class

This means:

  1. Users install only the drivers they need
  2. Resolution works correctly with pnpm's strict node_modules
  3. The pattern matches how ESLint, Babel, and other plugin systems work

Lifecycle

  1. Application calls getEmailDriver() or getStorageDriver()
  2. Loader checks cache — returns immediately if already loaded
  3. Reads environment variable (EMAIL_DRIVER / STORAGE_DRIVER)
  4. For built-in drivers: instantiates directly
  5. For external drivers: dynamically imports the npm package, instantiates the default export
  6. Caches the instance for future calls

Email Drivers

Interface

interface EmailDriver {
  readonly name: string;
  send(message: EmailMessage): Promise<SendResult>;
}

interface EmailMessage {
  to: string;
  from?: string;
  subject: string;
  html: string;
  text?: string;
}

interface SendResult {
  success: boolean;
  messageId?: string;
  error?: string;
}

Selection Logic

EMAIL_DRIVERNODE_ENVResult
Not setdevelopmentConsole driver (built-in)
Not setproductionError — must be configured
consoleanyConsole driver
toast-driver-email-mailgunanyMailgun driver (npm package)

Console Driver (built-in)

The default development driver. Logs emails to the console and captures them for test assertions.

No configuration needed.

import { getEmailDriver, ConsoleEmailDriver } from '@toast/drivers';

// Automatic in development
const driver = await getEmailDriver();
await driver.send({
  to: 'user@example.com',
  subject: 'Magic Link',
  html: '<a href="...">Sign in</a>',
});

// In tests: verify emails were sent
const console = driver as ConsoleEmailDriver;
const sent = console.getSentEmails();
expect(sent[0].message.to).toBe('user@example.com');
console.clearSent(); // Clean up between tests

Console output:

============================================================
EMAIL SENT (console driver)
============================================================
Message ID: console-1708000000000-1
To:         user@example.com
Subject:    Magic Link
------------------------------------------------------------
HTML:
<a href="...">Sign in</a>
============================================================

Mailgun Driver

Production email delivery via the Mailgun API. Package: toast-driver-email-mailgun (in drivers/email-mailgun/).

Installation:

pnpm add toast-driver-email-mailgun

Environment variables:

VariableRequiredDescription
EMAIL_DRIVERYesSet to toast-driver-email-mailgun
MAILGUN_API_KEYYesMailgun API key
MAILGUN_DOMAINYesSending domain (e.g., mg.example.com)
MAILGUN_REGIONNous (default) or eu
EMAIL_FROMNoDefault sender address

Usage:

// Just set environment variables — the driver loads automatically
const driver = await getEmailDriver();
const result = await driver.send({
  to: 'user@example.com',
  subject: 'Welcome',
  html: '<p>Hello!</p>',
});

if (!result.success) {
  console.error('Email failed:', result.error);
}

Features:

  • US and EU region support
  • Configurable default from address
  • Detailed error messages from Mailgun API responses
  • Input validation before API calls

For more details, see the Mailgun driver README.

Resend Driver

There is no built-in Resend driver yet, but writing one is straightforward. See Writing a Custom Driver for a complete Resend example, or the Resend driver README for a ready-to-use template.

Troubleshooting

EMAIL_DRIVER not set in production

Toast requires an explicit EMAIL_DRIVER when NODE_ENV=production. Set it to a package name (e.g., toast-driver-email-mailgun) or console for testing.

# Error: EMAIL_DRIVER must be configured in production
EMAIL_DRIVER=toast-driver-email-mailgun

Driver package not found

If you see Cannot find module 'toast-driver-email-...', ensure the package is installed in the application root (not in packages/drivers):

# Install in the app that calls getEmailDriver()
cd apps/api
pnpm add toast-driver-email-mailgun

Mailgun 401 Unauthorized

Check that MAILGUN_API_KEY is set correctly and that the API key has permission to send from MAILGUN_DOMAIN. EU customers must also set MAILGUN_REGION=eu.

Emails not arriving (development)

In development, the console driver is used by default. Emails are logged to stdout, not actually sent. Check your terminal output for the EMAIL SENT (console driver) banner.


Storage Drivers

Interface

interface StorageDriver {
  readonly name: string;
  upload(input: UploadInput): Promise<UploadResult>;
  delete(key: string): Promise<void>;
  exists(key: string): Promise<boolean>;
}

interface UploadInput {
  data: Buffer | Readable;
  key: string; // e.g., "site-id/images/abc123.jpg"
  contentType: string; // e.g., "image/jpeg"
}

interface UploadResult {
  key: string;
  url: string; // Public URL
  size: number; // Bytes
}

Selection Logic

STORAGE_DRIVERResult
Not setnull — storage not configured
toast-driver-storage-s3S3 driver (npm package)

Unlike email, there is no built-in storage driver. Local development uses MinIO (Docker) which provides the exact S3 API — keeping the development and production paths consistent.

S3 Driver

Works with any S3-compatible provider: AWS S3, Cloudflare R2, MinIO, Backblaze B2.

Package: toast-driver-storage-s3 (in drivers/storage-s3/).

Installation:

pnpm add toast-driver-storage-s3

Environment variables:

VariableRequiredDescription
STORAGE_DRIVERYesSet to toast-driver-storage-s3
S3_ENDPOINTYesS3-compatible endpoint URL
S3_ACCESS_KEY_IDYesAccess key
S3_SECRET_ACCESS_KEYYesSecret key
S3_BUCKETYesBucket name
S3_REGIONNoRegion (defaults to auto)
S3_PUBLIC_URLYesBase URL for public file access

Usage:

import { getStorageDriver } from '@toast/drivers';

const driver = await getStorageDriver();
if (driver) {
  // Upload
  const result = await driver.upload({
    data: fileBuffer,
    key: `${siteId}/images/${nanoid()}.jpg`,
    contentType: 'image/jpeg',
  });
  console.log('Public URL:', result.url);

  // Check existence
  const exists = await driver.exists('site-id/images/abc123.jpg');

  // Delete
  await driver.delete('site-id/images/abc123.jpg');
}

Features:

  • Automatic retry with exponential backoff for transient errors (timeouts, throttling, 5xx)
  • Path traversal protection (rejects .. sequences and control characters)
  • Stream and Buffer input support
  • Path-style addressing for MinIO/R2 compatibility

Provider examples:

# MinIO (local development)
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_BUCKET=toast-uploads
S3_PUBLIC_URL=http://localhost:9000/toast-uploads

# Cloudflare R2
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID=<r2-access-key>
S3_SECRET_ACCESS_KEY=<r2-secret-key>
S3_BUCKET=toast-uploads
S3_REGION=auto
S3_PUBLIC_URL=https://cdn.example.com

# AWS S3
S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
S3_ACCESS_KEY_ID=<aws-access-key>
S3_SECRET_ACCESS_KEY=<aws-secret-key>
S3_BUCKET=toast-uploads
S3_REGION=us-east-1
S3_PUBLIC_URL=https://toast-uploads.s3.us-east-1.amazonaws.com

Writing a Custom Driver

Drivers are npm packages that export a default class implementing a driver interface.

Step 1: Create the Package

mkdir drivers/email-resend
cd drivers/email-resend
pnpm init

Step 2: Implement the Interface

// src/resend.driver.ts
import type { EmailDriver, EmailMessage, SendResult } from '@toast/drivers';

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

  private readonly apiKey: string;

  constructor() {
    const apiKey = process.env['RESEND_API_KEY'];
    if (!apiKey) {
      throw new Error('RESEND_API_KEY is required');
    }
    this.apiKey = apiKey;
  }

  async send(message: EmailMessage): Promise<SendResult> {
    try {
      const response = await fetch('https://api.resend.com/emails', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          from: message.from ?? 'noreply@example.com',
          to: message.to,
          subject: message.subject,
          html: message.html,
          text: message.text,
        }),
      });

      if (!response.ok) {
        const error = await response.text();
        return {
          success: false,
          error: `Resend API error: ${error}`,
        };
      }

      const data = (await response.json()) as { id: string };
      return { success: true, messageId: data.id };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : String(error),
      };
    }
  }
}

Step 3: Export as Default

// src/index.ts
export { default } from './resend.driver.js';

Step 4: Configure and Use

EMAIL_DRIVER=toast-driver-email-resend
RESEND_API_KEY=re_xxxxx

The driver will be automatically loaded by getEmailDriver().

Requirements

  • Default export — the loader imports module.default
  • Constructor takes no arguments — configuration comes from environment variables
  • Implements the interface — TypeScript will catch missing methods
  • Throws on missing config — fail fast at startup, not on first use

Configuration Reference

Email

VariableValuesDescription
EMAIL_DRIVERconsole, package nameDriver selection
EMAIL_FROMEmail addressDefault sender (used by Mailgun)
MAILGUN_API_KEYAPI keyMailgun authentication
MAILGUN_DOMAINDomainMailgun sending domain
MAILGUN_REGIONus, euMailgun API region

Storage

VariableValuesDescription
STORAGE_DRIVERPackage nameDriver selection
S3_ENDPOINTURLS3-compatible API endpoint
S3_ACCESS_KEY_IDKeyS3 access key
S3_SECRET_ACCESS_KEYKeyS3 secret key
S3_BUCKETNameS3 bucket name
S3_REGIONRegion codeS3 region (default: auto)
S3_PUBLIC_URLURLBase URL for public file access

On this page