launch-quickly
Patterns

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-disable with 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 → todo this instead
route → dbcall a feature query; the feature owns the transaction
route → feature internalsimport the barrel: @/features/x, not @/features/x/queries
feature → another feature's internalsimport that feature's barrel
ui → featurekeep ui generic; lq upgrade regenerates those files
component → dbmove the read into a feature query and pass data in as props
block → dbmarketing sections render, they do not query
proxy → anything but envthe 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.

On this page