agents/skills/cf-starter-gotchas/SKILL.md
Fork and template gotchas (env import, routes, typegen, forms, D1, Turbo, HMR, new DO packages). Use when working on apps/web or durable-objects, or when behavior diverges from this stack’s conventions.
npx skillsauth add firtoz/cf-multiworker-boilerplate cf-starter-gotchasInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
These trip up new contributors and agents most often. For commands and checklists, see cf-starter-workflow.
Worker bindings and env — Import the typed env from the Workers virtual module, not from React Router context: import { env } from "cloudflare:workers". Do not use context.cloudflare.env (or similar) for Cloudflare bindings in this stack. More: cf-workers-patterns.mdc.
Generated artifacts
packages/db/src/schema.ts or durable-objects/<name>/src/schema.ts; set drizzle.config.ts driver (d1-http vs durable-sqlite); run bun run db:generate or the package’s db:generate.drizzle/*.sql, drizzle/meta/*.json, driver migration wrappers, React Router +types, lockfiles, .alchemy/. PRs should flag hand-written Drizzle output unless explicitly intentional.Route path export — Each file under apps/web/app/routes/ should export its path for @firtoz/router-toolkit (forms, typed submitters), matching app/routes.ts (export const route: RoutePath<"/login"> = "/login";). routing/SKILL.md.
Regenerate types and verify often
alchemy.run.ts, or env changes: bun run typegen (repo root), then bun run typecheck and bun run lint during the task—not only before a PR.turbo run typegen:prod then turbo run typecheck:prod. cf-starter-workflow.ALCHEMY_PASSWORD and CHATROOM_INTERNAL_SECRET have no in-repo defaults. bun run setup / setup:local → variable browser (TTY). bun run setup -- --yes or CI=true → auto-fill only regeneratable secrets into .env.local. setup:staging / setup:prod → stage dotfiles (copy from .env.local offered in the browser).Loaders and actions return Promise<MaybeError<...>>
success / fail / MaybeError from @firtoz/maybe-error (not from @firtoz/router-toolkit).loaderData.success. Actions: prefer formAction. form-submissions, routing.Index route + formAction / useDynamicSubmitter → 405
formAction, useDynamicSubmitter, await submitter.submitJson(...), and exported route paths./?index; POST / alone will not hit the index action.Export formSchema (and related router-toolkit exports) for typed submitters when you use them. form-submissions/SKILL.md.
Alchemy + D1
CF_STARTER_APPS.database, package cf-starter-db) defines D1Database with migrationsDir → packages/db/drizzle.mainDb from cf-starter-db/alchemy.D1_DATABASE_ID for the default flow; do not add runtime CREATE TABLE fallbacks in loaders/actions—fix schema / migrations / local state instead.Turbo / stale typegen — If route types look wrong, run turbo run typegen:local --force (or typegen:prod after env changes). turborepo/SKILL.md.
JSDoc — Do not use */ inside a /** ... */ block (it ends the comment early). General TypeScript gotcha.
Empty or stale local D1
bun run db:generate after schema changes.
bun run dev (repo root) so cf-starter-db + web apply packages/db/drizzle.
Still no such table? Confirm migration output, D1Database.migrationsDir still packages/db/drizzle, restart dev; only then consider resetting local Alchemy/D1 state (documented troubleshooting—not routine).
Biome check --write — Can modify files after you think you are done; re-run bun run lint or review the diff before finishing.
Dev server port
server.hmr.port: client + SSR each run an HMR WebSocket server.@cloudflare/vite-plugin ignores Vite HMR (sec-websocket-protocol: vite…) and forwards other /api/ws/* upgrades to Miniflare.Prod D1 / visitors errors — If /visitors fails after deploy, confirm bun run deploy:prod (or packages/alchemy-utils/src/alchemy-cli.ts deploy database with the same STAGE) completed and D1 migrations ran; see Alchemy D1Database.
New Durable Object / worker package
import type { CloudflareEnv } and new Hono<{ Bindings: CloudflareEnv }>() so c.env is typed.workers/rpc.ts (no import from ../env there). Add package.json#exports "./workers/rpc" when consumers need it.WorkerRef / cross-worker: one direction uses workspace:*; the other uses a relative ../<pkg>/workers/rpc import to avoid Turbo cycles.dev filter.<pkg>#destroy:* with dependsOn → matching cf-starter-web#destroy:*.apps/web workspace dep; apps/web/alchemy.run.ts binding; apps/web/workers/app.ts forwarder if WebSockets.src/schema.ts, drizzle.config.ts with driver: "durable-sqlite", package db:generate; never hand-edit generated migrations.workers/app.ts to tsconfig.cloudflare.json include—it can break web Env./api/my-feature/ws/)./websocket on the DO.@firtoz/socka (chatroom-do, packages/chat-contract). Avoid hand-rolled JSON wire protocols unless the user wants raw WS.useMemo: window, document, WebSocket, canvas, localStorage, other DOM APIs.useEffect, handlers, ClientOnly, guarded client-only code.window.location only when called from client-only code—or pass origin in.localhost / 127.0.0.1 / placeholder WS URLs as runtime defaults.using in local devusing-compatible; Vite SSR / some Miniflare paths return plain Response.using res = await api.get(…) throws Symbol.dispose errors → use const res = … or guard disposers.using api = honoDoFetcherWithName(…) is usually safer (library guards missing stubs).webUrl for 5173 but the port is dead after a crash..alchemy/pids/cf-starter-web.pid.json and .alchemy/web/local/cf-starter-web.json..alchemy/logs/cf-starter-web.log exists (empty file is fine), then bun run dev. Never commit .alchemy/.CLOUDFLARE_API_TOKEN vs CLOUDFLARE_ACCOUNT_ID[CloudflareStateStore] 404 text/html, wrong workers.dev subdomain, confusing deploy failures..env.staging / .env.production.CLOUDFLARE_ACCOUNT_ID = Environment variable (workflows use vars); token = Secret.POSTHOG_* — optional like WEB_*; empty → no analytics.posthogRequirements from apps/web/env.requirements.ts; delete unused apps/web/app/** helpers (component, analytics*.ts); remove extra alchemy.run.ts bindings; bun remove @posthog/* / posthog-js from apps/web if present.durable-objects/*, apps/web, or /api/ws).development
Repo-root commands, typegen and typecheck cadence, lint, deploy, adding packages with bun, and Alchemy app layout. Use at the start of a task, before PR, or when choosing turbo/typegen commands.
development
Fork and template gotchas (env import, routes, typegen, forms, D1, Turbo, HMR, new DO packages). Use when working on apps/web or durable-objects, or when behavior diverges from this stack’s conventions.
testing
Turborepo task configuration patterns for monorepo management. Use when configuring turbo.json tasks, setting up task dependencies, managing cache inputs/outputs, or working with cross-package dependencies in the monorepo.
development
React Router v7 routing patterns and environment variable configuration. Use whenever you touch React Router–related code (routes, links, params, loaders, actions, route config, or env in route context).