Skip to content

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.

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.

The module’s default export is the provider’s contributions:

src/Contributions.ts
import * as Contributions from "alchemy/Contributions";
export default Contributions.make({
name: "stripe",
});

name identifies the provider: it’s the subcommand in alchemy provider stripe.

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.

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.

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:.

src/StateStore.ts
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.

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.

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)),
),
  • 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 local or configured, is an error.