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 instanceWhy 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.