> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/alchemy-run/alchemy/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Providers

> Build custom resource providers for Alchemy

# Custom Providers

Alchemy allows you to create **custom resource providers** for any cloud service or API. This guide shows you how to implement custom resources following Alchemy's conventions and patterns.

## Resource Structure

A custom resource consists of three parts:

1. **Props Interface** - Input configuration
2. **Output Type** - Resource attributes after creation
3. **Resource Implementation** - Lifecycle management (create, update, delete)

## Creating a Basic Resource

### Step 1: Define Interfaces

```typescript theme={null}
import type { Context } from "alchemy";
import { Resource } from "alchemy";

/**
 * Input properties for MyResource
 */
export interface MyResourceProps {
  /**
   * Name of the resource
   * @default {app}-{stage}-{id}
   */
  name?: string;
  
  /**
   * Description of the resource
   */
  description: string;
  
  /**
   * Optional API key for authentication
   */
  apiKey?: string | Secret;
  
  /**
   * Whether to adopt existing resources
   * @default false
   */
  adopt?: boolean;
  
  /**
   * Internal resource ID (for state management)
   * @internal
   */
  resourceId?: string;
}

/**
 * Output type for MyResource
 * Uses Omit pattern to separate input from output
 */
export type MyResource = Omit<MyResourceProps, "adopt"> & {
  /**
   * Alchemy resource ID
   */
  id: string;
  
  /**
   * Physical name of the resource
   */
  name: string;
  
  /**
   * Provider-assigned resource ID
   */
  resourceId: string;
  
  /**
   * API key (always wrapped as Secret in output)
   */
  apiKey?: Secret;
  
  /**
   * Resource URL
   */
  url: string;
  
  /**
   * Creation timestamp
   */
  createdAt: number;
};
```

### Step 2: Implement Resource Lifecycle

```typescript theme={null}
import { Secret } from "alchemy";
import { logger } from "alchemy/util/logger";

/**
 * Creates and manages MyResource instances
 * 
 * @example
 * const resource = await MyResource("my-resource", {
 *   description: "Example resource",
 *   apiKey: alchemy.secret.env.API_KEY
 * });
 */
export const MyResource = Resource(
  "custom::MyResource",
  async function (
    this: Context<MyResource>,
    id: string,
    props: MyResourceProps
  ): Promise<MyResource> {
    // Get resource ID from props or previous state
    const resourceId = props.resourceId ?? this.output?.resourceId;
    const adopt = props.adopt ?? this.scope.adopt;
    
    // Generate physical name
    const name = props.name 
      ?? this.output?.name 
      ?? this.scope.createPhysicalName(id);
    
    // Handle deletion phase
    if (this.phase === "delete") {
      if (!resourceId) {
        logger.warn(`No resourceId for ${id}, skipping delete`);
        return this.destroy();
      }
      
      try {
        // Call provider API to delete
        await deleteResource(resourceId);
      } catch (error) {
        if (error.code !== "NOT_FOUND") {
          throw error;
        }
      }
      
      return this.destroy();
    }
    
    // Prepare request payload (unwrap secrets)
    const requestBody = {
      name,
      description: props.description,
      apiKey: Secret.unwrap(props.apiKey)
    };
    
    let result;
    
    if (resourceId) {
      // UPDATE existing resource
      result = await updateResource(resourceId, requestBody);
    } else {
      try {
        // CREATE new resource
        result = await createResource(requestBody);
      } catch (error) {
        if (error.code === "ALREADY_EXISTS") {
          if (!adopt) {
            throw new Error(
              `Resource "${name}" already exists. Use adopt: true to adopt it.`,
              { cause: error }
            );
          }
          
          // Adopt existing resource
          const existing = await findResourceByName(name);
          if (!existing) {
            throw new Error(
              `Resource "${name}" not found for adoption`
            );
          }
          
          result = await updateResource(existing.id, requestBody);
        } else {
          throw error;
        }
      }
    }
    
    // Return output matching MyResource type
    return {
      id,
      name: result.name,
      resourceId: result.id,
      description: props.description,
      apiKey: props.apiKey ? Secret.wrap(props.apiKey) : undefined,
      url: result.url,
      createdAt: result.created_at
    };
  }
);
```

### Step 3: Add Type Guard

```typescript theme={null}
import { ResourceKind } from "alchemy";

/**
 * Type guard to check if a value is MyResource
 */
export function isMyResource(resource: any): resource is MyResource {
  return resource?.[ResourceKind] === "custom::MyResource";
}
```

## Advanced Patterns

### Input Normalization

For resources accepting flexible input types (e.g., `string | Secret`), use a wrapper function:

```typescript theme={null}
/**
 * Public interface - accepts flexible types
 */
export function MyResource(
  id: string,
  props: MyResourceProps
): Promise<MyResource> {
  return _MyResource(id, {
    ...props,
    // Normalize secret to always be Secret type
    apiKey: typeof props.apiKey === "string"
      ? secret(props.apiKey)
      : props.apiKey
  });
}

/**
 * Internal implementation - guaranteed normalized types
 */
const _MyResource = Resource(
  "custom::MyResource",
  async function (
    this: Context<MyResource>,
    id: string,
    props: Omit<MyResourceProps, "apiKey"> & { apiKey?: Secret }
  ): Promise<MyResource> {
    // Implementation with guaranteed Secret type
  }
);
```

### Resource Replacement

Handle immutable properties with resource replacement:

```typescript theme={null}
export const MyResource = Resource(
  "custom::MyResource",
  async function (this: Context<MyResource>, id, props) {
    const name = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    
    // Trigger replacement if immutable property changed
    if (this.phase === "update" && this.output?.name !== name) {
      return this.replace();
    }
    
    // Normal lifecycle logic...
  }
);
```

### Conditional Deletion

Support opt-out deletion for data resources:

```typescript theme={null}
export interface MyDatabaseProps {
  /**
   * Whether to delete the database when removed from Alchemy
   * @default true
   */
  delete?: boolean;
}

export const MyDatabase = Resource(
  "custom::MyDatabase",
  async function (this: Context<MyDatabase>, id, props) {
    if (this.phase === "delete") {
      if (props.delete !== false && this.output?.resourceId) {
        try {
          await deleteDatabase(this.output.resourceId);
        } catch (error) {
          if (error.code !== "NOT_FOUND") throw error;
        }
      }
      return this.destroy();
    }
    
    // Create/update logic...
  }
);
```

<Warning>
  Only use conditional deletion for data resources (databases, storage). Always delete compute resources (workers, functions).
</Warning>

### Local Development Support

Support local development mode:

```typescript theme={null}
export const MyResource = Resource(
  "custom::MyResource",
  async function (this: Context<MyResource>, id, props) {
    const name = props.name ?? this.scope.createPhysicalName(id);
    
    // Return mock data in local mode
    if (this.scope.local) {
      return {
        id,
        name,
        resourceId: "local-mock-id",
        description: props.description,
        apiKey: props.apiKey ? Secret.wrap(props.apiKey) : undefined,
        url: `http://localhost:8080/${id}`,
        createdAt: Date.now()
      };
    }
    
    // Production logic...
  }
);
```

### Retry with Exponential Backoff

Handle transient errors:

```typescript theme={null}
import { withExponentialBackoff } from "alchemy/util/retry";

const result = await withExponentialBackoff(
  async () => {
    return await createResource(requestBody);
  },
  (error) => {
    // Retry on specific transient errors
    return error.code === "RATE_LIMITED" || error.code === "TIMEOUT";
  },
  30,    // max attempts
  100    // initial delay (ms)
);
```

## API Client Pattern

Create a reusable API client:

```typescript theme={null}
interface MyProviderApiOptions {
  apiKey: string;
  baseUrl?: string;
}

class MyProviderApi {
  constructor(private options: MyProviderApiOptions) {}
  
  private async request<T>(path: string, init?: RequestInit): Promise<T> {
    const response = await fetch(
      `${this.options.baseUrl ?? "https://api.provider.com"}${path}`,
      {
        ...init,
        headers: {
          "Authorization": `Bearer ${this.options.apiKey}`,
          "Content-Type": "application/json",
          ...init?.headers
        }
      }
    );
    
    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }
    
    return response.json();
  }
  
  async createResource(data: any) {
    return this.request("/resources", {
      method: "POST",
      body: JSON.stringify(data)
    });
  }
  
  async updateResource(id: string, data: any) {
    return this.request(`/resources/${id}`, {
      method: "PUT",
      body: JSON.stringify(data)
    });
  }
  
  async deleteResource(id: string) {
    return this.request(`/resources/${id}`, {
      method: "DELETE"
    });
  }
  
  async getResource(id: string) {
    return this.request(`/resources/${id}`);
  }
}
```

## Type Patterns

### Flat Properties

Prefer flat properties over nested objects:

```typescript theme={null}
// ✅ Good: Flat properties
export interface MyResourceProps {
  name?: string;
  region: string;
  timeout: number;
  memory: number;
}

// ❌ Avoid: Nested configuration
export interface MyResourceProps {
  name?: string;
  config: {
    region: string;
    timeout: number;
    memory: number;
  };
}
```

### Omit Pattern for Outputs

```typescript theme={null}
// Output type removes input-only fields and adds computed fields
export type MyResource = Omit<MyResourceProps, "adopt" | "resourceId"> & {
  id: string;
  resourceId: string;
  url: string;
  createdAt: number;
};
```

### Internal Types

Mark internal API types:

```typescript theme={null}
/**
 * Raw API response
 * @internal
 */
interface MyResourceApiResponse {
  id: string;
  name: string;
  created_at: number;
}
```

## Testing Custom Resources

Write comprehensive tests:

```typescript theme={null}
import { describe, expect } from "vitest";
import { destroy } from "alchemy/test";
import "alchemy/test/vitest";
import { MyResource } from "./my-resource";

const test = alchemy.test(import.meta, {
  prefix: "test"
});

describe("MyResource", () => {
  test("creates, updates, and deletes", async (scope) => {
    let resource;
    
    try {
      // CREATE
      resource = await MyResource("test", {
        description: "Test resource",
        apiKey: "test-key"
      });
      
      expect(resource.id).toBe("test");
      expect(resource.url).toBeTruthy();
      
      // UPDATE
      resource = await MyResource("test", {
        description: "Updated description",
        apiKey: "test-key"
      });
      
      expect(resource.description).toBe("Updated description");
      
    } finally {
      // DELETE
      await destroy(scope);
      
      // Verify deletion
      await expect(
        getResource(resource.resourceId)
      ).rejects.toThrow();
    }
  });
});
```

## Best Practices

<Steps>
  ### Follow Naming Conventions

  Use provider-specific naming:

  ```typescript theme={null}
  // Provider prefix :: Resource name
  "aws::S3Bucket"
  "cloudflare::Worker"
  "vercel::Project"
  "custom::MyResource"
  ```

  ### Handle All Phases

  Always handle create, update, and delete:

  ```typescript theme={null}
  if (this.phase === "delete") {
    // Delete logic
    return this.destroy();
  }

  if (resourceId) {
    // Update logic
  } else {
    // Create logic
  }
  ```

  ### Validate Input

  Check for required fields and valid values:

  ```typescript theme={null}
  if (!props.description) {
    throw new Error("description is required");
  }

  if (props.timeout && props.timeout < 0) {
    throw new Error("timeout must be positive");
  }
  ```

  ### Use Secret for Sensitive Values

  Always wrap secrets:

  ```typescript theme={null}
  // Input accepts string | Secret
  apiKey?: string | Secret;

  // Output always wraps as Secret
  apiKey: Secret.wrap(props.apiKey);

  // API calls unwrap
  apiKey: Secret.unwrap(props.apiKey)
  ```

  ### Support Adoption

  Allow adopting existing resources:

  ```typescript theme={null}
  if (error.code === "ALREADY_EXISTS" && adopt) {
    const existing = await findResourceByName(name);
    result = await updateResource(existing.id, requestBody);
  }
  ```

  ### Provide Clear Error Messages

  ```typescript theme={null}
  throw new Error(
    `Resource "${name}" already exists. Use adopt: true to adopt it.`,
    { cause: error }
  );
  ```
</Steps>

## Complete Example

Here's a complete custom resource implementation:

```typescript theme={null}
import type { Context } from "alchemy";
import { Resource, ResourceKind, Secret } from "alchemy";
import { logger } from "alchemy/util/logger";

// Props interface
export interface MyApiResourceProps {
  name?: string;
  endpoint: string;
  apiKey?: string | Secret;
  timeout?: number;
  adopt?: boolean;
  resourceId?: string;
}

// Output type
export type MyApiResource = Omit<MyApiResourceProps, "adopt"> & {
  id: string;
  name: string;
  resourceId: string;
  apiKey?: Secret;
  url: string;
  createdAt: number;
};

// Type guard
export function isMyApiResource(resource: any): resource is MyApiResource {
  return resource?.[ResourceKind] === "custom::MyApiResource";
}

// Resource implementation
export const MyApiResource = Resource(
  "custom::MyApiResource",
  async function (
    this: Context<MyApiResource>,
    id: string,
    props: MyApiResourceProps
  ): Promise<MyApiResource> {
    const resourceId = props.resourceId ?? this.output?.resourceId;
    const adopt = props.adopt ?? this.scope.adopt;
    const name = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    
    // Local development mode
    if (this.scope.local) {
      return {
        id,
        name,
        resourceId: "local-mock",
        endpoint: props.endpoint,
        apiKey: props.apiKey ? Secret.wrap(props.apiKey) : undefined,
        timeout: props.timeout,
        url: `http://localhost:3000/${id}`,
        createdAt: Date.now()
      };
    }
    
    // Delete phase
    if (this.phase === "delete") {
      if (!resourceId) {
        logger.warn(`No resourceId for ${id}, skipping delete`);
        return this.destroy();
      }
      
      try {
        await fetch(`https://api.example.com/resources/${resourceId}`, {
          method: "DELETE"
        });
      } catch (error) {
        logger.error(`Error deleting ${id}:`, error);
      }
      
      return this.destroy();
    }
    
    // Prepare payload
    const payload = {
      name,
      endpoint: props.endpoint,
      apiKey: Secret.unwrap(props.apiKey),
      timeout: props.timeout ?? 30000
    };
    
    let result;
    
    if (resourceId) {
      // Update
      const response = await fetch(
        `https://api.example.com/resources/${resourceId}`,
        {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload)
        }
      );
      result = await response.json();
    } else {
      // Create
      const response = await fetch(
        "https://api.example.com/resources",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload)
        }
      );
      result = await response.json();
    }
    
    return {
      id,
      name: result.name,
      resourceId: result.id,
      endpoint: props.endpoint,
      apiKey: props.apiKey ? Secret.wrap(props.apiKey) : undefined,
      timeout: props.timeout,
      url: result.url,
      createdAt: result.created_at
    };
  }
);
```

## Next Steps

* [Creating Resources](/guides/creating-resources) - Use your custom resources
* [Testing](/guides/testing) - Test custom resources
* [AGENTS.md](https://github.com/alchemy-run/alchemy/blob/main/AGENTS.md) - Full provider implementation guide
