Skip to main content
Voyant keeps a deliberately small extension vocabulary. Modules are the components the platform is built from, and customization happens at two seams: adapters and providers, which change an implementation inside the deployment, and apps, which run entirely outside it. This page covers the first seam.
Plugins no longer exist. Voyant used to have a generic plugin kind; it is retired, and a workspace package that declares voyant.kind: "plugin" now fails the architecture verification. Everything that used to be a plugin is an adapter, a provider, an extension, or an app. If you are migrating, the mapping is at the bottom of this page.

Which one do I need?

Provider

You want to replace one concrete implementation with another behind an existing contract. Payments, notifications, object storage, search.

Adapter

You want to talk to a specific external system, and that integration owns more than a single contract: routes, webhooks, subscribers, or several providers at once.

Extension

You want to change how an existing module behaves, not swap an implementation. See Extensions.

App

You are adding a genuinely new capability with its own records and lifecycle. Build it as an app that runs outside the deployment.
Modules are not an extension point. They are the components the platform itself is built from, and a deployment cannot add, drop, or replace one. If you need new capability, build an app; if you need to change existing behavior, build an extension.
The short test: if the question is “how do I swap one implementation for another?”, the answer is a provider. If the package exists primarily to talk to an external system, it is an adapter, even when it also ships providers.

Providers

A provider implements a narrow contract for one execution seam and hides the vendor behind it. Nothing calling the contract should be able to tell which provider is selected. Providers are first-class deployment graph units:
  • package.json#voyant.kind is "provider"
  • the graph manifest uses schemaVersion: "voyant.provider.v1"
A provider should implement one contract, stay focused on that seam, and keep vendor-specific detail entirely internal.

Roles, not vendors

A provider role is defined by what it does, not by who supplies it. Object storage is one role with built-in values memory and s3-compatible — AWS S3, Cloudflare R2, Google Cloud Storage’s XML API, MinIO and similar all configure the s3-compatible value through endpoint and credential settings rather than getting a role each. This matters when you write application code: modules resolve logical stores such as media and documents. They never resolve a vendor bucket or a runtime binding. A custom storage package declares a selected storage.object provider factory and returns the same logical-store resolver contract. Payments works the same way. @voyant-travel/payments defines the adapter contract — initiate, status, verify — plus the provider catalog and remote transport. Netopia and Voyant Pay are entries in that catalog, not bespoke integrations wired into the booking flow. See Choose or swap a provider for the mechanics.

Adapters

An adapter connects Voyant to an external vendor or service. It is the right shape when the integration is broader than a single contract. Adapters are first-class deployment graph units:
  • package.json#voyant.kind is "adapter"
  • the graph manifest uses schemaVersion: "voyant.adapter.v1"
An adapter may expose:
  • one or more providers
  • a small extension over an existing module
  • route or webhook wiring
  • subscriber or job wiring owned by the integration package
It may not introduce a new domain module by the back door. If the integration turns out to own its own records and lifecycle, it is a module, and it should be built as one. @voyant-travel/voyant-connect-adapter is the reference example: it brings Connect supplier connectivity into a deployment on top of the external Connect SDK.

Selection happens in the graph

Neither seam is a runtime plugin registry. Adapters and providers are ordinary units in the resolved deployment graph, selected when the deployment is composed. Object storage, key-value, rate limiting, and search all resolve this way. Two consequences worth knowing up front:
  • A standard module cannot be dropped or replaced. Substitution happens at adapters and providers, never at business modules. A deployment gets the same bookings module as everyone else; what it can change is which payment provider that module ends up calling.
  • Runtime contributors must declare every returned port on the owning graph unit. A package cannot quietly contribute a capability it did not declare.

Migrating from plugins

If you have a package that predates the retirement of the plugin kind, it maps to one of these: Accounting and CRM integrations such as SmartBill are apps, not adapters, because their code runs outside the Voyant process entirely. That distinction is the subject of the next page.

Choose or swap a provider

The step-by-step version.

Apps

The other seam: code that runs outside the deployment.