Custom Auth Provider
The Custom Provider guide builds a
Stripe Product provider whose lifecycle handlers need an API key. Give them
a lazy StripeCredentials service backed
by an Auth Provider, so the key comes from the configured
Profile, from env vars in CI, or from a refreshable
session, never from props or hardcoded config. For a token-shaped credential
the whole provider is generated by makeStoredAuthProvider from a field list;
the in-repo Neon provider
is exactly this shape.
For the profile method contract, CI resolver, registry, and lazy resolution, see Auth Providers.
Declare a lazy credentials service
Section titled “Declare a lazy credentials service”The service value is an Effect, not a resolved struct:
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";
export class StripeCredentials extends Context.Service< StripeCredentials, Effect.Effect<{ apiKey: Redacted.Redacted<string> }>>()("StripeCredentials") {}Provider Layers are built on every CLI invocation — before any Profile is
configured — so nothing may resolve at Layer construction. Consumers run the
Effect only when an operation executes, and a session-backed implementation can
put refresh logic inside it. Every built-in credentials service has this shape
(CloudflareEnvironment, AWSEnvironment, the distilled SDK Credentials
tags).
Generate the Auth Provider
Section titled “Generate the Auth Provider”makeStoredAuthProvider turns a field list into a complete provider:
interactive prompts, flag-driven configuration
(--method stored --set apiKey=...), schema-validated persistence at
~/.alchemy/credentials/{profile}/stripe-stored.json, login/logout,
structured details for alchemy profile show, and a typed NeedsReauth
failure when the credential file is missing.
import { AuthError } from "alchemy/Auth/AuthProvider";import { makeStoredAuthProvider } from "alchemy/Auth/StoredAuthProvider";import { getEnvRedactedRequired } from "alchemy/Auth/Env";import * as Effect from "effect/Effect";import * as Redacted from "effect/Redacted";
export const STRIPE_AUTH_PROVIDER_NAME = "Stripe";
export type StripeResolvedCredentials = { type: "apiKey"; apiKey: Redacted.Redacted<string>; source: { type: "stored" | "env" };};
export const { layer: StripeAuth, storedSchema: StripeStoredCredentials } = makeStoredAuthProvider<StripeResolvedCredentials>({ provider: STRIPE_AUTH_PROVIDER_NAME, fields: [ { name: "apiKey", label: "Stripe Secret API Key", secret: true, validate: (value) => value.startsWith("sk_") ? undefined : "Expected an sk_... key", }, ], toResolved: (values, source) => ({ type: "apiKey", apiKey: Redacted.make(values.apiKey!), source: { type: source }, }), readEnvironment: getEnvRedactedRequired("STRIPE_API_KEY").pipe( Effect.map((apiKey) => ({ type: "apiKey" as const, apiKey, source: { type: "env" as const }, })), ), environment: [ { name: "STRIPE_API_KEY", required: true, secret: true, description: "Stripe secret API key.", }, ], });fields drive interactive prompts and --set; each field name is also the
property persisted in the credentials file, alongside method: "stored"
(the --method value). Validation applies to both paths.
The generated storedSchema validates credential reads and writes.
readEnvironment resolves CI credentials, while environment documents
the variables it consumes. Missing stored credentials appear as
NeedsReauth in the profile UI.
Bridge the Auth Provider to the credentials service
Section titled “Bridge the Auth Provider to the credentials service”lazyCredentials connects the two: it resolves through the registered
provider (environment credentials, then CI, then the selected Profile) on
first use, caches the result, and maps it onto the service’s shape:
// src/stripe/Credentials.ts (additions)import { lazyCredentials } from "alchemy/Auth/Resolve";import * as Layer from "effect/Layer";import { STRIPE_AUTH_PROVIDER_NAME, type StripeResolvedCredentials,} from "./AuthProvider.ts";
export const fromAuthProvider = () => Layer.effect( StripeCredentials, lazyCredentials<StripeResolvedCredentials>()(STRIPE_AUTH_PROVIDER_NAME, (creds) => ({ apiKey: creds.apiKey, })).pipe( // `StripeCredentials` is typed as never failing, so a failure dies; the // engine still reports it as `CredentialsRequired`. Effect.map(Effect.orDie), ), );Nothing resolves until a handler runs the service’s Effect, and within one
process read (and its file lock) runs once. Credentials that expire pass
{ expiresAt } so a long dev session re-resolves instead of keeping a dead
token. A failed resolution fails with CredentialsUnavailable, which the
engine reports as CredentialsRequired; when the service is a client SDK’s
own Credentials tag, name its error channel as the second type argument
(lazyCredentials<Resolved, ConfigError>()).
Bundle into providers()
Section titled “Bundle into providers()”Merge the auth machinery into the same providers() Layer that carries the
resource providers, mirroring
Neon/Providers.ts:
import { CredentialsStoreLive } from "alchemy/Auth/Credentials";import { ProfileStoreLive } from "alchemy/Auth/Profile";import * as Provider from "alchemy/Provider";import * as Layer from "effect/Layer";import { StripeAuth } from "./AuthProvider.ts";import * as Credentials from "./Credentials.ts";import { StripeProduct, StripeProductProvider } from "./Product.ts";
export class Providers extends Provider.ProviderCollection<Providers>()( "Stripe",) {}
export const providers = () => Layer.effect(Providers, Provider.collection([StripeProduct])).pipe( Layer.provide(StripeProductProvider()), Layer.provideMerge(Credentials.fromAuthProvider()), Layer.provideMerge(StripeAuth), Layer.provideMerge(ProfileStoreLive), Layer.provideMerge(CredentialsStoreLive), Layer.orDie, );StripeAuth registers the provider so the alchemy CLI discovers it;
ProfileStoreLive and CredentialsStoreLive give the auth machinery its
file-system services. (Providers that call APIs through an Effect HttpClient
also merge FetchHttpClient.layer here, as Neon does.)
Consume it from a lifecycle handler
Section titled “Consume it from a lifecycle handler”Inside reconcile (or delete, read), double-yield the service — the first
yield* gets the lazy Effect out of the service, the second runs it:
reconcile: Effect.fn(function* ({ news, output }) { const { apiKey } = yield* yield* StripeCredentials; const stripe = new Stripe(Redacted.value(apiKey)); // observe / ensure / sync — see the Custom Provider guide}),This is the same idiom the built-ins use — yield* AWSEnvironment.current,
yield* yield* CloudflareEnvironment — and it’s what keeps credentials
demanded exactly when an operation runs, never at Layer construction.
Try it
Section titled “Try it”alchemy deploy # first run: prompts for credentials, saves them to the profilealchemy profile refresh --provider Stripe # re-authenticate without changing the methodalchemy profile edit --reconfigure Stripe # replace the stored credentials interactively
# non-interactive (agents, scripts): value from env, stdin, or a literalalchemy profile edit --add stripe --method stored --set apiKey=env:STRIPE_API_KEYop read op://vault/stripe/key | alchemy profile edit --add stripe --method stored --set apiKey=-
alchemy profile show # renders your provider's detailsalchemy provider check-env --provider stripe # CI preflight for STRIPE_API_KEYIn CI, profile resolution is bypassed, so setting STRIPE_API_KEY is all a
pipeline needs and no profile file is created.
Where next
Section titled “Where next”- Auth Providers documents the profile contract, CI resolver, registry, and lazy resolution.
- Custom Provider — the lifecycle handlers these credentials feed.
- Profiles — switching, inspecting, and storing the per-provider config.