Getting started
pnpm install # install all workspace deps
pnpm dev:web # run the frontend (Vite)
pnpm dev:api # run the Worker locally (wrangler dev)
pnpm dev:docs # run this documentation site (VitePress)
pnpm build # build the frontend
pnpm build:docs # build the docs into apps/web/dist/docs
pnpm deploy # build both, then deploy the Worker (wrangler deploy)
pnpm fmt # format everything (oxfmt)
pnpm lint # check formatting, then lint (oxfmt --check + oxlint)Each app keeps its own environment file:
apps/web/.env—VITE_*-prefixed values exposed to client code by Vite at build time (seeapps/web/vite.config.js).apps/api/.env— Worker secrets (e.g.GEMINI_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,GROQ_API_KEY— the Worker tries each configured AI provider in order, skipping any without a key set). Cloudflare Workers AI needs no key — it's wired up via theAIbinding inwrangler.jsoncinstead, and serves as the no-external-dependency fallback if every other provider is unset or fails.
Both are gitignored.
Documentation site
This site is VitePress. Pages are plain Markdown under apps/docs/docs, and the config is apps/docs/docs/.vitepress/config.mts — the .mts extension matters, because apps/docs/package.json has no "type": "module" and VitePress is ESM-only.
The dependency tracks the 2.0 alpha on purpose: vitepress@latest is still 1.6.4 from August 2025, and it would drag in a second Vite major (5) alongside the Vite 8 that apps/web builds on. The range is ^2.0.0-alpha.19, so the lockfile decides the exact version — today 2.0.0-alpha.19, and a lockfile refresh can move it to a later alpha or to 2.x once that ships. See Frontend migration for the full reasoning.
- The sidebar is explicit. A new page does not appear until it is listed in
themeConfig.sidebar. That order is also the order of the sections inllms.txt. - Links between pages are relative and keep their
.mdextension (./search-images.md), which is what lets VitePress rewrite them and fail the build on a dead one. baseis/docs/and the output goes toapps/docs/doc_build, whichpnpm build:docsthen copies intoapps/web/dist/docs. Run it after the web build — Vite emptiesapps/web/disteach time, so apnpm buildafterpnpm build:docsdeletes the docs again.pnpm deployand the deploy workflow both run the two in that order for you.cleanUrlsis on, and Cloudflare's defaultauto-trailing-slashasset handling resolves/docs/introtointro.html.llms.txtandllms-full.txtcome fromvitepress-plugin-llms, which also emits a Markdown twin of every page next to its HTML./docs/sitemap.xmlis VitePress's built-in sitemap, with<lastmod>taken from git. It is separate from the Worker's dynamic/sitemap.xml(which covers the app's own pages and every published lesson);robots.txtadvertises both.
Code quality
The repo uses the Oxc toolchain — oxfmt for formatting and oxlint for linting. They replaced Prettier and ESLint, so eslint.config.js, .prettierrc and .prettierignore no longer exist.
.oxfmtrc.json(root) —printWidthis pinned to80(Prettier's default, and what the existing code is wrapped to; oxfmt's own default is100), plus the ignore list that used to live in.prettierignore.apps/api/.oxfmtrc.json— the Worker keeps its own style (tabs, single quotes, 140 columns), previouslyapps/api/.prettierrc. Oxfmt discovers nested configs automatically, so runningpnpm fmtfrom the root still applies it..oxlintrc.json(root) — theeslintandreactplugins with thecorrectnesscategory as errors, mirroring the old flat config'sjs/recommended+eslint-plugin-react+eslint-plugin-react-hookssetup. Per-appoverridessupply the right globals for each runtime: browser forapps/web, worker + Node (plusWebSocketPair,WebSocketRequestResponsePairandHTMLRewriter) forapps/api, and Node forapps/mcp.packages/coreis linted against theworkerenv — the narrowest of the three — so that anything reaching for a browser-only global there fails the lint rather than breaking at runtime inside the Worker. The modules that legitimately need the DOM are grouped undersrc/browser/, which is the only path opted back intobrowser.
eslint/no-undef is enabled explicitly: it's part of ESLint's js/recommended but not of oxlint's correctness category, and it's what makes those globals and env blocks do anything.
Oxlint enables its unicorn, oxc and typescript plugins by default; the config lists plugins explicitly to keep them off, so the rule set stays the one this codebase was written against. Two rules the ESLint config switched off — react/prop-types and react-hooks/set-state-in-effect — are not implemented by oxlint, so there is nothing to disable.
react-hooks/exhaustive-deps is a warning, and pnpm lint does not pass --deny-warnings, matching the previous ESLint behaviour of not failing CI on it.
Continuous integration
.github/workflows/ci.yml runs on every pull request and on master after a merge: install (with --frozen-lockfile), pnpm lint, pnpm test, then the web and docs builds.
pnpm test is pnpm -r test, which runs each workspace's own suite — packages/core and apps/api under vitest (the Worker's through @cloudflare/vitest-pool-workers, so its sanitizer tests run against the real runtime), and apps/mcp under node --test. apps/docs has no test script and is skipped.
The build runs without the VITE_* secrets — they are injected only for the real deploy, and a pull request from a fork could not read them anyway. It still resolves every import and runs the full bundler, which is what catches a bad module path or a broken chunk boundary.
.github/workflows/deploy.yml is separate and still triggers only on a push to master. It lints and builds again before deploying, but by then the change has already been merged — CI is what gates the merge.
Dependency updates
.github/dependabot.yml runs Dependabot every Monday against two ecosystems:
- npm — a single root entry covers all workspace packages, since Dependabot resolves pnpm workspaces from the root
pnpm-lock.yaml. - github-actions — the actions used by
.github/workflows/.
Minor and patch bumps are grouped (dev tooling, React, Radix UI, Cloudflare, then everything else split by production/development) so a routine week lands as a handful of PRs; majors open individually because they need review.
@cfworker/json-schema is ignored — it's patched via patchedDependencies in pnpm-workspace.yaml, so bumping it requires regenerating patches/@cfworker__json-schema@<version>.patch by hand.