How providers reach the platform
A deployment assembles its providers into one typed container and passes it tocreateVoyantApp({ providers }). In the operator application that container is
built by buildOperatorProviders() in src/api/composition.ts, and app.ts
hands it straight to the platform:
buildOperatorProviders() returns a container that satisfies the platform’s
FrameworkProviders contract. Each entry is a deployment-supplied resolver or
factory the platform calls when it needs that capability. Swapping a provider
means changing one entry in this container, nothing in the platform and nothing
in the modules.
Swap a storage backend
Storage targets a singleStorageProvider contract from
@voyant-travel/storage/types. Picking a backend is choosing which factory
creates that provider. See the Storage page
for the full contract and each backend’s options.
1
Pick a built-in provider
The
@voyant-travel/storage package ships three providers, each a factory
that satisfies the same contract.2
Wrap it in the service
Most deployments use one backend, so
createStorageService(provider) wraps a
single provider as a named StorageService with the same
upload / delete / signedUrl / get surface.3
Inject it through the provider container
Hand the factory to the deployment’s provider container so module code
resolves storage from the deployment rather than constructing it. In the
operator application this is the No module changes, because every module already targets
createOperatorDocumentStorage entry on the
container returned by buildOperatorProviders().To move from R2 to S3, swap only the factory call:StorageProvider.Swap a notification transport
Notifications use a list of providers rather than one.createNotificationService([...])
routes each send by its channel, and later providers override earlier ones on
channel conflict. That ordering rule is the seam: register a local console sink
first for development, then layer the real transports on top.
createNotificationService. Feature code picks a channel, never a vendor,
so changing transport is a configuration change with no code edit.
Bring your own provider
The built-in providers are convenience, not a closed set. Any deployment can implement a contract against another vendor and register it in place of the shipped factory. The platform only sees the interface.A custom storage backend
Implement theStorageProvider interface from @voyant-travel/storage/types,
then pass your provider to createStorageService exactly like a built-in one:
A custom notification transport
Notifications make the bring-your-own path first-class. ImplementNotificationProvider from @voyant-travel/notifications/types against any
transport (raw Resend, Twilio, SES) and register it in the service list in place
of the cloud providers. Because later providers win on channel conflict, you can
keep the local sink for other channels and override just the one you replace:
sms provider. Feature code keeps naming a channel, so
nothing else changes.
Dispatch by provider name with sendWith(name, payload) when you need to target
a specific transport rather than route by channel.
Payments are a provider too
Payments follow the same shape, behind one contract rather than one integration per processor.@voyant-travel/payments
defines the payment adapter contract (voyant.payment-adapter.v1), the
provider catalog, and the remote transport. Processors are entries in that
catalog: Netopia and Voyant Pay are providers, not bespoke wiring inside the
booking flow.
The contract is deliberately narrow — initiate, status, verify — and the
platform’s vocabulary is its own rather than any processor’s. A provider maps
its own stage names onto shared values:
- Session state:
pending,requires_redirect,processing,authorized,paid,failed,cancelled,expired. - Operation status:
accepted,declined,pending,failed. - Checkout handoff:
redirectorembedded, negotiated between what the provider supports and what the caller accepts. - Disputes:
openedandunder_revieware open;won,lostandwithdrawnare resolutions. Every card processor produces these, so the contract models them once. - Errors:
CAPABILITY_NOT_SUPPORTED,IDEMPOTENCY_KEY_REUSED,INVALID_REQUEST,PROVIDER_UNAVAILABLE,ADAPTER_FAILURE.
PaymentAdapterCapabilities, and callers negotiate
against them rather than assuming. That is what makes a processor swap a
configuration change instead of a rewrite: finance code never learns which
provider it is talking to.
To support a different processor, implement the same contract and register the
provider in the catalog. See
Adapters and providers.
Next steps
Modules
Where a provider sits in the module, adapter, provider, extension taxonomy.
Storage
The full
StorageProvider contract and each built-in backend’s options.Adapters and providers
When to build which, and how the deployment graph selects it.
Configuration
How a deployment composes modules and wires the providers behind them.