Writing an adapter
branchly grows by adapters, and adapters are deliberately small: the SQLite datasource is under a hundred lines, the direnv resolver under fifty. This section walks through everything you need to write one — and the conformance test kit tells you when it’s correct.
Pick your axis
Section titled “Pick your axis”An adapter implements exactly one of branchly’s four axes:
| You want to support… | Write a… | Interface |
|---|---|---|
| A database / provisioning backend | datasource | DatasourceAdapter |
| An ORM / migration tool | migrator | MigratorAdapter |
| A way to expose the connection to the app | resolver | ConnectionResolver |
| A version control system | (open an issue first — git is the only one so far!) | Vcs |
All four interfaces are exported as types from the branchly package.
The anatomy
Section titled “The anatomy”Every adapter package follows the same shape: it default-exports a factory function that receives the adapter’s config block and returns the adapter object.
import { writeFile } from 'node:fs/promises';
import type { ConnectionResolver } from 'branchly';
export interface MyResolverOptions { readonly file?: string;}
export const createMyResolver = (options: MyResolverOptions = {}): ConnectionResolver => ({ id: 'my-resolver', apiVersion: 1, inject: (connection) => writeFile(options.file ?? '.connection', `${connection}\n`, 'utf8'),});
export default createMyResolver;Three things the loader checks at startup, with friendly errors if they’re missing:
- A default-exported factory. The kernel dynamically imports your package and calls
default(options). - Options flow from the config. Everything in the user’s adapter block — minus nothing, the whole object — is passed to your factory.
{ use: 'my-resolver', file: '.envrc' }arrives asoptions, so document your options and give them sensible defaults. apiVersion. Your returned adapter declares which interface version it targets (currently1). If branchly and the adapter disagree, the user gets an actionable message — “upgrade the adapter or branchly so the two agree” — instead of a stack trace. Bump it only when branchly announces a breaking interface change.
Naming and resolution
Section titled “Naming and resolution”Users select adapters with use. Short names resolve by convention to first-party packages: use: 'postgres' → @branchly/datasource-postgres. Anything containing @ or / is used verbatim — so publish your adapter under any name and it’s selectable as:
export default defineConfig({ datasource: { use: '@yourorg/branchly-datasource-cockroach' },});We suggest naming third-party packages branchly-<axis>-<name> or @scope/branchly-<axis>-<name> so they’re easy to find.
The development loop
Section titled “The development loop”- Scaffold a package with the interface type from
branchlyand a default-exported factory. - Add
@branchly/adapter-test-kitas a dev dependency and wire up the conformance suite — it encodes the real contract, invariant by invariant. - Iterate until the kit is green.
- Publish — or PR it into the branchly monorepo; contributions are very welcome.