1---
2title: Auth Providers
3description: An Auth Provider produces the credentials for a cloud's API calls — resolved lazily as Effects, so tokens and role sessions can refresh instead of pinning one static key.
4---
5
6Resources never take API keys as props. Each cloud registers an
7**Auth Provider** — the service that produces credentials for that
8cloud's API calls. A [Profile](/environments/profiles) stores which
9auth method you picked; the Auth Provider turns that choice into
10live credentials whenever lifecycle code actually calls the cloud.
11
12## The contract
13
14An Auth Provider is a named record of five profile methods plus an optional
15CI environment resolver and its declared environment contract:
16
17```typescript
18// alchemy/Auth/AuthProvider
19export interface AuthProviderImpl<
20 Config extends { method: string },
21 Credentials,
22> {
23 readonly configSchema: Schema.Codec<Config>;
24 configure(profileName: string): Effect.Effect<Config, AuthError>;
25 configureWith?(
26 profileName: string,
27 input: { method: string; values: Record<string, string> },
28 ): Effect.Effect<Config, AuthError>;
29 readonly configureMethods?: ReadonlyArray<ConfigureMethod>;
30 login(profileName: string, config: Config): Effect.Effect<void, AuthError>;
31 logout(profileName: string, config: Config): Effect.Effect<void, AuthError>;
32 details(
33 profileName: string,
34 config: Config,
35 ): Effect.Effect<ProviderDetails, AuthError | NeedsReauth>;
36 read(
37 profileName: string,
38 config: Config,
39 ): Effect.Effect<Credentials, AuthError | NeedsReauth>;
40 readonly readEnvironment?: Effect.Effect<Credentials, AuthError>;
41 readonly environment?: ReadonlyArray<EnvironmentVariable>;
42}
43```
44
45`configure` handles interactive setup. `configureWith` handles `--method`
46and `--set`; its inputs are declared by `configureMethods`. `login` and
47`logout` manage stored secrets, `details` supplies redacted profile status,
48and `read` resolves credentials.
49
50`configSchema` describes the provider's manifest entry. Stored entries are
51user-editable JSON that may come from another alchemy version. Alchemy decodes
52every load against this schema. An invalid entry fails with a reconfigure hint
53instead of reaching provider code that matches
54exhaustively on `method`. The `makeStoredAuthProvider` factory supplies it
55automatically.
56
57`details` and `read` use the tagged `NeedsReauth` error for missing, expired,
58or rotated credentials. The profile UI renders it as "needs re-login."
59
60`readEnvironment` never receives or creates a profile. It is used whenever
61every required variable in the provider's contract is present in config (the
62process environment, `.env`, or `--env-file`), taking precedence over any
63selected profile, and always when `CI=true`. See
64[environment variables outside CI](/environments/profiles#environment-variables-outside-ci).
65`environment` declares every variable `readEnvironment` consumes
66(`name`, `required`, `secret`, `description`, `alternatives`). Alchemy validates
67this declaration during registration. Registration fails when a provider has
68`readEnvironment` but no declaration. `alchemy provider check-env` uses the
69declaration as a CI preflight.
70
71Providers register by name into a single `AuthProviders` registry
72via `AuthProviderLayer`, inside each cloud's `providers()` Layer:
73
74```typescript
75// alchemy/Auth/AuthProvider
76export class AuthProviders extends Context.Service<
77 AuthProviders,
78 {
79 [providerName: string]: AuthProvider;
80 }
81>()("AuthProviders") {}
82```
83
84That's how [`alchemy profile edit`](/cli/profile#profile-edit) works: it
85imports your stack, reads the registry, and runs the selected providers'
86`configure`/`login`. The factory also wraps `read` and `logout` in a
87cross-process file lock (so two processes never refresh credentials
88simultaneously) and serializes interactive flows so prompts from
89different providers don't interleave. The file lock does not cover `configure`
90or `login`, because a browser grant may take minutes and block every concurrent
91`read`. A provider whose login path
92silently refreshes a rotate-on-use token takes the lock around just that
93read-refresh-persist section itself.
94
95## Credentials are lazy Effects
96
97Nothing in alchemy holds credentials as a plain value. The per-cloud
98environment services are `Context.Service`s whose **service value is
99itself an Effect**:
100
101```typescript
102// alchemy/Cloudflare/CloudflareEnvironment
103export class CloudflareEnvironment extends Context.Service<
104 CloudflareEnvironment,
105 Effect.Effect<CloudflareResolvedCredentials>
106>()("Cloudflare::CloudflareEnvironment") {
107 readonly kind = "Environment" as const;
108}
109```
110
111AWS is the same shape, and goes one level deeper — the resolved
112environment holds its credentials as an Effect too, so expiring SSO
113and assumed-role sessions re-resolve on each access:
114
115```typescript
116// alchemy/AWS/Environment
117export interface AWSEnvironmentShape {
118 accountId: AccountID;
119 region: RegionID;
120 credentials: Effect.Effect<ResolvedCredentials, CredentialsError>;
121 endpoint?: string;
122 profile?: string;
123}
124
125export class AWSEnvironment extends Context.Service<
126 AWSEnvironment,
127 Effect.Effect<AWSEnvironmentShape>
128>()("AWS::Environment") {
129 static current = AWSEnvironment.use((env) => env);
130 readonly kind = "Environment" as const;
131}
132```
133
134This laziness is the design point. Provider Layers are built on
135every CLI invocation — before a Profile may even exist — so the
136Layer can't bake in a resolved key. Handing consumers an Effect
137instead means resolution happens at the point of use, and the Effect
138can embed refresh logic: an OAuth token or IAM role session renews
139itself instead of pinning whatever key was live at startup.
140
141## What lifecycle code yields
142
143Inside `reconcile`/`read`/`delete`, a handler yields the environment
144twice — once to get the Effect out of the service, once to run it:
145
146```typescript
147// Cloudflare (packages/alchemy/src/Cloudflare/Calls/App.ts)
148const { accountId } = yield* yield* CloudflareEnvironment;
149
150// AWS — AWSEnvironment.current does the double yield for you
151const { accountId, region } = yield* AWSEnvironment.current;
152```
153
154Most handlers never touch credentials at all. They call distilled
155SDK operations, and those operations require the SDK's `Credentials`
156service — again a lazy Effect:
157
158```typescript
159// @distilled.cloud/cloudflare (Neon and Planetscale are the same shape)
160export class Credentials extends Context.Service<
161 Credentials,
162 Effect.Effect<ResolvedCredentials, CredentialsError, never>
163>()("CloudflareCredentials") {}
164```
165
166The distilled HTTP client re-runs that Effect on **every API
167request**, so whatever refresh logic the Effect embeds runs
168per-request. Each cloud's `providers()` Layer supplies this service
169from the active Profile's Auth Provider — the handler just calls
170the API.
171
172## How Profiles select an Auth Provider
173
174A Profile stores one `{ method }` config per provider in
175`~/.alchemy/profiles.json` (secrets live separately under
176`~/.alchemy/credentials/{profile}/`). Each cloud bridges that config
177to its credential service with a `fromProfile`/`fromAuthProvider`
178Layer:
179
180```typescript
181// alchemy/Cloudflare/CloudflareEnvironment
182export const fromProfile = () =>
183 Layer.effect(
184 CloudflareEnvironment,
185 Effect.gen(function* () {
186 const { resolve } = yield* resolveProviderConfig<
187 CloudflareAuthConfig,
188 CloudflareResolvedCredentials
189 >(CLOUDFLARE_AUTH_PROVIDER_NAME);
190 return yield* resolve.pipe(
191 Effect.orDie,
192 Effect.cached,
193 );
194 }),
195 );
196```
197
198Outside CI, `resolveProviderConfig` selects the current profile and loads its
199stored config. An unconfigured provider fails with the exact
200`alchemy profile edit` command to run. Resolution never starts a login flow.
201The provider's `read` method then materializes credentials. Profile selection
202(`--profile`, `$ALCHEMY_PROFILE`, `alchemy profile`) is covered in
203[Profiles](/environments/profiles).
204
205### Environment credentials
206
207When `CI=true`, `resolveProviderConfig` bypasses profile selection and calls
208the provider's `readEnvironment` capability. It does not manufacture an
209`{ method: "env" }` profile entry or touch the local profile files. See the
210[provider environment-variable table](/environments/profiles#ci-environment-variables-by-provider)
211for the exact inputs.
212
213Outside CI, the same capability runs first whenever the provider's required
214variables are all present in config (process environment, `.env`, or
215`--env-file`) — even when `--profile` or `ALCHEMY_PROFILE` selected a
216profile. The check is per provider, so one provider can come from the
217environment while the others in the same run come from the profile. Alchemy
218logs the variables it used. Unset them to fall back to the profile.
219
220## Refresh in practice
221
222Cloudflare's OAuth method shows why `read` is an Effect and not a
223stored value. Every resolve checks expiry and refreshes proactively:
224
225```typescript
226// alchemy/Cloudflare/Auth/AuthProvider — inside read (method: "oauth")
227const fresh =
228 creds.expires > Date.now() + 10_000
229 ? creds
230 : yield* OAuthClient.refresh(creds).pipe(
231 Effect.tap((refreshed) =>
232 store.write(
233 profileName,
234 "cloudflare-oauth",
235 OAuthClient.OAuthCredentials,
236 refreshed,
237 ),
238 ),
239 Effect.mapError(
240 (e) =>
241 new AuthError({
242 message: "Cloudflare OAuth refresh failed. Run: alchemy profile refresh --profile default --provider Cloudflare",
243 cause: e,
244 }),
245 ),
246 );
247```
248
249Because `read` runs under the cross-process file lock, two
250concurrent deploys can't double-spend the single-use refresh token.
251Users can force the same provider-specific login/refresh operation without
252changing configuration by running `alchemy profile refresh`, optionally with a
253named profile and repeatable `--provider` filters. `profile edit
254--reconfigure` is reserved for changing the method, account, scopes, or other
255provider configuration.
256One caveat for precision: the `fromProfile` Layers above pipe
257resolution through `Effect.cached`, so the Profile-level `read` runs
258once per process — ongoing refresh happens inside the credential
259Effects the SDKs re-run per request (and in proactive refreshes like
260the one above), not by re-reading the Profile from disk on every
261call.
262
263## Where next
264
265- [Custom Auth Provider](/environments/custom-auth-provider) — build and register your own.
266- [Profiles](/environments/profiles) — pick, switch, and inspect the stored auth config.
267- [Custom Provider](/infrastructure-as-code/custom-provider) — the lifecycle side that consumes these credentials.