Diagnoses PhoenixKit installation, migration, and runtime issues.
Runs a comprehensive suite of checks covering database connectivity, pool configuration, PgBouncer detection, migration state, lock conflicts, and application configuration. Prints a clear pass/fail report with actionable remediation steps.
Usage
$ mix phoenix_kit.doctor
$ mix phoenix_kit.doctor --prefix=auth
$ mix phoenix_kit.doctor --exit-code
Options
--prefix- Database schema prefix. When omitted, resolves fromconfig :phoenix_kit, :prefix, then"public"— the same resolutionmix phoenix_kit.update/--statususe, so a prefixed install is diagnosed against the schema it actually lives in.--exit-code- Exit non-zero when any check FAILED. Without it this task prints "N failures" and still exits 0, so a deploy script that runs it cannot act on the result — the same silent successmix phoenix_kit.status --exit-codeexists to remove. Warnings never fail the run; they are advisory by construction and several fire on healthy installs. Off by default so deploys that run this purely for its report keep passing.
Checks Performed
- Repo Detection — Can we find and start the Ecto repo?
- DB Connectivity — Can we execute a simple query?
- Pool Configuration — Pool size, checkout timeout, queue settings
- PgBouncer Detection — Is PgBouncer between app and PostgreSQL?
- Migration State — PhoenixKit version (COMMENT), schema_migrations alignment
- Module Schema Versions — Modules owning their own chain, vs what their code expects
- Schema Drift — Columns a migration should have added but the DB lacks
- Pending Migrations — Migration files not yet recorded in schema_migrations
- UUID Column Types — Detects varchar uuid columns that crash Ecto on startup
- UUID Primary Keys — Detects primary keys that are not the expected uuid type
- NULL UUIDs in FK Sources — Detects NULL uuids that cause infinite backfill loops
- Orphaned FK References — Detects orphaned rows behind an existing FK
constraint, whether it is already VALID (a trigger-bypassed write, a
bulk load, direct catalog surgery) or still NOT VALID (would block its
own VALIDATE), and existing constraints still sitting NOT VALID with
nothing currently blocking them — V176 validates those in place; this
just tells you before it does. Two boundaries on what "checked" means
here: discovery reads
pg_constraint, so a relationship with no FK constraint declared at all is outside this check's scope and is not examined; and discovery matches both the owning table and the referenced table to the schema being checked (--prefix), so a FK whose referenced table lives in a different schema is outside scope too, even though the owning table itself was checked - Lock Conflicts — Any blocked or long-running queries?
- Orphaned Connections — Idle-in-transaction or stuck connections
- Oban Configuration — Queues and plugins that consume pool connections
- Oban Cron Queues — Does every crontab worker have its queue configured?
- PhoenixKit Supervisor — What's running (update_mode vs full)?
- Child Start Order — Does the Repo start before PhoenixKit/Oban in application.ex?
- Update Mode — Is update_mode active?
- daisyUI Version — Is the host's vendored daisyUI recent enough?
- User Dashboard (deprecated) — Is the host still on the retired dashboard?
- Sitemap Discoverability — Is the sitemap actually reachable?
- Crawler Visibility — noindex on a production-looking host, or a staging-looking host left indexable
- Demo Auth Pages — Are the demo auth routes still exposed?
- Manifest Repair (dry-run) —
PhoenixKit.Migrations.Repair.verify/1runs read-only against the generatedPhoenixKit.Migrations.ExpectedSchemamanifest as an additional, non-fatal check (never:fail). Passes and says so if the manifest has been removed or overridden away in this checkout. - Git Hooks — is
.githooks/pre-commitenabled viacore.hooksPath? Only runs inside a checkout of phoenix_kit itself (.githooks/pre-commitis a phoenix_kit-repo convention, not something installed into a consuming host app) — silently skipped otherwise.
Summary
Functions
Reports crontab entries whose queue this node does not run.
The process exit status --exit-code should produce: 1 when any check
failed, 0 otherwise.
The "Git Hooks" verdict, as a pure function of what could actually be observed.
Functions
Reports crontab entries whose queue this node does not run.
Public so it can be unit-tested directly against config keyword lists, for
the same reason as exit_code/1: it is the pure decision inside a task whose
run/1 needs a live app and a database.
The process exit status --exit-code should produce: 1 when any check
failed, 0 otherwise.
Public because run/1 is not a unit-test seam (it starts the app and needs a
real database) — this is the pure decision behind the flag, in the same shape
as Mix.Tasks.PhoenixKit.Status.exit_code/2 and
Mix.Tasks.PhoenixKit.Repair.exit_code/1.
Only :fail gates. A :warn is advisory by construction — several fire on
perfectly healthy installs (a capped pool under update_mode, an unreadable
application.ex) — and gating on them would make the flag unusable, which is
how a task ends up back at "reports a problem and exits 0".
The "Git Hooks" verdict, as a pure function of what could actually be observed.
Public for the same reason exit_code/1 is: run/1 is not a unit-test seam,
so the decision is tested on its own.
The point of the three-way inputs is that this check must be able to say "I could not tell" instead of guessing. A check that reports "hook not installed" when it merely failed to look is worse than no check: it is confidently wrong, and it sends the reader to fix something that is not broken.
That distinction is not free, and the obvious implementation gets it wrong:
git config --get core.hooksPath exits 1 both when the key is unset and
when the current directory is not a git repository at all (verified, not
assumed). So repository-ness is probed separately, and only inside a
repository is exit 1 read as the fact "not configured".
:repo—{:ok, common_dir}when git answered,:unknownotherwise (not a repository, git missing, anything else).:hooks_path—{:ok, value}|:unset(a fact) |:unknown(a gap).:tracked?— whether.githooks/pre-commitexists in this checkout.:shadow—{:ok, path}for a leftover hook in the common hooks dir (worktrees do not have their own),:none, or:unknown.