Alchemy (v2) Documentation Audit
Ambitious, often well-written docs for a pre-release Infrastructure-as-Effects framework — undermined by API-shape drift between pages, a state-property contradiction that makes the flagship intro examples fail the tutorial's own type check, and a parallel v1 docs universe (alchemy.run + the GitHub README) that never points here.
1. Three incompatible API shapes for the same resource across core pages (critical)
Location: https://v2.alchemy.run/what-is-alchemy/ vs https://v2.alchemy.run/getting-started/, /cloudflare/tutorial/part-1/, /migrating-from-v1/, /infrastructure-as-code/resource/
Problem: The docs use three different, mutually incompatible spellings for the same R2 bucket API. The "What is Alchemy?" page declares resources flat — Cloudflare.R2Bucket("Bucket"), Cloudflare.D1Database("DB") — and binds with Cloudflare.R2Bucket.bind(Bucket). Getting Started, Tutorial Part 1, Migrating from v1, and the Resource concept page all use the namespaced form Cloudflare.R2.Bucket("Bucket"). The migration guide then binds with yet another form: Cloudflare.R2.ReadWriteBucket(Bucket). Only one of these can match the actual alchemy@next exports; the others are code that will not compile.
Consequence: A developer (or coding agent) reading the conceptual intro and then the tutorial gets two contradictory module shapes with no signal which is current. Agents fail hardest here: they extract Cloudflare.R2Bucket.bind(Bucket) from the intro page, hit a type error, and have no way to know the namespaced form on other pages is the real one. Humans burn time; agents silently generate broken infra code.
The fix: Pick the canonical API surface, regenerate every code block on "What is Alchemy?" and the migration guide's binding section to use it, and add a CI check (the reference is already generated from JSDoc) that lints doc snippets against the actual exports.
2. The state property is "required and type-enforced" — except on the pages that omit it (critical)
Location: https://v2.alchemy.run/cloudflare/tutorial/part-1/, https://v2.alchemy.run/what-is-alchemy/, homepage hero, https://v2.alchemy.run/state-store/
Problem: Tutorial Part 1 states flatly: "TypeScript is unhappy: the state property is required. Every Stack needs a state store." Yet the homepage hero example and both Stack examples on "What is Alchemy?" pass only { providers: Cloudflare.providers() } with no state at all. Meanwhile the State Store page says "By default, state is stored on disk in the .alchemy/ directory" — language that implies a default exists and state is optional.
Consequence: Three surfaces, three answers: required (tutorial), omitted (intro/homepage), defaulted (state-store page). A developer copying the intro example gets a compile error the docs told them wouldn't happen — or the state-store page's "default" claim is stale and the whole section misleads. Either way, the very first thing every Alchemy program needs (a Stack) has contradictory setup instructions.
The fix: Decide whether state has a local-disk default or is required, then align the tutorial's type-error narrative, the State Store page's "by default" section, and every Stack example on the homepage and intro page to that one answer.
3. GitHub README and the production domain document only the incompatible v1 API, with no pointer to v2 (significant)
Location: https://github.com/alchemy-run/alchemy (README), https://alchemy.run/
Problem: The project's two highest-traffic discovery surfaces describe a different product. The GitHub README documents the v1 async/await API (const app = await alchemy(...), entrypoint:, bindings:, app.finalize()) and links its Getting Started to https://alchemy.run/getting-started — the v1 site, which still brands itself "Infrastructure as TypeScript" with await-style code. Neither surface mentions v2, alchemy@next, or v2.alchemy.run; the v2 site carries no banner in the other direction either. The v2 migration guide confirms the APIs are incompatible ("Your v1 state is not compatible with v2").
Consequence: Anyone arriving via GitHub or a search engine lands on v1 docs and learns an API that alchemy@next rejects wholesale (await → yield*, entrypoint → main, no finalize()). Coding agents that index the README — the most-scraped file in any repo — will generate v1 code against a v2 install and vice versa, with zero signal that two doc universes exist.
The fix: Add a version banner/switcher to both sites and a prominent v1-vs-v2 section at the top of the README linking to v2.alchemy.run and the migration guide.
4. Local state helper has two different names on two different pages (significant)
Location: https://v2.alchemy.run/cloudflare/tutorial/part-1/ vs https://v2.alchemy.run/aws/setup/
Problem: Tutorial Part 1 says to use State.localState() imported "from alchemy/State" for on-disk state. AWS Setup calls the same facility Alchemy.localState(). These are different import paths and namespaces for one helper.
Consequence: One of the two snippets doesn't exist in the package. A developer following the AWS setup path (or an agent that read only that page) writes Alchemy.localState() and gets a "property does not exist" error, with the correct spelling hidden on a Cloudflare tutorial page they have no reason to read.
The fix: Standardize on one name/import and correct the other page; this is exactly the class of drift a snippet-compilation CI check would catch.
5. Generated API reference promises input/output tables it doesn't deliver (minor)
Location: https://v2.alchemy.run/providers/aws/s3/bucket/ (and the Providers section preamble in llms.txt)
Problem: The llms.txt Providers preamble says each reference page "documents the resource's input properties (with types, defaults, and constraints), output attributes, and Quick Reference / Examples sections." The sampled S3 Bucket page consists of example blocks only — no input/output attribute tables. Its examples also import alchemy/AWS/S3 and use lowercase logical IDs ("my-bucket") where every concept page uses PascalCase ("Bucket"), adding a third stylistic dialect.
Consequence: Developers and agents looking up "what properties does Bucket accept, and what are the defaults" — the whole point of a reference page — find prose and examples instead of the promised schema, and must go read src/AWS/S3/Bucket.ts.
The fix: Make the bun generate:api-reference pipeline actually emit the typed input/output tables the preamble advertises, and normalize logical-ID casing between reference and concept pages.
6. llms.txt links a docs graph with no llms-full.txt and a 404 sitemap (minor)
Location: https://v2.alchemy.run/llms.txt, /llms-full.txt, /sitemap.xml
Problem: The site ships a genuinely good 148 KB llms.txt navigation index ("so an agent can pick the right page in one hop"), but /llms-full.txt and /sitemap.xml both return 404.
Consequence: Agents that follow the llms.txt convention expect the companion full-content file; without it, every page still costs a fetch-and-render round trip, and crawlers get no sitemap fallback for indexing.
The fix: Generate /llms-full.txt (the content already lives under /src/content/docs/) and emit a sitemap from the same source.
7. Install command ships a malformed-looking semver range (minor)
Location: https://v2.alchemy.run/getting-started/ (Install section, raw HTML)
Problem: The install command pins "effect@>=4.0.0-beta.97|| >=4.0.0" — a range with a missing space before the || (and the same pattern repeated for both @effect/platform-* packages). The Exa cache of Tutorial Part 1 also showed an unrendered effect@{effectVersion} template placeholder in its Install section, since fixed in the live HTML — evidence this string is templated and fragile.
Consequence: npm's semver parser tolerates the missing space, but the mangled range signals a templating bug that already leaked a raw {effectVersion} placeholder once; a stricter package manager or a future template regression turns the very first command in the docs into an error.
The fix: Fix the template to emit a well-formed range (>=4.0.0-beta.97 || >=4.0.0) and add a smoke test that runs the rendered install command.
What they do well
- Honest failure-mode documentation: the Secrets page has an explicit "Footgun" section, the
unsafe nukepage bluntly explains it deletes untracked resources and is hidden from--helpon purpose, and the Astro/Nuxt pages state plainly what is a TODO, unverified, or impossible today. - Agent-aware engineering: a real llms.txt navigation index with one-line page summaries, and the CLI auto-detects agent environments (
CLAUDECODE,CURSOR_AGENT, etc.) to force plain, non-prompting output.
Top 3 recommendations
- Type-check every doc snippet against
alchemy@nextin CI — findings 1, 2, 4, and 7 are all the same failure class (drifted or templated code the compiler would catch). - Resolve the
statecontradiction: one answer, stated identically on the tutorial, the state-store page, and every Stack example. - Bridge the v1/v2 split: version banners on both sites and a v2 pointer at the top of the GitHub README.