Troubleshooting
Real symptoms from real builds — organised by what you actually see, not by which subsystem is at fault. Every entry here cost someone time.
Nothing on this page is hypothetical. Every entry came out of a friction log kept while building a product on this template, and each one cost somebody between ten minutes and an hour.
They are grouped by what you see, because that is what you have when you arrive. The subsystem at fault is usually not the one you are looking at.
Everything passes and the feature is broken
The most expensive category by a wide margin, and the reason this template pushes so hard on control-testing your own tests.
A test passes that could not have failed
Three real examples, all green, all worthless:
- A concurrency test that never interleaved. "I wrote 'fire two transitions
at once, assert one loses', it went green, and it was worthless: the two
transactions ran in sequence rather than interleaving, so it never exercised
SELECT … FOR UPDATEat all." The fix is to hold the lock in a competing transaction. - A negative test that passed for the wrong reason. zod 4's
z.uuid()validates the RFC 9562 version and variant nibbles, so00000000-0000-0000-0000-000000000001— the placeholder everyone reaches for — is invalid. A test asserting "this bad status is rejected" passed because the id beside it was rejected first. - An e2e that posted one comment. The bug appeared from the second onward. See "the button says Posting… forever" below.
What to do: after writing an assertion, reintroduce the defect it is supposed to catch and watch it go red. A test that has never failed has not been tested.
The form submits the wrong number
useActionForm submits the schema's output — handleSubmit hands the
resolver's parsed values to the action — and the action then re-parses that with
the same schema. So every schema must be idempotent under a double parse.
The shipped examples all happen to be ('' → null short-circuits .nullable();
z.coerce.number() is idempotent on a number), which makes the constraint
invisible until you write the first transform that is not one.
A dollars-to-cents
.transform()oncreateInvoiceSchemawould have filed every $12.50 invoice for $1,250, with types, lint,lq checkand a happy-path e2e all green.
What to do: keep unit conversion in the form component, not the schema. If a transform must live in the schema, pin idempotency with a test.
The button says "Posting…" forever, but the row is in the database
useActionForm runs the action inside a transition and calls onSuccess
inside it too. A router.refresh() there joins the transition that owns
pending, so pending never clears.
The first submit of a session works. From the second onward the button hangs and the new row never appears — though it committed fine.
Nothing catches this: lq check green, 252 unit and integration tests green, and
an e2e that submits once is green too.
What to do: move the refresh into an effect rather than onSuccess. And if
the list you are refreshing sits inside its own Suspense boundary, the refresh
re-suspends it — render it inline instead.
A date silently clears when you edit a record
<input type="date"> accepts only 'YYYY-MM-DD'. Handed a Date, the browser
rejects the value and renders the field empty, so opening a record and saving
blanks the date.
Both lq check and the generated tests passed throughout. See
ADR-0009 — the column must be
date(..., { mode: 'string' }), and src/lib/format.ts keeps the string intact
end to end.
The entire e2e suite fails and you changed nothing
A mass failure spanning unrelated specs is almost never your change. A real regression is narrow. Four causes, in the order they are worth checking:
1. A sibling project stole your port
Every project generated from this template defaults to E2E_PORT=3100 with
reuseExistingServer on. Two projects on one machine means one suite silently
attaches to the other product's server:
55 tests failed against someone else's app — with plausible errors like "heading 'Assistant' not found" that look exactly like my own bugs.
Note the process is named next-server, so pkill -f "next start" does not
match it.
2. You ran the suite twice in quick succession
The second run attaches to the first one's server while it is still shutting down. The port answers, so Playwright proceeds — then 35 to 40 of 49 tests fail at once across specs you never touched.
3. You killed the server with kill -9
That leaves the port held. Playwright sees nothing listening, starts its own, and
next start dies with EADDRINUSE — after which every test hits a dead server
and times out.
I saw 16 failures spread across specs I had never touched and spent real time hunting for a regression in permissions, connection pooling and cold-start timing before reading far enough up the log to find the actual line.
The signal — ⨯ Failed to start server — is buried in hundreds of lines of
expected UNAUTHORIZED noise from the signed-out smoke tests. Scroll up.
4. A helper that submits without waiting
An e2e helper that submits a form and returns lets the next page.goto() abort
the in-flight write. It passes on an idle machine and fails the moment the box is
loaded. The generated spec's create helper has exactly this shape.
Related: clicking a status change and immediately navigating races the action.
Playwright auto-waits for elements, not for a startTransition plus
router.refresh() to settle. Assert the next state before navigating.
Before believing any of it: check the port is free (lsof -ti:3100), kill
gracefully, and run again.
lq check passes but next build fails
Both known causes are the same shape: a server-only module reaching the client
graph, which nothing but a full build reports.
A client component imported another feature's barrel
The barrel re-exports queries.ts, which is server-only and imports the db
client — so the whole postgres driver gets pulled into the browser bundle.
The rule "import features only through the barrel" and the rule "barrels re-export server-only queries" are in direct tension, and the only thing that reports it is a full production build.
What to do: move the shared pure helper somewhere neither server-only nor
feature-private — src/lib/format.ts is usually the right home.
A client component imported policy.ts
Same failure, narrower cause. A slice's policy.ts is server-only, so a
status→label map that lives beside the transition rules cannot be read by the
buttons that render them. Put labels in the feature's badge module instead.
The page is stale after a mutation
revalidate is a typed registry precisely so a stale surface is a type error
rather than a silent bug — revalidatePath('/projcets') is a typo the compiler
shrugs at and the UI punishes.
If the surface you need is not on it, add it. The shipped dashboard was
itself missing revalidate.dashboard() and kept showing pre-mutation counts. The
registry did its job the moment someone tried to use it (.dashboard did not
typecheck); the gap was that nothing had.
The data is there but the page shows nothing
You are on the free plan
Reading the audit log is gated on a paid plan, separately from recording, which
happens on every plan. An e2e that moves a record and opens /settings/audit
finds nothing — against a completely working audit trail.
e2e/audit.spec.ts seeds a subscription row for this reason. Any new test
asserting on that page has to do the same.
A query returned zero rows
That is usually row-level security working. A request-path query runs as a role
that cannot bypass it, so a query reaching the wrong tenant returns nothing
rather than erroring — see ADR-0002. Check
the TenantContext you passed before suspecting the query.
After an upgrade
lq check crashes instead of linting
TypeError: Key "rules": Key "@launchquickly/no-ad-hoc-formatting":
Could not find "no-ad-hoc-formatting" in plugin "@launchquickly".The delivered lint config references rules that ship in a newer plugin than the one installed. Fixed for projects on 0.3.4 and later, where vendored tarballs carry the template version so the specifier changes and pnpm re-extracts.
On an older project: regenerate the lockfile and force the install. Neither alone is enough — the lockfile pins an integrity hash and pnpm's store keeps the old extraction.
Files were withheld
The upgrade refuses to deliver a file the project could not compile, and names what is missing. Two shapes:
needs @/components/ui/select— a primitive you do not have. Install it.needs @/lib/format (formatNumber, formatEnum)— the module exists but your copy does not export those. Add them; your own helpers are untouched.
Anything still withheld after that is a layer this project does not use, which is a fine outcome — nothing was written, so the project still builds.
Files from an earlier upgrade do not compile
Reported separately, because there is nothing to withhold — they are already on disk. An older planner wrote them before the check existed. Add the missing exports, or delete the files.
Generator output that surprises people
- The isolation test can fail on the first run for a
ref(members)field: the fixture seeded a random uuid, which violates the composite foreign key the same generator emitted. Seed the org's own member id — do not delete the field from the fixture. loading.tsxcascades into child routes. The list skeleton renders for the detail page too, so opening one record flashes a five-row table and reflows. Add a detail skeleton.- A
references()on a tenant table is not enough. Postgres runs foreign-key checks as the system, so RLS does not apply to them — a plain reference lets one organization point at another's row. Use a composite FK against(organization_id, id).
Small things that waste ten minutes
pnpm db:psql -- -c "select 1"ignores the-c. The extra arguments land after thesh -cstring rather than inside it. Read the URL out of.env.localand callpsqldirectly for one-shot queries.- The post-edit hook reports the same in-progress errors on every edit. A refactor touching six files reports the same five type errors six times. Useful at the end of a change, noisy in the middle of one.
- Deleting the example slice touches more than the listed files — the
revalidate registry, the schema barrel, the
projectslimit insrc/config/billing.tsrendered in the plan picker, the sample permissions in the action-client tests, the protected-route list ine2e/smoke.spec.ts, and the projects cleanup in the e2e session fixture. Most is caught bylq check; the e2e and billing ones are not. e2e/projects.spec.tsis not only example code. About half of it tests the list convention — pagination, sorting, filter chips, the field-error path, the soft-404. Deleting the demo deletes the only end-to-end coverage those have. Port it into your own spec.