8. Add a cloud adapter
The cloud-adapter spine lets Mnemose target any cloud by implementing a small set of typed TypeScript port interfaces. This guide walks through adding a third provider (Azure) end-to-end.
Prerequisite: 6. Local development running.
The port surface
All ports live in packages/cloud-adapters/src/ports/. No application code
imports a cloud SDK directly — only the platform factory and adapter
implementations do. The shipped implementation today is Cloudflare
(packages/cloud-adapters/src/cf/); GcpPlatformConfig / AwsPlatformConfig
types remain in platform.ts for extension providers, but their adapter
implementations are removed.
| Platform-level port | What it does |
|---|---|
EventBus | Topic-shaped command + event transport (Cloudflare Queues in the CF implementation) |
BlobStorage | Upload, download, signed URLs |
Secrets | Read/write secret-shaped values (KV + Workers Secrets in CF) |
Kms | Key management port |
Scheduler | Cron-shaped scheduled job creation |
Workflow | Long-running multi-step workflow orchestration |
IdentityProvider | OIDC token verification (CF Access JWT in CF) |
CloudCredentials | Mint a BoundCredential for a customer cloud project (WIF / STS-shaped) |
| IaaS port | What it does |
|---|---|
IaaS.compute | List / start / stop / create / delete instances + disks |
IaaS.iam | Add / remove / list bindings, test permissions, create service accounts |
IaaS.network | List networks, subnets, firewall rules |
IaaS.storage | List / get / create buckets |
IaaS.security | List / mute security findings |
Every IaaS port method accepts an IaaSContext whose credential field
is a discriminated union — implementations narrow it to their own variant
and throw on a mismatch.
1. Credential variant
export type AzureCredentialConfig = { provider: "azure"; tenantId: string; // Azure AD tenant clientId: string; // App registration client ID federatedSubject: string; // Workload identity federation subject};
export type AzureBoundCredential = { provider: "azure"; accessToken: string; expiresAt: Date;};
export type CredentialConfig = GcpCredentialConfig | AwsCredentialConfig | AzureCredentialConfig;export type BoundCredential = GcpBoundCredential | AwsBoundCredential | AzureBoundCredential;2. Provider directory
packages/cloud-adapters/src/azure/ ├── index.ts # createAzurePlatform factory ├── credentials.ts # AzureCloudCredentials → BoundCredential ├── service-bus-event-bus.ts # implements EventBus over Azure Service Bus ├── blob-storage.ts # implements BlobStorage over Azure Blob ├── key-vault-secrets.ts # implements Secrets over Key Vault ├── iaas-compute.ts # implements IaaS.compute ├── iaas-iam.ts # implements IaaS.iam (RBAC) ├── iaas-network.ts ├── iaas-storage.ts └── iaas-security.tsEvery file imports only @azure/* SDKs — never @google-cloud/* or
@aws-sdk/*.
3. Implement a port
Example pattern — narrow the credential, call the SDK, return the typed result:
import { AuthorizationManagementClient } from "@azure/arm-authorization";
export class AzureIaaSIam implements IaaSIam { async addBinding(ctx: IaaSContext, input: AddBindingInput) { if (ctx.credential.provider !== "azure") { throw new Error(`Expected azure credential, got ${ctx.credential.provider}`); } const client = new AuthorizationManagementClient( { getToken: async () => ({ token: ctx.credential.accessToken, expiresOnTimestamp: ctx.credential.expiresAt.getTime() }) }, ctx.subscriptionId, ); // ... }}The credential narrow at the top of every method is mandatory: TypeScript will catch wrong-provider usage at compile time, and the runtime throw is defence in depth.
4. Factory
export function createAzurePlatform(config: AzurePlatformConfig): CloudPlatform { return { eventBus: new AzureServiceBusEventBus(config.serviceBus), blobStorage: new AzureBlobStorage(config.storage), secrets: new AzureKeyVaultSecrets(config.keyVault), cloudCredentials: new AzureCloudCredentials(config.wif), iaas: { compute: new AzureIaaSCompute(), iam: new AzureIaaSIam(), network: new AzureIaaSNetwork(), storage: new AzureIaaSStorage(), security: new AzureIaaSSecurity(), }, // ... };}Export from packages/cloud-adapters/src/index.ts.
5. Domain schema
Add the provider variant to the CloudProviderSchema:
export const CloudProviderSchema = z.enum(["gcp", "aws", "azure"]);Update any GraphQL enum that mirrors this.
6. Wire delegateCloudProject
The delegate-cloud-project handler must accept the new provider’s
credential fields. Add the Azure branch:
case "azure": return { provider: "azure", tenantId: input.azureTenantId, clientId: input.azureClientId, federatedSubject: input.azureFederatedSubject, };No other handler needs to change — they depend on the CloudPlatform
interface, not on any specific provider.
7. Tests
test("AzureIaaSIam throws on wrong credential provider", async () => { const iam = new AzureIaaSIam(); await expect( iam.addBinding({ credential: { provider: "gcp", /* ... */ } }, /* ... */), ).rejects.toThrow(/Expected azure credential/);});Use mocks for the Azure SDK — full integration tests run against real
Azure subscriptions in a separate suite gated on AZURE_TEST_CREDS.
8. Optional: AzureCloud Pulumi support
If you want Pulumi to deploy Azure-flavoured infrastructure, add a new
stack file under infra/Pulumi.azure-*.yaml and branch on the provider in
infra/index.ts. This is a substantial change — out of scope for adding
a customer-cloud adapter (which only operates on customer-owned Azure
resources, not Mnemose’s runtime).
What you do not need to change
Adding a new provider does not touch:
- Any command handler (they accept
CloudPlatform, not specific providers) - The gateway resolvers
- The agent
- The Fleet panel
- The console UI
This is the design goal of the adapter spine: provider additions are closed under the platform interface.