---
title: "node ace adonia:doctor"
description: "Diagnose installer artifacts, runtime wiring, packages, storage, and queues."
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# node ace adonia:doctor

The doctor command diagnoses installer and runtime configuration and aggregates boot-time warnings.
The implementation lives in `packages/devtools/src/doctor/`.

```
node ace adonia:doctor
node ace adonia:doctor --only=shield-xsrf --only=cookie-domain
```

The command **never boots the application**. Every finding is derived from the file system and
from static analysis (ts-morph) of the app's own TypeScript — which is the point: most of what it
diagnoses is a reason the app fails to boot, or boots wrong and silent.

Exit code is `1` when any check **fails**, `0` otherwise. Warnings never fail CI: a `warn` means
"suspicious, or undecidable statically" (a computed `cookie.domain`, a missing config file), and
a doctor that fails the build on "I could not tell" is a doctor people disable.

## Checks

| id | Fails when | Spec |
| --- | --- | --- |
| `init-hook` | `hooks.init` in `adonisrc.ts` has no `indexAdoniaResources()` (warns when it runs before `indexEntities()`) | §3.1, §14 |
| `stale-codegen` | `.adonisjs/adonia/*` is missing, older than the newest `app/adonia/**` file, or byte-different from a fresh generation | §14 |
| `panel-access-middleware` | `start/kernel.ts`'s `router.named({ … })` lacks `adonia.panel-access` | §3.7, §5.2 |
| `cookie-domain` | a domain-mounted panel's host is not covered by `config/session.ts`'s `cookie.domain` | §5.2, §19 |
| `vite-allowed-hosts` | `vite.config.ts`'s `server.allowedHosts` misses a panel domain | §5.2 |
| `inertia-layout` | `inertia/app.tsx` wraps every resolved page — `adonia/*` included — in the app's own layout | §13.4 |
| `shield-xsrf` | Shield's `csrf.enabled` or `csrf.enableXsrfCookie` is off | §19 |
| `authorization-fallback` | `authorization.fallback` resolves to `'allow'` in production | §4, §12, §20 |
| `unscoped-resources` | tenancy is configured but a reachable resource declares no `static tenantScope` | §5.3 |
| `action-events-table` | action events are enabled but no migration creates `adonia_action_events` | §15.1 |

### Notes on the sharper ones

**`stale-codegen`** compares timestamps first (cheap), then re-runs the §14 pipeline in memory and
diffs the result against what is committed. The regeneration evaluates resource modules in the
codegen worker — the same isolated evaluation the init hook performs, not an app boot. A byte
difference is always real: §14 guarantees deterministic output.

**`cookie-domain`** requires the *leading dot* when the cookie is scoped to a parent domain. A
panel on `admin.acme.com` with `cookie.domain: 'acme.com'` fails; `'.acme.com'` passes. No
`domain` at all is fine — a host-only cookie is returned to whatever host served it, including
every tenant host of a `:tenant.acme.com` pattern.

**`inertia-layout`** exists because the symptom has no error attached to it: packaged pages are
self-chroming (they render `PanelShell` with the panel's brand, nav, breadcrumbs and toaster), so
an app layout on top of them shows the site header around the panel and flashes every message
twice — once as the app's toast, once as Adonia's. The installer applies the `withAdoniaLayout`
guard on a fresh app (§3 step 6); this check covers apps installed before it, apps whose entry the
installer could not recognize, and apps that grew a layout afterwards. An entry shape the analysis
cannot model warns rather than fails, and always prints the exact edit.

**`authorization-fallback`** understands both `fallback: 'allow'` written literally and
`fallback: env.get('KEY', …)` resolved against `.env.production` / `.env.production.local`.

**`unscoped-resources`** is a static preview of the boot error `E_ADONIA_UNSCOPED_RESOURCE`
(§5.3 fails closed). Tenancy is detected by a panel builder calling `.tenant()`/`.tenantParam()`;
the manifest's panel → resource mapping narrows the blame to resources that tenant panel
registers, falling back to every resource module when the manifest cannot answer.

## Adding a check

One module per check under `src/doctor/`, exporting a `DoctorCheck`:

```ts
import type { DoctorCheck } from '@adonia/devtools/doctor'

export const myCheck: DoctorCheck = {
  id: 'my-check',
  title: 'Human-readable heading',
  async run(context) {
const config = await context.parse('config/adonia.ts')
return config ? { status: 'ok', message: '…' } : { status: 'warn', message: '…', fix: '…' }
  },
}
```

Then add it to `DOCTOR_CHECKS` in `src/doctor/registry.ts`, and add a fixture app state under
`tests/fixtures/doctor/<check-id>/{pass,fail}/` holding **only** the files the check reads —
which is also the proof it reads nothing else. A check that throws is reported as a failing
finding naming itself; it never crashes the command.

`DoctorContext` gives you the app root, the parsed `adonisrc.ts`, the §14 manifest (plus
`manifestError` when the file is present but unusable), and memoized `read`/`parse`/`list`/
`newest`/`mtime` accessors. Static-analysis helpers for config literals — `defaultExportObject`,
`propertyAt`, `stringValue`, `booleanValue`, `arrayElements`, `envReference` — live in
`src/doctor/source.ts` and return `undefined` for anything computed, which the caller reports as
a `warn` rather than a guess.

Source: https://adonia.pages.dev/doctor/index.mdx
