Skip to content

feat: add provider-neutral stack secret managers - #1502

Open
isakgustavsen wants to merge 17 commits into
alchemy-run:mainfrom
isakgustavsen:codex/varlock-secret-manager
Open

feat: add provider-neutral stack secret managers#1502
isakgustavsen wants to merge 17 commits into
alchemy-run:mainfrom
isakgustavsen:codex/varlock-secret-manager

Conversation

@isakgustavsen

@isakgustavsen isakgustavsen commented Sep 4, 2026

Copy link
Copy Markdown

Adds optional stack-level secret managers so provider credentials and application configuration can come from Doppler, Infisical, AWS Secrets Manager, or an external Effect integration. A stack selects its manager with secrets: Provider.secrets(...); application code continues using Config and Config.redacted.

Public API and configuration

  • Export the generic SecretManager service, layer type, resolver options, and SecretManagerError from the package root. Managers implement resolve({ stack, stage, dev }) and return an Effect containing either a ConfigProvider or { provider, bindings }. Provider-only adapter implementations remain compatible.
  • Support ordinary Effect layers, including scoped resources and typed SecretManagerError failures during layer construction and resolution.
  • Make default configuration available during manager setup and resolution, then compose managed values above it. Explicit profile overrides retain highest precedence; stacks without a manager retain existing behavior.
  • Resolve configuration before stack execution and provider/state construction, and use it in authentication discovery, provider environment checks, profile selection, and stage-aware nuke operations. Config-less backend operations remain independent of stack loading.

Typed application bindings

  • Export makeSecretManager and generic result/integration types. An external adapter can expose a standard layer plus a typed bindings accessor whose property names and Config types come from its schema or generated declarations.
  • Applications opt in with secrets: config.layer, then const bindings = yield* config.bindings and env: { ...bindings }. The accessor uses the current session's result without loading the manager again.
  • The adapter selects application keys and their sensitivity. Explicit Worker env entries resolve Config.redacted as secret_text, strings as plain_text, and other JSON values as json. Adapter-owned runtime metadata can use Config.succeed; deployment-only values remain available through the provider without being forwarded automatically.
  • Concurrent stages retain independent results. New sessions refresh binding keys and sensitivity as well as values. Tests cover inferred Worker binding types, session isolation, and the actual Worker binding classifier.

Local development

  • Forward optional dev to secret-manager resolution and built-in adapter selectors. Local emulation and a deployment can share the same stage while selecting different values. Session caching uses the effective development mode, including inherited settings and explicit overrides; programmatic stack evaluation forwards its mode too.

  • Forward requested configuration nodes to local RPC sidecars over their existing WebSocket, without placing forwarded secret values in session URLs or adding them to child environment variables.

  • Preserve Effect configuration path transformations, including nested providers, fallback sources, and custom transforms applied in the sidecar.

  • Track observed values and missing keys across transformed views. Unchanged reloads retain provider contexts; changed configuration closes and rebuilds them so cached credentials refresh.

  • Forward Cloudflare tooling credentials and profile selection to Vite children through the private stdin configuration payload. Missing parent credentials cannot fall back to stale child credentials. Application Config.redacted bindings continue through the existing Worker binding path.

Included integrations and scope

  • Doppler: config-scoped service tokens and optional project/config selectors.
  • Infisical: token or Universal Auth, project/environment/path selectors, imports, and regional or self-hosted endpoints.
  • AWS Secrets Manager: string-valued JSON SecretString objects, secret/version/region selectors, and static or standard AWS credential-chain authentication.
  • Remove the bundled Varlock adapter, alchemy/Varlock export, optional varlock peer dependency, and Varlock-specific filtering and documentation. External adapters can implement the public contract; the generic layer contains no Varlock-specific behavior.

The Secrets & Config and Stack guides document the API, configuration precedence, included providers, and external layer construction. Managers resolve after stack-module import, so configuration reads belong inside Effects and Layers.

Validation

  • Core, adapter, CLI/session, Cloudflare credential, Vite credential transport, and local RPC regression suites: 102 passed, 2 skipped across 15 files. The skips are existing Node RPC source fixtures unsupported by the installed Node version.
  • Full workspace TypeScript build and formatting checks passed; targeted lint reported no errors (two pre-existing Worker.ts warnings).
  • API JSDoc validation and documentation generation passed.
  • Live cloud deployment and a full Vite dev smoke test have not been performed.

@isakgustavsen
isakgustavsen marked this pull request as draft September 4, 2026 22:35
@isakgustavsen
isakgustavsen marked this pull request as ready for review September 4, 2026 23:46
@theoephraim

Copy link
Copy Markdown

Good timing as I was literally just testing out alchemy + varlock integration today. I came to a similar conclusion that we need custom config providers.

But also have a few small notes about how to make the integration even better so would love to coordinate.

Also thinking it may make sense to just expose the new config provider surface, and reference varlock in the docs, but let Varlock own the plugin/implementation. This avoids coupling to a specific Varlock version.

what’s your timeline for getting this merged?

@isakgustavsen

Copy link
Copy Markdown
Author

I have some concerns when it comes to varlock owning it because of the amount of breaking changes happening here, but we can discuss on Discord. Happy to collaborate

@isakgustavsen
isakgustavsen marked this pull request as draft September 5, 2026 11:30
@isakgustavsen

Copy link
Copy Markdown
Author

Converted back to draft while locking down the config and usage

@isakgustavsen isakgustavsen changed the title feat: add Varlock stack secret manager feat: add stack secret manager Sep 6, 2026
@theoephraim

Copy link
Copy Markdown

Took a closer look at the latest head. The generic SecretManager seam (secrets layer, resolve({ stack, stage }) => ConfigProvider, orElse(managed, fallback), threaded through sessions/auth/providers/nuke, types exported from the root) is exactly the shape we were hoping for. A few suggestions, assuming we (varlock) own the varlock adapter as @varlock/alchemy-integration:

  1. Drop alchemy/Varlock and the optional varlock peer. The adapter calls our root load(), which is still a // TODO function whose contract is a side effect, and then spawns a child process to contain that, snapshots the child's entire process.env into the provider, and fails if console methods changed. Those are guards against internals we never promised. We'll implement the interface against a proper programmatic API on our side (values + metadata, no globals) and pin alchemy as a peer range. If you'd rather not rely on us tracking the beta, happy to contribute and maintain src/Varlock in-tree until the interface settles.

  2. Remove withoutPrivateVarlockBinding from SecretManager.ts. It hardcodes a varlock key into the generic layer and applies it to every manager's fallback. It also blocks the pattern that works today: __VARLOCK_ENV: Config.redacted("__VARLOCK_ENV") gives Workers varlock's log redaction and leak detection. If a manager wants to hide keys, an optional hidden set on the manager would be the place.

  3. Consider letting resolve return metadata beside the provider, e.g. { provider, sensitive?: Set<string> }. ConfigProvider values are plain strings, so sensitivity is lost and everything managed still binds as secret_text. With it, non-sensitive values could bind as plain_text and Config.string("SOME_SECRET") could error. Doppler/Infisical/AWS can populate it too. Worth deciding before the interface freezes.

  4. Two things to double-check: that the manager re-resolves on alchemy dev stack reloads, and that local-provider sidecars which resolve credentials use the managed provider rather than inherited process.env (with an in-memory provider they won't see the values unless routed).

  5. Docs: the warning box about varlock not exposing a "public binding producer" and the link to [Feature Request] Public API to build the __VARLOCK_ENV Worker binding for non-wrangler deploy tools (Alchemy/SST/Pulumi) dmno-dev/varlock#735 can go; that's shipping, and the linked anchor is being replaced by a "Deploying with Alchemy" section. One line noting that @currentEnv=$ALCHEMY_STAGE needs a mapping for dev_<user> stages would also save a support thread.

@isakgustavsen isakgustavsen changed the title feat: add stack secret manager feat: add provider-neutral stack secret managers Sep 6, 2026
@isakgustavsen
isakgustavsen marked this pull request as ready for review September 6, 2026 21:37
@isakgustavsen

Copy link
Copy Markdown
Author

This should be ready now. Varlock was removed so the maintainer can deliver that himself, but it contains the most common secrets manager solutions now, with more being added once this is merged like 1pass and Proton pass

@theoephraim

theoephraim commented Sep 7, 2026

Copy link
Copy Markdown

Thanks for the quick turnaround, the provider-neutral contract and makeSecretManager bindings are great to build against. We have @varlock/alchemy-integration working on this branch (single schema and one-schema-per-service, typed bindings, __VARLOCK_ENV blob). One small ask before the contract freezes:

Forward the dev flag to resolve. SecretManagerResolveOptions is { stack, stage }, but alchemy dev and alchemy deploy share the same dev_<user> stage, and a config source needs to tell them apart: under local emulation the Worker wants http://localhost:4321-style values, live it wants the deployed URLs. The flag already exists on the session side (dev?: boolean in Alchemist/Session.ts and routes/stack.ts), it just isn't forwarded at the resolveSecretManager({ stack, stage, ... }) call:

export interface SecretManagerResolveOptions {
  readonly stack: string;
  readonly stage?: string;
  /** true under `alchemy dev` (local emulation), same as the session option */
  readonly dev?: boolean;
}

With that, stage-to-environment mapping can live in the user's schema (varlock: APP_ENV=remap($ALCHEMY_STAGE, "production", production, /^(pr-|dev_)/, preview, ...) plus a check on the dev flag), and nobody has to deploy to a separate stage just to keep dev and deployed config apart. Optional field, so existing adapters are unaffected.

@sam-goodwin

sam-goodwin commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

You can get it from const { dev } = yield* AlchemyContext but passing it through explicitly is helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants