# `mix phoenix_kit.doctor`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/mix/tasks/phoenix_kit.doctor.ex#L1)

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 from
    `config :phoenix_kit, :prefix`, then `"public"` — the same resolution
    `mix phoenix_kit.update` / `--status` use, 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 success `mix phoenix_kit.status
    --exit-code` exists 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

  1. **Repo Detection** — Can we find and start the Ecto repo?
  2. **DB Connectivity** — Can we execute a simple query?
  3. **Pool Configuration** — Pool size, checkout timeout, queue settings
  4. **PgBouncer Detection** — Is PgBouncer between app and PostgreSQL?
  5. **Migration State** — PhoenixKit version (COMMENT), schema_migrations alignment
  6. **Module Schema Versions** — Modules owning their own chain, vs what their code expects
  7. **Schema Drift** — Columns a migration should have added but the DB lacks
  8. **Pending Migrations** — Migration files not yet recorded in schema_migrations
  9. **UUID Column Types** — Detects varchar uuid columns that crash Ecto on startup
 10. **UUID Primary Keys** — Detects primary keys that are not the expected uuid type
 11. **NULL UUIDs in FK Sources** — Detects NULL uuids that cause infinite backfill loops
 12. **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
 13. **Schema-Declared Relations Without a DB FK** — Every `belongs_to`
     PhoenixKit's own Ecto schemas declare, cross-referenced against
     `pg_constraint` for 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_key` on the
     `belongs_to`), never guessed from a column name — so it also cannot
     see a soft reference that isn't declared as a `belongs_to` at all
     (e.g. a plain field, or a polymorphic `*_uuid`/`*_type` pair).
 14. **Lock Conflicts** — Any blocked or long-running queries?
 15. **Orphaned Connections** — Idle-in-transaction or stuck connections
 16. **Oban Configuration** — Queues and plugins that consume pool connections
 17. **Oban Cron Queues** — Does every crontab worker have its queue configured?
 18. **PhoenixKit Supervisor** — What's running (update_mode vs full)?
 19. **Child Start Order** — Does the Repo start before PhoenixKit/Oban in application.ex?
 20. **Update Mode** — Is update_mode active?
 21. **daisyUI Version** — Is the host's vendored daisyUI recent enough?
 22. **User Dashboard (deprecated)** — Is the host still on the retired dashboard?
 23. **Sitemap Discoverability** — Is the sitemap actually reachable?
 24. **Crawler Visibility** — noindex on a production-looking host, or a
     staging-looking host left indexable
 25. **Demo Auth Pages** — Are the demo auth routes still exposed?
 26. **Manifest Repair (dry-run)** — `PhoenixKit.Migrations.Repair.verify/1`
     runs read-only against the generated
     `PhoenixKit.Migrations.ExpectedSchema` manifest as an additional,
     non-fatal check (never `:fail`). Passes and says so if the manifest
     has been removed or overridden away in this checkout.
 27. **Git Hooks** — does `core.hooksPath` point at a directory that has an
     executable `pre-commit` in it? Any directory qualifies — `.githooks`
     is only the convention this checkout happens to track. Only runs
     inside a checkout of phoenix_kit itself (that convention is a
     phoenix_kit-repo thing, not something installed into a consuming host
     app) — silently skipped otherwise.

# `check_cron_queues`

```elixir
@spec check_cron_queues(keyword() | nil | term()) :: {:pass | :warn, String.t()}
```

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.

# `exit_code`

```elixir
@spec exit_code([{String.t(), {:pass | :warn | :fail, String.t()}}]) :: 0 | 1
```

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".

# `git_hooks_verdict`

```elixir
@spec git_hooks_verdict(map()) :: {:pass | :warn, String.t()}
```

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".

A second trap, independent of the first: `:hooks_path` holding anything
other than the literal string `.githooks` does not mean the hook is broken.
`core.hooksPath` names a directory, and a user is free to point it anywhere
— any directory with an executable `pre-commit` in it works exactly the way
`.githooks` does; `.githooks` is only the name this checkout's own
convention happens to use. Treating one specific value as the only correct
answer reports a perfectly working setup as broken, and its "fix" tells the
reader to point at a location that has nothing to do with why. So this
checks the property that actually matters — an executable `pre-commit` at
wherever `core.hooksPath` points — never string equality with `.githooks`.

  * `:repo` — `{:ok, common_dir}` when git answered, `:unknown` otherwise
    (not a repository, git missing, anything else).
  * `:hooks_path` — `{:ok, value}` | `:unset` (a fact) | `:unknown` (a gap).
  * `:pre_commit_executable` — whether the directory `:hooks_path` names has
    an executable `pre-commit` in it: `:yes` | `:no` | `:unknown` (only
    meaningful when `:hooks_path` is `{:ok, _}`; see `pre_commit_executable?/1`).
  * `:tracked?` — whether `.githooks/pre-commit` exists in this checkout.
  * `:shadow` — `{:ok, path}` for a leftover hook in the **common** hooks dir
    (worktrees do not have their own), `:none`, or `:unknown`.

# `integration_key_result`

```elixir
@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.

# `pre_commit_executable?`

```elixir
@spec pre_commit_executable?(String.t()) :: :yes | :no | :unknown
```

Whether `dir` has an executable `pre-commit` in it.

The property `git_hooks_verdict/1` actually cares about, regardless of what
the configured directory is named or where it lives — `core.hooksPath`
works identically for any directory that has this file, `.githooks` is not
special to git itself.

Public for the same reason `exit_code/1` is: a test seam over a filesystem
fact, so `git_hooks_verdict/1` above can stay a pure function of an
already-decided map instead of doing its own I/O.

A relative `dir` is resolved the same way git resolves a relative
`core.hooksPath`: against the top of the working tree (see githooks(5)),
which is also where this task itself runs.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
