Debugging
The tools to reach for when something is broken — doctor, the error overlay, ctx.log, the Studio, insights, and live logs.
Last updated:
Lunora gives you several places to look when something misbehaves. Working outward from the cheapest, roughly in the order you should try them.
Start with lunora doctor
Most "it doesn't work at all" failures are configuration, not code. doctor is a
read-only preflight that checks the things that silently break a project:
lunora doctorIt reports pass / warn / fail for the SHARD Durable Object binding, D1
database_id values left at the scaffold placeholder, a placeholder
send_email destination, unfilled secrets in .dev.vars, whether
LUNORA_ADMIN_TOKEN is set, container classes declared but not exported by your
worker entry, and @lunora/* packages that have drifted apart in version or mix
release channels.
It exits non-zero on any hard failure, so it also works as a CI gate.
Catch it before it runs
Two commands fail loudly at build time rather than in production:
lunora verify # codegen dry-run + tsc --noEmit; writes nothing
lunora analyze # wrangler dry-deploy: bundle size, heaviest modules, _generated stateverify catches codegen drift and type errors. analyze is how you find out a
dependency doubled your bundle — see Runtime & bundling.
The error overlay
Under the Vite plugin, runtime and codegen failures render in the browser rather
than only in the terminal. Lunora registers solution finders on top of the
overlay, so framework-specific failures come with an actionable fix rather than a
stack trace: a missing or non-literal defineSchema(...), a reserved or
duplicate table name, an invalid .jurisdiction(...), a non-literal unique
flag, a container or workflow class missing from your worker entry, and runtime
unique-constraint and conflict errors.
Every code Lunora throws is documented in the errors reference — start there when you have a code and no idea what it means.
Structured logging with ctx.log
console.log works, but ctx.log is attributed to the function it was called
from, so its output is filterable in the Studio and in log streams:
import { mutation, v } from "@/lunora/_generated/server";
export const send = mutation.input({ text: v.string() }).mutation(async ({ ctx, args: { text } }) => {
ctx.log.info("sending message", { length: text.length });
});Prefer structured fields over interpolated strings — they stay queryable once the log leaves your terminal. For spans and traces around sub-operations, see Observability.
The Studio
lunora dev runs the Studio alongside the worker (or open it any time with
lunora view). It is the fastest way to answer "what is actually in the
database" and "what did that function do":
- Data — browse and edit rows, so you can confirm a mutation wrote what you expected.
- Functions — the discovered function list, with the arguments each takes.
- Logs — recent invocations with their arguments, results, and errors.
- Issues — errors grouped by fingerprint, so ten thousand instances of one bug read as one row.
- Advisors — schema and query lints, including the unindexed reads that turn into slowness later.
See the Studio guide for the full tour.
Find the slow or failing function
lunora insights ranks the live worker's functions three ways — write-conflict
hot-spots, error hot-spots, and latency outliers:
lunora insights
lunora insights --json # pipe into CI or a scriptError hot-spots include the most recent error message, which is usually enough to identify the bug without reproducing it. Write-conflict hot-spots point at OCC contention.
Watch production
lunora logs streams live logs from a deployed worker:
lunora logs
lunora logs --status error
lunora logs --search "checkout" --env staging
lunora logs --format json | jq .Filter by status to see only failures, and by search text to follow one code path. For durable, queryable history rather than a live tail, wire a log sink — see Observability and Logpush.
Debugging authentication
Auth failures are their own category because the identity is resolved before your function runs. Work through it in this order:
- Is an identity present at all? Log
ctx.auth.userIdat the top of the function.undefinedmeans the token never resolved — the problem is in the client or the auth wiring, not your handler. - Is the token reaching the worker? Check the request carries the bearer
token.
AUTH_HEADERS_MISSINGmeans auth is not wired up correctly. - Is it an authorization failure instead?
FORBIDDENmeans the identity resolved and was rejected.RLS_REQUIREDmeans the table is secure by default and no policy resolved for this caller — see RLS. - Does it work with auth off? Temporarily calling the function through an internal path isolates whether the bug is in the policy or the query.
Reset and start clean
When local state has drifted into a shape you no longer trust:
lunora reset # clear local Miniflare state
lunora reset --all # also drop .lunora-cache
lunora seed # repopulate with deterministic fake datalunora seed --seed 7 always produces the same rows, so a bug reproduced on a
seed is reproducible for everyone else too.
Reproduce it in a test
Once you can see the bug, pin it. The lunoraTest in-memory harness runs
functions without a worker, so a failing case becomes a fast regression test —
see Testing.