Lint as the contract
Architecture lives in tooling, with prose for context. The docs explain intent; lint makes drift visible before review.
The problem
Section titled “The problem”Architecture docs rot. New teammates miss local conventions. Agents can generate plausible code in the wrong layer. Reviewers should spend their time on product behavior, not rediscovering that a route now contains business logic or a component reads environment state directly.
BoringStack treats lint as the executable part of the architecture. AGENTS.md, CLAUDE.md, and AGENT_CONTRACT.md explain intent; custom ESLint plugins make the important parts fail the build.
How it works
Section titled “How it works”Every template ships prose contracts and lint gates together. The prose says where routes go, when to use a service, how to write a Drizzle query, why process.env is forbidden outside the env validator. The plugins turn those patterns into parseable, repeatable errors.
bun run validate / bun run validate is the merge gate: if lint fails, the diff does not ship. Prose is the why; lint is what ships.
What gets enforced
Section titled “What gets enforced”- Layer boundaries. Routes stay thin, services own business logic, components render view objects, and feature folders keep one semantic concern per file.
- Data safety. Account-scoped queries require filters, multi-write paths require transactions, cache keys are namespaced, and Stripe webhooks are idempotent.
- Security invariants. JWT cookies, OAuth state/PKCE, raw webhook bodies, env access, and secret-safe logging are checked mechanically.
- Operational hygiene. Structured log events, test placement, dead-code detection, env/schema drift checks, and no inline disables keep the merge gate honest.
- Agent-friendly feedback. Violations point at the file, rule, and local fix. Humans and agents get the same contract.
- Review focuses on behavior. Reviewers spend energy on behavior instead of rediscovering folder and boundary rules enforced by ESLint.
Plugin inventory
Section titled “Plugin inventory”Use this as the reference catalog once the overview above makes sense.
Shared across apps/api + apps/ui
Section titled “Shared across apps/api + apps/ui”-
resource-architecture.
@boring-stack-pkg/eslint-plugin-resource-architecture. Per-feature route/service/types split. No business logic in routes; no HTTP in services. -
module-boundaries.
@boring-stack-pkg/eslint-plugin-module-boundaries. Single-semantic-module files. No mixed-concern dumps. -
structured-logging.
@boring-stack-pkg/eslint-plugin-structured-logging. Pino discipline. Noconsole.log, no string-interpolated log lines, no logging secrets. Thetyped-event-namesrule validates everylogger.{level}({event})literal against the canonicalLOG_EVENTSconst tuple so log events stay a closed set. -
env-access.
@boring-stack-pkg/eslint-plugin-env-access.process.env/import.meta.envonly inside the env validator; everywhere else uses validated config. -
test-conventions.
@boring-stack-pkg/eslint-plugin-test-conventions.tests/mirrorssrc/; no orphan tests. -
code-flow.
@boring-stack-pkg/eslint-plugin-code-flow. Control-flow discipline.prefer-early-return(no needless nesting after a guard) andno-template-trim-empty-ternary(bans the inline${a} ${b}.trim() === "" ? fallback : …anti-pattern; extract to a named util likebuildDisplayName(…)so the construction is testable in one place). Also enforces blank-line padding beforethrowandreturn` so the exit branch is visually distinct. -
comment-hygiene.
@boring-stack-pkg/eslint-plugin-comment-hygiene.no-narration-commentsflags AI-generated narration like “Here we…” / “Now we…” / “Let’s…”.no-pr-reference-commentsflags#123/PR 42/ GitHub URLs embedded in code (they belong in the PR description, not the source).
Built-in + third-party rules wired at the same gate
Section titled “Built-in + third-party rules wired at the same gate”Two non-custom rules earn their place alongside the @boring-stack-pkg plugins:
-
eslint-comments/no-use.
@eslint-community/eslint-plugin-eslint-comments.no-useis set toerrorwith{ allow: [] }. Zero inline disables; defence-in-depth on top of the source-text ban thatlint:metaalready enforces. -
multiline-comment-style: starred-block. Built-in ESLint rule. Any
//-style block that spans three or more consecutive lines must be a single/* … */block. Auto-fixable; keeps WHY-comments visually distinct from inline notes. -
no-restricted-syntax: ban inline new Date().toISOString(). Built-in ESLint rule with a project-specific selector. Every call site must use the
now()util fromsrc/lib/time/now.tsso timestamps have a single mockable source and a single place to change formatting if the contract ever shifts.
apps/api only
Section titled “apps/api only”-
elysia.
@boring-stack-pkg/eslint-plugin-elysia. TypeBox on every route, no untyped handlers, plugin registration patterns.route-must-check-abilityrequires every handler that destructuresmembershipfrom context to authorize explicitly: either readmembership.roleor callrequireAbility/enforceLimit. -
drizzle-conventions.
@boring-stack-pkg/eslint-plugin-drizzle-conventions. Schema and query patterns, index naming,withusage.account-scoped-tables-require-whererequires every query against an account-scoped table (accountMemberships,accountInvitations,accountFeatureOverrides,accountPlans) to filter byaccountId(tenant isolation enforced at the query level). -
db-transactions.
@boring-stack-pkg/eslint-plugin-db-transactions. Multi-write paths inside a transaction. -
jwt-cookies.
@boring-stack-pkg/eslint-plugin-jwt-cookies. Cookie attributes and JWT verify call sites. -
oauth-security.
@boring-stack-pkg/eslint-plugin-oauth-security. OAuth state and PKCE on the server flow. -
bullmq.
@boring-stack-pkg/eslint-plugin-bullmq. Queue/worker idempotency, failed handlers, name discipline. -
cache-keys.
@boring-stack-pkg/eslint-plugin-cache-keys. Namespaced cache keys and TTL discipline. -
audit-log.
@boring-stack-pkg/eslint-plugin-audit-log. Audit writes on flagged mutations. -
stripe-webhooks.
@boring-stack-pkg/eslint-plugin-stripe-webhooks. Signature verification, idempotency, raw-body access.
apps/ui only
Section titled “apps/ui only”-
react-component-architecture.
@boring-stack-pkg/eslint-plugin-react-component-architecture. Component anatomy, hooks,classNamediscipline, file naming, prop ordering. Themax-hooks-per-filerule caps top-level hooks per file (default 4) so god-modules like a 7-hook*.queries.tseither get split or fail the gate. -
tanstack-query-cache.
@boring-stack-pkg/eslint-plugin-tanstack-query-cache. On*.queries.ts, when keys are built as[...PREFIX, …], cache writes must use matcher-style APIs (setQueriesData,cancelQuerieswithexact: false/predicate, etc.) instead ofsetQueryData/getQueryDataon the bare prefix alone; otherwise only one entry updates while hooks still read the spread key. -
i18n-keys.
@boring-stack-pkg/eslint-plugin-i18n-keys. Static string keys int("…")/i18n.t("…")must exist in the canonical English catalog (src/lib/i18n/locales/en/common.jsonin the template). Catches typos and orphan keys at lint time.
How they’re wired
Section titled “How they’re wired”Each template’s flat ESLint config (eslint.config.mjs in apps/ui, eslint.config.js in apps/api) imports these plugins from node_modules/. They are devDependencies installed from npm as exact-version @boring-stack-pkg/eslint-plugin-* pins; source lives in the boringstack-xyz/eslint-plugins monorepo, releases run through Changesets, and every published version carries an OIDC-signed provenance attestation. The 7-day minimumReleaseAge quarantine that protects the rest of the dependency tree is exempted for the @boring-stack-pkg/* scope (it’s our own code; the quarantine guards against compromised upstream publishes we don’t control). The plugins ship their own recommended configs where applicable; we extend those and keep severities at error so the merge gate has real teeth.
Where lint:meta is wired (bun run lint:meta / bun run lint:meta), it adds extra contract checks on top of ESLint, package.json pin parity, CI/pre-push parity, env cascade drift, source-text bans, and test-sibling requirements. The full machine-readable catalog lives on lint:meta rules; implementation is under scripts/lint-meta/cli.ts with fixture-based tests in tests/lint-meta/. For which script runs each merge-gate command, see Scripts & tooling.
Merge gate (same contract in apps/api and apps/ui):
bun run validate # typecheck + lint + knip + testsbun run lint # architecture rules onlybun run lint:fix # autofix what can be fixed mechanicallybun run knip # unused files, exports, and dependenciesThe same merge gate runs in three places so a broken change cannot reach main. A husky pre-commit hook runs bun run lint-staged against the staged files, the cheap, scoped check on every commit. A husky pre-push hook (installed via prepare on bun run install / bun install; for infra/compose and .github, run ./scripts/install-hooks.sh once) then runs the full validate locally before anything leaves your machine: typecheck, lint, knip, tests, build, osv-scanner against the lockfile, and (when workspace apps are present) an OpenAPI schema drift check across apps/api and apps/ui. CI re-runs the same gate so a --no-verify push is still caught.
Knip sits alongside the lint plugins, surfacing unused files, unused exports, and unused dependencies. It runs as part of validate so dead code can’t drift in unnoticed.
Why this matters for agent-driven development
Section titled “Why this matters for agent-driven development”Lint turns architecture into a closed loop: code is written, violations surface as parseable errors, the fix is specific (wrong layer, missing schema, env read outside the validator). Review time stays on product logic instead of rediscovering folder rules.
For humans and agents alike, docs explain and lint enforces. Both matter; lint is what keeps the merge gate honest.
Contributing back
Section titled “Contributing back”The plugins are MIT-licensed and developed in the open. If you find a gap, file an issue or PR against the relevant plugin repo (linked above). Rules that prove themselves in production end up promoted to recommended configs.
Related
Section titled “Related”- Why BoringStack; where this fits the broader product thesis.
- lint:meta rules; static repo enforcement rules that run alongside ESLint.
- Scripts & tooling; which script file each merge-gate command runs.
- Agent docs as an index; the docs side of the same loop.
AGENTS.mdis a navigation table overdocs/agents/. - Stack at a glance; the dependency inventory.
- API template overview; backend layers these rules protect.
- Architecture rules; the UI-specific component anatomy.