rust-crm — decisión de arquitectura

Proyecto: ~/nef/rust-crm. CRM mínimo local-only para clientes de negocio pequeño (contactos + notas), construido como ejercicio real para aprender Rust.

Contexto

Ya existían 4 intentos previos de CRM en Rails (mvp-crm, crm-demo, _CRM_ERP, erp-crm-drm-files, todos ~24-26 ago 2026), abandonados. Y hay un Twenty CRM (TypeScript) en producción real (twenty-vw-crm, self-hosted PikaPods) — ese sigue siendo la herramienta de trabajo, este proyecto no lo reemplaza. rust-crm es explícitamente aprendizaje, no producto.

Stack elegido

  • SQLite (no Postgres) — el “motor” corre embebido en el proceso, no como servicio aparte. Correcto mientras sea local-only, un solo usuario. Si algún día se necesita acceso multi-dispositivo, el repo layer ya usa sintaxis compatible con Postgres (RETURNING, placeholders numerados) — el swap sería cambiar connection string + feature flag de sqlx, no reescribir.
  • axum + sqlx + askama + htmx — server-rendered HTML, sin build step de JS. htmx solo para el único bit dinámico (agregar nota sin reload).
  • sqlx con query_as en runtime, no macros query!/query_as! — evita necesitar DATABASE_URL en compile-time para un proyecto que arranca de cero. Camino de upgrade documentado en el README del repo.

Patrones de “good practices” aplicados

  • Newtype ids (ContactId, NoteId) — previene pasar el id equivocado, costo cero en runtime.
  • Un solo AppError enum → IntoResponse, mapeo HTTP en un solo lugar.
  • Capa repo separada de handlers — SQL nunca toca HTTP, handlers nunca tocan SQL. Testeable sin servidor (tests/contacts_repo.rs, 7 tests contra SQLite real en archivo temporal, no mocks).
  • NewContact/NewNote como tipos separados de Contact/Note — un form nunca puede inyectar id/timestamps.

Roadmap explícito (no construido aún, a propósito — YAGNI)

Companies, deals/pipeline, tasks/reminders, auth. Se agregan cuando haya necesidad real, no antes.

Estado al cerrar esta sesión

Compila limpio, 0 warnings, 7/7 tests pasan, smoke test manual completo (crear/listar/filtrar/nota/cambio de estado/delete con cascada/404/400) contra servidor real. Git inicializado (via cargo new), sin commit aún — pendiente que Nef lo pida explícitamente.