Skip to main content

Scopes

Scopes provide hierarchical organization and isolation for your infrastructure. They enable you to group related resources, manage different environments (dev, staging, prod), and control resource lifecycle at multiple levels.

What is a Scope?

A scope is a container for resources that provides:
  • Namespacing: Unique resource identification within a scope
  • State isolation: Each scope maintains its own state
  • Hierarchical organization: Scopes can contain child scopes
  • Environment separation: Different stages (dev, prod) are separate scopes
  • Credential management: Scopes can override provider credentials
Every Alchemy application has at least two scopes: the root scope (app name) and a stage scope (e.g., “dev”, “prod”).

Scope Hierarchy

Alchemy applications have a built-in scope hierarchy:
my-app (root scope)
└── dev (stage scope)
    ├── api (resource)
    ├── database (resource)
    └── storage (child scope)
        └── images (resource)
The fully qualified name (FQN) reflects this hierarchy:
my-app/dev/api
my-app/dev/storage/images

Creating Scopes

Application Scope

Create the root application scope with alchemy():
import { alchemy } from "alchemy";

const app = await alchemy("my-app");

// Resources created here are in the "my-app/dev" scope
const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});

await app.finalize();
The stage scope (“dev”) is automatically created based on the --stage flag or ALCHEMY_STAGE environment variable.

Child Scopes

Create child scopes with alchemy.run():
import { alchemy } from "alchemy";

const app = await alchemy("my-app");

await alchemy.run("storage", async (scope) => {
  // Resources here are in "my-app/dev/storage"
  
  const imageBucket = await R2Bucket("images", {
    name: "user-images"
  });
  
  const documentBucket = await R2Bucket("documents", {
    name: "user-documents"
  });
});

await app.finalize();

Scope Properties

Scopes provide access to configuration and metadata:
const app = await alchemy("my-app", {
  stage: "prod",
  password: process.env.ALCHEMY_PASSWORD
});

await alchemy.run("database", async (scope) => {
  console.log(scope.appName);    // "my-app"
  console.log(scope.stage);      // "prod"
  console.log(scope.name);       // "database"
  console.log(scope.scopeName);  // "database"
  console.log(scope.chain);      // ["my-app", "prod", "database"]
  console.log(scope.local);      // false (true if --local flag)
  console.log(scope.watch);      // false (true if --watch flag)
  console.log(scope.phase);      // "up" (or "destroy", "read")
});

Scope Options

Stage Configuration

Specify which environment to deploy to:
# Deploy to production
bun ./alchemy.run.ts --stage prod

# Deploy to development (default)
bun ./alchemy.run.ts --stage dev
Or programmatically:
const app = await alchemy("my-app", {
  stage: "prod"
});
Different stages maintain completely separate state. Resources in “dev” are independent from resources in “prod”.

Deployment Phases

Scopes operate in different phases:
PhaseDescriptionTrigger
"up"Create or update resourcesDefault behavior
"destroy"Delete all resources--destroy flag
"read"Read-only, no changes--read flag or --app for non-selected apps
const app = await alchemy("my-app");

await alchemy.run("api", async (scope) => {
  if (scope.phase === "destroy") {
    console.log("Cleaning up resources...");
  }
  
  // Resources automatically respect the phase
  const worker = await Worker("api", {
    entrypoint: "./src/api.ts"
  });
});

await app.finalize();

Local Development

Enable local simulation mode:
bun ./alchemy.run.ts --local
const app = await alchemy("my-app", {
  local: true
});

await alchemy.run("api", async (scope) => {
  console.log(scope.local); // true
  
  // Resources can provide local alternatives
  const worker = await Worker("api", {
    entrypoint: "./src/api.ts"
  });
  // Returns local worker URL instead of deployed URL
});
Local mode allows you to develop and test infrastructure code without deploying to the cloud.

Watch Mode

Automatically redeploy on file changes:
bun --watch ./alchemy.run.ts
Resources can detect watch mode:
await alchemy.run("api", async (scope) => {
  if (scope.watch) {
    console.log("Watch mode enabled - auto-redeploying on changes");
  }
});

Scope State Management

Scopes can store and retrieve custom data:
await alchemy.run("cache", async (scope) => {
  // Store data in scope state
  await scope.set("lastDeployment", new Date().toISOString());
  
  // Retrieve data
  const lastDeployment = await scope.get<string>("lastDeployment");
  console.log(`Last deployed: ${lastDeployment}`);
  
  // Delete data
  await scope.delete("lastDeployment");
});
Scope state is separate from resource state and persists across deployments.

Physical Name Generation

Scopes provide utilities for generating deterministic resource names:
await alchemy.run("services", async (scope) => {
  const name = scope.createPhysicalName("api");
  // Returns: "my-app-services-api-dev"
  
  const shortName = scope.createPhysicalName("db", "-", 20);
  // Returns: "my-app-services-d" (truncated to 20 chars)
});
Format: {appName}-{scope-chain}-{id}-{stage}

Provider Credentials

Scopes can override provider credentials:
declare module "alchemy" {
  interface ProviderCredentials {
    aws?: {
      accessKeyId: string;
      secretAccessKey: string;
      region: string;
    };
  }
}

const app = await alchemy("my-app", {
  aws: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    region: "us-east-1"
  }
});

await alchemy.run("eu-services", {
  aws: {
    ...app.providerCredentials.aws,
    region: "eu-west-1"  // Override region for this scope
  }
}, async (scope) => {
  // Resources in this scope use eu-west-1
});

Scope Lifecycle

Initialization

Scopes are initialized when created:
const app = await alchemy("my-app");
// State store is initialized
// Stage scope is created

await alchemy.run("services", async (scope) => {
  // Child scope is initialized
  // Resources can be created
});
// Child scope is automatically finalized

await app.finalize();
// Root and stage scopes are finalized
// Orphaned resources are cleaned up

Finalization

Finalization handles cleanup and orphan detection:
1

Execute deferred operations

Any operations registered with scope.defer() are executed
2

Finalize child scopes

All child scopes are finalized recursively
3

Detect orphaned resources

Resources that exist in state but not in code are identified
4

Destroy orphans

Orphaned resources are deleted using the configured destroy strategy
5

Cleanup processes

Any cleanup functions registered with scope.onCleanup() are called
const app = await alchemy("my-app");

// Create resources
const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});

// This is critical - finalizes scopes and cleans up orphans
await app.finalize();
Always call app.finalize() at the end of your script. Without it, orphaned resources won’t be cleaned up.

Process Management

Scopes can spawn and manage long-running processes:
await alchemy.run("dev-server", async (scope) => {
  // Spawn a process that's automatically tracked
  const url = await scope.spawn("vite", {
    command: "vite",
    args: ["--port", "3000"],
    extract: (line) => {
      const match = line.match(/Local:\s+(http:\/\/[^\s]+)/);
      return match?.[1];
    }
  });
  
  console.log(`Dev server running at: ${url}`);
});
Processes spawned with scope.spawn() are automatically stopped when the process exits.

Cleanup Handlers

Register cleanup functions to run when the process exits:
await alchemy.run("tunnel", async (scope) => {
  const tunnel = await startTunnel();
  
  scope.onCleanup(async () => {
    console.log("Closing tunnel...");
    await tunnel.close();
  });
});

Scope Isolation

Each scope maintains isolated state:
const app = await alchemy("my-app");

await alchemy.run("api", async (apiScope) => {
  await apiScope.set("version", "1.0.0");
});

await alchemy.run("worker", async (workerScope) => {
  const version = await workerScope.get("version");
  console.log(version); // undefined - different scope
});

await app.finalize();

Error Handling

Scopes track errors and propagate them:
await alchemy.run("failing-service", async (scope) => {
  try {
    const worker = await Worker("api", {
      entrypoint: "./invalid-path.ts"  // This will fail
    });
  } catch (error) {
    scope.fail();
    throw error;
  }
});
// Scope is marked as failed
// Finalization will skip orphan cleanup for failed scopes

Adoption Mode

Adopt existing resources that aren’t yet managed:
bun ./alchemy.run.ts --adopt
const app = await alchemy("my-app", {
  adopt: true
});

// Will adopt existing resources instead of erroring on conflicts
const bucket = await R2Bucket("existing-bucket", {
  name: "my-existing-bucket"
});
Adoption is useful when migrating existing infrastructure to Alchemy.

Quiet Mode

Suppress creation/update/deletion logs:
bun ./alchemy.run.ts --quiet
const app = await alchemy("my-app", {
  quiet: true
});

Best Practices

1

Use meaningful scope names

Choose names that describe the purpose: "api", "database", "storage"
2

Organize by lifecycle

Group resources that should be deployed/destroyed together
3

Leverage scope chain

Access parent scope properties via scope.parent when needed
4

Always finalize

Call await app.finalize() to ensure proper cleanup
5

Use stage isolation

Maintain separate stages for dev, staging, and production

Advanced Patterns

Conditional Scopes

Create scopes conditionally:
const app = await alchemy("my-app");

if (app.stage === "prod") {
  await alchemy.run("monitoring", async () => {
    // Production-only monitoring resources
  });
}

await app.finalize();

Scoped Secrets

Different scopes can use different passwords:
await alchemy.run("secure-service", {
  password: process.env.SECURE_PASSWORD
}, async (scope) => {
  // Secrets in this scope use SECURE_PASSWORD
  const apiKey = alchemy.secret(process.env.API_KEY);
});

Next Steps

Resources

Learn about creating and managing resources

State Management

Understand how state is stored and managed

Secrets

Secure sensitive configuration values

Lifecycle

Deep dive into deployment phases

Build docs developers (and LLMs) love