1---
2title: Custom Auth Provider
3description: Add profile and CI credentials for a third-party API with a generated AuthProvider.
4---
5
6The [Custom Provider](/infrastructure-as-code/custom-provider) guide builds a
7Stripe `Product` provider whose lifecycle handlers need an API key. This guide
8builds a lazy `StripeCredentials` service backed
9by an Auth Provider, so the key comes from the configured
10[Profile](/environments/profiles), from env vars in CI, or from a refreshable
11session, never from props or hardcoded config. For a token-shaped credential
12the whole provider is generated by `makeStoredAuthProvider` from a field list;
13the in-repo [Neon provider](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Neon/AuthProvider.ts)
14is exactly this shape.
15
16For the profile method contract, CI resolver, registry, and lazy resolution,
17see [Auth Providers](/environments/auth-providers).
18
19## Declare a lazy credentials service
20
21The service value is an `Effect`, not a resolved struct:
22
23```typescript
24// src/stripe/Credentials.ts
25import * as Context from "effect/Context";
26import * as Effect from "effect/Effect";
27import * as Redacted from "effect/Redacted";
28
29export class StripeCredentials extends Context.Service<
30 StripeCredentials,
31 Effect.Effect<{ apiKey: Redacted.Redacted<string> }>
32>()("StripeCredentials") {}
33```
34
35Provider Layers are built on every CLI invocation — before any Profile is
36configured — so nothing may resolve at Layer construction. Consumers run the
37Effect only when an operation executes, and a session-backed implementation can
38put refresh logic inside it. Every built-in credentials service has this shape
39(`CloudflareEnvironment`, `AWSEnvironment`, the distilled SDK `Credentials`
40tags).
41
42## Generate the Auth Provider
43
44`makeStoredAuthProvider` turns a field list into a complete provider:
45interactive prompts, flag-driven configuration
46(`--method stored --set apiKey=...`), schema-validated persistence at
47`~/.alchemy/credentials/{profile}/stripe-stored.json`, `login`/`logout`,
48structured `details` for `alchemy profile show`, and a typed `NeedsReauth`
49failure when the credential file is missing.
50
51```typescript
52// src/stripe/AuthProvider.ts
53import { AuthError } from "alchemy/Auth/AuthProvider";
54import { makeStoredAuthProvider } from "alchemy/Auth/StoredAuthProvider";
55import { getEnvRedactedRequired } from "alchemy/Auth/Env";
56import * as Effect from "effect/Effect";
57import * as Redacted from "effect/Redacted";
58
59export const STRIPE_AUTH_PROVIDER_NAME = "Stripe";
60
61export type StripeResolvedCredentials = {
62 type: "apiKey";
63 apiKey: Redacted.Redacted<string>;
64 source: { type: "stored" | "env" };
65};
66
67export const { layer: StripeAuth, storedSchema: StripeStoredCredentials } =
68 makeStoredAuthProvider<StripeResolvedCredentials>({
69 provider: STRIPE_AUTH_PROVIDER_NAME,
70 fields: [
71 {
72 name: "apiKey",
73 label: "Stripe Secret API Key",
74 secret: true,
75 validate: (value) =>
76 value.startsWith("sk_") ? undefined : "Expected an sk_... key",
77 },
78 ],
79 toResolved: (values, source) => ({
80 type: "apiKey",
81 apiKey: Redacted.make(values.apiKey!),
82 source: { type: source },
83 }),
84 readEnvironment: getEnvRedactedRequired("STRIPE_API_KEY").pipe(
85 Effect.map((apiKey) => ({
86 type: "apiKey" as const,
87 apiKey,
88 source: { type: "env" as const },
89 })),
90 ),
91 environment: [
92 {
93 name: "STRIPE_API_KEY",
94 required: true,
95 secret: true,
96 description: "Stripe secret API key.",
97 },
98 ],
99 });
100```
101
102`fields` drive interactive prompts and `--set`; each field name is also the
103property persisted in the credentials file, alongside `method: "stored"`
104(the `--method` value). Validation applies to both paths.
105The generated `storedSchema` validates credential reads and writes.
106`readEnvironment` resolves CI credentials, while `environment` documents
107the variables it consumes. Missing stored credentials appear as
108`NeedsReauth` in the profile UI.
109
110## Bridge the Auth Provider to the credentials service
111
112`fromAuthProvider()` connects the two: look up the registered provider, load
113(or interactively configure) the Profile's `{ method }`, run `read`, and
114provide the result as the `StripeCredentials` Effect:
115
116```typescript
117// src/stripe/Credentials.ts (additions)
118import { resolveProviderConfig } from "alchemy/Auth/Profile";
119import type { StoredAuthConfig } from "alchemy/Auth/StoredAuthProvider";
120import * as Layer from "effect/Layer";
121import {
122 STRIPE_AUTH_PROVIDER_NAME,
123 type StripeResolvedCredentials,
124} from "./AuthProvider.ts";
125
126export const fromAuthProvider = () =>
127 Layer.effect(
128 StripeCredentials,
129 Effect.gen(function* () {
130 const { resolve } = yield* resolveProviderConfig<
131 StoredAuthConfig,
132 StripeResolvedCredentials
133 >(STRIPE_AUTH_PROVIDER_NAME);
134
135 return yield* resolve.pipe(
136 Effect.map((creds) => ({ apiKey: creds.apiKey })),
137 Effect.orDie,
138 Effect.cached,
139 );
140 }),
141 );
142```
143
144Two details matter. Outside CI, `resolveProviderConfig` applies the standard
145profile selection order, then returns the stored config or runs `configure`
146and persists the answer. In CI it bypasses profiles and returns
147`readEnvironment`. A missing explicit local Profile fails with a command to
148create it. And the value returned to `Layer.effect` is the *Effect*
149(closed by `Effect.cached`), not its result: nothing resolves until a handler
150runs it, and within one process `read` (and its file lock) runs at most once.
151Every built-in bridge — Neon, Planetscale, Cloudflare — has this exact shape.
152(Credentials that expire should use `cacheUntilExpiry` from
153`alchemy/Auth/CredentialsCache` instead of `Effect.cached`, so a long dev
154session re-resolves instead of keeping a dead token.)
155
156## Bundle into `providers()`
157
158Merge the auth machinery into the same `providers()` Layer that carries the
159resource providers, mirroring
160[`Neon/Providers.ts`](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Neon/Providers.ts):
161
162```typescript
163// src/stripe/Providers.ts
164import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
165import { ProfileStoreLive } from "alchemy/Auth/Profile";
166import * as Provider from "alchemy/Provider";
167import * as Layer from "effect/Layer";
168import { StripeAuth } from "./AuthProvider.ts";
169import * as Credentials from "./Credentials.ts";
170import { StripeProduct, StripeProductProvider } from "./Product.ts";
171
172export class Providers extends Provider.ProviderCollection<Providers>()(
173 "Stripe",
174) {}
175
176export const providers = () =>
177 Layer.effect(Providers, Provider.collection([StripeProduct])).pipe(
178 Layer.provide(StripeProductProvider()),
179 Layer.provideMerge(Credentials.fromAuthProvider()),
180 Layer.provideMerge(StripeAuth),
181 Layer.provideMerge(ProfileStoreLive),
182 Layer.provideMerge(CredentialsStoreLive),
183 Layer.orDie,
184 );
185```
186
187`StripeAuth` registers the provider so the alchemy CLI discovers it;
188`ProfileStoreLive` and `CredentialsStoreLive` give the auth machinery its
189file-system services. (Providers that call APIs through an Effect `HttpClient`
190also merge `FetchHttpClient.layer` here, as Neon does.)
191
192## Consume it from a lifecycle handler
193
194Inside `reconcile` (or `delete`, `read`), double-yield the service — the first
195`yield*` gets the lazy Effect out of the service, the second runs it:
196
197```typescript
198// src/stripe/Product.ts
199reconcile: Effect.fn(function* ({ news, output }) {
200 const { apiKey } = yield* yield* StripeCredentials;
201 const stripe = new Stripe(Redacted.value(apiKey));
202 // observe / ensure / sync — see the Custom Provider guide
203}),
204```
205
206This is the same idiom the built-ins use — `yield* AWSEnvironment.current`,
207`yield* yield* CloudflareEnvironment` — and it's what keeps credentials
208demanded exactly when an operation runs, never at Layer construction.
209
210## Try it
211
212```sh
213alchemy deploy # first run: prompts for credentials, saves them to the profile
214alchemy profile refresh --provider Stripe # re-authenticate without changing the method
215alchemy profile edit --reconfigure Stripe # replace the stored credentials interactively
216
217# non-interactive (agents, scripts): value from env, stdin, or a literal
218alchemy profile edit --add stripe --method stored --set apiKey=env:STRIPE_API_KEY
219op read op://vault/stripe/key | alchemy profile edit --add stripe --method stored --set apiKey=-
220
221alchemy profile show # renders your provider's details
222alchemy provider check-env --provider stripe # CI preflight for STRIPE_API_KEY
223```
224
225In CI, profile resolution is bypassed, so setting `STRIPE_API_KEY` is all a
226pipeline needs and no profile file is created.
227
228## Where next
229
230- [Auth Providers](/environments/auth-providers) documents the profile contract,
231 CI resolver, registry, and lazy resolution.
232- [Custom Provider](/infrastructure-as-code/custom-provider) — the lifecycle
233 handlers these credentials feed.
234- [Profiles](/environments/profiles) — switching, inspecting, and storing the
235 per-provider config.