Workspace Drivers
Drivers in FullProduct.dev are designed to keep features copy‑pasteable across projects while letting you swap implementations (Stripe vs. another provider, Mongo vs. another DB) with minimal refactors. They live as workspace packages and are wired up through a generated registry.
What is a driver?
Workspace‑based abstraction: Each driver type has a core package (e.g. @payments/driver) that defines the shared API surface via Zod schemas and signatures, plus one or more implementation packages (e.g. @payments/stripe).
Portable by design: Implementations live in their own workspace, so you can copy the folder between projects. Registry scripts stitch everything together.
Key files and scripts
Driver signatures: Shared input/output schemas and function signatures live at the root of the core driver package in driver.signature.ts (e.g. @payments/driver/driver.signature.ts). Implementations import from here to guarantee a consistent API.
Driver implementations: Implementations place their module in drivers/{implementation}.{driverType}.ts inside the implementation workspace. They validate against the shared signature and export a driver object.
Registry generator: Running the generator scans all workspaces for drivers and builds typed registries and options.
// -i- Auto generated with "npx turbo run @green-stack/core#collect-drivers"
import { driver as stripePaymentsDriver } from '@payments/stripe/drivers/stripe.payments.ts'
/* --- Payments -------------------------------------------------------------------------------- */
export const paymentsDrivers = {
'stripe': stripePaymentsDriver,
}e.g. as generated in this part of our collect-drivers script:
/** --- createDriversFileContent() ------------------------------------------------------------- */
/** -i- Creates the file content for the drivers file */
const createDriversFileContent = (ctx: {
driverImports: string,
driverTypeExports: string,
driverEntryLines: string,
}) => [
`// -i- Auto generated with "npx turbo run @green-stack/core#collect-drivers"`,
`${ctx.driverImports}\n`,
`${ctx.driverTypeExports}`,
].join('\n')Generated outputs
Driver options and types: All available implementations get listed in a central config:
// -i- Auto generated with "npx turbo run @green-stack/core#collect-drivers"
/* --- Constants ------------------------------------------------------------------------------- */
export const DRIVER_OPTIONS = {
payments: {
stripe: 'stripe',
},
db: {
mongoose: 'mongoose',
mockDB: 'mock-db',
}
} as constTyped driver registries:
Each driver type gets a generated barrel at @app/registries/drivers/{type}.drivers.generated.ts (e.g. payments.drivers.generated.ts), exposing a map from implementation key to its driver object.
Selecting a driver
Pick the active implementation in your app config:
// --- Drivers ---
drivers: createDriverConfig({
db: DRIVER_OPTIONS.db.mongoose,
payments: DRIVER_OPTIONS.payments.stripe,
}),This choice powers thin entry modules like @payments/driver/index.ts that forward calls to the selected implementation while keeping a consistent import surface:
import { paymentsDrivers } from '@app/registries/drivers/payments.drivers.generated'
import { appConfig } from '@app/config'
import { syncCustomerData } from './resolvers/syncCustomerData.resolver'
/* --- Determine Main Payments driver ---------------------------------------------------------- */
const paymentsDriver = paymentsDrivers[appConfig.drivers.payments]
if (!paymentsDriver) throw new Error(
`No Payments driver found for key "${appConfig.drivers.payments}". ` +
`Make sure the driver is installed and the driver key is correct.`,
)
/* --- Re-export Driver Resources -------------------------------------------------------------- */
export const provider = paymentsDriver['provider']
export const ensureCustomer = paymentsDriver['ensureCustomer']
export const startCheckout = paymentsDriver['startCheckout']
export const syncPaymentData = syncCustomerData
export const startPortalSession = paymentsDriver['startPortalSession']Adding a driver implementation
Here’s an example of a new driver implementation for e.g. a new payment provider like Stripe.
- driver.signature.ts ← shared Zod signatures
- yourprovider.{driverType}.ts
- package.json ← name: '@payments/yourprovider'
- payments.drivers.generated.ts ← auto updated
- drivers.config.ts ← auto updated
- appConfig.ts ← select driver
-
Create
drivers/{implementation}.{driverType}.tsin your provider workspace. -
Export
driverthat conforms to the shared signature in your provider workspace. -
Run
npm run collect:driversto update the registry config & driver files. -
Select your driver in
appConfig.tsin your@app/coreworkspace.
How driver collection works
The collect-drivers script scans feature and package workspaces for driver files matching the file name convention /drivers/{provider}.{driverType}.ts + an exported driver constant validated by the main driver signature. It derives the driver type and key from the filename and writes the registry and config.
// Get all driver file paths in /features/ & /packages/ workspaces
const featureDriverPaths = globRel('../../features/**/drivers/*.*.ts')
const packageDriverPaths = globRel('../../packages/**/drivers/*.*.ts')
const allDriverPaths = [...featureDriverPaths, ...packageDriverPaths]
// ...
// Skip if not a valid driver
const driverFileContents = fs.readFileSync(driverFilePath, 'utf-8')
let isValidDriver = driverFileContents.includes('export const driver = validate')
const driverExportLine = driverFileContents.split('\n').find((line) => line.includes('export const driver ='))
isValidDriver = !!driverExportLine?.includes('Driver(')
if (!isValidDriver) return acc
// Get driver type & name from filename
const driverFilename = innerDriverFilePath.replace('/drivers/', '')
const driverFileModuleName = driverFilename.replace('.tsx', '').replace('.ts', '')
const [driverName, driverType] = driverFileModuleName.split('.')
// Get import path from workspace
const driverImportPath = `${driverWorkspace}/drivers/${driverFilename}`
const driverKey = driverName === 'mock' ? `mock-${driverType}` : driverNameAdding drivers through plugins
You can pull in driver workspaces via our git‑based plugins and installable pull-requests using the CLI:
npx @fullproduct/universal-app install with/payments-stripeThis adds the implementation workspace. The npm run dev or collect:drivers scripts will regenerate the related registries automatically. In the end you only need to set appConfig.drivers to the desired option, if our git plugin does not already do that for you.
Avoid hasty abstractions
Drivers are optional abstractions. They shine when you want portability and provider flexibility without leaking implementation details across your codebase.
When to use drivers
- You agree with our core concepts and want to optimize for a way of working for copy-pasteable features.
- You’d like to avoid a big refactor if you need to change a provider.
- You’re fine with a potentially smaller API surface and less flexibility to achieve this.
When to avoid drivers
- You want to keep your codebase as simple and maintainable as possible.
- You want to be able to pull out all the stops for each library / provider you use.
- You’re fine with a potentially huge refactor if you need to change a provider.
Using providers directly
Regardless of where you stand on driver abstractions, you can always still use providers directly in your codebase.
Just import the package (e.g. stripe or mongoose) directly and use it as you would normally.
In some cases, this can be even better than using the driver because using it directly gives you more control over e.g. db queries or other performance optimizations which might not (yet) be available in the driver.
