Skip to content

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 portWhat it does
EventBusTopic-shaped command + event transport (Cloudflare Queues in the CF implementation)
BlobStorageUpload, download, signed URLs
SecretsRead/write secret-shaped values (KV + Workers Secrets in CF)
KmsKey management port
SchedulerCron-shaped scheduled job creation
WorkflowLong-running multi-step workflow orchestration
IdentityProviderOIDC token verification (CF Access JWT in CF)
CloudCredentialsMint a BoundCredential for a customer cloud project (WIF / STS-shaped)
IaaS portWhat it does
IaaS.computeList / start / stop / create / delete instances + disks
IaaS.iamAdd / remove / list bindings, test permissions, create service accounts
IaaS.networkList networks, subnets, firewall rules
IaaS.storageList / get / create buckets
IaaS.securityList / 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

packages/cloud-adapters/src/cloud-credentials.ts
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.ts

Every 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:

packages/cloud-adapters/src/azure/iaas-iam.ts
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

packages/cloud-adapters/src/azure/index.ts
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:

packages/domain/src/cloud-project.ts
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

packages/cloud-adapters/src/azure/iaas-iam.test.ts
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.

Reference