Skip to main content

State Management

Alchemy maintains a persistent state file for each resource to track infrastructure across deployments. This state enables Alchemy to detect changes, perform updates, and clean up orphaned resources.

What is State?

State in Alchemy is a JSON representation of your deployed infrastructure that includes:
  • Resource metadata: ID, type, fully qualified name
  • Resource props: Input parameters from previous deployment
  • Resource output: Actual values returned by the provider
  • Custom data: Additional state stored by resources
  • Status: Current lifecycle status of the resource
State files are how Alchemy knows what’s already deployed and what needs to change.

State Structure

Each resource’s state follows this structure:
interface State {
  // Resource type (e.g., "cloudflare::Worker")
  kind: string;
  
  // Logical ID
  id: string;
  
  // Fully qualified name (e.g., "my-app/dev/api")
  fqn: string;
  
  // Sequence number for ordering
  seq: number;
  
  // Current lifecycle status
  status: "creating" | "created" | "updating" | "updated" | "deleting" | "deleted";
  
  // Input props from last deployment
  props: ResourceProps;
  
  // Previous props (used during updates)
  oldProps?: ResourceProps;
  
  // Resource output from provider
  output: Resource;
  
  // Custom state data
  data: Record<string, any>;
}

Where State is Stored

By default, state is stored in the .alchemy directory:
.alchemy/
  my-app/
    dev/
      api.json          # Worker resource state
      database.json     # Database resource state
    prod/
      api.json
      database.json
Each file contains the state for one resource in one stage.
The .alchemy directory should be added to .gitignore for local development. For production, use a persistent state store.

State Lifecycle

Creating State

When a resource is first created:
1

Resource creation

Alchemy invokes the resource handler in “create” phase
2

State initialization

State file is created with status “creating”
3

Provider call

Resource is provisioned via the provider’s API
4

State persistence

Final state is saved with status “created” and output values
const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// State file: .alchemy/my-app/dev/api.json created

Updating State

When resource props change:
1

State comparison

Alchemy compares new props with props in state
2

Update detection

If different, resource handler is called in “update” phase
3

State update

Previous props are stored in oldProps, new props replace props
4

Provider update

Resource is updated via provider API
5

State save

Updated state is persisted with new output values
// First deployment
const worker = await Worker("api", {
  entrypoint: "./src/api.ts",
  compatibilityFlags: ["nodejs_compat"]
});

// Second deployment - props changed
const worker = await Worker("api", {
  entrypoint: "./src/api.ts",
  compatibilityFlags: ["nodejs_compat", "streams_enable_constructors"]
});
// Worker is updated, state reflects new props

Deleting State

When a resource is removed from code:
1

Orphan detection

During finalization, Alchemy detects resources in state but not in code
2

Deletion phase

Resource handler is called with phase “delete”
3

Cleanup

Resource is destroyed via provider API
4

State removal

State file is deleted
// First deployment
const worker = await Worker("api", { ... });
const bucket = await R2Bucket("storage", { ... });

// Second deployment - bucket removed
const worker = await Worker("api", { ... });
// During finalization:
// - bucket is identified as orphaned
// - bucket is destroyed
// - bucket.json is deleted

State Stores

Alchemy supports pluggable state storage backends.

File System State Store (Default)

Stores state in local .alchemy directory:
const app = await alchemy("my-app");
// Uses FileSystemStateStore by default
File system state is not suitable for team environments or CI/CD. Use a persistent state store for production.

Cloudflare State Store

Stores state in Cloudflare KV:
import { CloudflareStateStore } from "alchemy/cloudflare";

const app = await alchemy("my-app", {
  stateStore: (scope) => new CloudflareStateStore(scope, {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    apiToken: process.env.CLOUDFLARE_API_TOKEN!,
    namespaceId: process.env.STATE_KV_NAMESPACE_ID!
  })
});

AWS S3 State Store

Stores state in S3:
import { S3StateStore } from "alchemy/aws";

const app = await alchemy("my-app", {
  stateStore: (scope) => new S3StateStore(scope, {
    bucket: "my-alchemy-state",
    region: "us-east-1"
  })
});

Custom State Store

Implement your own state store:
import type { StateStore, Scope, State } from "alchemy";

class MyStateStore implements StateStore {
  constructor(private scope: Scope) {}

  async init(): Promise<void> {
    // Initialize storage backend
  }

  async deinit(): Promise<void> {
    // Cleanup storage backend
  }

  async list(): Promise<string[]> {
    // Return all resource IDs
  }

  async count(): Promise<number> {
    // Return number of resources
  }

  async get(key: string): Promise<State | undefined> {
    // Retrieve state for resource
  }

  async getBatch(ids: string[]): Promise<Record<string, State>> {
    // Retrieve multiple states
  }

  async all(): Promise<Record<string, State>> {
    // Retrieve all states
  }

  async set(key: string, value: State): Promise<void> {
    // Persist state for resource
  }

  async delete(key: string): Promise<void> {
    // Remove state for resource
  }
}

const app = await alchemy("my-app", {
  stateStore: (scope) => new MyStateStore(scope)
});
Custom state stores enable integration with any storage backend: databases, cloud storage, version control, etc.

State Operations

Reading State

Access state within a resource:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    // Current state
    const state = this.output; // undefined in create phase
    
    // Previous props
    const prevProps = this.props; // undefined in create phase
    
    if (this.phase === "update") {
      console.log("Previous name:", this.output.name);
      console.log("New name:", props.name);
    }
    
    // ...
  }
);

Custom State Data

Store additional data in state:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    // Store custom data
    await this.set("deployCount", 
      (await this.get<number>("deployCount") ?? 0) + 1
    );
    
    // Read custom data
    const deployCount = await this.get<number>("deployCount");
    console.log(`Deployed ${deployCount} times`);
    
    // Delete custom data
    await this.delete("lastError");
    
    // ...
  }
);
Custom state data persists across deployments and is separate from resource props and output.

Scope State

Scopes can also store state:
await alchemy.run("api", async (scope) => {
  // Store at scope level
  await scope.set("version", "2.0.0");
  
  // Read from scope
  const version = await scope.get<string>("version");
  
  // Delete from scope
  await scope.delete("version");
});

State Serialization

Alchemy automatically serializes complex types:

Secrets

Secrets are encrypted before storage:
const worker = await Worker("api", {
  bindings: {
    API_KEY: alchemy.secret.env.API_KEY
  }
});

// In state file:
// {
//   "props": {
//     "bindings": {
//       "API_KEY": {
//         "@secret": "encrypted-base64-value"
//       }
//     }
//   }
// }

Resources

Resource references are serialized by FQN:
const database = await D1Database("db", { ... });
const worker = await Worker("api", {
  bindings: {
    DB: database
  }
});

// In state file:
// {
//   "props": {
//     "bindings": {
//       "DB": {
//         "@resource": "my-app/dev/db"
//       }
//     }
//   }
// }

Dates and Special Types

Custom serializers handle complex types:
const resource = await MyResource("item", {
  createdAt: new Date()
});

// Date is serialized to ISO string
// Custom deserializers restore original types on read

State Locking

Alchemy uses mutexes to prevent concurrent state modifications:
// Automatic locking during state operations
await scope.set("key", "value");
// Lock is released after operation

// Manual locking for complex operations
await scope.dataMutex.lock(async () => {
  const current = await scope.get<number>("counter");
  await scope.set("counter", current + 1);
});
State locking prevents race conditions when multiple resources access shared state.

State Migration

When changing state stores:
1

Export existing state

Read all state from current store
2

Initialize new store

Configure new state store
3

Import state

Write state to new store
4

Verify

Run deployment with --read to verify state
5

Cutover

Update all deployments to use new store
// Migration script
import { FileSystemStateStore } from "alchemy";
import { S3StateStore } from "alchemy/aws";

const oldStore = new FileSystemStateStore(scope);
const newStore = new S3StateStore(scope, { ... });

const states = await oldStore.all();
for (const [key, state] of Object.entries(states)) {
  await newStore.set(key, state);
}

Debugging State

Inspect state files directly:
# View all resources in dev stage
ls .alchemy/my-app/dev/

# View specific resource state
cat .alchemy/my-app/dev/api.json | jq .
Or programmatically:
await alchemy.run("debug", async (scope) => {
  const resourceIds = await scope.state.list();
  console.log("Resources:", resourceIds);
  
  for (const id of resourceIds) {
    const state = await scope.state.get(id);
    console.log(`${id}:`, state);
  }
});

State Best Practices

1

Use persistent storage in production

File system state doesn’t work in CI/CD or team environments
2

Never manually edit state

Let Alchemy manage state automatically
3

Back up state regularly

State is critical - losing it means losing track of your infrastructure
4

Use different stores per stage

Isolate dev and prod state completely
5

Monitor state size

Large state files can slow down deployments

Troubleshooting

State Corruption

If state is corrupted:
# Force re-creation of resources
bun ./alchemy.run.ts --force

Lost State

If state is lost:
# Adopt existing resources
bun ./alchemy.run.ts --adopt

State Conflicts

If state conflicts with reality:
# Read current state without changes
bun ./alchemy.run.ts --read

# Force update to match code
bun ./alchemy.run.ts --force

Advanced Patterns

State Versioning

Track state versions:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    const version = (await this.get<number>("stateVersion") ?? 0) + 1;
    await this.set("stateVersion", version);
    
    // Perform migrations based on version
    if (version === 2) {
      // Migrate from v1 to v2
    }
    
    // ...
  }
);

State Inspection

Expose state for debugging:
await alchemy.run("inspect", async (scope) => {
  const allState = await scope.state.all();
  
  console.log("State summary:");
  for (const [id, state] of Object.entries(allState)) {
    console.log(`  ${id}: ${state.kind} (${state.status})`);
  }
});

Next Steps

Scopes

Understand scope-level state management

Resources

Learn how resources interact with state

Secrets

See how secrets are encrypted in state

Lifecycle

Understand state transitions during lifecycle

Build docs developers (and LLMs) love