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 - Schema-Declared Relations Without a DB FK — Every
belongs_toPhoenixKit's own Ecto schemas declare, cross-referenced againstpg_constraintfor a matching foreign key. Reports the COUNT found with no DB-level FK — informational, not a failure: some are intentional (a federated/soft reference cannot carry a FK across an optional module boundary, see V179/V180). This is the complement to check 12's own stated gap above: check 12 only ever sees a relationship that already HAS a declared FK constraint; this one finds relationships Ecto declares that never got one. Derived entirely from what the schema itself declares (owner_keyon thebelongs_to), never guessed from a column name — so it also cannot see a soft reference that isn't declared as abelongs_toat all (e.g. a plain field, or a polymorphic*_uuid/*_typepair). - 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.
The "Integration Key" verdict, rendered from one complete report.
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.
@spec integration_key_result( PhoenixKit.Integrations.Encryption.key_report(), boolean() ) :: {:pass | :warn | :fail, String.t()}
The "Integration Key" verdict, rendered from one complete report.
Takes the report and a display choice, and nothing else. That is the whole point of the signature: this check previously received the advice, a fingerprint note and a storage location as separate arguments, each computed without reference to the others, and three consecutive rounds of fixes each produced a message that contradicted itself — a fingerprint note beside "no key resolved at all", a storage location for a key stored nowhere, a claim of a weaker key where none existed.
There is no longer an argument through which those pieces could disagree: the fingerprint and the tier that produced it are one term inside the report, and they are absent together when there is no key.
Public as a test seam, for the same reason exit_code/1 is: the defects lived
in this rendering, where tests over the diagnosis could not reach them.