Skip to main content

Extension Points

Most VEF extension points are explicit FX groups.

This page covers the DI extension helpers that register into FX groups, replace defaults through fx.Decorate, or supply singleton values through fx.Supply.

Helper mechanisms​

The helper name prefix is not enough to tell how the value is wired. Use the mechanism:

MechanismHelpers
fx.Provide + fx.ResultTags group appendProvideAPIResource, ProvideAuthStrategy, ProvideAuthenticator, ProvideMiddleware, ProvideSPAConfig, ProvideCQRSBehavior, ProvideChallengeProvider, ProvideMCPTools, ProvideMCPResources, ProvideMCPResourceTemplates, ProvideMCPPrompts, ProvideEventTransport, ProvideEventPublishMiddleware, ProvideEventConsumeMiddleware, ProvideApprovalLifecycleHook, ProvideApprovalAggregator, ProvideApprovalAssigneeResolver, ProvideApprovalCCResolver, ProvideApprovalInitiatorResolver, ProvideDataSourceProvider, ProvideJSLib, ProvideCronJobHandler, ProvideIntegrationOutboundAuthScheme, ProvideIntegrationInboundAuthScheme, ProvideIntegrationInboundHandler, ProvideSessionRevocationListener
fx.Supply with group tagsSupplySPAConfigs
fx.Decorate replacementSupplyFileACL, SupplyURLKeyMapper, SupplyBusinessRefProvider, SupplyBusinessRefResolver, ProvideEventMetricsRecorder, ProvideEventErrorSink, ProvideApprovalFormSchemaParser, ProvideApprovalGlobalsResolver
plain fx.Supply valueSupplyMCPServerInfo

Replacement helpers are single-service overrides, not append-only extension points. Register only one implementation unless you intentionally want a later FX option to replace an earlier one.

API and app groups​

  • vef:api:resources
  • vef:api:auth_strategies
  • vef:app:middlewares

Helpers:

  • vef.ProvideAPIResource(...)
  • vef.ProvideAuthStrategy(...)
  • vef.ProvideMiddleware(...)

ProvideAuthStrategy appends a custom api.AuthStrategy into the auth-strategy group. The strategy is selected by the name returned from Name() through api.AuthConfig.Strategy; built-in strategies are none, bearer, signature, ip, api_key, and http_basic.

The app.Middleware contract​

vef.ProvideMiddleware(...) appends into vef:app:middlewares, and the constructor must return the public app.Middleware:

type Middleware interface {
Name() string
Order() int
Apply(router fiber.Router)
}

Negative orders register before the route handlers and positive orders after, each sorted ascending; the zero default registers in the before group.

The contract lives in a public package for a reason that applies to every extension point here: fx matches a group by exact type, and a provider whose type does not match is dropped without an error — the application boots and the extension simply never runs. Structural identity does not help, since two named interfaces with the same method set are different types. Before that split, ProvideMiddleware was unusable by every real host, and the failure looked like a route that silently refused to register.

A middleware that registers a real route owns its own authentication: the API auth middleware runs inside operation dispatch only, so contextx.Principal(ctx) outside /api returns nil forever. Dispatch security.AuthManager yourself.

Minimal module example​

var Module = vef.Module(
"app:user",
vef.ProvideAPIResource(NewUserResource),
vef.ProvideMiddleware(NewAuditMiddleware),
)

API parameter injection​

  • vef:api:handler_param_resolvers
  • vef:api:factory_param_resolvers

These extend request-time and startup-time handler injection.

CQRS​

  • vef:cqrs:behaviors

Helper:

  • vef.ProvideCQRSBehavior(...)

Security​

  • vef:security:challenge_providers
  • vef:security:session_revocation_listeners

Helpers:

  • vef.ProvideChallengeProvider(...) — appends a security.ChallengeProvider into the group security/auth evaluates in Order(). The constructor must return security.ChallengeProvider itself: fx keys the group by type, so a constructor returning a concrete provider type is dropped without an error. A provider applies to every login; to scope one to some login mechanisms, return it wrapped with security.NewFilteredChallengeProvider (see Authentication: Scoping challenges to login mechanisms)
  • vef.ProvideAuthenticator(...) — appends a security.Authenticator into the authenticator group the security.AuthManager aggregates. The authenticator claims a login type through Supports(authType); the built-in types are password, the configured token mechanism (jwt_token or opaque_token), refresh, signature, and — while trust login is enabled — trust_code
  • vef.ProvideSessionRevocationListener(...) — appends a security.SessionRevocationListener observing logout, concurrent-login eviction, and administrative kicks; see Session Management
// security.SessionRevocationListener
OnSessionsRevoked(ctx context.Context, revocations []SessionRevocation)

type SessionRevocation struct {
SessionID string
UserID string
}

Cron job handlers​

  • vef:cron:job_handlers

Helper:

  • vef.ProvideCronJobHandler(...)

Registers a durable cron.JobHandler with the schedule store — exactly one handler per job name; duplicate names fail startup. Handlers optionally ship a default schedule via cron.WithDefaultSchedule. See Durable Schedules.

// cron.JobHandler
Name() string
Execute(ctx context.Context, execution Execution) error

type Execution struct {
RunID string
ScheduleID string
ScheduleName string
JobName string
ScheduledAt time.Time
Params json.RawMessage
}

JS engine libraries​

  • vef:js:libs

Helper:

  • vef.ProvideJSLib(...)

Contributes a js.Lib to the shared js.Engine: a lib whose name matches a built-in replaces it within its tier (always-on vs opt-in catalog), a new name joins the opt-in catalog. See JS Engine.

Integration engine (requires vef.IntegrationModule)​

  • vef:integration:outbound_auth_schemes
  • vef:integration:inbound_auth_schemes
  • vef:integration:inbound_handlers

Helpers:

  • vef.ProvideIntegrationOutboundAuthScheme(...) — custom integration.OutboundAuthScheme; a name matching a built-in replaces it
  • vef.ProvideIntegrationInboundAuthScheme(...) — custom integration.InboundAuthScheme; same replacement rule
  • vef.ProvideIntegrationInboundHandler(...) — the business handler serving one inbound contract; exactly one handler per contract code

The default integration.RouteResolver is replaced with plain fx.Decorate when routing lives outside the itg_route table. See Integration Engine.

Event bus​

  • vef:event:transports
  • vef:event:publish-middlewares
  • vef:event:consume-middlewares

Helpers:

  • vef.ProvideEventTransport(...)
  • vef.ProvideEventPublishMiddleware(...)
  • vef.ProvideEventConsumeMiddleware(...)

The transport helper registers custom event/transport.Transport implementations. Publish middleware runs before a frame is handed to a transport; consume middleware runs around subscriber handlers.

Two event integrations replace framework defaults instead of appending group members:

  • vef.ProvideEventMetricsRecorder(...) decorates the default event.MetricsRecorder.
  • vef.ProvideEventErrorSink(...) decorates the async publish error sink.

Data source providers​

  • vef:datasource:providers

Helper:

  • vef.ProvideDataSourceProvider(...)

A datasource.Provider loads additional data source specs during startup, after the primary and static TOML sources are already registered. Every returned datasource.Spec is registered into the datasource.Registry; a name collision with TOML or another provider fails boot.

The primary source is reserved under datasource.PrimaryName ("primary"). It comes from vef.data_sources.primary, is exposed as the framework-wide orm.DB, and cannot be mutated through the dynamic registry API. Static non-primary TOML entries are seeded before provider specs. Provider order is undefined; Provider.Load errors and any registry conflict abort application startup, and Provider.Name is used in diagnostic messages.

Top-level datasource APIs:

APIContract
datasource.ConnectionInfoResult returned by Registry.TestConnection; exported field datasource.ConnectionInfo.Version is string.
datasource.ErrClosedReturned by datasource.Registry.Register, datasource.Registry.Update, or datasource.Registry.Unregister after registry shutdown begins.
datasource.ErrExistsReturned by datasource.Registry.Register when the name is already registered.
datasource.ErrNameInvalidReturned by datasource.Registry.Register or datasource.Registry.Update when the name is empty or contains whitespace/control characters.
datasource.ErrNotFoundReturned by datasource.Registry.Get, datasource.Registry.Kind, datasource.Registry.Update, or datasource.Registry.Unregister when the name is not registered.
datasource.ErrPrimaryReservedReturned by datasource.Registry.Register, datasource.Registry.Update, or datasource.Registry.Unregister for datasource.PrimaryName.
datasource.PrimaryNameConstant "primary", the reserved TOML primary source name.
datasource.ProviderStartup provider interface with datasource.Provider.Name and datasource.Provider.Load.
datasource.ReconcileOptionFunctional option type for Registry.Reconcile.
datasource.ReconcileOptionsOption state with exported field datasource.ReconcileOptions.DryRun (bool).
datasource.ReconcileReportReconcile result with exported fields datasource.ReconcileReport.Added, datasource.ReconcileReport.Updated, datasource.ReconcileReport.Removed, and datasource.ReconcileReport.Errors.
datasource.RegisterOptionFunctional option type for Registry.Update and Registry.Unregister.
datasource.RegisterOptionsOption state with exported field datasource.RegisterOptions.CloseGrace (time.Duration).
datasource.RegistryInjectable registry interface for primary lookup, named lookup, mutation, reconcile, probing, and health checks.
datasource.SpecDesired or provider-supplied source with exported fields datasource.Spec.Name and datasource.Spec.Config.
datasource.WithCloseGrace(d)Sets RegisterOptions.CloseGrace only when d > 0; delayed closes are asynchronous and are cut short by shutdown.
datasource.WithReconcileDryRun()Sets ReconcileOptions.DryRun so Reconcile reports the diff without opening or closing connections.

datasource.Registry methods:

MethodContract
datasource.Registry.PrimaryReturns the primary orm.DB; equivalent to Get(datasource.PrimaryName) but does not return an error.
datasource.Registry.GetReturns the registered orm.DB; unknown names return datasource.ErrNotFound.
datasource.Registry.HasReports whether a name is currently registered.
datasource.Registry.NamesReturns all registered names, including primary, in stable lexical order.
datasource.Registry.KindReturns the configured config.DBKind; unknown names return datasource.ErrNotFound.
datasource.Registry.RegisterOpens and pings a new non-primary source before inserting it. Duplicate names return datasource.ErrExists; a failed conflict path closes the new pool.
datasource.Registry.UpdateOpens and pings the replacement before swapping it in. Failed updates leave the old entry untouched; successful updates close the old pool asynchronously and honor datasource.WithCloseGrace.
datasource.Registry.UnregisterRemoves a non-primary source, then closes the old pool asynchronously and honors datasource.WithCloseGrace.
datasource.Registry.ReconcileSerializes reconcile calls, ignores empty and primary specs, sorts add/update/remove buckets, and records per-name failures in ReconcileReport.Errors without returning a top-level error for partial failure.
datasource.Registry.TestConnectionOpens a throwaway connection, queries the server version, closes it, returns datasource.ConnectionInfo, and never mutates the registry. The probe is capped by the internal 5s timeout while still respecting caller cancellation/deadline.
datasource.Registry.HealthCheckPings primary and all non-primary entries in parallel and returns a name-to-error map; nil errors mean reachable sources.

SPA​

  • vef:spa

Helpers:

  • vef.ProvideSPAConfig(...)
  • vef.SupplySPAConfigs(...)

Storage integration​

Helpers:

  • vef.SupplyFileACL(...)
  • vef.SupplyURLKeyMapper(...)

SupplyFileACL replaces the default storage.FileACL. The default only grants read access to keys under pub/; applications that store private files should provide a business-specific ACL.

SupplyURLKeyMapper replaces the default storage.ProxyURLKeyMapper, which maps /storage/files/<key> proxy URLs back to object keys. Override it when rich-text or markdown content embeds CDN URLs or any other URL form that must map back to storage object keys. Use storage.IdentityURLKeyMapper only when content embeds bare object keys directly. This default is for the framework DI graph; direct storage.NewFiles(...) calls normalize a nil mapper to storage.IdentityURLKeyMapper.

MCP​

  • vef:mcp:tools
  • vef:mcp:resources
  • vef:mcp:templates
  • vef:mcp:prompts

Helpers:

  • vef.ProvideMCPTools(...)
  • vef.ProvideMCPResources(...)
  • vef.ProvideMCPResourceTemplates(...)
  • vef.ProvideMCPPrompts(...)
  • vef.SupplyMCPServerInfo(...)

Approval lifecycle hooks​

  • vef:approval:lifecycle_hooks

Helper:

  • vef.ProvideApprovalLifecycleHook(...)

approval.InstanceLifecycleHook implementations run synchronously inside the approval engine transaction — OnInstanceCreated at instance creation and OnInstanceTransition(from, to) at every instance status transition. Returning an error rolls back the surrounding approval command. Invocation order across hooks is unspecified (FX value groups carry no ordering). Use event subscriptions or approval.BindCommand for asynchronous integrations that should run after commit.

Approval aggregators and form schema​

  • vef:approval:aggregators

Helpers:

  • vef.ProvideApprovalAggregator(...)
  • vef.ProvideApprovalFormSchemaParser(...)

ProvideApprovalAggregator registers a custom detail-table aggregator for approval field conditions alongside the built-in sum / count / avg. The constructor must return an approval.Aggregator; the condition evaluator picks it up by its AggregateKind. Boot fails if a built-in aggregate kind is left unregistered.

ProvideApprovalFormSchemaParser replaces the framework's default approval.FormSchemaParser (the built-in vef-framework-react form-editor parser). The replacement is wholesale, not additive: every deployed form schema goes through it, so it must understand every designer document the host submits. Parsing runs once at flow deploy; versions deployed earlier keep the form_fields they were persisted with.

Approval principal-resolution kinds​

  • vef:approval:assignee_resolvers
  • vef:approval:cc_resolvers
  • vef:approval:initiator_resolvers

Helpers:

  • vef.ProvideApprovalAssigneeResolver(...)
  • vef.ProvideApprovalCCResolver(...)
  • vef.ProvideApprovalInitiatorResolver(...)

Who may approve, who gets copied, and who may start a flow are three open registries: the deployable kinds are exactly those with a registered resolver. A resolver whose kind matches a built-in replaces it in place, keeping the designer's option order; any other kind is appended in ascending kind order, because an fx value group arrives randomized and appending in arrival order would reshuffle the designer's dropdown on every restart. Two host resolvers claiming one kind fail at boot rather than resolving last-wins.

Each resolver's Describe() returns the approval.KindDescriptor that drives both the designer's input and save-time validation — see Events & Integration.

Approval condition globals​

Helper:

  • vef.ProvideApprovalGlobalsResolver(...)

Replaces the default no-op approval.InstanceGlobalsResolver, whose result is resolved server-side at instance start and snapshotted onto Instance.Globals. Globals steer condition branches, so they are deliberately not part of the start-instance request payload: an applicant able to supply them could steer their own approval. The helper composes through fx.Decorate, so a resolver that needs orm.DB or any other dependency declares it as a constructor parameter.

Approval business binding​

Helpers:

  • vef.SupplyBusinessRefProvider(...)
  • vef.SupplyBusinessRefResolver(...)

SupplyBusinessRefProvider replaces the default no-op approval.BusinessRefProvider. It runs inside start_instance when Flow.BindingMode == BindingBusiness and lets the host resolve or allocate the business row, returning the opaque Instance.BusinessRef.

SupplyBusinessRefResolver replaces the default approval.BusinessRefResolver, which resolves Instance.BusinessRef into the approval.BusinessRecordKey matched against the flow's configured BusinessBindingConfig.KeyColumns (single-column refs verbatim, composite refs as a JSON object). Register one when the ref uses another shape or resolving the key requires a host lookup.

The business-state projection itself is owned by the engine. Hosts extend around it with approval.InstanceLifecycleHook, event subscriptions, or approval.BindCommand; they no longer replace the projection path.

Logging​

Helper:

  • vef.NamedLogger(name)

Use this root-package convenience function when integration code needs a framework logx.Logger outside dependency injection. It returns logx.Logger; the logx package itself exposes the Level constants, Level.String(), the Logger interface contract, and LoggerConfigurable[T] for immutable components that return a logger-configured copy from WithLogger.

ORM transaction commit hook​

  • orm.OnCommit(ctx, fn) — registers a callback to run once the orm.DB.RunInTx scope owning the context has committed. The callback receives a cancellation-detached context and cannot report failure; by the time it runs the transaction is already durable. Calling it outside an active RunInTx / RunInReadOnlyTx scope returns orm.ErrNoCommitScope.

This is the seam for work that must happen if and only if the surrounding unit of work became durable — dispatching an in-process event, invalidating a cache — without that work being able to fail the transaction. It is the underlying mechanism used by the tx_memory event transport to deliver events after commit.

A simple decision rule​

When deciding how to extend VEF, ask:

  • has the framework already reserved a group for this concept?
  • should the extension participate in startup and lifecycle management?
  • do you want dependencies between modules to stay explicit and testable?

If the answer is yes, route through an FX group instead of implicit global state or a hand-rolled singleton.

See also​