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

Deploy Cloudflare OS to your own account with alchemy. Everything is alchemy code, owned by the package it deploys — the wrangler.jsonc files, the release-manifest machinery, and everything that parsed them are deleted; each package's src/alchemy.ts is the single source of truth for deploy, local dev, and tests.

// alchemy.run.ts — your company's repo
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, OperatingSystemData } from "cloudflare-os";
import Context from "cloudflare-os/context";
import GitHub from "cloudflare-os/github";
import Mcp from "cloudflare-os/mcp";

export default Alchemy.Stack(
  "AcmeOS",
  { providers: Cloudflare.providers(), state: Cloudflare.state() },
  Effect.gen(function* () {
    // User data is a separate, protected resource — retained on destroy.
    const data = yield* OperatingSystemData("Data");

    const github = GitHub({
      clientId: Config.string("GITHUB_CLIENT_ID"),
      clientSecret: Config.redacted("GITHUB_CLIENT_SECRET"),
    });

    const os = yield* OperatingSystem("OS", {
      domain: "os.acme.com",
      admins: ["sam@acme.com"],
      data,
      // sign-in providers are references — a typo can't lock you out
      auth: { providers: [github], disablePasswordAuth: true },
      gatekeepers: [
        github,
        Context({ sharingDomain: "production" }),
        Mcp({ enabled: false }),
      ],
      // presence of `ai` = gateway mode; the gateway, its token, and the
      // stored provider keys are provisioned, not referenced
      ai: { providers: { anthropic: Config.redacted("ANTHROPIC_API_KEY") } },
    });

    return { url: os.url };
  }),
);

alchemy deploy provisions the router (public origin + assets + domain), the workshop-backend, one Worker per enabled gatekeeper, the storage, and the AI gateway. alchemy destroy tears it all down — except the data.

Each package owns its deployment, as code

// packages/gatekeeper-github/src/alchemy.ts — the package's root export
export const workerConfig = {
  name: "gatekeeper-github",
  id: "GitHub",
  entry: "github.ts",
  prebuild: "build:configurator",
  compatibilityFlags: ["allow_irrevocable_stub_storage", "nodejs_als"],
  migrations: [
    { tag: "v0", newSqliteClasses: ["UserAccount", "GitHubGatekeeperImpl"] },
  ],
} as const satisfies WorkerConfig;

export const GitHub = (config: GitHubConfig): Gatekeeper =>
  Gatekeeper({
    name: "github",
    oauth: true, // usable as a sign-in provider
    worker: Gatekeeper.Worker(workerConfig, {
      dir: packageRoot(import.meta),
      env: {
        CLIENT_ID: Gatekeeper.secret(config.clientId),
        CLIENT_SECRET: Gatekeeper.secret(config.clientSecret),
      },
    }),
  });

export default GitHub;
  • The deploy contract (Gatekeeper, Gatekeeper.Worker, toWranglerConfig) lives in @gadgets/workshop-shared/alchemy; backend/router/frontend/typed-storage export <pkg>/alchemy the same way.
  • cloudflare-os/<name> re-exports each package's module — same file inside the monorepo and from npm.
  • run-dev-server.js (local dev, unchanged UX), the integration-test harness, and the pool-workers vitest configs all derive wrangler-shaped config in memory from the exported workerConfig — no config files, one source of truth. scripts/release/ and the golden manifest are deleted.

A custom gatekeeper is just a Worker

const worker = yield* Cloudflare.Worker("Acme", {
  main: "./src/worker.ts",
  compatibility: { date: "2026-02-02" },
  env: { AcmeThing: Cloudflare.DurableObject("AcmeThing") },
});

gatekeepers: [worker]; // GATEKEEPER_ACME derives from the id

Protected user data

const data = yield* OperatingSystemData("Data"); // retained on `alchemy destroy`
const data = yield* OperatingSystemData("Data", {
  blueprints: existingKv,     // adopt existing storage
  removalPolicy: "destroy",   // ephemeral stacks (tests, previews) only
});

Verified live end to end by packages/cloudflare-os/test/OperatingSystem.test.ts (deploy → HTTP probes → destroy), on alchemy's own test harness.

Status

Draft until an alchemy release ships Cloudflare.WorkerEntrypoint (alchemy-run/alchemy#1097, merged); this checkout consumes a local build via pnpm overrides. Intended end state, per review discussion: CI publishes this package as a prebuilt release (worker bundles, frontend variants, checksums), so npm install cloudflare-os needs no monorepo checkout — the per-package alchemy code here is the source of truth that pipeline drives.

🤖 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.

@sam-goodwin sam-goodwin Aug 8, 2026

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.

If i was doing it my way, i would not have the deployment.jsonc/wrangler.json and just export typescript with the alchemy code in it. I did it this way conservatively because the repository is currently based on wrangler.

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