Providers

A provider resolves one scheme. It implements mamori.Provider (Scheme() + Resolve), optionally WatchableProvider (native push) and BatchProvider (one call for many refs). Register with the database/sql pattern - a blank import is enough:

import _ "github.com/xavidop/mamori/providers/vault"

Every provider that ships in this repo passes the conformance kit (see Write a provider). Pick one from the sidebar for its scheme, ref grammar, auth, and examples.

The Errors column shows which providers classify a failure beyond not_found, mapping backend-specific errors onto mamori.ErrorKind values like permission_denied, unauthenticated, unavailable, rate_limited, and invalid. The classification sweep across the whole catalog is now complete, and every provider falls into exactly one of three honest states:

  • - classifies real backend errors beyond not_found: forty-four rows across thirty-nine modules. A row lists every scheme its module classifies, so aws-sm:// and aws-ps:// share one and k8s-secret:// and k8s-cm:// share one, while the core module’s env:, dotenv://, file:// and exec: each get a row of their own.
  • no (chain preserved) - firebase-rtdb, growthbook, and flagsmith have no backend-specific error vocabulary to map, so a non-not-found failure still reports unknown, but Resolve wraps the underlying error with %w rather than flattening it, so errors.Is/errors.As still reach it. This is not classification; it is proof that the chain survives even where there is nothing more specific to name.
  • n/a (no error surface) - unleash, configcat, split, and viper wrap a client surface with no per-key error at all: the flag SDKs return only bool/string, and Viper’s own read API has no error return (Get returns any, IsSet returns bool). Resolve can only ever produce not_found or a client-construction error. Each is explicitly exempt from the conformance kit’s ErrorClassification case via providertest.Config.NoResolveErrors, a deliberate, greppable opt-out rather than a silent gap.

Don’t read either non-✅ state as broken: not_found is detected everywhere regardless of this column, and neither state claims to see permission or availability errors that provider genuinely cannot observe.

The Close column says what each provider’s Close() actually releases. mamori never closes a provider for you: a provider instance belongs to whoever constructed it, and Watcher.Close() releases only what mamori itself created (see Who closes a provider). Thirty-one modules ship a Close; the rows reading none needed hold no releasable handle and have no Close method at all, so there is nothing to forget. Two rows read terminal only, releases nothing: sqlite opens and closes a fresh *sql.DB inside every Resolve, and firebase-rtdb holds no client of its own, but both keep a Close so that a caller sweeping Close across their siblings is not surprised to find them alone still serving. Where a cell says injected, the provider holds no client of its own, so Close releases idle connections only for a client you supplied, and never closes that client. Whatever the cell says, Close is terminal: afterwards Resolve reports errors.Is(err, mamori.ErrUnavailable) locally. It does not stop a Watch that is already running, which is its own subject: see Close does not stop a Watch. Each provider’s own page has the full ownership rules.

SchemePageSensitiveWatchErrorsClose
env:envnopollnone needed
dotenv://dotenvnofsnotifynone needed
file://filenofsnotifynone needed
exec:execyespollnone needed
mamori://mamori (client)passthroughnative (SSE)each endpoint’s idle HTTP conns
aws-sm:// aws-ps://AWSyes / securepollnone needed
aws-appconfig://AWS AppConfignopollnone needed
vault://Vaultyeslease-aware pollnone needed
gcp-sm://GCPyespollcloses a self-built client
azure-kv://Azureyespollnone needed
azure-appconfig://Azure AppConfignopollnone needed
doppler://Doppleryespollinjected HTTP client’s idle conns
infisical://Infisicalyespollinjected HTTP client’s idle conns
hcp-vs://HCP Vault Secretsyespollinjected HTTP client’s idle conns
scaleway-sm://Scaleway Secret Manageryespollinjected HTTP client’s idle conns
bitwarden-sm://Bitwarden Secrets Manageryespollinjected HTTP client’s idle conns
op://1Passwordyespollinjected HTTP client’s idle conns
sops://SOPSyesfsnotifynone needed
supabase://Supabase Vaultyespollinjected HTTP client’s idle conns
postgres://PostgreSQLnonative (LISTEN/NOTIFY)closes a self-opened pool
mysql://MySQLnopollcloses a self-opened *sql.DB
sqlite://SQLitenofsnotifyterminal only, releases nothing
mongodb://MongoDBnonative (change streams)disconnects a self-dialed client
dynamodb://DynamoDBnopollnone needed
cosmos://Cosmos DBnopoll (ETag)none needed
redis://Redisnonative (keyspace)closes a self-built client
consul://Consulnonativenone needed
etcd://etcdnonativecloses a self-dialed client
nacos://Nacosnonativeinjected HTTP client’s idle conns
vercel-gc://Vercel Global Confignopoll (digest)injected HTTP client’s idle conns
cloudflare-kv://Cloudflare Workers KVnopollinjected HTTP client’s idle conns
heroku://Heroku Config Varsyespollinjected HTTP client’s idle conns
https://Generic HTTPSper-endpointpoll (conditional GET)injected endpoint clients’ idle conns
k8s-secret:// k8s-cm://Kubernetesyes / nonativeself-built clientset’s idle conns
firestore://Firestorenonative (snapshots)closes a self-built client
firebase-rc://Remote Confignopollinjected HTTP client’s idle conns
firebase-rtdb://Realtime DBnonative (streaming)no (chain preserved)terminal only, releases nothing
s3://Amazon S3nopoll (ETag)none needed
gcs://Google GCSnopoll (generation)closes a self-built reader client
azblob://Azure Blobnopoll (ETag)none needed
launchdarkly://LaunchDarklynonative (streaming)closes a self-built client
unleash://Unleashnopolln/a (no error surface)closes a self-built client
flagsmith://Flagsmithnopollno (chain preserved)none needed
configcat://ConfigCatnopolln/a (no error surface)closes the SDK client (stops polling)
split://Splitnopolln/a (no error surface)destroys a self-built client
growthbook://GrowthBooknopollno (chain preserved)closes the SDK client
flipt://Fliptnopollnone needed
goff://GO Feature Flagnopollnone needed
openfeature://OpenFeaturenopollnone needed
posthog://PostHognopollinjected HTTP client’s idle conns
viper://Vipernopolln/a (no error surface)none needed

Choosing and configuring a provider

Most providers auto-register a zero-config instance that reads ambient credentials (env vars, the AWS/GCP/Azure default credential chains, in-cluster Kubernetes config). A blank import is then all you need. When you must configure a provider explicitly - a region, an address, an injected client - construct it and pass it with WithProvider:

import awsprov "github.com/xavidop/mamori/providers/aws"

cfg, err := mamori.Load[Config](ctx,
	mamori.WithProvider(awsprov.NewSecretsManager(awsprov.WithRegion("eu-west-1"))),
)

WithProvider takes precedence over the registry for that scheme, for that call only.

The mamori (client) provider is the one shipped provider with no zero-config default at all: a mamori:// binding name only means something relative to one specific config server, so it is always constructed explicitly with mamoriprov.New(mamoriprov.Config{Endpoint: ...}) and passed via WithProvider, never registered from a blank import. It is also structurally different from every other row in the table above: it is not itself a secret manager, database, or flag service, it is a client for the config server, a fan-out process that fronts one of those backends and re-serves it to many callers. Sensitive and error classification are marked “passthrough” for it because both are whatever its upstream backend reports, carried through the hop unchanged, rather than a property fixed by this provider itself.

Watch behavior

  • native - the backend pushes changes (Kubernetes watch API, Consul blocking queries, a Nacos long-poll listener, a mamori config server’s /v1/watch Server-Sent Events stream). mamori subscribes directly.
  • fsnotify - a local file is watched for writes (built-in file://, sops://).
  • lease-aware poll - polling, but Value.NotAfter from a Vault lease triggers a refresh before expiry.
  • poll - mamori polls on WithPollInterval with jitter, using Value.Version to detect change.

Provider authors implement the smallest interface native to their backend and never fake a watch with an internal ticker - mamori supplies the poller.

Because mamori supplies the poller, it also supplies the retry cadence for the three polled rows: WithBackoff spaces out retries for a ref that keeps failing to resolve. It does not reach a native provider, which owns its stream and its own reconnect behavior; see each provider’s page for that.