Skip to content

feat: alchemy deployment — the cloudflare-os package with the OperatingSystem resource - #24

Draft
sam-goodwin wants to merge 1 commit into
cloudflare:mainfrom
sam-goodwin:sam/alchemy-deploy
Draft

feat: alchemy deployment — the cloudflare-os package with the OperatingSystem resource#24
sam-goodwin wants to merge 1 commit into
cloudflare:mainfrom
sam-goodwin:sam/alchemy-deploy

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Aug 5, 2026

Copy link
Copy Markdown

Make alchemy the recommended way to deploy Cloudflare OS: a new public package, cloudflare-os (packages/alchemy), whose OperatingSystem resource provisions the whole system.

// alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
import { OperatingSystem } from "cloudflare-os";
import { GitHub } from "cloudflare-os/gatekeepers";

export default Alchemy.Stack(
  "AcmeOS",
  { providers: Cloudflare.providers(), state: Cloudflare.state() },
  Effect.gen(function* () {
    const os = yield* OperatingSystem("OS", {
      domain: "os.acme.com",
      admins: ["sam@acme.com"],
      auth: { gatekeepers: ["github"], disablePasswordAuth: true },
      gatekeepers: [
        GitHub({
          clientId: Config.string("GITHUB_CLIENT_ID"),
          clientSecret: Config.redacted("GITHUB_CLIENT_SECRET"),
        }),
      ],
      ai: { providers: { anthropic: Config.redacted("ANTHROPIC_API_KEY") } },
    });

    return { url: os.url };
  }),
);
pnpm --dir packages/alchemy run deploy    # up
pnpm --dir packages/alchemy run destroy   # gone

Every prop is optional — OperatingSystem("OS") alone deploys a working OS at the router's workers.dev URL. Gatekeepers are configured with plugin functions; context, scheduler, and mcp deploy by default:

gatekeepers: [
  GitHub({ clientId, clientSecret }),        // credentialed: configuring = installing
  Context({ sharingDomain: "production" }),  // reconfigure a default
  Mcp({ enabled: false }),                   // opt out of a default
  HomeAssistant(),                           // opt in to a non-default
]

A custom gatekeeper is any Worker (or any package following the standard capnweb layout):

// inside the Stack's Effect.gen
const acme = yield* Cloudflare.Worker("AcmeWorker", {
  main: "./src/worker.ts",   // exports GatekeeperVendor + capability classes
  env: {
    CACHE: Cloudflare.KV.Namespace("Cache"),
    ACME_API_TOKEN: Config.redacted("ACME_API_TOKEN"),
    AcmeThing: Cloudflare.DurableObject("AcmeThing"),
  },
});

gatekeepers: [Gatekeeper({ name: "acme", worker: acme })]
// or: Gatekeeper({ name: "acme", package: "my-acme-gatekeeper" })

The ai prop provisions the AI Gateway, its access token, and Secrets-Store-backed provider keys — nothing to create in the dashboard:

ai: {
  gateway: { cacheTtl: 60 },
  providers: {
    anthropic: Config.redacted("ANTHROPIC_API_KEY"),
    openai: Config.redacted("OPENAI_API_KEY"),
  },
  workersAi: true,
  limits: { dailyLlmCallLimit: 200 },
}

Live e2e coverage deploys a full OS, verifies it over HTTP, and destroys it:

pnpm --dir packages/alchemy run test:e2e

Draft until an alchemy release including Cloudflare.WorkerEntrypoint (alchemy#1097) ships to npm — until then pnpm-workspace.yaml carries local-tarball overrides that only resolve in the authoring checkout.

🤖 Generated with Claude Code

…ngSystem resource

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@@ -0,0 +1,69 @@
{
"name": "cloudflare-os",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name might be wrong. Should itbe @gadgets/alchemy or something else? Up to you guys how you want to distribute

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably @cloudflare/cloudflare-os-alchemy or the like (I know its a mouthful)

@sam-goodwin

Copy link
Copy Markdown
Author

This term gatekeepers doesn't make sense to me. An MCP is a gatekeeper?

Please provide feedback on the desired DX

@oiwa-coder

Copy link
Copy Markdown

would love this as well

@ndisidore ndisidore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love if CI could produce and publish whatever we call the npm package containing:

  • Prebuilt Worker module bundles.
  • Prebuilt frontend variants for password/OAuth and Access.
  • Static asset manifests and blobs.
  • The complete deployment manifest.
  • Full DO migration/lifecycle declarations.
  • Checksums and source commit metadata.
  • Supported configuration schema.

It seems like composable primitives would work well with a convenience facade, i.e.

const data = yield* OperatingSystemData("Data", {
  protect: true,
});

const github = yield* BuiltinGatekeeper("GitHub", {
  release: CloudflareOs.release.gatekeepers.github,
  credentials: { clientId, clientSecret },
});

const os = yield* OperatingSystem("OS", {
  release: CloudflareOs.release,
  data,
  auth: OAuthAuth({ providers: [github.auth] }),
  admins: ["admin"],
  gatekeepers: [
    github,
    BuiltinGatekeeper.Context(),
    BuiltinGatekeeper.Scheduler(),
  ],
});

| undefined;
}

interface RawWranglerConfig {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a second, incomplete deployment-config parser. It silently ignores several things the existing release manifest handles or explicitly rejects, so the two deployment paths can drift without anyone noticing.

Could this consume the existing release manifest instead? Otherwise, I think it needs to share the same strict parser and fail on anything it doesn’t understand.

main: NodePath.join(dir, raw.main),
compatibilityDate: raw.compatibility_date,
compatibilityFlags: raw.compatibility_flags ?? [],
durableObjects: (raw.migrations ?? []).flatMap(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible 🐛
Flattening the migration history down to class names loses tags, ordering, renames, deletes, and transfers. That’s probably enough for a fresh deployment today, but not for upgrades or adoption.

I think we need to preserve the full DO lifecycle and test deploy → write data → upgrade.

);
}
// Durable Object classes are reached via `ctx.exports`
// (`enable_ctx_exports`); the bindings exist to declare the classes and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any services actually use this compat flag so this may require updates to all the gatekeepers

Effect.gen(function* () {
const gatekeeperSet = resolveGatekeepers(props.gatekeepers);

// ── Storage ────────────────────────────────────────────────────────

@ndisidore ndisidore Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that they’re owned internally by the convenience component?

I’d rather have a separate protected OperatingSystemData resource, with support for caller-owned existing storage and explicit destructive deletion.

This would own:

  • Blueprint KV
  • Avatar KV
  • Blueprint R2
  • Any long-lived shared storage

@@ -0,0 +1,69 @@
{
"name": "cloudflare-os",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably @cloudflare/cloudflare-os-alchemy or the like (I know its a mouthful)

"alchemy": ">=2.0.0-beta.67",
"effect": ">=4.0.0-beta.100 || >=4.0.0"
},
"dependencies": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package depends on unpublished workspace packages and then runs their builds in the consumer’s environment. Those builds rely on dev dependencies and monorepo-root scripts that won’t be present after a normal npm install.

What are your thoughts on the npm package carrying prebuilt, checksummed Worker and frontend artifacts instead of rebuilding the monorepo in customer projects?

if (props.admins !== undefined) backendEnv.ADMINS = props.admins;
const auth = props.auth;
if (auth?.gatekeepers?.length) {
backendEnv.AUTH_GATEKEEPERS = auth.gatekeepers.join(",");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this typed against the current gatekeeper list? Or at least validated?

I'm worried Aatypo here can leave the whole installation without a login method: the list is non-empty, password auth gets disabled, but no usable auth gatekeeper appears

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants