@homeflare/alchemy
v0.4.0
Published
Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo and Talos.
Downloads
1,640
Readme
@homeflare/alchemy
Custom Alchemy providers for gaps the vendor SDK leaves.
bun add @homeflare/alchemy [email protected] [email protected] \
@effect/[email protected] [email protected] [email protected]⛔ Every one of those is required, and you also need an overrides block — see
Peers below. The install succeeds without them and
the import throws.
★ Why a custom provider at all. When Alchemy has no property for something, the
alternative is a runbook step a human runs once — and a plan can never show a missing
runbook step. Covering the gap with a resource means the drift is visible in plan like
everything else.
R2BucketLock
An R2 bucket's lock rules, declared rather than applied by hand. A lock rule is a retention floor: while a rule covers an object, no API call, no lifecycle rule and no credential can delete it.
import { R2BucketLock } from '@homeflare/alchemy';
import { providers } from '@homeflare/alchemy/cloudflare';
export class BackupLock extends R2BucketLock('backup-lock', {
bucketName: 'my-backups',
jurisdiction: 'default',
rules: [{ id: 'keep-30d', enabled: true, condition: { type: 'Age', maxAgeSeconds: 2_592_000 } }],
}) {}Add the provider layer to your stack:
import { providers } from '@homeflare/alchemy/cloudflare';
// …then provide `providers()` alongside Cloudflare.providers()Website.Astro / Website.Vite
House flags on Alchemy's own stacks — the two this estate actually ships.
import { astroWebsite, viteWebsite } from '@homeflare/alchemy/cloudflare';
const site = yield * astroWebsite('subnetcalc', { rootDir });
const app = yield * viteWebsite('aimto', { rootDir: webRoot });⛔ Astro needs disable_nodejs_process_v2 — workerd process-v2 makes every page return
[object Object]. Vite / TanStack Start does not. Alchemy injects
@alchemy.run/frontend-frameworks/astro; do not add @astrojs/cloudflare.
⛔ Not Nextjs. Website.Nextjs hashes source and plans as create against a live
Worker. Adopt that shape with Worker, not a helper here.
⚠️ The rule set is REPLACE, not merge. The API PUTs the whole set, so a rule omitted
from rules is a rule deleted. That is the same shape as the Cloudflare API itself.
⛔ Deletion is refused by design. Removing a lock is removing a retention floor, which is the one operation this resource exists to make hard. It retains on destroy.
Credentials
CLOUDFLARE_API_TOKEN is read from the environment at call time, never at module scope.
⛔ Mint a short-lived, scoped token — do not reuse a long-lived one, and never a Global
API Key. An empty value fails closed with a message saying so, because an empty render is
a denied grant rather than a missing file, and Bearer + nothing 401s in a way that reads
like a bad credential.
Peers — and one override you need
bun add @homeflare/alchemy [email protected] [email protected] \
@effect/[email protected] [email protected] [email protected]⚠️ Peers, not dependencies: Alchemy's resource registry and Effect's context both break if two copies load in one process.
⛔ Add this to your package.json, or the install works and the import throws:
{
"overrides": {
"effect": "4.0.0-rc.115",
"@effect/platform-node": "4.0.0-rc.115",
"@effect/platform-node-shared": "4.0.0-rc.115",
"@effect/platform-bun": "4.0.0-rc.115",
"rolldown": "1.2.8"
}
}🔴 Why, measured 2026-09-16 on 0.1.0 and re-checked 2026-09-17 against Alchemy 78.
Effect's rc line is not semver-compatible with itself. Alchemy 78's peer is
effect >= 4.0.0-rc.115; an unlocked @effect/platform-node-shared still floats to the
next rc and breaks at import. Pin the whole set.
| what resolves | what happens |
| ------------------------------------------------------------- | ------------------------------------------------------ |
| Alchemy 77 + effect → rc.115 (measured 2026-09-16) | TypeError: Config.string is not a function at import |
| @effect/platform-node-shared newer than the pinned effect | Cannot find module 'effect/ByteSize' |
| rolldown → 1.2.9 via vite's ~1.2.6 (measured 2026-09-16) | GET …/rolldown-1.2.9.tgz - 404 at bun add |
| no mime (measured 2026-09-17 against Alchemy 78) | Cannot find package 'mime' from cloudflare-runtime |
Alchemy 78 adapted to rc.115 — that first row is why we used to pin 112, not a reason to stay there. The override is still the only thing that holds the set together.
⚠️ @effect/platform-node is required, not optional: Alchemy's module graph reaches
Cloudflare/Workers/WorkerBridge → @effect/platform-node/NodeServices even when you only
import the Proxmox subpath.
⚠️ So is mime. Alchemy 78's @alchemy.run/cloudflare-runtime imported it without
declaring it (measured 2026-09-17). 79 declares it; the peer stays so a consumer
that followed the 78 README does not drop a required line.
⚠️ So is cloudflare. It was marked optional in 0.1.1, which claimed the /cloudflare
subpath would degrade without it — measured 2026-09-16, the subpath does not load at all:
Cannot find package 'cloudflare'. An optional peer should mean a feature is absent, not
that an import fails.
License
MIT © Timothy Schneider
