Provider Contributions
Some of a provider runs before any stack is loaded: alchemy profile
configures its credentials, alchemy state --backend opens its state
store, and alchemy provider <name> runs its commands. A provider
package declares these in a contributions module, and the CLI reads it
from the project’s dependencies.
Take the Stripe provider from
Custom Provider and
Custom Auth Provider, packaged
as @acme/alchemy-stripe.
Declare the module
Section titled “Declare the module”List the module under alchemy.contributes in the package’s
package.json, and export it:
{ "name": "@acme/alchemy-stripe", "exports": { ".": "./src/index.ts", "./Contributions": "./src/Contributions.ts" }, "alchemy": { "contributes": ["@acme/alchemy-stripe/Contributions"] }}A specifier must name the package that declares it. It resolves through
that package’s exports.
Name the provider
Section titled “Name the provider”The module’s default export is the provider’s contributions:
import * as Contributions from "alchemy/Contributions";
export default Contributions.make({ name: "stripe",});name identifies the provider: it’s the subcommand in
alchemy provider stripe.
Load fields lazily
Section titled “Load fields lazily”The CLI imports every contributions module on start, so a field points
at code instead of importing it. Contributions.lazy imports a module
the first time the field is used:
export default Contributions.make({ name: "stripe", auth: Contributions.lazy( () => import("./AuthProvider.ts"), (module) => module.StripeAuth, ),});auth is the Auth Provider layer. With it, alchemy profile edit can
configure Stripe credentials in a project whose stack hasn’t run yet.
Add a CLI command
Section titled “Add a CLI command”cli.provider adds a subcommand under alchemy provider. The
description shows in --help without loading the command:
cli: { provider: { description: "Manage Stripe prerequisites", load: Contributions.lazy( () => import("./Cli.ts"), (module) => module.stripeCommand, ), },},stripeCommand is an effect/cli Command named stripe. A provider
whose command fails to load doesn’t break the others: running it reports
the error.
Add a state store
Section titled “Add a state store”stateStores maps a --backend name to a store:
stateStores: { stripe: { description: "State in the profile's Stripe account", load: Contributions.lazy( () => import("./StateStore.ts"), (module) => module.stateStore, ), },},The store’s layer is the provider’s own state function: the one a
stack passes as state:.
import type { StateStoreImpl } from "alchemy/Contributions";import { state } from "./State.ts";
export const stateStore: StateStoreImpl = { layer: state };state returns a State layer from the store’s own options interface.
For the ones a profile alone can’t tell it, contribute an effect CLI flag,
and alchemy state takes it with --backend:
import * as Flag from "effect/cli/Flag";
stripe: { description: "State in the profile's Stripe account", flags: { name: Flag.String("name").pipe( Flag.withDescription("A store other than the account's default, by name"), ), }, load: Contributions.lazy(Each flag parses into the store’s options under its key, and the store
types those options itself. Contributions.make checks the two agree: every
flag must set an option the store declares, with a value it accepts, under a
key the options aren’t spread next to (type, backend, profile,
envFile, force, limit or since).
export interface StripeStateStoreOptions { readonly name?: string;}
export const stateStore: StateStoreImpl<StripeStateStoreOptions> = { layer: state };The CLI makes every store flag optional, so a flag that isn’t given is
undefined, and the store falls back to its own default. Here a
missing name is the account’s default store, and any other name is a
separate store. With the store contributed,
alchemy state list --backend stripe --name team-a opens it from the profile
alone, and an option the store has no flag for is an error.
Stores that set the same option with the same flag, parsed the same way, share it on the command line, and its help lists each store’s description. A flag is left out, with a warning, when it:
- is optional, or has a default or an alias,
- conflicts with another store’s: its name sets a different option there, the same option uses a different flag, or the shared flag parses differently,
- or clashes with one of
alchemy state’s own, such as--profile.
The CLI then refuses that option for the store, rather than handing it a value another store’s flag parsed.
Add a store lifecycle
Section titled “Add a store lifecycle”A store that needs provisioning adds a lifecycle. Each operation is
optional, and alchemy state reports the ones a store doesn’t have:
export const stateStore: StateStoreImpl<StripeStateStoreOptions> = { layer: state };export const stateStore: StateStoreImpl<StripeStateStoreOptions> = { layer: state, lifecycle: { bootstrap, teardown },};| Operation | Runs for |
|---|---|
bootstrap |
alchemy state bootstrap |
teardown |
alchemy state teardown |
logs |
alchemy state logs |
tailLogs |
alchemy state logs --tail |
Each operation receives the store’s options spread into its input, as in
bootstrap: ({ name, force }) => ..., and Alchemy provides the
profile’s config and the provider’s auth. The built-in stores are
the reference:
AWS/Alchemist/StateStore.ts
and
Cloudflare/Alchemist/StateStore.ts.
What contributed code can use
Section titled “What contributed code can use”Every host of contributed code provides the same services:
Contributions.ContributionServices for auth and state stores (platform,
HTTP client, profiles, credentials, the auth registry), and
Contributions.CliServices for commands (those plus the terminal). A
field that needs anything else doesn’t type-check. Provide it in the
field’s own layer:
auth: Contributions.lazy( () => import("./AuthProvider.ts"), (module) => module.StripeAuth.pipe(Layer.provide(StripeHttpClient)),),How the CLI picks providers
Section titled “How the CLI picks providers”- The project’s dependencies are read first, then alchemy’s own providers.
- When two providers share a name, the first one found is used. A provider in its own package therefore replaces the copy still inside alchemy.
- A provider that fails to load is skipped with a warning.
- State store names are global. Two providers contributing the same
name, or
localorconfigured, is an error.
Where next
Section titled “Where next”- Custom Auth Provider: the
authlayer alchemy state: the commands a state store serves- State Store: where resource state lives