Plugin Development
Select the extension boundary
Production AuthCrunch plugins normally live in separate repositories as
independently versioned Go modules. In-repository category references use
plugins/<category>/<name>, including synthetic/mock implementations that prove
the integration contract. This directory convention is not runtime registration.
The two working secrets plugins are ordinary importable packages with explicit
constructors. Neither uses Go's plugin.Open, RPC, side-effect registration, or
an AuthCrunch plugin SDK. Neither imports go-authcrunch or Caddy. Both declare
package secrets, so consumers importing both need distinct aliases.
Keep three responsibilities distinct:
- Backend library: retrieve or operate on backend data through a focused Go
API; own backend configuration, validation, transport, and data conversion.
- Consumer adapter: select and construct the library, bind backend-specific
arguments, satisfy the consumer's interface, and own host registration,
placeholder resolution, provisioning, and disposal.
- AuthCrunch runtime: consume validated configuration and enforce identity,
authentication, authorization, and credential semantics in the owning package.
The reusable dependency direction is consumer → adapter → backend library.
A consumer can also call a library directly. Importing a module only makes its
API available; it does not add a kind to AuthCrunch's configuration dispatch.
The separate static host adapter
and AWS host adapter
provide concrete evidence of that boundary. Their registration and provisioning
are host-framework behavior, not capabilities exported by the backend modules.
Use sqlite-ticket-provider to maintain
trusted ticket issuance, browser binding and the optional portal HTTP login hook.
Use sqlite-registration to maintain durable
email confirmation, recoverable account creation and the optional portal hook.
Use sqlite-messaging to maintain the durable
SQLite outbox, runtime provider bindings and registration notification tests.
Use sqlite-identity-store to maintain the
SQLite password backend, atomic login evidence, enrollment and portal tests.
Use credential-authenticators to maintain
SQLite API keys, context-aware authentication, cache isolation and gatekeeper tests.
Use secrets-plugins to integrate or develop secrets
providers, compare the static and AWS clients, bind secret references, and
verify retrieval, data types, metadata, and refresh behavior.
Use claims-enrichment to maintain request-time
attribute backends, their gatekeeper/validator hooks, and the static claims plugin.
Use cryptographic-signing to maintain the
local PS256 plugin, its parser, refresh-engine composition, public JWKS, and
strict core verification.
Use external-authorization to maintain
required request-time decisions, the HTTP JSON plugin, its public parsers, and
gatekeeper/validator enforcement across cached credentials and OAuth sessions.
Use session-and-refresh-storage to maintain
the SQLite refresh store, its parser, atomic durability contracts, and engine
consumer tests.
Read the development blueprint when
starting a plugin, introducing a category, or reviewing whether an extension
has a complete consumer integration and validation plan.
Select the repository workflow
Read repository ownership and bootstrapping
to create a standalone plugin from scratch, write its AGENTS.md and local
skills, load AuthCrunch guidance from another checkout or GitHub revision, or
design a companion host plugin's guidance. A standalone repository must route
agents to AuthCrunch's shared plugin/category contracts and its own local
implementation skills. Keep guidance revisions separate from runtime versions.
Read synthetic reference plugins to implement
an in-repository category example under plugins/<category>/<name>, such as
plugins/secrets/sqlite, with a typed parser and real consumer acceptance.
Inspect availability first; the layout and acceptance contract do not imply a
reference implementation or new category API already exists.
Many backend plugins also need a separate companion for an upstream host such
as caddy-security. That host's plugin-development skills should depend on
AuthCrunch's shared guidance and supplement it with host-local contracts. The
companion then adds its own backend-to-host binding and tests. Keep this
dependency directional; a plain backend does not require host guidance or
host-framework imports. Templates and source-resolution rules are in the
repository workflow reference.
Choose a plugin category
Read the relevant section of the category catalog
to select a consumer, check current support, define configuration and lifecycle,
and choose category-specific acceptance tests. These are architectural categories;
the README separates working secrets modules from additional project scaffolds.
A category or repository name alone does not establish an implementation.
| Category | Responsibility | Current extension boundary |
|---|
| Secrets | Retrieve values for configuration | SQLite bound-record plugin and external libraries; specialized skill above |
| Identity stores | Account lookup, authentication, and supported account operations | SQLite password store and configured ids.IdentityStore injection into NewPortal |
| Identity providers | Authenticate through a federation/protocol backend | SQLite tickets through optional idp.HTTPLoginProvider and direct portal injection |
| Credential authenticators | Validate Basic credentials or API keys | SQLite API keys and authproxy.Authenticator through Gatekeeper.AddAuthenticators |
| Messaging | Deliver registration or notification messages | SQLite outbox and messaging.Config.AddProvider runtime injection |
| Registration workflows | Manage enrollment, confirmation, approval, and account creation | SQLite enrollment and optional ConfirmationProvider via Portal.AddUserRegistry; root dispatch is local-only |
| Session and refresh storage | Store token families and enforce atomic rotation/revocation | SQLite plugin and tokenrefresh.Store engine injection; portal selection needs wiring |
| Cryptographic signing | Sign approved claims with a selected key | tokenrefresh.Signer and local PS256 plugin at engine level; general portal/OIDC signing needs integration |
| External authorization | Evaluate an authenticated subject's access to a resource | Public decision interface and gatekeeper/validator attachment; HTTP JSON plugin available |
| Claims enrichment | Retrieve and validate additional identity attributes | enrichment.Backend and validator/gatekeeper attachment; static claims plugin available |
Current core boundaries
Check these files before proposing an API. Names in a plugin repository or
README are not proof of a core registration point.
| Concern | Current authority | Consequence for extensions |
|---|
| Root assembly | Config, NewServer | No plugin registry, plugin configuration list, or secrets-manager loader exists here. Resolve external values before validation/construction. |
| Identity stores | config, dispatch/interface | The shared dispatcher accepts local and ldap; implementing the interface alone does not register another kind. |
| Identity providers | config, dispatch/interface | The shared dispatcher accepts oauth and saml; a new OAuth driver is different from a new provider kind. |
| Direct portal composition | PortalParameters and NewPortal | A Go host can inject configured identity stores, identity providers, and SSO providers directly, without using the shared factories. |
| Credential records | configuration, credential dispatch | credentials.Config holds credential definitions; it is not a secrets-backend registry. |
| Standalone server | authdb entry point | Installing a Go module does not make it loadable through authdb JSON. An embedding integration must be implemented explicitly. |
For direct composition, construct/configure the backend and pass it through
authn.PortalParameters.IdentityStores, IdentityProviders, or
SingleSignOnProviders. Select its GetName() in the corresponding
PortalConfig name list; NewPortal requires Configured() to be true. Check
the current interface and any feature-specific capability checks, not just the
constructor. The embedding host owns those injected backends: Portal.Close
releases portal resources but does not close shared stores/providers. This is
an existing injection path; adding a new Kind to root Config/NewServer
or authdb remains separate dispatcher work.
A secrets provider supplies configuration values. An identity store owns account
lookup and operations; an identity provider owns authentication evidence. A
messaging extension would own delivery. Choose an interface matching that
responsibility instead of forcing every category through GetSecret or a
universal map-based Execute method.
For a new category, first identify an existing public injection point. When
there is none, describe the necessary core change as proposed work: typed
configuration, reusable parser, dispatch/application API, lifecycle ownership,
and consumer tests. Do not document invented RegisterPlugin, pkg/plugins,
or pkg/secrets APIs as available. A factory registry is a design choice that
needs its own ownership, duplicate-name, concurrency, and lifecycle contract.
Preserve responsibility and compatibility
- Keep backend SDK dependencies in the backend module and host-framework
dependencies in its adapter. Do not make core depend on every backend.
- Record module path, import alias, provider kind, configured instance ID, and
backend resource locator separately. They identify different things.
- Prefer consumer-owned interfaces with compile-time conformance checks. Go
method signatures must match exactly; similarly named methods are insufficient.
- Preserve published constructors when extending working modules. A typed config
constructor or optional capability can be additive; an incompatible method
change needs a deliberate versioning and adapter migration plan.
- Keep declarative configuration apart from mutable clients, caches, resolved
secrets, and goroutines. Treat a failed construction or reload as unpublished
state and dispose resources already acquired.
- Assign ownership explicitly. Server.Close closes the
components it constructs and owns; it cannot discover arbitrary clients an
embedding application created beforehand.
The repository scope, parser architecture, and test contracts remain owned by
coding-directives and
testing-and-ci. A task in this checkout stays here;
external implementations can be inspected as evidence. A separately authorized
plugin-repository task applies this portable guidance in its own workspace.
Reusable bootstrap templates and companion guidance belong in these references;
task-specific Caddy implementation handoffs still belong only in ignored tmp/.
Acceptance decisions
- A new standalone plugin can be built from category guidance without a global
AuthCrunch skill installation or assumed sibling checkout. Its
AGENTS.md
routes to verified upstream guidance and its existing local implementation skill.
- A synthetic plugin uses
plugins/<category>/<name>, exercises public consumer
APIs, and participates in repository validation; a directory alone is not
proof of an implemented plugin or loader.
- A companion's guidance layers core, host, and local contracts without adding
host dependencies to the reusable backend or implying another repository was
modified by a core-only task.
- A new secrets backend can be used by a plain Go consumer without importing a
host framework; a host adapter separately proves registration and lifecycle.
- Static and path-addressed clients are compared by exact signatures; an adapter
binds a path rather than claiming the clients implement the same interface.
- A proposed identity-store backend distinguishes direct
NewPortal injection
from root configuration dispatch. It identifies validator/factory changes
only when the requested integration needs a new shared-dispatch kind.
- A new plugin type gets its own domain contract and, when warranted, a narrow
skill routed from this skill. It does not inherit secrets-only methods.
- Messaging/registration interfaces are traced through their concrete config
containers and consumers before claiming a configurable backend exists.
- Engine-level storage/signing injection is distinguished from full portal
integration; request-time claims enrichment has explicit validator/gatekeeper
attachment, and external authorization has a required-decision hook and HTTP JSON backend.
- Documentation distinguishes inspected source, tests actually executed, desired
new behavior, and integration that remains unavailable.