Debugging

Debugging

Find out what a VitNode app is actually doing - the AdminCP Debug Panel, the system log, the Router and Query devtools, and React Scan.

Four surfaces, each answering a different question. Two of them exist only in development - the devtools and React Scan. The other two, the AdminCP Debug Panel and the server's own output, are how you see inside a production deployment, which is exactly when you need to.

Where to look first

SymptomLook here
A request 500s in productionDebug Panel → System Logs
A screen shows data that has moved onQuery devtools → find the key
A route loaded the wrong thingRouter devtools → loader data
A page feels slow to type in or scrollReact Scan → count the re-renders
A service looks configured but does nothingCore → System → Integrations in the AdminCP
A background job never ranDebug Panel → Queue Tasks

The Debug Panel

/admin/core/debug, and the quickest way in is the avatar menu in the top-right of the AdminCP: Debug Panel. It is also the one AdminCP screen with no sidebar entry, so that menu (or the admin search) is how you get there.

Three sections, top to bottom:

SectionWhat it shows
Clear CacheA button, top-right. Marks every cached query stale and re-runs every matched route loader.
Queue TasksFour counters - pending, processing, completed, failed - and a table of what is pending or processing right now, with its attempt count and when it is next available.
System LogsEvery line anything wrote with c.get('log'), newest first, sortable by type, plugin or date.

Click any log row for the full record: the log id, the request method and path, the status code, the IP address, the user agent, the user who caused it (linked straight to their AdminCP profile) and the untruncated message. That detail dialog is usually the whole debugging session.

The screen is gated on the debug staff module - can_view to open it, can_clear_cache for the button - and Hono re-checks that on every request, so hiding the menu entry is a courtesy rather than the boundary. See staff permissions.

In production, the log is the only copy of the error

An unhandled error in a route is caught by the API's error handler, written to the system log as Unhandled error: …, and answered to the browser as the string Internal Server Error with no detail at all. In development the same handler returns the real message in the response body instead. So a 500 you cannot reproduce locally is a 500 you read in the Debug Panel.

Devtools in development

The scaffold mounts a floating devtools button in the bottom-right corner of every page, with two panels behind it. It comes from two places, and both are in a generated app already:

vite.config.ts
import { devtools } from '@tanstack/devtools-vite'

plugins: [
  devtools(), 
]
src/routes/__root.tsx
<body suppressHydrationWarning>
  {children}

  {import.meta.env.DEV ? ( 
    <TanStackDevtools
      config={{ position: 'bottom-right' }}
      plugins={[
        { name: 'TanStack Router', render: <TanStackRouterDevtoolsPanel /> },
        { name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
      ]}
    />
  ) : null}

  <Scripts />
</body>

That import.meta.env.DEV is why nothing ships to production: the whole block is compiled out of a vite build.

PanelReach for it when
TanStack RouterYou want the matches for the current URL, each route's loader data, and what its head resolved to. The answer to "why is this page rendering that".
TanStack QueryA screen is showing stale data. Find the key, then find the mutation that should have invalidated it.

Navigate around with the Query panel open and the shape of data loading becomes obvious: a route's loader warms its entries before React renders, and the screen reads the same ones back.

Server-side requests are not in the panel

A loader that runs during SSR runs in the Node process, so its output lands in the terminal running vite dev - not in a browser panel. The Query panel shows the entry once it is dehydrated into the page.

React Scan

React Scan draws an outline around every component as it re-renders, which turns "this page feels heavy" into a number. It is off by default; flip debug in the shared config:

src/vitnode.config.ts
export const vitNodeConfig = buildConfig({
  debug: true, 
  i18n: {
    defaultLocale: 'en',
    locales: [{ code: 'en', name: 'English' }],
  },
  metadata: {
    shortTitle: 'VitNode',
    title: 'VitNode',
  },
  plugins: [],
  theme: {
    defaultTheme: 'system',
  },
})

The provider loads react-scan lazily and only when debug is on and NODE_ENV is development, so leaving debug: true committed cannot leak the overlay into production. It is still worth turning back off - the outlines are distracting once you have your answer.

Once you have found the culprit, Performance covers what to do about it.

Where output goes

DestinationWhat arrives there
The core_logs tableEvery c.get('log') call, with the request that made it. Read it in the Debug Panel.
The server's stdoutThe same lines, colour-coded and prefixed [VitNode] Error (@your/plugin):.
The browser consoleClient-side errors, and React Scan's own output.
The terminalVite's dev-server output, plus anything a loader or server function logged during SSR.

There is no request logger, and no log-level switch: every level is written every time, to both the table and the console. If you want less noise, log less. Logging is the whole surface.

Gotchas

Every log line is a database INSERT, and nothing prunes them

c.get('log') writes a row per call and no cron job deletes any of them, so a debug line in a hot request path grows the table for as long as the installation runs. Log events, not traffic.

Behind a proxy, every log line says the same IP

The IP on a log row - and the rate-limit bucket - is the address of the socket VitNode is talking to, because X-Forwarded-For is a header the caller writes and nobody should get to pick their own audit trail. Behind a reverse proxy that socket is the proxy, so every visitor looks like the same one. Put the per-visitor rate limit in the proxy itself, where the address is real.

Clear Cache clears the front end, not Redis

The button invalidates the TanStack Query cache and the router's loaders, which is the front end's memory of the API. The API's own Redis-backed cache is expired by the mutations that change the data, not from this screen.

Next