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
- How Drivers Work
- Email Drivers
- Storage Drivers
- Writing a Custom Driver
- Configuration Reference
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 selection —
EMAIL_DRIVERandSTORAGE_DRIVERenv 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 classThis means:
- Users install only the drivers they need
- Resolution works correctly with pnpm's strict
node_modules - The pattern matches how ESLint, Babel, and other plugin systems work
Lifecycle
- Application calls
getEmailDriver()orgetStorageDriver() - Loader checks cache — returns immediately if already loaded
- Reads environment variable (
EMAIL_DRIVER/STORAGE_DRIVER) - For built-in drivers: instantiates directly
- For external drivers: dynamically imports the npm package, instantiates the default export
- 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_DRIVER | NODE_ENV | Result |
|---|---|---|
| Not set | development | Console driver (built-in) |
| Not set | production | Error — must be configured |
console | any | Console driver |
toast-driver-email-mailgun | any | Mailgun 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 testsConsole 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-mailgunEnvironment variables:
| Variable | Required | Description |
|---|---|---|
EMAIL_DRIVER | Yes | Set to toast-driver-email-mailgun |
MAILGUN_API_KEY | Yes | Mailgun API key |
MAILGUN_DOMAIN | Yes | Sending domain (e.g., mg.example.com) |
MAILGUN_REGION | No | us (default) or eu |
EMAIL_FROM | No | Default 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
fromaddress - 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-mailgunDriver 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-mailgunMailgun 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_DRIVER | Result |
|---|---|
| Not set | null — storage not configured |
toast-driver-storage-s3 | S3 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-s3Environment variables:
| Variable | Required | Description |
|---|---|---|
STORAGE_DRIVER | Yes | Set to toast-driver-storage-s3 |
S3_ENDPOINT | Yes | S3-compatible endpoint URL |
S3_ACCESS_KEY_ID | Yes | Access key |
S3_SECRET_ACCESS_KEY | Yes | Secret key |
S3_BUCKET | Yes | Bucket name |
S3_REGION | No | Region (defaults to auto) |
S3_PUBLIC_URL | Yes | Base 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.comWriting 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 initStep 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_xxxxxThe 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
| Variable | Values | Description |
|---|---|---|
EMAIL_DRIVER | console, package name | Driver selection |
EMAIL_FROM | Email address | Default sender (used by Mailgun) |
MAILGUN_API_KEY | API key | Mailgun authentication |
MAILGUN_DOMAIN | Domain | Mailgun sending domain |
MAILGUN_REGION | us, eu | Mailgun API region |
Storage
| Variable | Values | Description |
|---|---|---|
STORAGE_DRIVER | Package name | Driver selection |
S3_ENDPOINT | URL | S3-compatible API endpoint |
S3_ACCESS_KEY_ID | Key | S3 access key |
S3_SECRET_ACCESS_KEY | Key | S3 secret key |
S3_BUCKET | Name | S3 bucket name |
S3_REGION | Region code | S3 region (default: auto) |
S3_PUBLIC_URL | URL | Base URL for public file access |