- Docs
- Concepts
- Providers and adapters
Providers and adapters
How small interfaces, self-registering adapters, and the container make backend swaps a config change.
Every backend concern is one small interface with several adapters, resolved only by the container. Changing sqlite to postgres or memory to redis stays a config edit, never a handler rewrite.
The mental model
Section titled “The mental model”| Role | Rule |
|---|---|
| Interface | Stays small. Never add methods (it breaks implementers); add a new interface instead. |
| Adapter | Self-registers; one subdirectory per backend. |
| Container | Sole resolver; owns construction, caching, retry, and close. |
| Config | Selects the adapter (DB__PRIMARY__ADAPTER=postgres) plus options. |
Application code speaks the interface; the container speaks the registry:
conn, err := db.Open(db.SQLite, db.Options{Path: ":memory:"})Each package follows one pattern:
// Package db: one small interface.type DB interface { Query(ctx context.Context, query string, args ...any) (Rows, error) Exec(ctx context.Context, query string, args ...any) (int64, error) Ping(ctx context.Context) error Close(ctx context.Context) error Dialect() string}
// Adapter sqlite registers itself; postgres registers itself elsewhere.// Call Register once from main (no init magic); app code holds no globals.func Register() { _ = db.Register(db.SQLite, New) }Swap an adapter with config
Section titled “Swap an adapter with config”Development uses zero infrastructure; production changes names only:
DB__PRIMARY__ADAPTER=sqliteDB__PRIMARY__PATH=data/app.dbCACHE__DEFAULT__ADAPTER=memoryQUEUE__DEFAULT__ADAPTER=memoryDB__PRIMARY__ADAPTER=postgresDB__PRIMARY__DSN=postgres://app:secret@db:5432/appDB__PRIMARY__MAX_CONNS=20CACHE__DEFAULT__ADAPTER=redisCACHE__DEFAULT__ADDR=redis:6379Env equivalents use <SERVICE>__<NAME>__<FIELD> with no prefix: DB__PRIMARY__ADAPTER=postgres, DB__PRIMARY__MAXCONNS=20, CACHE__DEFAULT__ADAPTER=redis. Handlers keep calling c.DB() and c.Cache(); register the adapter module for the name you choose. zever doctor reports per-service health. Under zero-infra defaults, the bookings example documents expected FAILs (missing API key, JWT secret, cities file, embed FS): config gaps, not framework bugs.
Most swaps are invisible to handlers: placeholders stay ? on both db drivers (postgres rewrites to $n before pgx) and route params read through router.Param. Where backends differ, the adapter fails fast or documents the gap. Two known gaps:
MinConnsis a pool minimum on postgres butSetMaxIdleConnson sqlite.- Regex route constraints exist on fiber but not stdhttp.
Pick from the adapter matrix
Section titled “Pick from the adapter matrix”An excerpt follows. The full table covers all 37 services from ai to workflow, each with a default adapter (zero-infra except ai): see the adapters matrix.
| Service | Adapters | Default |
|---|---|---|
db |
sqlite, postgres |
sqlite |
cache |
memory, redis, db |
memory |
queue |
memory, redis, db |
memory |
router |
fiber, stdhttp |
stdhttp |
auth |
jwt, session, oidc |
jwt |
session |
memory, redis, db |
memory |
storage |
local, s3, r2 |
local |
search |
db, postgres, meilisearch, sqlite |
db |
vectorstore |
db, sqlite, pgvector, qdrant |
db |
payment / billing |
stub, stripe, paddle |
stub |
log |
noop, zerolog, slog, pretty |
slog |
secrets |
env, vault |
env with ZEVER prefix |
Add a new adapter
Section titled “Add a new adapter”- Implement the existing interface exactly: no extra exported methods on the facade.
- Parse options with typed readers and return
InvalidOptionsErroron unparsable values. - Never log raw option maps; expose redaction through
Redact/RedactedServices. - Make shutdown cheap and idempotent: implement
Close(ctx),Close(), orStop()so the container’s probing finds it. - Keep tests hermetic (sqlite
:memory:, in-memory fakes) and gate live tests behind env such asPOSTGRES_DSN.
Unexported adapter structs are named driver with receiver d; timeouts are time.Duration with Default*Timeout constants.
Design decisions and trade-offs
Section titled “Design decisions and trade-offs”- Close shape is a provider contract. See Container for which signature each adapter takes.
- Fail closed. Unsupported ORM features return
dialect.ErrUnsupportedByDialect(prefixedorm:) instead of wrong SQL, strict config rejects unknown keys at load, and redaction masks secrets everywhere.
Troubleshooting
Section titled “Troubleshooting”The swap changes nothing at runtime
Cause: an env override (CACHE__DEFAULT__ADAPTER) beats the file. Fix: unset or correct the env variable.
An accessor fails with UnknownAdapterError
Cause: a typo in the adapter name (config accepts any non-empty name), or its Register() was never called. Fix: match the matrix spelling (postgres, not postgresql) and register the adapter module before resolving.
A handler sees different SQL on postgres
Cause: a misunderstanding of the placeholder rewrite. Fix: keep writing ?; the postgres driver rewrites to $n before pgx.
A fiber-only route constraint breaks stdhttp
Cause: regex constraints exist on fiber only. Fix: restructure the route using constructs both drivers share, or pin fiber.
A live adapter test fails in CI
Cause: the test needs a real backend but no DSN is set. Fix: gate it behind env (POSTGRES_DSN='postgres://user:pass@localhost:5432/test?sslmode=disable').