curl -fsSL https://bun.sh/install | bashWe use bun workspaces, so a simple bun i installs all the dependencies:
bun iTo compile all typescript and bundle the CLI, run:
bun run buildType-check and lint (with oxfmt):
bun checkAll tests are in ./alchemy/test/* or ./alchemy/test/{providerName}/* and we use Vitest as our test runner.
Caution
Each provider requires its own credentials, and there are roughly 22 of them. The authoritative list can be found in the Repository Stack.
When making a change, you can just run the relevant tests instead of setting up every secret.
E.g. to run the Cloudflare tests, you'll need to configure the following environment variables
CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_API_KEYCLOUDFLARE_EMAIL
And then run the tests:
bun vitest run ./alchemy/test/cloudflareOr run a specific test suite:
bun vitest run ./alchemy/test/cloudflare/worker.test.tsOr run a specific test:
bun vitest run ./alchemy/test/cloudflare/worker.test.ts -t "create, update, and delete worker (ESM format)"Alchemy is a TypeScript-native Infrastructure-as-Code repository where your job is to implement "Resource" providers for various cloud services following strict conventions and patterns.
Each provider follows a consistent directory layout:
alchemy/
src/
{provider}/
README.md
{resource}.ts
test/
{provider}/
{resource}.test.ts
alchemy-web/
guides/
{provider}.md # guide on how to get started with the {provider}
docs/
providers/
{provider}/
index.md # overview of usage and link to all the resources for the provider
{resource}.md # example-oriented reference docs for the resource
examples/
{provider}-{qualifier?}/ # only add a qualifier if there are more than one example for this {provider}
package.json
tsconfig.json
alchemy.run.ts
README.md
src/
# source code
Note: Each Resource has one .ts file, one test suite, and one documentation page.
Create your resource file in alchemy/src/{provider}/{resource}.ts:
import { Context } from "../context.ts";
export interface {Resource}Props {
// input props with JSDoc comments
}
export interface {Resource} extends Resource<"{provider}::{resource}"> {
// output props including input props plus additional fields like id, createdAt
}Use the pseudo-class pattern with proper Context typing:
/**
* {overview}
*
* @example
* ## {Example Title}
*
* {concise description}
*
* {example snippet}
*/
export const {Resource} = Resource(
"{provider}::{resource}",
async function (this: Context<>, id: string, props: {Resource}Props): Promise<{Resource}> {
if (this.phase === "delete") {
// Handle deletion
if (this.output?.id) {
// Call API to delete resource
// Log errors but don't throw on 404s
}
return this.destroy();
} else {
let response;
if (this.phase === "update" && this.output?.id) {
// Update existing resource
response = await api.put(/* update call */);
} else {
// Create new resource
response = await api.post(/* create call */);
}
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const data = await response.json();
return {
id: data.id,
...props,
// other computed properties
};
}
}
);- Validate immutable properties during updates
- Use
this.phaseto determine operation type ("create", "update", "delete") - Return
this.destroy()for deletion - Return
{...}for creation/update with all required properties - Check response status directly instead of relying on exceptions
- Handle 404s gracefully during deletion
When resources accept multiple flexible input types (e.g., string | Secret, string | Resource), use a public wrapper function to normalize inputs before passing to the internal Resource.
Note: This pattern is only needed when your Props interface has properties with union types that require normalization. If all props accept single types, skip this pattern and use the Resource directly.
// Public interface - accepts flexible types
export function MyResource(id: string, props: MyResourceProps): Promise<MyResource> {
return _MyResource(id, {
...props,
secret: typeof props.secret === "string"
? secret(props.secret)
: props.secret,
database: typeof props.database === "string"
? props.database
: props.database
});
}
// Internal implementation - guaranteed normalized types
const _MyResource = Resource(
"provider::MyResource",
async function (
this: Context<MyResource>,
id: string,
props: Omit<MyResourceProps, "secret"> & { secret: Secret }
): Promise<MyResource> {
// Implementation with guaranteed types
}
);This pattern enables:
- Flexible API for users (accepts string or Resource/Secret)
- Type-safe implementation (guaranteed normalized types)
- Clear separation of concerns
Every resource MUST export a type guard function using ResourceKind:
import { ResourceKind } from "../resource.ts";
export function isMyResource(resource: any): resource is MyResource {
return resource?.[ResourceKind] === "provider::MyResource";
}Use in conditional logic:
function processBinding(binding: any) {
if (isMyResource(binding)) {
// TypeScript now knows this is MyResource
console.log(binding.id);
}
}Prefer Omit over extends for output types to cleanly separate input and computed properties:
// ✅ PREFERRED: Clear separation of input vs output
export type MyResource = Omit<MyResourceProps, "delete" | "secret"> & {
/**
* The ID assigned by the provider
*/
id: string;
/**
* Secret value (guaranteed wrapped)
*/
secret: Secret;
/**
* Resource type identifier
*/
type: "my-resource";
/**
* Creation timestamp
*/
createdAt: number;
};
// ❌ AVOID: Mixing input and output concerns
export interface MyResource extends MyResourceProps {
id: string;
// Input props are now part of the output type
}Use the scope to generate deterministic physical names with defaults:
const name = props.name
?? this.output?.name // Preserve on update
?? this.scope.createPhysicalName(id); // Default: {app}-{stage}-{id}When an immutable property changes, signal replacement via this.replace():
if (this.phase === "update" && this.output.name !== name) {
return this.replace(); // Deletes old, creates new
}Support opt-out deletion with a delete?: boolean property.
Note: This pattern is typically used for data resources only (databases, storage buckets, key-value stores, etc.). Compute resources (workers, functions, containers) should always be deleted when removed from Alchemy without an opt-out option.
export interface MyResourceProps {
/**
* Whether to delete the resource when removed from Alchemy
* @default true
*/
delete?: boolean;
}
if (this.phase === "delete") {
if (props.delete !== false && this.output?.id) {
try {
await api.delete(`/resources/${this.output.id}`);
} catch (error) {
if (error.status !== 404) throw error;
}
}
return this.destroy();
}Mark internal types used only for API serialization with JSDoc @internal:
/**
* Raw provider API response for resource creation
* @internal
*/
interface ResourceApiResponse {
id: string;
name: string;
created_at: number;
// API field names may differ from output
}Use exponential backoff for transient errors:
import { withExponentialBackoff } from "../util/retry.ts";
const result = await withExponentialBackoff(
async () => {
return await extractProviderResult<ApiResponse>(
`create resource "${name}"`,
api.post("/resources", requestBody)
);
},
(error) => {
// Retry condition: specific transient errors
return error.code === 1002 || error instanceof TimeoutError;
},
30, // maximum attempts
100, // initial delay in ms
);When designing input props, if you have a property that references another entity by ID (e.g., tableId, bucketArn), instead use:
// ✅ Good: Lift the Resource into alchemy abstraction
table: string | Table
// ❌ Avoid: Raw ID references
tableId: stringThis allows referencing both external entities by name and alchemy resources directly.
Create comprehensive end-to-end tests in alchemy/test/{provider}/{resource}.test.ts:
import { describe, expect } from "vitest";
import { destroy } from "../../src/destroy.ts"
import { BRANCH_PREFIX } from "../util.ts";
import "../../src/test/vitest.ts";
const test = alchemy.test(import.meta, {
prefix: BRANCH_PREFIX,
});
describe("{Provider}", () => {
test("{test case}", async (scope) => {
const resourceId = `${BRANCH_PREFIX}-{id}` // deterministic, unique ID
let resource: {Resource}
try {
// CREATE
resource = await {Resource}("{id}", {
// props
})
expect(resource).toMatchObject({
// assertions
})
// UPDATE
resource = await {Resource}("{id}", {
// updated props
})
expect(resource).toMatchObject({
// updated assertions
})
} finally {
await destroy(scope);
await assert{Resource}DoesNotExist(resource)
}
})
});
async function assert{Resource}DoesNotExist(resource: {Resource}) {
// Call API to verify resource no longer exists
// Throw test error if it still exists
}- Always use try-finally: Ensure cleanup happens even if assertions fail
- Destroy scope in finally: Call
destroy(scope)to clean up all resources - Make tests idempotent: Use deterministic, non-random IDs so failed tests can be re-run
- Test create, update, delete: Cover the full resource lifecycle
- Test failed cases: Include negative test cases for error conditions
- Use direct API verification: Verify changes using the provider's API client
- Use BRANCH_PREFIX: Creates unique test resource names across all tests
- Use
BRANCH_PREFIXfor deterministic, non-colliding resource names - Pattern:
${BRANCH_PREFIX}-test-resource-type - Keep names consistent and descriptive
When implementing resources that interact with external APIs:
- Minimal abstraction: Use thin wrappers around fetch rather than complex SDK clients
- Explicit path construction: Build API paths explicitly at call sites
- Direct HTTP status handling: Check response status codes directly
- Explicit JSON parsing: Parse JSON responses explicitly where needed
- Public properties: Expose properties like
api.accountIdpublicly - Minimal error transformation: Preserve original error details
Provide comprehensive documentation of all Resources for the provider with relevant links. This serves as design and internal documentation.
Each resource requires:
- Examples: Multiple @example blocks showing distinct use cases
- JSDoc comments: For all properties and interfaces
- Clear descriptions: Of what the resource does and when to use it
Create a getting started guide in ./alchemy-web/docs/guides/{provider}.md that walks users through:
- Installation and setup
- Credential configuration
- Creating their first resource
- Deploying and testing
- Cleanup/teardown
Always run these commands before committing:
# Fix code formatting and linting
bun format
# Run tests (targets changed files vs main)
bun run test
# Or run specific tests during development
bun vitest ./alchemy/test/... -t "..."