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
- Never hand-author files meant to be generated.
- Drizzle: edit
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.
- Generated-only:
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
- After routes,
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.
- Bindings or
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<...>>
- Import
success / fail / MaybeError from @firtoz/maybe-error (not from @firtoz/router-toolkit).
- Loaders: annotate return type; narrow with
loaderData.success. Actions: prefer formAction. form-submissions, routing.
-
Index route + formAction / useDynamicSubmitter → 405
- In-app: use
formAction, useDynamicSubmitter, await submitter.submitJson(...), and exported route paths.
- Outside the router (curl, plain HTML, tools): POSTs to an index route must use
/?index; POST / alone will not hit the index action.
- Prefer a non-index resource route for public create/join APIs.
-
Export formSchema (and related router-toolkit exports) for typed submitters when you use them. form-submissions/SKILL.md.
-
Alchemy + D1
- packages/db/alchemy.run.ts (
ALCHEMY_APP_IDS.database, package @internal/db) defines D1Database with migrationsDir → packages/db/drizzle.
- apps/web/alchemy.run.ts imports
mainDb from @internal/db/alchemy.
- Alchemy applies SQL on deploy/dev per D1 + Drizzle.
- Do not hand-manage
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