[ The sdxc command ]
Check a project
Lint a project for release drift, missing secrets, stale Worker types, migrations a new database cannot replay, layout and wiring mistakes, and the rules you add yourself, with sdxc doctor.
Last updated 2026-10-19
sdxc doctor
sdxc doctor runs every check against the project and prints one line per check, each marked
pass, warn, fail or skip, with the exact command or edit that fixes what it found:
pass pins Every @sdxc package is at release 2026.10.17
fail installed node_modules differs from package.json: @sdxc/cli 2026.9.4 is installed, package.json pins 2026.10.17. Fix: bun install
pass remix remix 3.0.0 is the peer release 2026.10.17 declares
pass env .env.example lists every secret cloudflare.config.ts declares
warn typegen The Worker's env types were never generated; .cloudflare/types/index.d.ts is missing. Fix: sdxc run typegen
skip migrations The project has no migrations
pass layout app/ holds only the layout's folders and nothing imports bootstrap/
warn routes Found 1 route no router maps: GET /pricing (routes/web.ts pricing) is mapped by no router. Fix: Map each route in its router initializer as router.map(route, lazy(() => import(…))); sdxc make controller writes and maps one
pass models app/models/index.ts registers every model in app/models/
skip ui No @sdxc/ui component is copied
A fail is something that breaks an install, a build, CI or a new database. A warn is a
convention the project strays from. A skip is a check with nothing to look at, such as a
project with no migrations, or one that could not reach the npm registry.
doctor changes nothing itself, and exits 1 when any check fails, so it fits a CI step or a
pre-commit hook. Warnings leave the exit code at 0.
The checks
They run in this order:
| Check | Fails or warns when | Fix |
|---|---|---|
pins | fail: an @sdxc package is behind its version at the project's release; warn: one is pinned to no release | sdxc upgrade <release> |
installed | fail: node_modules holds an @sdxc package or remix at another version than package.json pins, or lacks one | the install command |
remix | fail: remix differs from the version the project's release declares | sdxc upgrade <release> |
env | fail: .env.example leaves out a secret cloudflare.config.ts declares; warn: it lists a key no binding declares | the line to add, or the declaration |
typegen | warn: .cloudflare/types/index.d.ts is missing, or older than cloudflare.config.ts, .dev.vars or .env.example | sdxc run typegen |
migrations | fail: a make.migrations chain does not replay on an empty database with double-quoted strings off; warn: an accepted error no longer occurs | the migration to correct, or the entry |
layout | warn: a folder in app/ the layout does not have, or an import of bootstrap/ from app/, config/, database/, resources/ or routes/ | the file to move |
routes | warn: a route no router maps, or a controller or action that loads with the router | the mapping to write |
models | warn: a file in app/models/ calls createModel and app/models/index.ts does not register it | the registration to add |
ui | warn: a copied component is behind the installed release; edited copies are listed for review | sdxc ui update <names>, sdxc ui diff |
The project's release is the newest date among its @sdxc pins, so pins names each package
that lags behind the others. pins and remix read the npm registry; without it they report
skip and every other check still runs.
env exists because CI builds .dev.vars from .env.example before it generates the Worker's
types: a secret only your local .dev.vars holds types fine on your machine and fails there.
migrations replays each chain the way a new database or a new tenant gets it, with SQLite's
double-quoted string literals off, so a "value" meant as a string, or a rebuild that reads a
column by its new name, fails here instead of in production.
routes judges each mapping by what loads with the router. A controller mapped through
lazy() loads on the first request that reaches it. A controller with several actions passes
when each action sits behind lazy() or is an inline handler that loads its module with a
dynamic import(); an action that imports its code up front is named by its key path, as in
app/initializers/router.ts maps routes.board action edit without lazy().
Accept a migration that already ran
A migration production applied long ago can fail on an empty database, and rewriting it changes
nothing for the databases that ran it. List it under doctor.migrations.accept in
.config/sdxc.jsonc, with the file and the column its error names:
{
"doctor": {
"migrations": {
"accept": [{ "file": "0007_rename_slug.sql", "column": "slug" }],
},
},
}
The replay steps past that error alone, so a different error in the same file still fails, and
checks the rest of the chain. In a file split by Drizzle's --> statement-breakpoint, it skips
only the statement that raised the error and runs the ones after it; any other file is one step.
A passing line counts the accepted errors and lists each as a finding, and an entry whose error
no longer occurs warns, so the list shrinks once the migration is fixed.
Run some of them
sdxc doctor --only pins,remix
sdxc doctor --skip ui --skip typegen
--only and --skip take check names, repeated or comma-separated, your own checks included.
A name doctor does not have stops it with the list it has:
No check is named nope; sdxc doctor runs pins, installed, remix, env, typegen, migrations, layout, routes, models, ui
As JSON
--json prints the same report as one JSON document for an agent: ok is false when any
check failed, and each check carries its findings one per entry, and its fix:
{
"ok": true,
"checks": [
{
"name": "routes",
"status": "warn",
"message": "Found 1 route no router maps",
"findings": ["GET /pricing (routes/web.ts pricing) is mapped by no router"],
"fix": "Map each route in its router initializer as router.map(route, lazy(() => import(…))); sdxc make controller writes and maps one"
}
]
}
Add your own checks
A rule that belongs to your project, and to no other, is a module in .config/sdxc/checks/,
the doctor.checks key of your configuration. Each module default-exports one check, or an
array of them, made with defineCheck from @sdxc/kit/doctor:
import { defineCheck } from "@sdxc/kit/doctor";
export default defineCheck({
name: "no-env-in-views",
async run(ctx) {
let found = [];
for (let file of await ctx.files("resources")) {
if ((await ctx.readFile(file))?.includes("cloudflare:workers"))
found.push(file);
}
if (found.length === 0)
return { status: "pass", message: "Views read no bindings" };
return {
status: "warn",
message: "Views import env",
findings: found,
fix: "Pass the value in from the controller",
};
},
});
run returns { status, message, findings?, fix? }, with status one of pass, warn,
fail or skip, and the line prints like a built-in one. Its context reads the project and
changes nothing:
| Field | What it holds |
|---|---|
root | the project's root folder |
manifest | the parsed package.json |
config | the sdxc configuration, defaults filled in |
routes | every route, as sdxc routes --json lists them |
readFile | a file's text, or null when it is missing |
exists | whether a path exists |
files | every file under a folder, leaving out node_modules and dot-files |
Every path these take stays inside the project. A check's name is lowercase words joined by dashes, unique among your checks, and none of the built-in names. Checks run with the project's runtime; on Node, a check written in TypeScript needs Node 22.18 or newer.
A check that fails to load, throws, returns something other than a result, shares a name, or
runs longer than 30 seconds is reported as fail under its name, and every other check still
runs.
In a workflow
doctor is the step after anything that moves the project:
sdxc upgrade
sdxc doctor
A pins or remix failure names the sdxc upgrade <release> that fixes it,
sdxc upgrade catches up a lagging package even when no newer release is
out, and a ui warning names the copies sdxc ui update brings current. In CI, run it before
sdxc deploy so a missing secret or a migration a new database cannot
replay stops the pipeline before production.
Where to go next
Upgrade — move every package to a newer release.
List routes —
sdxc routes --unusedlists what theroutescheck warns about, with the controller files nothing loads.Install and pin — why every package in a project shares one release date.