# TODO
Open work only. Finished items are dropped rather than ticked — `CLAUDE.md` and
the documentation are where "what exists" is recorded, and a checklist that
keeps its corpses stops being read.
## Server
- [ ] **PostgreSQL beside SQLite.** Every query goes through `src/sqlite/` as a
runtime `sqlx::query`, so most of them port unchanged; what does not is
`Database::connect`'s two pragmas, the `rows_affected == 1` single-use
idiom the nonces and recovery codes rest on, and `migrations/` — frozen
since 0.1.0 and written in SQLite's dialect. Postgres therefore needs its
own migration set selected by the URL scheme, never edits to the files
already there.
## Observability
- [ ] **Histograms — request latency, and issuance latency.** The one thing a
metrics *library* would genuinely earn over the hand-rolled registry in
`src/metrics.rs`, since buckets are where the format stops being a
`write!` per series. Worth reconsidering the dependency at that point
rather than hand-rolling bucket boundaries; until then `latency_ms` on
the access line is what there is.
## Web admin
- [ ] **WebAuthn as a second factor kind** — *investigated and deferred; both
blocking checks were actually run.* `webauthn-rs` 0.5.5 is MPL-2.0, which
`deny.toml`'s allow list does not carry, and `webauthn-rs-core`
hard-depends on `openssl`/`openssl-sys` (non-optional), which this tree
has avoided at every turn. The 0.6.1-dev line drops OpenSSL for
`crypto-glue` but is a `-dev` prerelease — the category already refused
for htmx 4.x. Nothing in the design precludes it: another factor kind is
another `MfaStep` variant and another branch in `verify_second_factor`,
not a change to the state machine, and `admin.base_url` is already the
resolved origin an RP id would come from. The open choice is that
dependency versus hand-rolling COSE/CBOR on `ring` + `ciborium`
(Apache-2.0, already allowed) with attestation `none`.
- [ ] **Live view of a pending order** — `hx-trigger="every 5s"` on the order
fragment, so an operator watches a challenge resolve instead of
reloading. The fragment route exists already (`HX-Request` picks it);
what needs deciding is when polling **stops**, so a tab left open on a
terminal order does not poll for ever.
## Signers — local CA
- [ ] **An OCSP responder** — by far the largest item here: a signed response
per query, a delegated responder certificate (or the CA key doing double
duty), and a route that is emphatically not an ACME resource. Worth
deciding whether it is wanted at all before building it: the ecosystem
moved towards RFC 9773 renewal info plus a small CRL, and this server
serves both already. The pointer half is already cheap if this ever
lands: `id-ad-ocsp` is a second `AccessDescription` inside the
`authorityInfoAccess` extension `local_ca/policy.rs` builds today, so it
is one more caller of `access_description` and a key beside
`ca_issuer_urls`, not a rewrite.
## IPAM
- [ ] **phpIPAM's user/password session-token auth.** `src/ipam/phpipam/`
implements the static app-code scheme only ("SSL with App code"), which
is the direct analogue of NetBox's token and rotates in the environment.
The other scheme exchanges user credentials for a six-hour token, so it
needs a refresh loop and somewhere to keep the token — worth having only
if an estate's phpIPAM cannot be given an app code at all.
- [ ] **A third backend to test the `Ipam` trait properly.** NetBox and
phpIPAM between them already forced `sources` to be per-backend, the
transport to be shared, and a status to survive on the error type (a
phpIPAM `404` is an answer). A third — NIPAP, Infoblox, or a plain
HTTP/script hook mirroring `filter.custom` — is what would show whether
the seam generalises or merely spans those two.
## Notifications
- [ ] **Expiry reminders**, at most one message per certificate per week. One
piece is missing: nothing stores the leaf's notAfter (`orders` holds the
PEM, and a notAfter column is a **new** migration, never an edit to a
frozen one). The sweep itself is now a `JobHandler` returning
`Reschedule`, the shape `jobs::retention` already uses.
- [ ] Address them to the account's own `contact`, not only to the operator.
Every existing `NotifyEvent` goes wherever the backend is configured to
send; this would be the first whose recipient comes out of the data.