| 1 | --- |
| 2 | title: Custom Auth Provider |
| 3 | description: Add profile and CI credentials for a third-party API with a generated AuthProvider. |
| 4 | --- |
| 5 | |
| 6 | The [Custom Provider](/infrastructure-as-code/custom-provider) guide builds a |
| 7 | Stripe `Product` provider whose lifecycle handlers need an API key. This guide |
| 8 | builds a lazy `StripeCredentials` service backed |
| 9 | by an Auth Provider, so the key comes from the configured |
| 10 | [Profile](/environments/profiles), from env vars in CI, or from a refreshable |
| 11 | session, never from props or hardcoded config. For a token-shaped credential |
| 12 | the whole provider is generated by `makeStoredAuthProvider` from a field list; |
| 13 | the in-repo [Neon provider](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Neon/AuthProvider.ts) |
| 14 | is exactly this shape. |
| 15 | |
| 16 | For the profile method contract, CI resolver, registry, and lazy resolution, |
| 17 | see [Auth Providers](/environments/auth-providers). |
| 18 | |
| 19 | ## Declare a lazy credentials service |
| 20 | |
| 21 | The service value is an `Effect`, not a resolved struct: |
| 22 | |
| 23 | ```typescript |
| 24 | // src/stripe/Credentials.ts |
| 25 | import * as Context from "effect/Context"; |
| 26 | import * as Effect from "effect/Effect"; |
| 27 | import * as Redacted from "effect/Redacted"; |
| 28 | |
| 29 | export class StripeCredentials extends Context.Service< |
| 30 | StripeCredentials, |
| 31 | Effect.Effect<{ apiKey: Redacted.Redacted<string> }> |
| 32 | >()("StripeCredentials") {} |
| 33 | ``` |
| 34 | |
| 35 | Provider Layers are built on every CLI invocation — before any Profile is |
| 36 | configured — so nothing may resolve at Layer construction. Consumers run the |
| 37 | Effect only when an operation executes, and a session-backed implementation can |
| 38 | put refresh logic inside it. Every built-in credentials service has this shape |
| 39 | (`CloudflareEnvironment`, `AWSEnvironment`, the distilled SDK `Credentials` |
| 40 | tags). |
| 41 | |
| 42 | ## Generate the Auth Provider |
| 43 | |
| 44 | `makeStoredAuthProvider` turns a field list into a complete provider: |
| 45 | interactive prompts, flag-driven configuration |
| 46 | (`--method stored --set apiKey=...`), schema-validated persistence at |
| 47 | `~/.alchemy/credentials/{profile}/stripe-stored.json`, `login`/`logout`, |
| 48 | structured `details` for `alchemy profile show`, and a typed `NeedsReauth` |
| 49 | failure when the credential file is missing. |
| 50 | |
| 51 | ```typescript |
| 52 | // src/stripe/AuthProvider.ts |
| 53 | import { AuthError } from "alchemy/Auth/AuthProvider"; |
| 54 | import { makeStoredAuthProvider } from "alchemy/Auth/StoredAuthProvider"; |
| 55 | import { getEnvRedactedRequired } from "alchemy/Auth/Env"; |
| 56 | import * as Effect from "effect/Effect"; |
| 57 | import * as Redacted from "effect/Redacted"; |
| 58 | |
| 59 | export const STRIPE_AUTH_PROVIDER_NAME = "Stripe"; |
| 60 | |
| 61 | export type StripeResolvedCredentials = { |
| 62 | type: "apiKey"; |
| 63 | apiKey: Redacted.Redacted<string>; |
| 64 | source: { type: "stored" | "env" }; |
| 65 | }; |
| 66 | |
| 67 | export 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 |
| 103 | property persisted in the credentials file, alongside `method: "stored"` |
| 104 | (the `--method` value). Validation applies to both paths. |
| 105 | The generated `storedSchema` validates credential reads and writes. |
| 106 | `readEnvironment` resolves CI credentials, while `environment` documents |
| 107 | the 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 |
| 114 | provide the result as the `StripeCredentials` Effect: |
| 115 | |
| 116 | ```typescript |
| 117 | // src/stripe/Credentials.ts (additions) |
| 118 | import { resolveProviderConfig } from "alchemy/Auth/Profile"; |
| 119 | import type { StoredAuthConfig } from "alchemy/Auth/StoredAuthProvider"; |
| 120 | import * as Layer from "effect/Layer"; |
| 121 | import { |
| 122 | STRIPE_AUTH_PROVIDER_NAME, |
| 123 | type StripeResolvedCredentials, |
| 124 | } from "./AuthProvider.ts"; |
| 125 | |
| 126 | export 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 | |
| 144 | Two details matter. Outside CI, `resolveProviderConfig` applies the standard |
| 145 | profile selection order, then returns the stored config or runs `configure` |
| 146 | and persists the answer. In CI it bypasses profiles and returns |
| 147 | `readEnvironment`. A missing explicit local Profile fails with a command to |
| 148 | create it. And the value returned to `Layer.effect` is the *Effect* |
| 149 | (closed by `Effect.cached`), not its result: nothing resolves until a handler |
| 150 | runs it, and within one process `read` (and its file lock) runs at most once. |
| 151 | Every 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 |
| 154 | session re-resolves instead of keeping a dead token.) |
| 155 | |
| 156 | ## Bundle into `providers()` |
| 157 | |
| 158 | Merge the auth machinery into the same `providers()` Layer that carries the |
| 159 | resource 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 |
| 164 | import { CredentialsStoreLive } from "alchemy/Auth/Credentials"; |
| 165 | import { ProfileStoreLive } from "alchemy/Auth/Profile"; |
| 166 | import * as Provider from "alchemy/Provider"; |
| 167 | import * as Layer from "effect/Layer"; |
| 168 | import { StripeAuth } from "./AuthProvider.ts"; |
| 169 | import * as Credentials from "./Credentials.ts"; |
| 170 | import { StripeProduct, StripeProductProvider } from "./Product.ts"; |
| 171 | |
| 172 | export class Providers extends Provider.ProviderCollection<Providers>()( |
| 173 | "Stripe", |
| 174 | ) {} |
| 175 | |
| 176 | export 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 |
| 189 | file-system services. (Providers that call APIs through an Effect `HttpClient` |
| 190 | also merge `FetchHttpClient.layer` here, as Neon does.) |
| 191 | |
| 192 | ## Consume it from a lifecycle handler |
| 193 | |
| 194 | Inside `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 |
| 199 | reconcile: 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 | |
| 206 | This is the same idiom the built-ins use — `yield* AWSEnvironment.current`, |
| 207 | `yield* yield* CloudflareEnvironment` — and it's what keeps credentials |
| 208 | demanded exactly when an operation runs, never at Layer construction. |
| 209 | |
| 210 | ## Try it |
| 211 | |
| 212 | ```sh |
| 213 | alchemy deploy # first run: prompts for credentials, saves them to the profile |
| 214 | alchemy profile refresh --provider Stripe # re-authenticate without changing the method |
| 215 | alchemy profile edit --reconfigure Stripe # replace the stored credentials interactively |
| 216 | |
| 217 | # non-interactive (agents, scripts): value from env, stdin, or a literal |
| 218 | alchemy profile edit --add stripe --method stored --set apiKey=env:STRIPE_API_KEY |
| 219 | op read op://vault/stripe/key | alchemy profile edit --add stripe --method stored --set apiKey=- |
| 220 | |
| 221 | alchemy profile show # renders your provider's details |
| 222 | alchemy provider check-env --provider stripe # CI preflight for STRIPE_API_KEY |
| 223 | ``` |
| 224 | |
| 225 | In CI, profile resolution is bypassed, so setting `STRIPE_API_KEY` is all a |
| 226 | pipeline 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. |