Fixing lq check
What each lq check failure means and the recipe that fixes it — every custom lint rule, the boundaries graph, the structure checks, and the type errors that encode conventions. Use whenever lq check fails and the fix is not immediately obvious.
Generated from
.claude/skills/fixing-lq-check/SKILL.md, which ships in every project created from this template. Your agent loads it on demand; this page is the same text. Edit the skill, not this page.
lq check runs four steps: Types, Conventions (eslint), Migrations
(drizzle-kit), Structure (cross-file invariants). lq check --changed scopes
the first two to the diff and always runs Structure.
Read the message before changing anything. Each rule exists because its violation is otherwise silent, so "make the error go away" and "fix the problem" are often different edits.
Three things that are never the fix:
- an
eslint-disablewith no--reason any,!, or a cast to clear a type error- widening a rule so your code fits
If a rule seems genuinely wrong for a legitimate case, say so and propose the
narrower rule — do not disable it silently. There is one shipped exception in the
codebase (src/features/waitlist/actions.ts) and it carries a written reason.
Conventions — the custom rules
@launchquickly/no-unscoped-db
Do not import the raw database client here…
You imported @/db/client, or withSystem outside its allowlist.
Fix: use withTenant(ctx, tx => …) from @/db/tenant. That is what makes the
row-level security policies apply. withSystem bypasses RLS entirely and is
allowed only in src/jobs/**, src/app/api/webhooks/**, src/lib/auth/**,
scripts/** and test fixtures — and only when the work is genuinely cross-tenant.
Type-only imports are fine (import type { Transaction }) — they carry no runtime
access.
@launchquickly/require-action-client
…is exported from a 'use server' file. Only actions built with the action client may be exported
Every Server Action is a public POST endpoint. A bare exported function is an unauthenticated, unvalidated, unthrottled endpoint.
Fix: build it with the client.
export const doThing = action.input(schema).authed().mutation(async ({ input, ctx }) => …);If it is a helper rather than an endpoint, move it to a module without
'use server'. See the writing-server-actions skill.
@launchquickly/no-client-in-route-entry
'page.tsx' is a route entry and must stay a Server Component
Marking a route entry 'use client' moves its whole subtree to the client and
takes requireTenant and every server-side read with it.
Fix: keep the page a Server Component and move the interactive part into a leaf
component that is 'use client'. error.tsx is exempt — Next requires it to be a
client component.
@launchquickly/no-role-string-compare
Do not compare a role to '…'
Fix: can(ctx, 'thing.action'), or requireRole(ctx, 'admin'), and declare the
grant in src/config/permissions.ts. Scattered string comparisons mean adding a
role becomes a hunt through call sites, and the ones you miss fail silently.
@launchquickly/no-raw-throw-across-boundary
Throw an AppError instead of
new Error
Only fires in an actions.ts. Anything unrecognised becomes INTERNAL, whose
message is withheld, so the user sees "Something went wrong" and the real reason
lives only in the logs.
Fix: notFound(), forbidden(), conflict(), validation() from
@/lib/errors.
A bare new Error for a true invariant violation in mutations.ts ("insert
returned no row") is fine and deliberately not flagged — that one should be
opaque.
Conventions — the layering graph
boundaries/dependencies
There is no policy allowing dependencies from elements of type "X" to elements of type "Y"
The architecture denies every edge until it is named, so this means the import crosses a layer it should not.
| from → to | do this instead |
|---|---|
| route → db | call a feature query; the feature owns the transaction |
| route → feature internals | import the barrel: @/features/x, not @/features/x/queries |
| feature → another feature's internals | import that feature's barrel |
| ui → feature | keep ui generic; lq upgrade regenerates those files |
| component → db | move the read into a feature query and pass data in as props |
| block → db | marketing sections render, they do not query |
| proxy → anything but env | the proxy is redirect UX, not an authorization boundary |
Do not widen the graph to make an import legal. If an edge is genuinely missing from the architecture, say which one and why.
boundaries/no-unknown-files
File does not match any file pattern and does not belong to any known element
A new top-level directory under src/ that nobody has placed in the
architecture.
Fix: put the file in an existing layer. If a genuinely new layer is warranted,
that is a deliberate architecture change — add it to
packages/eslint-config/boundaries.js and say why.
no-restricted-imports
Covers what the graph cannot: it also sees type-only imports. Most commonly a
banned state library, a deep feature import from a route, or the proxy importing
@/db / @/lib/auth.
Structure — the cross-file invariants
These hold between files, so the file that changed is often not the file at fault.
tenant-table-needs-rls-policy
The most serious finding in the suite. A tenant-scoped table with no applied policy is readable across every organization.
Fix: lq generate feature <name> --fields "…" emits the table, its policy and
an isolation test in one migration.
If the migration exists but this still fires, it is not in
drizzle/meta/_journal.json and therefore never runs. db:migrate will report
success anyway. This check reads the journal, not the directory, because that bug
has shipped before.
feature-needs-barrel
Fix: add src/features/<name>/index.ts re-exporting the public surface.
Without it every consumer reaches into internals and the upgrade codemods can no
longer rewrite the feature safely.
feature-needs-tests
A feature with actions.ts or mutations.ts and no test at all — so its
authorization check has no evidence.
Fix: lq generate feature <name> ships a tenant-isolation test, or write one.
See the testing skill.
app-dir-is-routing-only
A non-route file in src/app.
Fix: move it to src/features/<name>/components/ or src/components/ and
import it from the route. Keeping src/app pure is what lets team mode relocate
every route under [orgSlug].
suspending-segment-has-no-loading (advisory)
Advisory — it will not fail the build. A page awaits data with no loading.tsx
above it, so navigation blocks instead of streaming a skeleton.
Fix: add loading.tsx rendering the feature's skeleton component.
feature-is-getting-large (advisory)
Past ~25 files a directory is usually two features. Split it, or say why it belongs together.
Types
Type errors here are often convention errors wearing a compiler message.
Property 'mutation' does not exist on type 'InputStage<…>'
You skipped the auth mode. The builder hides .mutation() until .input() and
.authed() / .public() have been called, and .public() additionally requires
.rateLimit().
Fix: add .authed(), or .public().rateLimit('bucket') if it must work for
anonymous callers.
Anything about TenantContext not being assignable
It is branded and can only be minted by createTenantContext. You are trying to
build one by hand.
Fix: get it from requireTenant(). In a test, unsafeTestContext exists and
is named that way for a reason.
Merge<FieldError, FieldErrorsImpl<…>> in a form
react-hook-form widens error types for nullable and transformed fields.
Fix: type the prop structurally as { message?: React.ReactNode }, the way
<Field> does. Do not import FieldError and do not cast.
exactOptionalPropertyTypes complaints
An absent property and an explicitly undefined one are different types here.
Fix: build objects with conditional spreads — ...(cond ? { k: v } : {}), not
k: cond ? v : undefined.
Migrations
drizzle-kit check reports journal and snapshot inconsistency.
Fix: run pnpm db:generate for a missing migration. Never hand-edit a
committed one — add a new one. A tenant table also needs its RLS policy in the
same migration.
A step said "unavailable"
The tool is not installed, so that convention went unverified. It is not a pass.
Fix: install dependencies, then re-run.
--changed said it skipped things
A skipped check has not passed. Run lq check with no flag before claiming the
task is done.
Testing
How to test in this project — the unit/integration/e2e split, the tenant-isolation test every feature needs, the signed-in Playwright fixture, and how to tell a real pass from a test that never checked anything. Use when adding tests or when a test passes and you are not sure it should have.
The software factory
The pipeline from a brief to a merged pull request — the label states, which command runs at each one, and when to stay in the driver's seat instead. Use when planning work or wondering how a ticket should move.