Documentation

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 → Log explorer
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 nothingDebug Panel → Services
A background job never ranDebug Panel → Cron & queue, Queue

The Debug Panel

The Debug Panel lives at /admin/core/debug. Open it from Debug Panel in your avatar menu, or type "Debug Panel" into the ⌘K admin search. When something needs attention, that menu item shows an amber warning icon - or a red error icon if anything is critical - so you know to look before anyone files a bug.

It works like a settings page: a side menu on the left (a Section drawer on phones) and one section on the right. Every section has its own URL, such as /admin/core/debug/cron, so you can bookmark it or paste it to a teammate.

SectionWhat it shows
OverviewA red alert when your VitNode version has a security update, everything that needs attention (click a row to jump to its section) with Check again, and a Systems list with Clear cache.
VitNode (/debug/vitnode)The running version, the update channel, the last update check and Check for updates. A red panel with the fixed version and release notes when a security update applies.
Plugins (/debug/plugins)Every installed plugin with its version, plus a warning when it needs a security update, runs different API and web versions, has an update, or its update source is unreachable.
Cron & queue (/debug/cron)The scheduler status card and the cron jobs table. See Cron jobs.
Cache (/debug/cache)Whether Redis is connected, unreachable or not configured (in memory), whether realtime works across instances, and Clear cache.
Search (/debug/search)Index status, the engine, recent sync errors and every collection with Reindex. See Search.
Services (/debug/services)Email, file storage, AI models, captcha and WebSocket - each active or not configured. Active ones have a test button, inactive ones a Set up link to the docs.
Log explorer (/debug/logs)Every line anything wrote with c.get('log'), newest first, sortable by type, plugin or date.
Queue (/debug/queue)Background tasks with their status, attempts and last error, filterable by status. See Queue tasks.
AI logs (/debug/ai)Every AI request: action, status, origin, model, cost and points, with filters.

Clear cache marks every cached query in the AdminCP stale and re-runs every matched route loader.

Click any row in Log explorer 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.

Opening the panel needs debug → can_view, and Clear cache needs debug → can_clear_cache. Each section also asks for its own permission and is hidden without it:

SectionPermission
VitNode, Pluginsplugins → can_view
Cron & queuecron → can_view
Cache, Search, Servicessystem → can_view
Queuequeue → can_view
AI logsai → can_view

Hono re-checks every permission on each request, so hiding a section 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}

  <DocumentScripts />
</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 fetching 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