| 1 | --- |
| 2 | title: Auth Providers |
| 3 | description: 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 | |
| 6 | Resources never take API keys as props. Each cloud registers an |
| 7 | **Auth Provider** — the service that produces credentials for that |
| 8 | cloud's API calls. A [Profile](/environments/profiles) stores which |
| 9 | auth method you picked; the Auth Provider turns that choice into |
| 10 | live credentials whenever lifecycle code actually calls the cloud. |
| 11 | |
| 12 | ## The contract |
| 13 | |
| 14 | An Auth Provider is a named record of five profile methods plus an optional |
| 15 | CI environment resolver and its declared environment contract: |
| 16 | |
| 17 | ```typescript |
| 18 | // alchemy/Auth/AuthProvider |
| 19 | export 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` |
| 46 | and `--set`; its inputs are declared by `configureMethods`. `login` and |
| 47 | `logout` manage stored secrets, `details` supplies redacted profile status, |
| 48 | and `read` resolves credentials. |
| 49 | |
| 50 | `configSchema` describes the provider's manifest entry. Stored entries are |
| 51 | user-editable JSON that may come from another alchemy version. Alchemy decodes |
| 52 | every load against this schema. An invalid entry fails with a reconfigure hint |
| 53 | instead of reaching provider code that matches |
| 54 | exhaustively on `method`. The `makeStoredAuthProvider` factory supplies it |
| 55 | automatically. |
| 56 | |
| 57 | `details` and `read` use the tagged `NeedsReauth` error for missing, expired, |
| 58 | or rotated credentials. The profile UI renders it as "needs re-login." |
| 59 | |
| 60 | `readEnvironment` never receives or creates a profile. It is used whenever |
| 61 | every required variable in the provider's contract is present in config (the |
| 62 | process environment, `.env`, or `--env-file`), taking precedence over any |
| 63 | selected 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 |
| 67 | this declaration during registration. Registration fails when a provider has |
| 68 | `readEnvironment` but no declaration. `alchemy provider check-env` uses the |
| 69 | declaration as a CI preflight. |
| 70 | |
| 71 | Providers register by name into a single `AuthProviders` registry |
| 72 | via `AuthProviderLayer`, inside each cloud's `providers()` Layer: |
| 73 | |
| 74 | ```typescript |
| 75 | // alchemy/Auth/AuthProvider |
| 76 | export class AuthProviders extends Context.Service< |
| 77 | AuthProviders, |
| 78 | { |
| 79 | [providerName: string]: AuthProvider; |
| 80 | } |
| 81 | >()("AuthProviders") {} |
| 82 | ``` |
| 83 | |
| 84 | That's how [`alchemy profile edit`](/cli/profile#profile-edit) works: it |
| 85 | imports your stack, reads the registry, and runs the selected providers' |
| 86 | `configure`/`login`. The factory also wraps `read` and `logout` in a |
| 87 | cross-process file lock (so two processes never refresh credentials |
| 88 | simultaneously) and serializes interactive flows so prompts from |
| 89 | different providers don't interleave. The file lock does not cover `configure` |
| 90 | or `login`, because a browser grant may take minutes and block every concurrent |
| 91 | `read`. A provider whose login path |
| 92 | silently refreshes a rotate-on-use token takes the lock around just that |
| 93 | read-refresh-persist section itself. |
| 94 | |
| 95 | ## Credentials are lazy Effects |
| 96 | |
| 97 | Nothing in alchemy holds credentials as a plain value. The per-cloud |
| 98 | environment services are `Context.Service`s whose **service value is |
| 99 | itself an Effect**: |
| 100 | |
| 101 | ```typescript |
| 102 | // alchemy/Cloudflare/CloudflareEnvironment |
| 103 | export class CloudflareEnvironment extends Context.Service< |
| 104 | CloudflareEnvironment, |
| 105 | Effect.Effect<CloudflareResolvedCredentials> |
| 106 | >()("Cloudflare::CloudflareEnvironment") { |
| 107 | readonly kind = "Environment" as const; |
| 108 | } |
| 109 | ``` |
| 110 | |
| 111 | AWS is the same shape, and goes one level deeper — the resolved |
| 112 | environment holds its credentials as an Effect too, so expiring SSO |
| 113 | and assumed-role sessions re-resolve on each access: |
| 114 | |
| 115 | ```typescript |
| 116 | // alchemy/AWS/Environment |
| 117 | export interface AWSEnvironmentShape { |
| 118 | accountId: AccountID; |
| 119 | region: RegionID; |
| 120 | credentials: Effect.Effect<ResolvedCredentials, CredentialsError>; |
| 121 | endpoint?: string; |
| 122 | profile?: string; |
| 123 | } |
| 124 | |
| 125 | export 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 | |
| 134 | This laziness is the design point. Provider Layers are built on |
| 135 | every CLI invocation — before a Profile may even exist — so the |
| 136 | Layer can't bake in a resolved key. Handing consumers an Effect |
| 137 | instead means resolution happens at the point of use, and the Effect |
| 138 | can embed refresh logic: an OAuth token or IAM role session renews |
| 139 | itself instead of pinning whatever key was live at startup. |
| 140 | |
| 141 | ## What lifecycle code yields |
| 142 | |
| 143 | Inside `reconcile`/`read`/`delete`, a handler yields the environment |
| 144 | twice — 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) |
| 148 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 149 | |
| 150 | // AWS — AWSEnvironment.current does the double yield for you |
| 151 | const { accountId, region } = yield* AWSEnvironment.current; |
| 152 | ``` |
| 153 | |
| 154 | Most handlers never touch credentials at all. They call distilled |
| 155 | SDK operations, and those operations require the SDK's `Credentials` |
| 156 | service — again a lazy Effect: |
| 157 | |
| 158 | ```typescript |
| 159 | // @distilled.cloud/cloudflare (Neon and Planetscale are the same shape) |
| 160 | export class Credentials extends Context.Service< |
| 161 | Credentials, |
| 162 | Effect.Effect<ResolvedCredentials, CredentialsError, never> |
| 163 | >()("CloudflareCredentials") {} |
| 164 | ``` |
| 165 | |
| 166 | The distilled HTTP client re-runs that Effect on **every API |
| 167 | request**, so whatever refresh logic the Effect embeds runs |
| 168 | per-request. Each cloud's `providers()` Layer supplies this service |
| 169 | from the active Profile's Auth Provider — the handler just calls |
| 170 | the API. |
| 171 | |
| 172 | ## How Profiles select an Auth Provider |
| 173 | |
| 174 | A 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 |
| 177 | to its credential service with a `fromProfile`/`fromAuthProvider` |
| 178 | Layer: |
| 179 | |
| 180 | ```typescript |
| 181 | // alchemy/Cloudflare/CloudflareEnvironment |
| 182 | export 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 | |
| 198 | Outside CI, `resolveProviderConfig` selects the current profile and loads its |
| 199 | stored config. An unconfigured provider fails with the exact |
| 200 | `alchemy profile edit` command to run. Resolution never starts a login flow. |
| 201 | The 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 | |
| 207 | When `CI=true`, `resolveProviderConfig` bypasses profile selection and calls |
| 208 | the 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) |
| 211 | for the exact inputs. |
| 212 | |
| 213 | Outside CI, the same capability runs first whenever the provider's required |
| 214 | variables are all present in config (process environment, `.env`, or |
| 215 | `--env-file`) — even when `--profile` or `ALCHEMY_PROFILE` selected a |
| 216 | profile. The check is per provider, so one provider can come from the |
| 217 | environment while the others in the same run come from the profile. Alchemy |
| 218 | logs the variables it used. Unset them to fall back to the profile. |
| 219 | |
| 220 | ## Refresh in practice |
| 221 | |
| 222 | Cloudflare's OAuth method shows why `read` is an Effect and not a |
| 223 | stored value. Every resolve checks expiry and refreshes proactively: |
| 224 | |
| 225 | ```typescript |
| 226 | // alchemy/Cloudflare/Auth/AuthProvider — inside read (method: "oauth") |
| 227 | const 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 | |
| 249 | Because `read` runs under the cross-process file lock, two |
| 250 | concurrent deploys can't double-spend the single-use refresh token. |
| 251 | Users can force the same provider-specific login/refresh operation without |
| 252 | changing configuration by running `alchemy profile refresh`, optionally with a |
| 253 | named profile and repeatable `--provider` filters. `profile edit |
| 254 | --reconfigure` is reserved for changing the method, account, scopes, or other |
| 255 | provider configuration. |
| 256 | One caveat for precision: the `fromProfile` Layers above pipe |
| 257 | resolution through `Effect.cached`, so the Profile-level `read` runs |
| 258 | once per process — ongoing refresh happens inside the credential |
| 259 | Effects the SDKs re-run per request (and in proactive refreshes like |
| 260 | the one above), not by re-reading the Profile from disk on every |
| 261 | call. |
| 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. |