ADR-0008: One ESLint flat config for the workspace
Edit this pageStatus: Accepted Date: 2026-08-14 Supersedes: ADR-0006
Context
ADR-0006 removed the CI lint step because
the workspace had no ESLint configuration at all — next lint opened its
interactive setup prompt, which CI has no terminal to answer. That record
listed its own reversal criteria: a flat config, the dependencies in the
lockfile, and the CI step restored and passing.
The blocker was environmental rather than technical. Adding a dependency means
regenerating pnpm-lock.yaml, CI installs with --frozen-lockfile, and the
session that found the problem could not run pnpm install — its
node_modules had been installed from WSL2 and were unreadable to the Windows
Node it had available.
That constraint is gone: the toolchain is now reachable through WSL, so the lockfile can be regenerated and the result verified before it is pushed.
Decision
A single flat config at the repository root, eslint.config.mjs, covering
every package. The root lint script is eslint ., and the per-package
next lint and eslint src scripts — none of which ever worked — are
removed.
eslint-config-next is deliberately not used. pnpm isolates workspace
dependencies, so a root-level config cannot resolve next, which that package
requires for its parser. Rather than hoist next to the root purely to satisfy
a linter, the rules worth having come from eslint-plugin-react-hooks
directly, which resolves without help.
The rule set is small and each entry earns its place:
| Rule | Why |
|---|---|
react-hooks/rules-of-hooks, exhaustive-deps | Stale-closure bugs are otherwise found in production, and nothing else catches them |
@typescript-eslint/no-explicit-any, ban-ts-comment | The codebase has zero of both today; this preserves that rather than describing it |
no-console | Structured logging is a project rule. A console.log in request-serving code writes to stdout with no fields and no redaction |
no-unused-vars | Leftovers from refactors, with ^_ as the documented opt-out |
CLI entry points and build scripts are exempted from no-console — they are
meant to talk to a terminal. Test files may use any.
What the first run found
Three errors across the whole repository, which is a good result for code that had never been linted.
The one that mattered: apps/server/server/auth/index.ts printed the magic
link to stdout. That is a single-use login credential written to the
container log, readable by anyone with log access — a finding the roadmap
had already recorded independently, and the linter located it on its first
execution.
It now refuses instead. There is no mail transport, so the honest failure is an
error the caller can see, not a silent success for a message that was never
sent. A development escape hatch, LOOMSCOPE_MAGIC_LINK_ECHO=true, prints it
with an explicit warning; without that flag the link is never logged at any
level.
The other two were a console.log in a build script and an unused caught
error.
Alternatives considered
One config per package. Rejected. The rules that matter here are project-wide, and three configs is three places to look and three chances to drift.
Hoist next to the root to keep eslint-config-next. Rejected. It would
put a framework dependency at the root of a workspace that also contains two Go
services, for the sake of a parser.
Adopt the full next/core-web-vitals set including jsx-a11y. Deferred,
not rejected. Accessibility rules are worth having, but turning them on in the
same change would have mixed a lint bootstrap with an accessibility audit. A
follow-up can enable them and work through what they find.
Consequences
Accepted costs.
- Four more development dependencies, and
pnpm installis slower. - Next-specific rules — image and font optimisation hints — are absent.
exhaustive-depsis a warning, not an error, so it can accumulate.
Gains.
make lintfinally does what it has always claimed.- The
no-consolerule mechanically enforces a rule the project had written down and violated in eight places. - CI now checks lint, typecheck, tests, migrations and secrets. Every one of those does what its name says.