agents/skills/multiworker-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-starter-kit multiworker-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 multiworker-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.alchemy.run.ts changed: bun run typegen then bun run typecheck from the repo root. multiworker-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
ALCHEMY_APP_IDS.database, package @internal/db) defines D1Database with migrationsDir → packages/db/drizzle.mainDb from @internal/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 bun run typegen -- --force. 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 @internal/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 bun run --cwd packages/db deploy:prod 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 @internal/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/ entry for the web workspace package (slug from apps/web/package.json name, e.g. @internal-web or scoped form depending on Alchemy) and the matching .alchemy/web/local/ JSON..alchemy/logs/ 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.requireEnvalchemy destroy loads alchemy.run.ts the same way alchemy deploy does: any requireEnv("…") at module scope must be set for destroy:preview too..env.staging loaded via alchemy-cli --stage staging|preview (same as deploy:preview / destroy:preview). In CI, mirror Turbo deploy (preview) env: on Destroy PR preview in .github/workflows/pr-deploy.yml — otherwise post-merge teardown fails with … is not set from a worker/DO alchemy.run.ts (e.g. APP_PUBLIC_BASE_URL).requireEnv for vars only needed at runtime, or use a destroy-safe placeholder where appropriate.trustedDependencies replaces Bun’s default script allowlistpackage.json trustedDependencies), only those package names may run install lifecycle scripts. The built‑in default list is used only when trustedDependencies is omitted (hasTrustedDependency in lockfile.zig).esbuild, msgpackr-extract, sharp, and workerd — the native/downloader-style packages that have needed install hooks here. After adding a dependency that legitimately requires scripts, add its published name (or run bun pm trust <name> / bun add --trust once) and re-run install.[install] ignoreScripts = true in bunfig.toml blocks all lifecycle scripts, including names in trustedDependencies — do not combine it with this allowlist approach.[install] optional = false is still incompatible at the repo level (Vite / Rolldown, workerd, Tailwind oxide, sharp platform packs).bunfig.toml)[install] minimumReleaseAge = 172800 (2 days in seconds) limits installs to npm versions published at least that long ago. Docs. Use minimumReleaseAgeExcludes for packages that must track bleeding-edge. Mostly affects new resolution, not rewriting an existing bun.lock.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.
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).
development
React patterns for callbacks, event handlers, and module-level constants. Use when writing React components, implementing event handlers, or defining constants.