# Write a plugin

Add a new adapter to an existing battery or ship an entirely new battery as a standalone Go module.

A plugin is a standalone Go module: a new adapter for an existing battery (`zever-adapter-<name>`), or an entirely new battery (`zever-battery-<name>`, dogfooded by `examples/sms`). Run the conformance kit before publishing.

## Add an adapter to an existing battery

A battery owns an interface plus a `Register`/`Open` pair. The container is the only resolver; adapters never self-register (no `init`, no globals). Each adapter lives at `adapters/<battery>/<name>` and exposes a constructor plus `Register()`. See `adapters/cache/memory/register.go`.

Adapters are plain strings (`type Adapter string`, such as `cache.Memory = "memory"`). `ParseAdapter` accepts any non-empty name, so a custom backend round-trips through file and env config; `String()` is the identity (`"unknown"` only for empty).

This `cache` adapter lives in module `zever-adapter-foo`:

```go title="foo/foo.go"
package foo // import "example.com/zever-adapter-foo"

import (
	"context"
	"time"

	"github.com/zenta-dev/zever/core/cache"
)

// driver implements all 8 cache.Cache methods.
type driver struct{ /* connection state */ }

func (d *driver) Get(ctx context.Context, key string) ([]byte, error) { panic("implement") }
func (d *driver) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
	panic("implement")
}
func (d *driver) SetIfAbsent(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error) {
	panic("implement")
}
func (d *driver) Delete(ctx context.Context, key string) error         { panic("implement") }
func (d *driver) Increment(ctx context.Context, key string) error      { panic("implement") }
func (d *driver) Decrement(ctx context.Context, key string) error      { panic("implement") }
func (d *driver) Exists(ctx context.Context, key string) (bool, error) { panic("implement") }
func (d *driver) Close(ctx context.Context) error                      { panic("implement") }

// New matches cache.Factory.
func New(opts cache.Options) (cache.Cache, error) {
	return &driver{}, nil
}
```

The host (your app, not the adapter package) registers it:

```go title="main.go"
const fooAdapter = cache.Adapter("foo")

if err := cache.Register(fooAdapter, foo.New); err != nil {
	return err
}
c, err := cache.Open(fooAdapter, cache.Options{})
```

With the container, set `cache: {adapter: foo}` (or `CACHE_ADAPTER=foo`) and the first `container.Cache()` call resolves your factory like any core adapter.

Follow these rules:

- Do not add methods to a public battery interface; propose a new capability interface instead (such as `cache.CompareAndSwapCache`).
- Call `opts.Validate()` first in `Open`; fail closed and join all violations.
- Keep errors sentinel-based, `errors.Is`-compatible, prefixed with the battery name (`cache:`); never put secrets or raw option maps in error text (use `config.RedactedServices`).
- Take `ctx` first for I/O and never store it in a struct.

## Prove parity with the conformance kit

Every battery with a kit exposes `Conformance` in `core/<b>/<b>test`. Run it in your tests and link the passing run from your README:

```go title="foo/foo_test.go"
import "github.com/zenta-dev/zever/core/cache/cachetest"

func TestConformance(t *testing.T) {
	t.Parallel()
	cachetest.Conformance(t, func(t *testing.T) cache.Cache {
		t.Helper()
		c, err := foo.New(cache.Options{})
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { _ = c.Close(t.Context()) })
		return c
	})
}
```

Kits exist for these batteries (import `github.com/zenta-dev/zever/core/<b>/<b>test`):

```text
ai analytics auth authz billing cache crypto db document eventbus flag geo i18n
idempotency job lock log mailer media middleware notification observability
outbox password payment permission queue ratelimit resilience router scheduler
search secrets session storage tenant vectorstore webhook workflow
```

## Ship an entirely new battery

Mirror the core shape: an interface, a string `Adapter`, typed `Options` with `Validate`, a `Register`/`Open` pair, and sentinel errors. Copy `examples/sms/sms.go` (the `SMS` interface, `Stub` adapter, `Options{From}`, `Register`, `Open`) and add a deterministic stub backend like `examples/sms/stub`.

1. Add the config entry. Plugins live under `config.Config.Plugins`, keyed by name with raw options the plugin validates itself (`map[string]Service[json.RawMessage]`, nil by default, so initialize it first):

    ```go
    cfg.Plugins = map[string]config.Service[json.RawMessage]{
    	"sms": {
    		Adapter: "stub",
    		Options: json.RawMessage(`{"from":"+15550000"}`),
    	},
    }
    ```

2. Register a validator so `Config.Validate` covers the plugin. Plugins without one skip validation and their options stay opaque. `_ADAPTER` (for example `SMS_ADAPTER=stub`) overrides the adapter from the environment; options stay raw and are not env-overridable:

    ```go
    if err := config.RegisterPluginValidator("sms", func(adapter string, opts json.RawMessage) error {
    	var o sms.Options
    	if err := json.Unmarshal(opts, &o); err != nil {
    		return err
    	}
    	return o.Validate()
    }); err != nil {
    	return err
    }
    ```

3. Register a versioned build func once at startup, then resolve per container. The build receives the container's `*config.Config`:

    ```go
    if err := container.RegisterPlugin[sms.SMS]("sms", container.PluginAPIVersion, func(cfg *config.Config) (sms.SMS, error) {
    	var opts sms.Options
    	if err := json.Unmarshal(cfg.Plugins["sms"].Options, &opts); err != nil {
    		return nil, err
    	}
    	return sms.Open(sms.Stub, opts)
    }); err != nil {
    	return err
    }

    c := container.New(cfg)
    svc, err := container.Resolve[sms.SMS](c, "sms")
    ```

Successful builds cache per `Container`; failures retry on the next `Resolve`. A wrong-typed value fails `Resolve` with a type-mismatch error, and resolved plugins close with `Container.Close`. `examples/sms/app/main.go` shows the full flow.

`zever add <battery>[/<adapter>]` scaffolds the `go.mod`, `internal/app/wiring.go` with typed `container.Provide` wiring. For a plugin, pass an explicit adapter and `--module <path[@version]>` (default `v0.1.0`):

```sh
zever add sms/stub --module example.com/zever-battery-sms
```

Flags are `--adapter`, `--force`, `--module`, and `--dry-run`. Run `go mod tidy` afterwards. See the [CLI reference](https://zenta-dev.github.io/zever/reference/cli/).

## Register only what you choose

There is no manifest file: the `go.mod` requires are the component list, and the generated `app.go` imports plus `Register()` calls are the manifest, emitted for the floor (`log`, `router`) plus your picks.

This mirrors the `database/sql` driver model: import alone resolves nothing, and registration without the `go.mod` require does not build. For every adapter, import its module, call `Register()` before first use, and require it in `go.mod`. An unregistered adapter fails resolution with `UnknownAdapterError`.

## Version and publish

`container.PluginAPIVersion` is one integer, currently `1`, bumped only on a breaking host-side change. `RegisterPlugin` rejects any other version, empty names, nil builds, and duplicate names, so mismatches fail at registration, never resolve. The pre-1.0 `0.x` core may break the plugin surface with release notes. Pin scaffolds with `--framework-version`.

- Publish from your own repo with independent semver tags (not a lockstep Zever tag).
- State the targeted `PluginAPIVersion` in your README and verify a mismatched `RegisterPlugin` fails before publishing.
- Link the passing conformance run from your README.
- Record changes in your own `CHANGELOG.md`.

## Keep plugins out of `.zen`

`.zen` schemas cannot reference plugin batteries. This is a scope boundary: the compiler resolves only core batteries (the job `queue:` name is never validated against registered adapters), while plugins wire at runtime. Model the domain in `.zen`, then wire the plugin in Go. Syntax: [Zen syntax](https://zenta-dev.github.io/zever/reference/zen-syntax/).

<details>
<summary>Why there is no schema-level plugin reference yet</summary>

A proposal (`uses sms;` plus an optional `@via(sms)` field attribute) was deferred:

- **A, `uses` declaration:** the resolver records and checks names at runtime. It touches the token table (`uses` becomes reserved), AST, parser, resolver and IR, formatter, `breaking`, grammars, LSP, and fixtures, plus a second manifest.
- **B, plugin-shipped `.zen` fragment:** reuses existing machinery, but nothing machine-checks the fragment against the Go code, and there is no import.
- **C, typed registry in the resolver:** rejected; `dsl/` must never import runtime code, and it adds global state.

Revisit A when a real plugin needs schema-visible surface (validation, OpenAPI, queue typing). Never choose C.
</details>

<details>
<summary>Why idempotency, lock, and ratelimit do not wrap cache</summary>

None maps cleanly onto cache reads and writes: `idempotency.Store` uses a `Begin`/`Complete`/`Forget` reservation protocol, `lock.Locker` issues holder-scoped leases with ownership-checked `Extend`/`Unlock`, and `ratelimit.Limiter` does token-bucket accounting returning `Decision{Allowed, RetryAfter, Remaining}`. The reuse boundary is shared primitives (`shared/cas`, `shared/redisclient`).
</details>

## Related

- [Providers and adapters](https://zenta-dev.github.io/zever/concepts/providers-adapters/)
- [The container](https://zenta-dev.github.io/zever/concepts/container/)
- [Configuration](https://zenta-dev.github.io/zever/guides/operate/configuration/)
