Summary
The state-store documentation says that a separate Cloudflare state store can
be selected with:
state: Cloudflare.state({
workerName: "alchemy-state-store-team-a",
}),
However, Cloudflare.state() currently accepts no options and always uses
STATE_STORE_SCRIPT_NAME ("alchemy-state-store").
The lower-level exported bootstrap() function does accept a workerName,
but that value is only propagated to parts of the bootstrap lifecycle. The
physical Worker definition, existing-store detection, login URL, credential
cache, and secret lookup continue to use the default state-store identity.
As a result, the documented per-team/dedicated-store configuration does not
appear to be usable end to end.
Versions checked
alchemy@2.0.0-beta.63
- Upstream
main at 7c926aa628c70d15610bb469634bda5bf70c769b
Documentation
The current documentation says:
Pass workerName to use a separate state store — for example, when you want
a dedicated store per Cloudflare account or per team.
|
### Customizing the worker name |
|
|
|
By default the state-store Worker is named `alchemy-state-store`. |
|
Pass `workerName` to use a separate state store — for example, when |
|
you want a dedicated store per Cloudflare account or per team: |
|
|
|
```typescript |
|
state: Cloudflare.state({ workerName: "alchemy-state-store-team-a" }), |
|
``` |
Compile-time reproduction
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"Example",
{
providers: Cloudflare.providers(),
state: Cloudflare.state({
workerName: "alchemy-state-store-team-a",
}),
},
Effect.void,
);
The call does not type-check because state has a zero-argument signature:
export const state = () => ...
Source:
|
export const state = () => |
|
Layer.effect( |
|
State, |
|
Effect.gen(function* () { |
|
const isCI = yield* CI; |
|
const scriptName = STATE_STORE_SCRIPT_NAME; |
|
const profileName = yield* ALCHEMY_PROFILE; |
|
const localStage = `${profileName}_${scriptName}`; |
Bootstrap propagation
bootstrap() does expose workerName and resolves it into scriptName:
|
export interface BootstrapOptions { |
|
/** @default "alchemy-state-store" */ |
|
workerName?: string; |
|
/** @default false */ |
|
force?: boolean; |
|
/** @default "default" */ |
|
profile?: string; |
|
} |
|
|
|
export const bootstrap = (options: BootstrapOptions = {}) => |
|
Effect.gen(function* () { |
|
const isCI = yield* CI; |
|
const profileName = options.profile ?? (yield* ALCHEMY_PROFILE); |
|
const scriptName = options.workerName ?? STATE_STORE_SCRIPT_NAME; |
|
const force = options.force ?? false; |
|
const localStage = `${profileName}_${scriptName}`; |
That name is used for the bootstrap stage and related messages, but several
subsequent operations still use the default identity:
-
The physical Worker resource is explicitly named
STATE_STORE_SCRIPT_NAME:
|
export default Worker( |
|
"Api", |
|
{ |
|
name: STATE_STORE_SCRIPT_NAME, |
|
main: import.meta.url, |
-
isStateStoreServing() always probes the default Worker URL:
|
/** |
|
* Does this account have a *functioning* state-store worker, |
|
* verified by checking the /version endpoint |
|
* |
|
*/ |
|
const isStateStoreServing = (accountId: string) => |
|
Effect.gen(function* () { |
|
const url = yield* workers.getSubdomain({ accountId }).pipe( |
|
Effect.map(({ subdomain }) => |
|
subdomain |
|
? `https://${STATE_STORE_SCRIPT_NAME}.${subdomain}.workers.dev` |
|
: undefined, |
|
), |
|
Effect.catch(() => Effect.succeed(undefined)), |
|
); |
|
if (url === undefined) return false; |
|
const { observed } = yield* checkStateStoreVersion(url); |
|
return observed !== undefined; |
-
loginWithCloudflare() reads the default auth-token secret, derives the
default Worker URL, and writes the shared default credentials file:
|
export const loginWithCloudflare = (profileName: string, force: boolean) => |
|
Effect.gen(function* () { |
|
const credStore = yield* CredentialsStore; |
|
const isCI = yield* CI; |
|
const { accountId } = |
|
yield* yield* CloudflareEnvironment.CloudflareEnvironment; |
|
|
|
if (!force) { |
|
// try and read from the cached credentials first if not forcing (force will always refresh) |
|
const credentials = yield* credStore.read<StoredStateStoreCredentials>( |
|
profileName, |
|
CREDENTIALS_FILE, |
|
); |
|
// Ignore a cache minted for a different account (or a legacy file with |
|
// no `accountId`) — reusing it would hand back the wrong account's |
|
// state-store URL. Fall through to re-derive against `accountId`. |
|
if ( |
|
credentials && |
|
!isStateStoreCredentialsStale(credentials, accountId) |
|
) { |
|
return credentials; |
|
} |
|
} |
|
|
|
// 1. Locate the single Secrets Store on the account. |
|
const stores = yield* SecretsStore.listStores({ accountId }); |
|
const store = stores.result[0]; |
|
if (!store) { |
|
return yield* Effect.fail( |
|
new AuthError({ |
|
message: |
|
"No Secrets Store found on this account. Deploy the state store first.", |
|
}), |
|
); |
|
} |
|
|
|
// 2. Fetch the auth-token from Secrets Store with a temporary edge-preview worker |
|
const authToken = yield* readSecretViaEdge( |
|
STATE_STORE_SCRIPT_NAME, |
|
store.id, |
|
AuthTokenSecretName, |
|
).pipe( |
|
Effect.retry({ |
|
while: (error) => |
|
isWorkersPreviewConfigurationError(error) || |
|
isTransientEdgeSessionError(error), |
|
// Cap the exponential delay at 2s so 15 retries stay within |
|
// ~30s instead of doubling unboundedly. |
|
schedule: Schedule.max([ |
|
Schedule.min([ |
|
Schedule.exponential(200), |
|
Schedule.spaced("2 seconds"), |
|
]), |
|
Schedule.recurs(15), |
|
]), |
|
}), |
|
); |
|
|
|
// 3. Derive the deployed worker URL. |
|
const { subdomain } = yield* workers.getSubdomain({ accountId }); |
|
const url = `https://${STATE_STORE_SCRIPT_NAME}.${subdomain}.workers.dev`; |
|
|
|
if (!isCI) { |
|
// 4. Persist credentials. The profile entry is managed by |
|
// `loadOrConfigure` when this is invoked through `configure`. |
|
yield* credStore |
|
.write<StoredStateStoreCredentials>(profileName, CREDENTIALS_FILE, { |
|
url, |
|
authToken: authToken.trim(), |
|
accountId, |
|
}) |
This suggests that calling:
yield* Cloudflare.bootstrap({
workerName: "alchemy-state-store-team-a",
});
can change the bootstrap stack/stage identity without consistently changing
the state-store service to which later operations attach.
I have not run this reproduction against a disposable Cloudflare account
because bootstrap can create or update account-level state infrastructure.
The compile-time mismatch and incomplete propagation above are visible
directly in the published API and current source.
Expected behavior
One of the following contracts should be made explicit:
Option A: named state stores are supported
Cloudflare.state({ workerName }) accepts the documented option.
- The same name is used consistently for:
- bootstrap state
- physical Worker identity
- availability and version checks
- Worker URL derivation
- login and credential-cache identity
- The intended treatment of the authentication and encryption secrets is
documented and tested: either per-store identities or explicitly shared
account-level identities.
- A test proves that the default store and a named store can coexist without
attaching to or refreshing the wrong backend.
Option B: one Cloudflare state store per account is supported
- Remove the
Cloudflare.state({ workerName }) example from the documentation.
- Remove or clarify
BootstrapOptions.workerName so it cannot imply a separate
usable state service.
- Document the account-wide Worker, credential, authentication, encryption,
and upgrade failure domain.
Actual behavior
The documentation advertises Option A, while the public state() API and
several runtime paths currently behave like Option B. bootstrap.workerName
sits between those two contracts and appears incomplete.
Summary
The state-store documentation says that a separate Cloudflare state store can
be selected with:
However,
Cloudflare.state()currently accepts no options and always usesSTATE_STORE_SCRIPT_NAME("alchemy-state-store").The lower-level exported
bootstrap()function does accept aworkerName,but that value is only propagated to parts of the bootstrap lifecycle. The
physical Worker definition, existing-store detection, login URL, credential
cache, and secret lookup continue to use the default state-store identity.
As a result, the documented per-team/dedicated-store configuration does not
appear to be usable end to end.
Versions checked
alchemy@2.0.0-beta.63mainat7c926aa628c70d15610bb469634bda5bf70c769bDocumentation
The current documentation says:
alchemy/website/src/content/docs/state-store/index.mdx
Lines 111 to 119 in 7c926aa
Compile-time reproduction
The call does not type-check because
statehas a zero-argument signature:Source:
alchemy/packages/alchemy/src/Cloudflare/StateStore/State.ts
Lines 56 to 63 in 7c926aa
Bootstrap propagation
bootstrap()does exposeworkerNameand resolves it intoscriptName:alchemy/packages/alchemy/src/Cloudflare/StateStore/State.ts
Lines 245 to 260 in 7c926aa
That name is used for the bootstrap stage and related messages, but several
subsequent operations still use the default identity:
The physical Worker resource is explicitly named
STATE_STORE_SCRIPT_NAME:alchemy/packages/alchemy/src/Cloudflare/StateStore/Api.ts
Lines 101 to 105 in 7c926aa
isStateStoreServing()always probes the default Worker URL:alchemy/packages/alchemy/src/Cloudflare/StateStore/State.ts
Lines 842 to 859 in 7c926aa
loginWithCloudflare()reads the default auth-token secret, derives thedefault Worker URL, and writes the shared default credentials file:
alchemy/packages/alchemy/src/Cloudflare/StateStore/State.ts
Lines 717 to 787 in 7c926aa
This suggests that calling:
can change the bootstrap stack/stage identity without consistently changing
the state-store service to which later operations attach.
I have not run this reproduction against a disposable Cloudflare account
because bootstrap can create or update account-level state infrastructure.
The compile-time mismatch and incomplete propagation above are visible
directly in the published API and current source.
Expected behavior
One of the following contracts should be made explicit:
Option A: named state stores are supported
Cloudflare.state({ workerName })accepts the documented option.documented and tested: either per-store identities or explicitly shared
account-level identities.
attaching to or refreshing the wrong backend.
Option B: one Cloudflare state store per account is supported
Cloudflare.state({ workerName })example from the documentation.BootstrapOptions.workerNameso it cannot imply a separateusable state service.
and upgrade failure domain.
Actual behavior
The documentation advertises Option A, while the public
state()API andseveral runtime paths currently behave like Option B.
bootstrap.workerNamesits between those two contracts and appears incomplete.